@tryinget/pi-activity-strip 0.4.0 → 0.6.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 +136 -157
- package/bin/pi-activity-strip-claude-hook.mjs +71 -0
- package/bin/pi-activity-strip.mjs +50 -60
- package/native/bin/linux-x64-gnu/artifact.json +21 -0
- package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
- package/native/panel/Cargo.lock +1045 -0
- package/native/panel/Cargo.toml +23 -0
- package/native/panel/src/app.rs +606 -0
- package/native/panel/src/card_view.rs +602 -0
- package/native/panel/src/main.rs +31 -0
- package/native/panel/src/protocol.rs +297 -0
- package/native/panel/src/runtime.rs +151 -0
- package/native/panel/src/style.css +250 -0
- package/package.json +16 -4
- package/src/broker/server.mjs +43 -7
- package/src/broker/session-store.mjs +41 -13
- package/src/client/broker-client.mjs +10 -3
- package/src/client/session-telemetry.mjs +54 -14
- package/src/common/activity-order.mjs +15 -9
- package/src/common/agent-identity.mjs +163 -0
- package/src/common/ak-tasks.mjs +354 -0
- package/src/common/alignment-controller.mjs +24 -42
- package/src/common/card-display.mjs +5 -0
- package/src/common/claude-events.mjs +190 -0
- package/src/common/claude-hook-config.mjs +56 -0
- package/src/common/claude-transcript.mjs +163 -0
- package/src/common/codex-transcript.mjs +153 -0
- package/src/common/compatibility.mjs +28 -13
- package/src/common/constants.mjs +10 -28
- package/src/common/contracts.d.ts +68 -4
- package/src/common/contracts.ts +105 -4
- package/src/common/ghostty-present.mjs +100 -0
- package/src/common/ghostty-theme.mjs +149 -0
- package/src/common/niri-focus.mjs +209 -140
- package/src/common/protocol.mjs +48 -16
- package/src/common/session-cards.mjs +105 -0
- package/src/common/status-report.mjs +53 -1
- package/src/common/surface-bindings.mjs +234 -0
- package/src/common/telemetry.mjs +8 -1
- package/src/common/terminal-identity.mjs +212 -0
- package/src/common/window-placement.mjs +59 -0
- package/src/common/workspace-view.mjs +161 -0
- package/src/native/agent-discovery.mjs +294 -0
- package/src/native/ak-runtime.mjs +164 -0
- package/src/native/codex-discovery.mjs +128 -0
- package/src/native/ghostty-tab-inventory.py +99 -0
- package/src/native/height-repair.mjs +154 -0
- package/src/native/main.mjs +499 -0
- package/src/native/panel-projection.mjs +161 -0
- package/src/native/placement.mjs +225 -0
- package/src/native/tab-inventory.mjs +188 -0
- package/src/native/theme-runtime.mjs +72 -0
- package/src/native/theme-source.mjs +197 -0
- package/src/{electron/niri-workspace-events.mjs → native/workspace-events.mjs} +20 -2
- package/src/common/electron.mjs +0 -57
- package/src/electron/main.mjs +0 -499
- package/src/electron/niri-native-window-runtime.mjs +0 -178
- package/src/electron/preload.cjs +0 -50
- package/src/electron/renderer-visibility-runtime.mjs +0 -107
- package/src/electron/workspace-view-runtime.mjs +0 -360
- package/src/ui/strip-html.mjs +0 -500
package/src/ui/strip-html.mjs
DELETED
|
@@ -1,500 +0,0 @@
|
|
|
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 } = {}) {
|
|
41
|
-
const pointerEvents = interactive ? "auto" : "none";
|
|
42
|
-
|
|
43
|
-
return `<!doctype html>
|
|
44
|
-
<html data-strip-visible="${initiallyVisible ? "true" : "false"}">
|
|
45
|
-
<head>
|
|
46
|
-
<meta charset="utf-8" />
|
|
47
|
-
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
48
|
-
<style>
|
|
49
|
-
:root {
|
|
50
|
-
color-scheme: dark;
|
|
51
|
-
--panel: rgba(9, 14, 24, 0.82);
|
|
52
|
-
--panel-strong: rgba(14, 21, 35, 0.96);
|
|
53
|
-
--line: rgba(255, 255, 255, 0.08);
|
|
54
|
-
--line-strong: rgba(255, 255, 255, 0.14);
|
|
55
|
-
--text: rgba(247, 250, 255, 0.96);
|
|
56
|
-
--muted: rgba(194, 205, 224, 0.74);
|
|
57
|
-
--dim: rgba(167, 180, 203, 0.48);
|
|
58
|
-
--accent: #79b8ff;
|
|
59
|
-
--current-line: color-mix(in srgb, var(--accent) 78%, white 12%);
|
|
60
|
-
--thinking: #8ab4ff;
|
|
61
|
-
--tool: #ffd166;
|
|
62
|
-
--waiting: #ff9f7a;
|
|
63
|
-
--success: #57d9a3;
|
|
64
|
-
--error: #ff7d7d;
|
|
65
|
-
--shadow: none;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
* { box-sizing: border-box; }
|
|
69
|
-
html[data-strip-visible="false"] { visibility: hidden; }
|
|
70
|
-
html, body {
|
|
71
|
-
margin: 0;
|
|
72
|
-
width: 100%;
|
|
73
|
-
height: 100%;
|
|
74
|
-
overflow: hidden;
|
|
75
|
-
background: transparent;
|
|
76
|
-
font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif;
|
|
77
|
-
pointer-events: ${pointerEvents};
|
|
78
|
-
}
|
|
79
|
-
body { padding: 4px 10px 6px; }
|
|
80
|
-
button { font: inherit; }
|
|
81
|
-
|
|
82
|
-
.strip {
|
|
83
|
-
display: grid;
|
|
84
|
-
grid-template-columns: 146px minmax(0, 1fr);
|
|
85
|
-
gap: 8px;
|
|
86
|
-
width: 100%;
|
|
87
|
-
height: 100%;
|
|
88
|
-
}
|
|
89
|
-
.brand, .cards {
|
|
90
|
-
backdrop-filter: blur(18px) saturate(1.25);
|
|
91
|
-
-webkit-backdrop-filter: blur(18px) saturate(1.25);
|
|
92
|
-
border: 1px solid var(--line);
|
|
93
|
-
box-shadow: var(--shadow);
|
|
94
|
-
}
|
|
95
|
-
.brand {
|
|
96
|
-
display: flex;
|
|
97
|
-
flex-direction: column;
|
|
98
|
-
justify-content: space-between;
|
|
99
|
-
padding: 10px 12px;
|
|
100
|
-
border-radius: 20px;
|
|
101
|
-
background: linear-gradient(180deg, rgba(12, 19, 33, 0.92), rgba(8, 13, 22, 0.82));
|
|
102
|
-
}
|
|
103
|
-
.brand__eyebrow, .meta, .tool, .card__state, .inspector__key, .placeholder__eyebrow {
|
|
104
|
-
font-family: "IBM Plex Mono", "JetBrains Mono", monospace;
|
|
105
|
-
letter-spacing: 0.12em;
|
|
106
|
-
text-transform: uppercase;
|
|
107
|
-
}
|
|
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; }
|
|
112
|
-
|
|
113
|
-
.cards {
|
|
114
|
-
display: flex;
|
|
115
|
-
gap: 8px;
|
|
116
|
-
align-items: flex-start;
|
|
117
|
-
min-width: 0;
|
|
118
|
-
padding: 6px 8px 8px;
|
|
119
|
-
border-radius: 20px;
|
|
120
|
-
overflow-x: auto;
|
|
121
|
-
overflow-y: hidden;
|
|
122
|
-
scrollbar-width: none;
|
|
123
|
-
background: linear-gradient(180deg, rgba(10, 16, 28, 0.9), rgba(6, 10, 18, 0.74));
|
|
124
|
-
}
|
|
125
|
-
.cards::-webkit-scrollbar { display: none; }
|
|
126
|
-
.cards--empty { align-items: center; justify-content: center; }
|
|
127
|
-
|
|
128
|
-
.card, .placeholder {
|
|
129
|
-
position: relative;
|
|
130
|
-
overflow: hidden;
|
|
131
|
-
border-radius: 16px;
|
|
132
|
-
border: 1px solid var(--line-strong);
|
|
133
|
-
background: linear-gradient(180deg, rgba(13, 20, 35, 0.96), rgba(9, 14, 24, 0.9));
|
|
134
|
-
}
|
|
135
|
-
.card {
|
|
136
|
-
appearance: none;
|
|
137
|
-
display: grid;
|
|
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;
|
|
145
|
-
}
|
|
146
|
-
.card::before {
|
|
147
|
-
content: "";
|
|
148
|
-
position: absolute;
|
|
149
|
-
inset: 0 auto 0 0;
|
|
150
|
-
width: 3px;
|
|
151
|
-
background: var(--state-color, var(--accent));
|
|
152
|
-
}
|
|
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; }
|
|
202
|
-
}
|
|
203
|
-
</style>
|
|
204
|
-
</head>
|
|
205
|
-
<body data-expanded="false">
|
|
206
|
-
<div class="strip">
|
|
207
|
-
<section class="brand" aria-label="Pi activity summary">
|
|
208
|
-
<div>
|
|
209
|
-
<div class="brand__eyebrow">π telemetry ribbon</div>
|
|
210
|
-
<div class="brand__title">Activity</div>
|
|
211
|
-
<div class="brand__subtitle">Calm order · live detail</div>
|
|
212
|
-
</div>
|
|
213
|
-
<div class="meta" id="meta">Waiting for sessions…</div>
|
|
214
|
-
</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>
|
|
217
|
-
</div>
|
|
218
|
-
|
|
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};
|
|
224
|
-
const meta = document.getElementById("meta");
|
|
225
|
-
const cards = document.getElementById("cards");
|
|
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 () => {}; },
|
|
233
|
-
};
|
|
234
|
-
let snapshot = { generatedAt: Date.now(), focusedSessionId: null, sessions: [] };
|
|
235
|
-
let orderedIds = [];
|
|
236
|
-
let nextOrderRefreshAt = 0;
|
|
237
|
-
let collapseTimer = null;
|
|
238
|
-
|
|
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)" };
|
|
241
|
-
|
|
242
|
-
function formatDuration(totalSeconds) {
|
|
243
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
244
|
-
const seconds = totalSeconds % 60;
|
|
245
|
-
return String(minutes) + ":" + String(seconds).padStart(2, "0");
|
|
246
|
-
}
|
|
247
|
-
function formatElapsed(session) {
|
|
248
|
-
const anchor = Number(session.agentStartedAt || session.startedAt || snapshot.generatedAt || Date.now());
|
|
249
|
-
return formatDuration(Math.max(0, Math.floor((Date.now() - anchor) / 1000)));
|
|
250
|
-
}
|
|
251
|
-
function formatLastSeen(session) {
|
|
252
|
-
const anchor = Number(session.lastEventAt || session.updatedAt || snapshot.generatedAt || Date.now());
|
|
253
|
-
return formatDuration(Math.max(0, Math.floor((Date.now() - anchor) / 1000)));
|
|
254
|
-
}
|
|
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>',
|
|
283
|
-
'</div>',
|
|
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;
|
|
318
|
-
}
|
|
319
|
-
function render() {
|
|
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;
|
|
323
|
-
meta.textContent = sessions.length
|
|
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…";
|
|
326
|
-
if (sessions.length === 0) {
|
|
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>';
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
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 || "";
|
|
408
|
-
}
|
|
409
|
-
|
|
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
|
-
});
|
|
490
|
-
api.subscribe((nextSnapshot) => {
|
|
491
|
-
snapshot = nextSnapshot || { generatedAt: Date.now(), focusedSessionId: null, sessions: [] };
|
|
492
|
-
syncOrder(orderedIds.length === 0);
|
|
493
|
-
render();
|
|
494
|
-
});
|
|
495
|
-
syncOrder(true);
|
|
496
|
-
render();
|
|
497
|
-
</script>
|
|
498
|
-
</body>
|
|
499
|
-
</html>`;
|
|
500
|
-
}
|