@tryinget/pi-activity-strip 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -6
- package/bin/pi-activity-strip.mjs +55 -5
- package/examples/simulate-sessions.mjs +6 -1
- package/extensions/activity-strip.js +8 -2
- package/extensions/activity-strip.ts +6 -0
- package/package.json +9 -8
- package/src/broker/server.mjs +24 -3
- package/src/broker/session-store.mjs +24 -4
- package/src/client/broker-client.mjs +23 -3
- package/src/client/launcher.mjs +6 -0
- package/src/client/session-telemetry.mjs +87 -8
- package/src/common/activity-order.mjs +68 -0
- package/src/common/alignment-controller.mjs +74 -0
- package/src/common/card-display.mjs +48 -0
- package/src/common/compatibility.mjs +14 -2
- package/src/common/constants.mjs +12 -0
- package/src/common/contracts.d.ts +22 -1
- package/src/common/contracts.ts +22 -1
- package/src/common/electron.mjs +6 -0
- package/src/common/format.mjs +6 -0
- package/src/common/niri-focus.mjs +322 -0
- package/src/common/protocol.mjs +9 -0
- package/src/common/status-report.mjs +6 -0
- package/src/common/telemetry.mjs +16 -3
- package/src/electron/main.mjs +307 -127
- package/src/electron/niri-native-window-runtime.mjs +178 -0
- package/src/electron/niri-workspace-events.mjs +81 -0
- package/src/electron/preload.cjs +40 -0
- package/src/electron/renderer-visibility-runtime.mjs +107 -0
- package/src/electron/workspace-view-runtime.mjs +360 -0
- package/src/ui/strip-html.mjs +371 -246
package/src/ui/strip-html.mjs
CHANGED
|
@@ -1,8 +1,47 @@
|
|
|
1
|
-
|
|
1
|
+
// ---
|
|
2
|
+
// summary: "generates the calm, interactive activity strip with accessible cards and rich detail reveal"
|
|
3
|
+
// read_when:
|
|
4
|
+
// - "changing strip markup, styling, state rendering, ordering, or interactions"
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
isActiveSession,
|
|
9
|
+
isMonitoringSession,
|
|
10
|
+
moveOrderItem,
|
|
11
|
+
reconcileActivityOrder,
|
|
12
|
+
} from "../common/activity-order.mjs";
|
|
13
|
+
import {
|
|
14
|
+
disambiguatedRepoLabel,
|
|
15
|
+
findDuplicateLabels,
|
|
16
|
+
isStalledSession,
|
|
17
|
+
} from "../common/card-display.mjs";
|
|
18
|
+
import {
|
|
19
|
+
ACTIVITY_STRIP_EVENT_STALL_MS,
|
|
20
|
+
ACTIVITY_STRIP_ORDER_REFRESH_MS,
|
|
21
|
+
} from "../common/constants.mjs";
|
|
22
|
+
|
|
23
|
+
const ORDER_RUNTIME = [
|
|
24
|
+
'const ACTIVE_STATES = new Set(["thinking", "tool", "waiting"]);',
|
|
25
|
+
isActiveSession.toString(),
|
|
26
|
+
isMonitoringSession.toString(),
|
|
27
|
+
reconcileActivityOrder.toString(),
|
|
28
|
+
moveOrderItem.toString(),
|
|
29
|
+
isStalledSession.toString(),
|
|
30
|
+
findDuplicateLabels.toString(),
|
|
31
|
+
disambiguatedRepoLabel.toString(),
|
|
32
|
+
].join("\n");
|
|
33
|
+
|
|
34
|
+
export function shouldRetainExpandedCard({ hovered, activeElement, documentFocused }) {
|
|
35
|
+
return Boolean(hovered || (documentFocused && activeElement));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const INTERACTION_RUNTIME = shouldRetainExpandedCard.toString();
|
|
39
|
+
|
|
40
|
+
export function createStripHtml({ interactive = true, initiallyVisible = true } = {}) {
|
|
2
41
|
const pointerEvents = interactive ? "auto" : "none";
|
|
3
42
|
|
|
4
43
|
return `<!doctype html>
|
|
5
|
-
<html>
|
|
44
|
+
<html data-strip-visible="${initiallyVisible ? "true" : "false"}">
|
|
6
45
|
<head>
|
|
7
46
|
<meta charset="utf-8" />
|
|
8
47
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
@@ -10,27 +49,25 @@ export function createStripHtml({ interactive = false } = {}) {
|
|
|
10
49
|
:root {
|
|
11
50
|
color-scheme: dark;
|
|
12
51
|
--panel: rgba(9, 14, 24, 0.82);
|
|
13
|
-
--panel-strong: rgba(14, 21, 35, 0.
|
|
52
|
+
--panel-strong: rgba(14, 21, 35, 0.96);
|
|
14
53
|
--line: rgba(255, 255, 255, 0.08);
|
|
15
54
|
--line-strong: rgba(255, 255, 255, 0.14);
|
|
16
55
|
--text: rgba(247, 250, 255, 0.96);
|
|
17
56
|
--muted: rgba(194, 205, 224, 0.74);
|
|
18
57
|
--dim: rgba(167, 180, 203, 0.48);
|
|
19
58
|
--accent: #79b8ff;
|
|
59
|
+
--current-line: color-mix(in srgb, var(--accent) 78%, white 12%);
|
|
20
60
|
--thinking: #8ab4ff;
|
|
21
61
|
--tool: #ffd166;
|
|
22
62
|
--waiting: #ff9f7a;
|
|
23
63
|
--success: #57d9a3;
|
|
24
64
|
--error: #ff7d7d;
|
|
25
|
-
--shadow:
|
|
65
|
+
--shadow: none;
|
|
26
66
|
}
|
|
27
67
|
|
|
28
|
-
* {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
html,
|
|
33
|
-
body {
|
|
68
|
+
* { box-sizing: border-box; }
|
|
69
|
+
html[data-strip-visible="false"] { visibility: hidden; }
|
|
70
|
+
html, body {
|
|
34
71
|
margin: 0;
|
|
35
72
|
width: 100%;
|
|
36
73
|
height: 100%;
|
|
@@ -39,10 +76,8 @@ export function createStripHtml({ interactive = false } = {}) {
|
|
|
39
76
|
font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif;
|
|
40
77
|
pointer-events: ${pointerEvents};
|
|
41
78
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
padding: 4px 10px 6px;
|
|
45
|
-
}
|
|
79
|
+
body { padding: 4px 10px 6px; }
|
|
80
|
+
button { font: inherit; }
|
|
46
81
|
|
|
47
82
|
.strip {
|
|
48
83
|
display: grid;
|
|
@@ -51,15 +86,12 @@ export function createStripHtml({ interactive = false } = {}) {
|
|
|
51
86
|
width: 100%;
|
|
52
87
|
height: 100%;
|
|
53
88
|
}
|
|
54
|
-
|
|
55
|
-
.brand,
|
|
56
|
-
.cards {
|
|
89
|
+
.brand, .cards {
|
|
57
90
|
backdrop-filter: blur(18px) saturate(1.25);
|
|
58
91
|
-webkit-backdrop-filter: blur(18px) saturate(1.25);
|
|
59
92
|
border: 1px solid var(--line);
|
|
60
93
|
box-shadow: var(--shadow);
|
|
61
94
|
}
|
|
62
|
-
|
|
63
95
|
.brand {
|
|
64
96
|
display: flex;
|
|
65
97
|
flex-direction: column;
|
|
@@ -68,76 +100,49 @@ export function createStripHtml({ interactive = false } = {}) {
|
|
|
68
100
|
border-radius: 20px;
|
|
69
101
|
background: linear-gradient(180deg, rgba(12, 19, 33, 0.92), rgba(8, 13, 22, 0.82));
|
|
70
102
|
}
|
|
71
|
-
|
|
72
|
-
.brand__eyebrow,
|
|
73
|
-
.meta,
|
|
74
|
-
.tool,
|
|
75
|
-
.card__state,
|
|
76
|
-
.placeholder__eyebrow {
|
|
103
|
+
.brand__eyebrow, .meta, .tool, .card__state, .inspector__key, .placeholder__eyebrow {
|
|
77
104
|
font-family: "IBM Plex Mono", "JetBrains Mono", monospace;
|
|
78
|
-
letter-spacing: 0.
|
|
105
|
+
letter-spacing: 0.12em;
|
|
79
106
|
text-transform: uppercase;
|
|
80
107
|
}
|
|
81
|
-
|
|
82
|
-
.
|
|
83
|
-
.
|
|
84
|
-
|
|
85
|
-
font-size: 10px;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.brand__title {
|
|
89
|
-
margin-top: 6px;
|
|
90
|
-
color: var(--text);
|
|
91
|
-
font-size: 20px;
|
|
92
|
-
font-weight: 650;
|
|
93
|
-
line-height: 1;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
.brand__subtitle {
|
|
97
|
-
margin-top: 6px;
|
|
98
|
-
color: var(--muted);
|
|
99
|
-
font-size: 11px;
|
|
100
|
-
line-height: 1.35;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
.meta {
|
|
104
|
-
color: var(--dim);
|
|
105
|
-
font-size: 11px;
|
|
106
|
-
}
|
|
108
|
+
.brand__eyebrow, .placeholder__eyebrow { color: var(--accent); font-size: 9px; }
|
|
109
|
+
.brand__title { margin-top: 4px; color: var(--text); font-size: 18px; font-weight: 650; line-height: 1; }
|
|
110
|
+
.brand__subtitle { margin-top: 4px; color: var(--muted); font-size: 10px; line-height: 1.25; }
|
|
111
|
+
.meta { color: var(--dim); font-size: 9px; line-height: 1.3; }
|
|
107
112
|
|
|
108
113
|
.cards {
|
|
109
114
|
display: flex;
|
|
110
115
|
gap: 8px;
|
|
111
|
-
align-items:
|
|
116
|
+
align-items: flex-start;
|
|
112
117
|
min-width: 0;
|
|
113
118
|
padding: 6px 8px 8px;
|
|
114
119
|
border-radius: 20px;
|
|
120
|
+
overflow-x: auto;
|
|
121
|
+
overflow-y: hidden;
|
|
122
|
+
scrollbar-width: none;
|
|
115
123
|
background: linear-gradient(180deg, rgba(10, 16, 28, 0.9), rgba(6, 10, 18, 0.74));
|
|
116
124
|
}
|
|
125
|
+
.cards::-webkit-scrollbar { display: none; }
|
|
126
|
+
.cards--empty { align-items: center; justify-content: center; }
|
|
117
127
|
|
|
118
|
-
.
|
|
119
|
-
align-items: center;
|
|
120
|
-
justify-content: center;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
.card,
|
|
124
|
-
.placeholder {
|
|
128
|
+
.card, .placeholder {
|
|
125
129
|
position: relative;
|
|
126
130
|
overflow: hidden;
|
|
127
131
|
border-radius: 16px;
|
|
128
132
|
border: 1px solid var(--line-strong);
|
|
129
|
-
background: linear-gradient(180deg, rgba(13, 20, 35, 0.96), rgba(9, 14, 24, 0.
|
|
133
|
+
background: linear-gradient(180deg, rgba(13, 20, 35, 0.96), rgba(9, 14, 24, 0.9));
|
|
130
134
|
}
|
|
131
|
-
|
|
132
135
|
.card {
|
|
136
|
+
appearance: none;
|
|
133
137
|
display: grid;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
flex: 0 0 224px;
|
|
139
|
+
height: 60px;
|
|
140
|
+
padding: 8px 10px 8px 12px;
|
|
141
|
+
color: inherit;
|
|
142
|
+
text-align: left;
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
transition: border-color 120ms ease, background 120ms ease, height 120ms ease;
|
|
139
145
|
}
|
|
140
|
-
|
|
141
146
|
.card::before {
|
|
142
147
|
content: "";
|
|
143
148
|
position: absolute;
|
|
@@ -145,229 +150,349 @@ export function createStripHtml({ interactive = false } = {}) {
|
|
|
145
150
|
width: 3px;
|
|
146
151
|
background: var(--state-color, var(--accent));
|
|
147
152
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
white-space: nowrap;
|
|
198
|
-
overflow: hidden;
|
|
199
|
-
text-overflow: ellipsis;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
.card__detail {
|
|
203
|
-
color: var(--muted);
|
|
204
|
-
font-size: 11px;
|
|
205
|
-
line-height: 1.2;
|
|
206
|
-
display: -webkit-box;
|
|
207
|
-
-webkit-line-clamp: 3;
|
|
208
|
-
-webkit-box-orient: vertical;
|
|
209
|
-
overflow: hidden;
|
|
210
|
-
min-height: 3.6em;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
.tool,
|
|
214
|
-
.elapsed {
|
|
215
|
-
color: var(--dim);
|
|
216
|
-
font-size: 9px;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
.placeholder {
|
|
220
|
-
display: flex;
|
|
221
|
-
flex-direction: column;
|
|
222
|
-
justify-content: center;
|
|
223
|
-
gap: 8px;
|
|
224
|
-
width: 100%;
|
|
225
|
-
max-width: 420px;
|
|
226
|
-
padding: 18px 20px;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
.placeholder__title {
|
|
230
|
-
color: var(--text);
|
|
231
|
-
font-size: 16px;
|
|
232
|
-
font-weight: 650;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
.placeholder__copy {
|
|
236
|
-
color: var(--muted);
|
|
237
|
-
font-size: 13px;
|
|
238
|
-
line-height: 1.45;
|
|
153
|
+
.card:hover, .card:focus-visible, .card[data-open="true"] {
|
|
154
|
+
border-color: color-mix(in srgb, var(--state-color, var(--accent)) 58%, white 4%);
|
|
155
|
+
background: linear-gradient(180deg, rgba(18, 28, 47, 0.99), rgba(10, 16, 28, 0.96));
|
|
156
|
+
outline: none;
|
|
157
|
+
}
|
|
158
|
+
.card[data-current="true"] {
|
|
159
|
+
border-color: var(--current-line);
|
|
160
|
+
outline: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
|
|
161
|
+
outline-offset: -3px;
|
|
162
|
+
}
|
|
163
|
+
.card[data-current="true"]::before { width: 5px; }
|
|
164
|
+
.card[data-current="true"]:focus-visible {
|
|
165
|
+
outline: 2px solid var(--current-line);
|
|
166
|
+
outline-offset: -4px;
|
|
167
|
+
}
|
|
168
|
+
body[data-expanded="true"] .card[data-open="true"] { height: 228px; }
|
|
169
|
+
.card__header, .card__footer { display: flex; justify-content: space-between; align-items: center; gap: 8px; min-width: 0; }
|
|
170
|
+
.card__label { min-width: 0; }
|
|
171
|
+
.card__repo { color: var(--text); font-size: 12px; font-weight: 650; line-height: 1.1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
172
|
+
.card__phase { margin-top: 4px; color: var(--muted); font-size: 10px; line-height: 1.1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
173
|
+
.card__state { flex: 0 0 auto; padding: 4px 6px; border-radius: 999px; background: color-mix(in srgb, var(--state-color, var(--accent)) 14%, transparent); border: 1px solid color-mix(in srgb, var(--state-color, var(--accent)) 34%, transparent); color: var(--state-color, var(--accent)); font-size: 8px; line-height: 1; }
|
|
174
|
+
.card__footer { margin-top: 5px; }
|
|
175
|
+
.tool, .elapsed { color: var(--dim); font-size: 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
176
|
+
.tool { max-width: 120px; }
|
|
177
|
+
|
|
178
|
+
.inspector {
|
|
179
|
+
display: none;
|
|
180
|
+
grid-template-columns: 58px minmax(0, 1fr);
|
|
181
|
+
gap: 6px 8px;
|
|
182
|
+
margin-top: 10px;
|
|
183
|
+
padding-top: 9px;
|
|
184
|
+
border-top: 1px solid var(--line);
|
|
185
|
+
}
|
|
186
|
+
body[data-expanded="true"] .card[data-open="true"] .inspector { display: grid; }
|
|
187
|
+
.inspector__key { color: var(--dim); font-size: 8px; }
|
|
188
|
+
.inspector__value { color: var(--muted); font-size: 10px; line-height: 1.25; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
|
189
|
+
.inspector__value--path { direction: rtl; text-align: left; white-space: nowrap; text-overflow: ellipsis; display: block; }
|
|
190
|
+
.activation { grid-column: 1 / -1; min-height: 14px; color: var(--dim); font-size: 9px; }
|
|
191
|
+
.activation[data-kind="error"] { color: var(--error); }
|
|
192
|
+
.activation[data-kind="success"] { color: var(--success); }
|
|
193
|
+
|
|
194
|
+
.placeholder { display: flex; flex-direction: column; justify-content: center; gap: 6px; width: 100%; max-width: 420px; padding: 12px 18px; }
|
|
195
|
+
.placeholder__title { color: var(--text); font-size: 14px; font-weight: 650; }
|
|
196
|
+
.placeholder__copy { color: var(--muted); font-size: 11px; line-height: 1.35; }
|
|
197
|
+
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
|
198
|
+
.card[data-stalled="true"] { opacity: 0.72; } .card[data-stalled="true"] .card__phase { text-decoration: line-through; }
|
|
199
|
+
|
|
200
|
+
@media (prefers-reduced-motion: reduce) {
|
|
201
|
+
.card { transition: none; }
|
|
239
202
|
}
|
|
240
203
|
</style>
|
|
241
204
|
</head>
|
|
242
|
-
<body>
|
|
205
|
+
<body data-expanded="false">
|
|
243
206
|
<div class="strip">
|
|
244
|
-
<section class="brand">
|
|
207
|
+
<section class="brand" aria-label="Pi activity summary">
|
|
245
208
|
<div>
|
|
246
209
|
<div class="brand__eyebrow">π telemetry ribbon</div>
|
|
247
210
|
<div class="brand__title">Activity</div>
|
|
248
|
-
<div class="brand__subtitle">
|
|
211
|
+
<div class="brand__subtitle">Calm order · live detail</div>
|
|
249
212
|
</div>
|
|
250
213
|
<div class="meta" id="meta">Waiting for sessions…</div>
|
|
251
214
|
</section>
|
|
252
|
-
<section class="cards cards--empty" id="cards"></section>
|
|
215
|
+
<section class="cards cards--empty" id="cards" aria-label="Live Pi sessions"></section>
|
|
216
|
+
<div class="sr-only" id="announcer" aria-live="polite"></div>
|
|
253
217
|
</div>
|
|
254
218
|
|
|
255
219
|
<script>
|
|
220
|
+
${ORDER_RUNTIME}
|
|
221
|
+
${INTERACTION_RUNTIME}
|
|
222
|
+
const ORDER_REFRESH_MS = ${ACTIVITY_STRIP_ORDER_REFRESH_MS};
|
|
223
|
+
const EVENT_STALL_MS = ${ACTIVITY_STRIP_EVENT_STALL_MS};
|
|
256
224
|
const meta = document.getElementById("meta");
|
|
257
225
|
const cards = document.getElementById("cards");
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
waiting: "waiting",
|
|
266
|
-
success: "done",
|
|
267
|
-
error: "error",
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
const STATE_COLORS = {
|
|
271
|
-
idle: "var(--accent)",
|
|
272
|
-
thinking: "var(--thinking)",
|
|
273
|
-
tool: "var(--tool)",
|
|
274
|
-
waiting: "var(--waiting)",
|
|
275
|
-
success: "var(--success)",
|
|
276
|
-
error: "var(--error)",
|
|
226
|
+
const announcer = document.getElementById("announcer");
|
|
227
|
+
const api = window.activityStrip || {
|
|
228
|
+
activate: async () => ({ ok: false, error: "Activity bridge unavailable" }),
|
|
229
|
+
setExpanded: async () => {},
|
|
230
|
+
onCollapse() { return () => {}; },
|
|
231
|
+
onVisibility() { return () => {}; },
|
|
232
|
+
subscribe() { return () => {}; },
|
|
277
233
|
};
|
|
234
|
+
let snapshot = { generatedAt: Date.now(), focusedSessionId: null, sessions: [] };
|
|
235
|
+
let orderedIds = [];
|
|
236
|
+
let nextOrderRefreshAt = 0;
|
|
237
|
+
let collapseTimer = null;
|
|
278
238
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
switch (character) {
|
|
282
|
-
case "&": return "&";
|
|
283
|
-
case "<": return "<";
|
|
284
|
-
case ">": return ">";
|
|
285
|
-
case '"': return """;
|
|
286
|
-
case "'": return "'";
|
|
287
|
-
default: return character;
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
}
|
|
239
|
+
const STATE_LABELS = { idle: "idle", thinking: "thinking", tool: "tool", waiting: "waiting", success: "done", error: "error" };
|
|
240
|
+
const STATE_COLORS = { idle: "var(--accent)", thinking: "var(--thinking)", tool: "var(--tool)", waiting: "var(--waiting)", success: "var(--success)", error: "var(--error)" };
|
|
291
241
|
|
|
292
242
|
function formatDuration(totalSeconds) {
|
|
293
243
|
const minutes = Math.floor(totalSeconds / 60);
|
|
294
244
|
const seconds = totalSeconds % 60;
|
|
295
245
|
return String(minutes) + ":" + String(seconds).padStart(2, "0");
|
|
296
246
|
}
|
|
297
|
-
|
|
298
247
|
function formatElapsed(session) {
|
|
299
248
|
const anchor = Number(session.agentStartedAt || session.startedAt || snapshot.generatedAt || Date.now());
|
|
300
249
|
return formatDuration(Math.max(0, Math.floor((Date.now() - anchor) / 1000)));
|
|
301
250
|
}
|
|
302
|
-
|
|
303
251
|
function formatLastSeen(session) {
|
|
304
|
-
const anchor = Number(session.updatedAt || snapshot.generatedAt || Date.now());
|
|
252
|
+
const anchor = Number(session.lastEventAt || session.updatedAt || snapshot.generatedAt || Date.now());
|
|
305
253
|
return formatDuration(Math.max(0, Math.floor((Date.now() - anchor) / 1000)));
|
|
306
254
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
'<
|
|
331
|
-
'<
|
|
332
|
-
'<div class="
|
|
333
|
-
'<div class="
|
|
334
|
-
'<div class="
|
|
335
|
-
'</div>',
|
|
336
|
-
'<div class="card__state">' + escapeHtml(stateLabel) + '</div>',
|
|
337
|
-
'</header>',
|
|
338
|
-
'<div>',
|
|
339
|
-
'<div class="card__phase">' + phase + '</div>',
|
|
340
|
-
'<div class="card__detail">' + detail + '</div>',
|
|
255
|
+
function sessionById(sessionId) {
|
|
256
|
+
return (Array.isArray(snapshot.sessions) ? snapshot.sessions : []).find((session) => session.sessionId === sessionId);
|
|
257
|
+
}
|
|
258
|
+
function safeDomId(sessionId) {
|
|
259
|
+
return "session-detail-" + String(sessionId).replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
260
|
+
}
|
|
261
|
+
function setText(card, selector, value) {
|
|
262
|
+
const target = card.querySelector(selector);
|
|
263
|
+
if (target) target.textContent = String(value ?? "");
|
|
264
|
+
}
|
|
265
|
+
function cardTemplate(sessionId) {
|
|
266
|
+
const card = document.createElement("button");
|
|
267
|
+
card.type = "button";
|
|
268
|
+
card.className = "card";
|
|
269
|
+
card.dataset.sessionId = sessionId;
|
|
270
|
+
card.dataset.current = "false";
|
|
271
|
+
card.dataset.open = "false";
|
|
272
|
+
card.setAttribute("aria-expanded", "false");
|
|
273
|
+
card.setAttribute("aria-describedby", safeDomId(sessionId));
|
|
274
|
+
card.innerHTML = [
|
|
275
|
+
'<div class="card__header"><div class="card__label"><div class="card__repo"></div><div class="card__phase"></div></div><div class="card__state"></div></div>',
|
|
276
|
+
'<div class="card__footer"><div class="tool"></div><div class="elapsed"></div></div>',
|
|
277
|
+
'<div class="inspector" id="' + safeDomId(sessionId) + '" role="tooltip">',
|
|
278
|
+
'<div class="inspector__key">detail</div><div class="inspector__value detail"></div>',
|
|
279
|
+
'<div class="inspector__key">prompt</div><div class="inspector__value prompt"></div>',
|
|
280
|
+
'<div class="inspector__key">reply</div><div class="inspector__value reply"></div>',
|
|
281
|
+
'<div class="inspector__key">path</div><div class="inspector__value inspector__value--path path"></div>',
|
|
282
|
+
'<div class="activation" aria-live="polite"></div>',
|
|
341
283
|
'</div>',
|
|
342
|
-
'<footer class="card__footer">',
|
|
343
|
-
'<div class="tool">' + tool + '</div>',
|
|
344
|
-
'<div class="elapsed">' + elapsed + ' · seen ' + lastSeen + '</div>',
|
|
345
|
-
'</footer>',
|
|
346
|
-
'</article>',
|
|
347
284
|
].join("");
|
|
285
|
+
return card;
|
|
286
|
+
}
|
|
287
|
+
function updateCard(card, session, duplicateLabels) {
|
|
288
|
+
const stalled = isStalledSession(session, Date.now(), EVENT_STALL_MS);
|
|
289
|
+
const stateColor = stalled ? "var(--waiting)" : STATE_COLORS[session.state] || "var(--accent)";
|
|
290
|
+
const stateLabel = stalled ? "stalled" : STATE_LABELS[session.state] || session.state || "idle";
|
|
291
|
+
const isCurrent = session.sessionId === snapshot.focusedSessionId;
|
|
292
|
+
card.style.setProperty("--state-color", stateColor);
|
|
293
|
+
card.dataset.stalled = stalled ? "true" : "false";
|
|
294
|
+
card.dataset.group = isMonitoringSession(session)
|
|
295
|
+
? "monitoring"
|
|
296
|
+
: isActiveSession(session)
|
|
297
|
+
? "active"
|
|
298
|
+
: "settled";
|
|
299
|
+
card.dataset.current = isCurrent ? "true" : "false";
|
|
300
|
+
if (isCurrent) card.setAttribute("aria-current", "true");
|
|
301
|
+
else card.removeAttribute("aria-current");
|
|
302
|
+
card.setAttribute("aria-label", (stalled ? "Stalled, " : isCurrent ? "Current terminal, " : "") + (session.repoLabel || "Pi session") + ", " + (stalled ? "no events recently" : session.phase || "Idle") + ". Press Enter to focus its Ghostty window.");
|
|
303
|
+
card.title = (isCurrent ? "Current terminal · " : "Focus ") + (session.repoLabel || "Pi session");
|
|
304
|
+
setText(card, ".card__repo", disambiguatedRepoLabel(session, duplicateLabels));
|
|
305
|
+
setText(card, ".card__phase", session.phase || "Idle");
|
|
306
|
+
setText(card, ".card__state", stateLabel);
|
|
307
|
+
setText(card, ".tool", session.toolName || session.toolTarget || "monitoring");
|
|
308
|
+
setText(card, ".elapsed", formatElapsed(session) + " · " + formatLastSeen(session));
|
|
309
|
+
setText(card, ".detail", session.detail || "Ready");
|
|
310
|
+
setText(card, ".prompt", session.lastPromptPreview || "—");
|
|
311
|
+
setText(card, ".reply", session.assistantPreview || "—");
|
|
312
|
+
setText(card, ".path", session.cwd || "—");
|
|
313
|
+
}
|
|
314
|
+
function syncOrder(regroup) {
|
|
315
|
+
const sessions = Array.isArray(snapshot.sessions) ? snapshot.sessions : [];
|
|
316
|
+
orderedIds = reconcileActivityOrder(sessions, orderedIds, { regroup });
|
|
317
|
+
if (regroup) nextOrderRefreshAt = Date.now() + ORDER_REFRESH_MS;
|
|
348
318
|
}
|
|
349
|
-
|
|
350
319
|
function render() {
|
|
351
320
|
const sessions = Array.isArray(snapshot.sessions) ? snapshot.sessions : [];
|
|
321
|
+
const byId = new Map(sessions.map((session) => [session.sessionId, session]));
|
|
322
|
+
const activeCount = sessions.filter(isActiveSession).length;
|
|
352
323
|
meta.textContent = sessions.length
|
|
353
|
-
? String(sessions.length) +
|
|
354
|
-
:
|
|
355
|
-
|
|
324
|
+
? String(activeCount) + " active · " + String(sessions.length - activeCount) + " settled · order " + String(Math.max(0, Math.ceil((nextOrderRefreshAt - Date.now()) / 1000))) + "s"
|
|
325
|
+
: "Waiting for sessions…";
|
|
356
326
|
if (sessions.length === 0) {
|
|
357
|
-
|
|
358
|
-
cards.
|
|
327
|
+
if (document.body.dataset.expanded === "true") setExpanded(null, false).catch(() => {});
|
|
328
|
+
cards.classList.add("cards--empty");
|
|
329
|
+
cards.innerHTML = '<div class="placeholder"><div class="placeholder__eyebrow">ready</div><div class="placeholder__title">No active Pi sessions yet</div><div class="placeholder__copy">Open Pi in Ghostty and the ribbon will populate automatically.</div></div>';
|
|
359
330
|
return;
|
|
360
331
|
}
|
|
361
|
-
|
|
362
|
-
cards.
|
|
363
|
-
|
|
332
|
+
cards.classList.remove("cards--empty");
|
|
333
|
+
cards.querySelector(".placeholder")?.remove();
|
|
334
|
+
const cardById = new Map(
|
|
335
|
+
[...cards.querySelectorAll(".card")].map((card) => [card.dataset.sessionId, card]),
|
|
336
|
+
);
|
|
337
|
+
for (const [sessionId, orphan] of cardById) {
|
|
338
|
+
if (!byId.has(sessionId)) {
|
|
339
|
+
orphan.remove();
|
|
340
|
+
cardById.delete(sessionId);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (
|
|
344
|
+
document.body.dataset.expanded === "true" &&
|
|
345
|
+
!cards.querySelector('.card[data-open="true"]')
|
|
346
|
+
) {
|
|
347
|
+
setExpanded(null, false).catch(() => {});
|
|
348
|
+
}
|
|
349
|
+
let targetIndex = 0;
|
|
350
|
+
const duplicateLabels = findDuplicateLabels(sessions);
|
|
351
|
+
for (const sessionId of orderedIds) {
|
|
352
|
+
const session = byId.get(sessionId);
|
|
353
|
+
if (!session) continue;
|
|
354
|
+
let card = cardById.get(sessionId);
|
|
355
|
+
if (!card) {
|
|
356
|
+
card = cardTemplate(sessionId);
|
|
357
|
+
cardById.set(sessionId, card);
|
|
358
|
+
}
|
|
359
|
+
updateCard(card, session, duplicateLabels);
|
|
360
|
+
const nodeAtTarget = cards.children[targetIndex] ?? null;
|
|
361
|
+
if (nodeAtTarget !== card) cards.insertBefore(card, nodeAtTarget);
|
|
362
|
+
targetIndex += 1;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function setExpanded(card, expanded) {
|
|
366
|
+
if (collapseTimer) { clearTimeout(collapseTimer); collapseTimer = null; }
|
|
367
|
+
for (const candidate of cards.querySelectorAll(".card")) {
|
|
368
|
+
const isOpen = candidate === card && expanded;
|
|
369
|
+
candidate.dataset.open = isOpen ? "true" : "false";
|
|
370
|
+
candidate.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
|
371
|
+
}
|
|
372
|
+
document.body.dataset.expanded = expanded ? "true" : "false";
|
|
373
|
+
await api.setExpanded(Boolean(expanded));
|
|
374
|
+
}
|
|
375
|
+
function scheduleCollapse() {
|
|
376
|
+
if (collapseTimer) clearTimeout(collapseTimer);
|
|
377
|
+
collapseTimer = setTimeout(() => {
|
|
378
|
+
const engagedCard = [...cards.querySelectorAll(".card")].find((candidate) =>
|
|
379
|
+
shouldRetainExpandedCard({
|
|
380
|
+
hovered: candidate.matches(":hover"),
|
|
381
|
+
activeElement: candidate === document.activeElement,
|
|
382
|
+
documentFocused: document.hasFocus(),
|
|
383
|
+
}),
|
|
384
|
+
);
|
|
385
|
+
if (engagedCard) {
|
|
386
|
+
setExpanded(engagedCard, true).catch(() => {});
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
setExpanded(null, false).catch(() => {});
|
|
390
|
+
}, 120);
|
|
391
|
+
}
|
|
392
|
+
async function activate(card) {
|
|
393
|
+
const session = sessionById(card.dataset.sessionId);
|
|
394
|
+
if (!session) return;
|
|
395
|
+
const activation = card.querySelector(".activation");
|
|
396
|
+
if (activation) { activation.dataset.kind = ""; activation.textContent = "Locating exact Ghostty session…"; }
|
|
397
|
+
let result;
|
|
398
|
+
try {
|
|
399
|
+
result = await api.activate(session.sessionId);
|
|
400
|
+
} catch {
|
|
401
|
+
result = { ok: false, error: "Focus bridge failed; nothing focused." };
|
|
402
|
+
}
|
|
403
|
+
if (activation) {
|
|
404
|
+
activation.dataset.kind = result?.ok ? "success" : "error";
|
|
405
|
+
activation.textContent = result?.ok ? "Focused Ghostty window." : (result?.error || "No exact window match; nothing focused.");
|
|
406
|
+
}
|
|
407
|
+
announcer.textContent = activation?.textContent || "";
|
|
364
408
|
}
|
|
365
409
|
|
|
366
|
-
|
|
410
|
+
cards.addEventListener("pointerover", (event) => {
|
|
411
|
+
const card = event.target.closest?.(".card");
|
|
412
|
+
if (card && !card.contains(event.relatedTarget)) setExpanded(card, true).catch(() => {});
|
|
413
|
+
});
|
|
414
|
+
cards.addEventListener("pointerout", (event) => {
|
|
415
|
+
const card = event.target.closest?.(".card");
|
|
416
|
+
if (card && !card.contains(event.relatedTarget)) scheduleCollapse();
|
|
417
|
+
});
|
|
418
|
+
cards.addEventListener("focusin", (event) => {
|
|
419
|
+
const card = event.target.closest?.(".card");
|
|
420
|
+
if (card) setExpanded(card, true).catch(() => {});
|
|
421
|
+
});
|
|
422
|
+
cards.addEventListener("focusout", (event) => {
|
|
423
|
+
const card = event.target.closest?.(".card");
|
|
424
|
+
if (card && !card.contains(event.relatedTarget)) scheduleCollapse();
|
|
425
|
+
});
|
|
426
|
+
window.addEventListener("blur", () => setExpanded(null, false).catch(() => {}));
|
|
427
|
+
document.addEventListener("pointerleave", () =>
|
|
428
|
+
setExpanded(null, false).catch(() => {}),
|
|
429
|
+
);
|
|
430
|
+
document.addEventListener("visibilitychange", () => {
|
|
431
|
+
if (document.hidden) setExpanded(null, false).catch(() => {});
|
|
432
|
+
});
|
|
433
|
+
cards.addEventListener("click", (event) => {
|
|
434
|
+
const card = event.target.closest?.(".card");
|
|
435
|
+
if (card) activate(card).catch(() => {});
|
|
436
|
+
});
|
|
437
|
+
cards.addEventListener("keydown", (event) => {
|
|
438
|
+
const card = event.target.closest?.(".card");
|
|
439
|
+
if (!card || (event.key !== "ArrowLeft" && event.key !== "ArrowRight")) return;
|
|
440
|
+
event.preventDefault();
|
|
441
|
+
const direction = event.key === "ArrowLeft" ? -1 : 1;
|
|
442
|
+
if (event.shiftKey) {
|
|
443
|
+
orderedIds = moveOrderItem(orderedIds, card.dataset.sessionId, direction);
|
|
444
|
+
nextOrderRefreshAt = Date.now() + ORDER_REFRESH_MS;
|
|
445
|
+
render();
|
|
446
|
+
const moved = [...cards.querySelectorAll(".card")].find((candidate) => candidate.dataset.sessionId === card.dataset.sessionId);
|
|
447
|
+
moved?.focus();
|
|
448
|
+
moved?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
|
|
449
|
+
announcer.textContent = "Session card moved " + (direction < 0 ? "left" : "right") + ".";
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const index = orderedIds.indexOf(card.dataset.sessionId);
|
|
453
|
+
const targetId = orderedIds[Math.max(0, Math.min(orderedIds.length - 1, index + direction))];
|
|
454
|
+
const target = [...cards.querySelectorAll(".card")].find((candidate) => candidate.dataset.sessionId === targetId);
|
|
455
|
+
target?.focus();
|
|
456
|
+
target?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
setInterval(() => {
|
|
460
|
+
if (Date.now() >= nextOrderRefreshAt) syncOrder(true);
|
|
461
|
+
render();
|
|
462
|
+
}, 1000);
|
|
463
|
+
api.onCollapse?.(() => setExpanded(null, false).catch(() => {}));
|
|
464
|
+
api.onVisibility?.(async (visible, isCurrent = () => true) => {
|
|
465
|
+
if (!visible) {
|
|
466
|
+
document.documentElement.dataset.stripVisible = "false";
|
|
467
|
+
if (document.hidden) return isCurrent();
|
|
468
|
+
await new Promise((resolve) =>
|
|
469
|
+
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
|
470
|
+
);
|
|
471
|
+
return isCurrent();
|
|
472
|
+
}
|
|
473
|
+
if (document.hidden) {
|
|
474
|
+
if (!isCurrent()) return false;
|
|
475
|
+
document.documentElement.dataset.stripVisible = "true";
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
await new Promise((resolve) =>
|
|
479
|
+
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
|
480
|
+
);
|
|
481
|
+
if (!isCurrent()) return false;
|
|
482
|
+
document.documentElement.dataset.stripVisible = "true";
|
|
483
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
484
|
+
if (!isCurrent()) {
|
|
485
|
+
document.documentElement.dataset.stripVisible = "false";
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
return true;
|
|
489
|
+
});
|
|
367
490
|
api.subscribe((nextSnapshot) => {
|
|
368
|
-
snapshot = nextSnapshot || { generatedAt: Date.now(), sessions: [] };
|
|
491
|
+
snapshot = nextSnapshot || { generatedAt: Date.now(), focusedSessionId: null, sessions: [] };
|
|
492
|
+
syncOrder(orderedIds.length === 0);
|
|
369
493
|
render();
|
|
370
494
|
});
|
|
495
|
+
syncOrder(true);
|
|
371
496
|
render();
|
|
372
497
|
</script>
|
|
373
498
|
</body>
|