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