dextunnel 0.1.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/LICENSE +211 -0
- package/README.md +112 -0
- package/SECURITY.md +27 -0
- package/SUPPORT.md +43 -0
- package/package.json +44 -0
- package/public/client-shared.js +1831 -0
- package/public/favicon.svg +11 -0
- package/public/host.html +29 -0
- package/public/host.js +2079 -0
- package/public/index.html +28 -0
- package/public/index.js +98 -0
- package/public/live-bridge-lifecycle.js +258 -0
- package/public/live-bridge-retry-state.js +61 -0
- package/public/live-selection-intent.js +79 -0
- package/public/remote-operator-state.js +316 -0
- package/public/remote.html +167 -0
- package/public/remote.js +3967 -0
- package/public/styles.css +2793 -0
- package/public/surface-view-state.js +89 -0
- package/public/voice-dictation.js +45 -0
- package/src/bin/desktop-rehydration-smoke.mjs +111 -0
- package/src/bin/dextunnel.mjs +41 -0
- package/src/bin/doctor.mjs +48 -0
- package/src/bin/launch-attest.mjs +39 -0
- package/src/bin/launch-status.mjs +49 -0
- package/src/bin/mobile-link-proxy.mjs +221 -0
- package/src/bin/mobile-proof.mjs +164 -0
- package/src/bin/mobile-transport-smoke.mjs +200 -0
- package/src/bin/probe-codex-app-server-write.mjs +36 -0
- package/src/bin/probe-codex-app-server.mjs +30 -0
- package/src/lib/agent-room-context.mjs +54 -0
- package/src/lib/agent-room-runtime.mjs +355 -0
- package/src/lib/agent-room-service.mjs +335 -0
- package/src/lib/agent-room-state.mjs +406 -0
- package/src/lib/agent-room-store.mjs +71 -0
- package/src/lib/agent-room-text.mjs +48 -0
- package/src/lib/app-server-contract.mjs +66 -0
- package/src/lib/app-server-runtime.mjs +60 -0
- package/src/lib/attachment-service.mjs +119 -0
- package/src/lib/bridge-api-handler.mjs +719 -0
- package/src/lib/bridge-runtime-lifecycle.mjs +51 -0
- package/src/lib/bridge-status-builder.mjs +60 -0
- package/src/lib/codex-app-server-client.mjs +1511 -0
- package/src/lib/companion-state.mjs +453 -0
- package/src/lib/control-lease-service.mjs +180 -0
- package/src/lib/debug-harness-service.mjs +173 -0
- package/src/lib/desktop-integration.mjs +146 -0
- package/src/lib/desktop-rehydration-smoke.mjs +269 -0
- package/src/lib/dextunnel-cli.mjs +122 -0
- package/src/lib/discovery-docs.mjs +1321 -0
- package/src/lib/fake-codex-app-server-bridge.mjs +340 -0
- package/src/lib/install-preflight.mjs +373 -0
- package/src/lib/interaction-resolution-service.mjs +185 -0
- package/src/lib/interaction-state.mjs +360 -0
- package/src/lib/launch-release-bar.mjs +158 -0
- package/src/lib/live-control-state.mjs +107 -0
- package/src/lib/live-payload-builder.mjs +298 -0
- package/src/lib/live-selection-transition-state.mjs +49 -0
- package/src/lib/live-transcript-state.mjs +549 -0
- package/src/lib/mobile-network-profile.mjs +39 -0
- package/src/lib/mock-codex-adapter.mjs +62 -0
- package/src/lib/operator-diagnostics.mjs +82 -0
- package/src/lib/repo-changes-service.mjs +527 -0
- package/src/lib/runtime-config.mjs +106 -0
- package/src/lib/selection-state-service.mjs +214 -0
- package/src/lib/session-store.mjs +355 -0
- package/src/lib/shared-room-state.mjs +473 -0
- package/src/lib/shared-selection-state.mjs +40 -0
- package/src/lib/sse-hub.mjs +35 -0
- package/src/lib/static-surface-service.mjs +71 -0
- package/src/lib/surface-access.mjs +189 -0
- package/src/lib/surface-presence-service.mjs +118 -0
- package/src/lib/surface-request-guard.mjs +52 -0
- package/src/lib/thread-sync-state.mjs +536 -0
- package/src/lib/watcher-lifecycle.mjs +287 -0
- package/src/server.mjs +1446 -0
|
@@ -0,0 +1,1831 @@
|
|
|
1
|
+
export function escapeHtml(value) {
|
|
2
|
+
return String(value)
|
|
3
|
+
.replaceAll("&", "&")
|
|
4
|
+
.replaceAll("<", "<")
|
|
5
|
+
.replaceAll(">", ">")
|
|
6
|
+
.replaceAll('"', """)
|
|
7
|
+
.replaceAll("'", "'");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function stableSurfaceClientId(surface = "surface") {
|
|
11
|
+
const normalizedSurface = String(surface || "surface").trim().toLowerCase() || "surface";
|
|
12
|
+
const storageKey = `dextunnel:surface-client:${normalizedSurface}`;
|
|
13
|
+
const fallback = `${normalizedSurface}-${Math.random().toString(16).slice(2)}-${Date.now().toString(36)}`;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const existing = window.sessionStorage.getItem(storageKey);
|
|
17
|
+
if (existing) {
|
|
18
|
+
return existing;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const nextId = window.crypto?.randomUUID?.() || fallback;
|
|
22
|
+
window.sessionStorage.setItem(storageKey, nextId);
|
|
23
|
+
return nextId;
|
|
24
|
+
} catch {
|
|
25
|
+
return window.crypto?.randomUUID?.() || fallback;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function bootstrapStorageKey(surface = "remote") {
|
|
30
|
+
return `dextunnel:surface-bootstrap:${String(surface || "remote").trim().toLowerCase() || "remote"}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function bootstrapExpired(bootstrap) {
|
|
34
|
+
const expiresAt = String(bootstrap?.expiresAt || "").trim();
|
|
35
|
+
if (!expiresAt) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const expiresAtMs = new Date(expiresAt).getTime();
|
|
40
|
+
return !Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function usableBootstrap(bootstrap, expectedSurface) {
|
|
44
|
+
const normalizedSurface =
|
|
45
|
+
String(bootstrap?.surface || "")
|
|
46
|
+
.trim()
|
|
47
|
+
.toLowerCase() || null;
|
|
48
|
+
if (!bootstrap?.accessToken || !bootstrap?.clientId) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (normalizedSurface !== expectedSurface) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return !bootstrapExpired(bootstrap);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getSurfaceBootstrap(expectedSurface = "remote") {
|
|
58
|
+
const expected = String(expectedSurface || "remote").trim().toLowerCase() || "remote";
|
|
59
|
+
const injectedBootstrap = window.__DEXTUNNEL_SURFACE_BOOTSTRAP__ || null;
|
|
60
|
+
let storedBootstrap = null;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const raw = window.sessionStorage.getItem(bootstrapStorageKey(expected));
|
|
64
|
+
storedBootstrap = raw ? JSON.parse(raw) : null;
|
|
65
|
+
} catch {
|
|
66
|
+
storedBootstrap = null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const injectedUsable = usableBootstrap(injectedBootstrap, expected);
|
|
70
|
+
const storedUsable = usableBootstrap(storedBootstrap, expected);
|
|
71
|
+
const bootstrap = injectedUsable ? injectedBootstrap : storedUsable ? storedBootstrap : injectedBootstrap;
|
|
72
|
+
|
|
73
|
+
if (!bootstrap?.accessToken || !bootstrap?.surface || !bootstrap?.clientId) {
|
|
74
|
+
throw new Error("Dextunnel surface bootstrap missing. Reload the page.");
|
|
75
|
+
}
|
|
76
|
+
if (String(bootstrap.surface).trim().toLowerCase() !== expected) {
|
|
77
|
+
throw new Error(`Expected ${expected} surface bootstrap, got ${bootstrap.surface}.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (injectedUsable || storedUsable) {
|
|
81
|
+
try {
|
|
82
|
+
window.sessionStorage.setItem(bootstrapStorageKey(expected), JSON.stringify(bootstrap));
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return bootstrap;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function withSurfaceHeaders(options = {}, accessToken = "") {
|
|
90
|
+
return {
|
|
91
|
+
...options,
|
|
92
|
+
headers: {
|
|
93
|
+
...(options.headers || {}),
|
|
94
|
+
"x-dextunnel-surface-token": accessToken
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function withSurfaceTokenUrl(url, accessToken = "") {
|
|
100
|
+
const nextUrl = new URL(url, window.location.origin);
|
|
101
|
+
nextUrl.searchParams.set("surfaceToken", accessToken);
|
|
102
|
+
return `${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function formatTimestamp(value) {
|
|
106
|
+
if (value == null || value === "") {
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const normalized = typeof value === "number" && value < 1e12 ? value * 1000 : value;
|
|
111
|
+
const date = new Date(normalized);
|
|
112
|
+
if (Number.isNaN(date.getTime())) {
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function formatSessionTimestamp(value) {
|
|
120
|
+
if (value == null || value === "") {
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const normalized = typeof value === "number" && value < 1e12 ? value * 1000 : value;
|
|
125
|
+
const date = new Date(normalized);
|
|
126
|
+
if (Number.isNaN(date.getTime())) {
|
|
127
|
+
return "";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return date.toLocaleString([], {
|
|
131
|
+
month: "short",
|
|
132
|
+
day: "numeric",
|
|
133
|
+
hour: "numeric",
|
|
134
|
+
minute: "2-digit"
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function formatRecoveryDuration(valueMs) {
|
|
139
|
+
const durationMs = Number(valueMs);
|
|
140
|
+
if (!Number.isFinite(durationMs) || durationMs <= 0) {
|
|
141
|
+
return "0.0s";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const seconds = durationMs / 1000;
|
|
145
|
+
if (seconds < 10) {
|
|
146
|
+
return `${seconds.toFixed(1)}s`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return `${Math.round(seconds)}s`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function humanize(value) {
|
|
153
|
+
return String(value || "")
|
|
154
|
+
.replaceAll("_", " ")
|
|
155
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
156
|
+
.replace(/\s+/g, " ")
|
|
157
|
+
.trim()
|
|
158
|
+
.toLowerCase();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function createRequestError(payload = {}, response = {}) {
|
|
162
|
+
const rawMessage = payload?.error || `${response?.url || "request"} failed with ${response?.status || 0}`;
|
|
163
|
+
const message = /surface access is missing or expired/i.test(rawMessage)
|
|
164
|
+
? "Session access expired. Reload Dextunnel."
|
|
165
|
+
: rawMessage;
|
|
166
|
+
const error = new Error(message);
|
|
167
|
+
error.name = "RequestError";
|
|
168
|
+
error.payload = payload || null;
|
|
169
|
+
error.state = payload?.state || null;
|
|
170
|
+
error.status = Number(response?.status || 0);
|
|
171
|
+
return error;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function currentSurfaceTranscript({ liveState = null, bootstrapSnapshot = null } = {}) {
|
|
175
|
+
const selectedThreadId = String(liveState?.selectedThreadId || "").trim();
|
|
176
|
+
const selectedSnapshotThreadId = String(liveState?.selectedThreadSnapshot?.thread?.id || "").trim();
|
|
177
|
+
const selectedTranscript = Array.isArray(liveState?.selectedThreadSnapshot?.transcript)
|
|
178
|
+
? liveState.selectedThreadSnapshot.transcript
|
|
179
|
+
: [];
|
|
180
|
+
|
|
181
|
+
if (selectedThreadId) {
|
|
182
|
+
return selectedSnapshotThreadId === selectedThreadId ? selectedTranscript : [];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (selectedSnapshotThreadId) {
|
|
186
|
+
return selectedTranscript;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return Array.isArray(bootstrapSnapshot?.transcript) ? bootstrapSnapshot.transcript : [];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function formatBusyMarqueeText(value = "", fallback = "Loading") {
|
|
193
|
+
const normalized = String(value || "")
|
|
194
|
+
.replace(/[.…]+\s*$/u, "")
|
|
195
|
+
.trim();
|
|
196
|
+
return normalized || fallback;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function describeThreadState({ pendingInteraction = null, status = null, thread = null } = {}) {
|
|
200
|
+
if (pendingInteraction) {
|
|
201
|
+
return "action required";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (status?.writeLock?.status) {
|
|
205
|
+
return `write ${humanize(status.writeLock.status)}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (thread?.activeTurnId) {
|
|
209
|
+
return "turn running";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (status?.lastWriteForSelection?.error) {
|
|
213
|
+
return "last write failed";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return "ready";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function describeDesktopSyncNote({
|
|
220
|
+
hasSelectedThread = false,
|
|
221
|
+
status = null
|
|
222
|
+
} = {}) {
|
|
223
|
+
if (!hasSelectedThread) {
|
|
224
|
+
return "Select a thread to reveal in Codex.";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const lastWrite = status?.lastWriteForSelection || status?.lastWrite || null;
|
|
228
|
+
if (lastWrite?.source === "remote" || lastWrite?.source === "external") {
|
|
229
|
+
return "Saved here. Reveal in Codex opens the thread there. Quit and reopen the Codex app manually to see new messages.";
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return "Reveal in Codex opens this thread in the app. Quit and reopen the Codex app manually to see newer messages from Dextunnel.";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function describeRemoteScopeNote({
|
|
236
|
+
hasSelectedThread = false,
|
|
237
|
+
channelLabel = ""
|
|
238
|
+
} = {}) {
|
|
239
|
+
if (!hasSelectedThread) {
|
|
240
|
+
return "Pick a thread first.";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const target = String(channelLabel || "").trim() || "the selected thread";
|
|
244
|
+
return `Shared thread. Sends to ${target}.`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function describeRemoteDesktopSyncNote({
|
|
248
|
+
hasSelectedThread = false,
|
|
249
|
+
status = null
|
|
250
|
+
} = {}) {
|
|
251
|
+
if (!hasSelectedThread) {
|
|
252
|
+
return "Desktop Codex can lag behind remote writes. Reveal in Codex navigates only.";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const lastWrite = status?.lastWriteForSelection || status?.lastWrite || null;
|
|
256
|
+
if (lastWrite?.source === "remote" || lastWrite?.source === "external") {
|
|
257
|
+
return "Sent here. Dextunnel is current. Desktop Codex may still need a quit and reopen to show this turn.";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return "Desktop Codex can lag behind remote writes. Reveal in Codex navigates only; quit and reopen Codex if you need desktop to catch up.";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function diagnosticSortWeight(item) {
|
|
264
|
+
if (item?.severity === "warn") {
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return 1;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function diagnosticLeaseOwnerLabel(lease = null) {
|
|
272
|
+
const raw = String(lease?.ownerLabel || lease?.owner || lease?.source || "").trim();
|
|
273
|
+
if (!raw) {
|
|
274
|
+
return "another surface";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return humanize(raw);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function describeOperatorDiagnostics({
|
|
281
|
+
diagnostics = [],
|
|
282
|
+
ownsControl = false,
|
|
283
|
+
status = null,
|
|
284
|
+
surface = "remote"
|
|
285
|
+
} = {}) {
|
|
286
|
+
const items = [];
|
|
287
|
+
const lease = status?.controlLeaseForSelection || null;
|
|
288
|
+
const normalizedSurface = String(surface || "remote").trim().toLowerCase() || "remote";
|
|
289
|
+
|
|
290
|
+
for (const diagnostic of Array.isArray(diagnostics) ? diagnostics : []) {
|
|
291
|
+
const code = String(diagnostic?.code || "").trim();
|
|
292
|
+
if (!code) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (code === "desktop_restart_required") {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (code === "host_unavailable" && normalizedSurface === "host") {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (code === "control_held") {
|
|
305
|
+
if (!lease || ownsControl) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
items.push({
|
|
310
|
+
code,
|
|
311
|
+
severity: diagnostic?.severity || "info",
|
|
312
|
+
title: diagnostic?.summary || "Control is currently held elsewhere.",
|
|
313
|
+
label: `control held by ${diagnosticLeaseOwnerLabel(lease)}`
|
|
314
|
+
});
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (code === "bridge_last_error") {
|
|
319
|
+
const bridgeOffline = (diagnostics || []).some((entry) => entry?.code === "bridge_unavailable");
|
|
320
|
+
if (bridgeOffline) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let label = "";
|
|
326
|
+
switch (code) {
|
|
327
|
+
case "bridge_unavailable":
|
|
328
|
+
label = "bridge offline";
|
|
329
|
+
break;
|
|
330
|
+
case "no_selected_room":
|
|
331
|
+
label = "select a room";
|
|
332
|
+
break;
|
|
333
|
+
case "host_unavailable":
|
|
334
|
+
label = "host offline";
|
|
335
|
+
break;
|
|
336
|
+
case "bridge_last_error":
|
|
337
|
+
label = "bridge error";
|
|
338
|
+
break;
|
|
339
|
+
default:
|
|
340
|
+
label = humanize(diagnostic?.summary || code);
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
items.push({
|
|
345
|
+
code,
|
|
346
|
+
severity: diagnostic?.severity || "info",
|
|
347
|
+
title: diagnostic?.summary || label,
|
|
348
|
+
detail: diagnostic?.detail || "",
|
|
349
|
+
label
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return items.sort((left, right) => diagnosticSortWeight(left) - diagnosticSortWeight(right));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function formatSurfaceAttachmentSummary(attachments = []) {
|
|
357
|
+
if (!Array.isArray(attachments) || attachments.length === 0) {
|
|
358
|
+
return "";
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return attachments
|
|
362
|
+
.map((attachment) => {
|
|
363
|
+
const label = String(attachment?.label || attachment?.surface || "surface").trim();
|
|
364
|
+
const state = String(attachment?.state || "open").trim();
|
|
365
|
+
const count = Number.isFinite(attachment?.count) && attachment.count > 1 ? ` x${attachment.count}` : "";
|
|
366
|
+
return `${label} ${state}${count}`;
|
|
367
|
+
})
|
|
368
|
+
.join(" // ");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function mergeSurfaceAttachments(attachments = [], localAttachment = null) {
|
|
372
|
+
const items = Array.isArray(attachments) ? attachments.filter(Boolean) : [];
|
|
373
|
+
if (!localAttachment?.surface) {
|
|
374
|
+
return items;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (items.some((attachment) => attachment?.surface === localAttachment.surface)) {
|
|
378
|
+
return items;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return [localAttachment, ...items];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function projectLabel(cwd) {
|
|
385
|
+
const parts = String(cwd || "")
|
|
386
|
+
.split("/")
|
|
387
|
+
.filter(Boolean);
|
|
388
|
+
|
|
389
|
+
if (parts.length >= 2) {
|
|
390
|
+
return parts.slice(-2).join("/");
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return cwd || "unknown";
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function projectLeaf(cwd) {
|
|
397
|
+
const parts = String(cwd || "")
|
|
398
|
+
.split("/")
|
|
399
|
+
.filter(Boolean);
|
|
400
|
+
|
|
401
|
+
return parts.at(-1) || "";
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function looksLikeTopicNoise(value) {
|
|
405
|
+
const text = String(value || "").trim();
|
|
406
|
+
if (!text) {
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (/^\[[^\]]+\]\([^)]+\)$/.test(text)) {
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return text.startsWith("[$");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function isGenericThreadName(name, cwd) {
|
|
418
|
+
const normalized = String(name || "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
419
|
+
if (!normalized) {
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (["session", "untitled", "untitled session", "new session"].includes(normalized)) {
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function threadDisplayTitle(thread) {
|
|
431
|
+
const channelLabel = String(thread?.channelLabel || "").replace(/\s+/g, " ").trim();
|
|
432
|
+
if (channelLabel) {
|
|
433
|
+
return channelLabel;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const name = String(thread?.name || "").replace(/\s+/g, " ").trim();
|
|
437
|
+
if (name && !isGenericThreadName(name, thread?.cwd)) {
|
|
438
|
+
return name;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const preview = String(thread?.preview || "").replace(/\s+/g, " ").trim();
|
|
442
|
+
if (preview && !looksLikeTopicNoise(preview) && preview.length <= 64 && preview.split(/\s+/).length <= 9) {
|
|
443
|
+
return preview;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (name) {
|
|
447
|
+
return name;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return "Current session";
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function sessionLabel(thread) {
|
|
454
|
+
const rawTitle = threadDisplayTitle(thread);
|
|
455
|
+
const title = rawTitle.length > 46 ? `${rawTitle.slice(0, 43)}...` : rawTitle;
|
|
456
|
+
const stamp = formatSessionTimestamp(thread.updatedAt);
|
|
457
|
+
return stamp ? `${title} - ${stamp}` : title;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function shortThreadId(value) {
|
|
461
|
+
const id = String(value || "").trim();
|
|
462
|
+
if (!id) {
|
|
463
|
+
return "";
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return id.length > 13 ? id.slice(0, 13) : id;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function groupThreadsByProject(threads) {
|
|
470
|
+
const groups = new Map();
|
|
471
|
+
|
|
472
|
+
for (const thread of threads || []) {
|
|
473
|
+
const cwd = thread.cwd || "";
|
|
474
|
+
const existing = groups.get(cwd) || {
|
|
475
|
+
cwd,
|
|
476
|
+
label: projectLabel(cwd),
|
|
477
|
+
threads: []
|
|
478
|
+
};
|
|
479
|
+
existing.threads.push(thread);
|
|
480
|
+
groups.set(cwd, existing);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return [...groups.values()]
|
|
484
|
+
.map((group) => ({
|
|
485
|
+
...group,
|
|
486
|
+
threads: group.threads.slice().sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
|
|
487
|
+
}))
|
|
488
|
+
.sort((a, b) => (b.threads[0]?.updatedAt || 0) - (a.threads[0]?.updatedAt || 0));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function shouldHideTranscriptEntry(entry) {
|
|
492
|
+
const text = normalizeTranscriptSource(entry?.text);
|
|
493
|
+
return (
|
|
494
|
+
!text ||
|
|
495
|
+
/^[.]+$/.test(text) ||
|
|
496
|
+
text.startsWith("Heartbeat:") ||
|
|
497
|
+
text.startsWith("Capability ladder updated.") ||
|
|
498
|
+
text.startsWith("Warning: The maximum number of unified exec processes") ||
|
|
499
|
+
looksLikeInternalContextEnvelope(text)
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function isSystemNoticeEntry(entry) {
|
|
504
|
+
return (
|
|
505
|
+
entry?.kind === "control_notice" ||
|
|
506
|
+
entry?.kind === "surface_notice" ||
|
|
507
|
+
entry?.kind === "selection_notice"
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function setPanelHidden(node, hidden) {
|
|
512
|
+
if (!node) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const nextHidden = Boolean(hidden);
|
|
517
|
+
node.classList.toggle("panel-hidden", nextHidden);
|
|
518
|
+
node.hidden = nextHidden;
|
|
519
|
+
if (nextHidden) {
|
|
520
|
+
node.setAttribute("aria-hidden", "true");
|
|
521
|
+
} else {
|
|
522
|
+
node.removeAttribute("aria-hidden");
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function sanitizeTranscriptText(text) {
|
|
527
|
+
return String(text || "")
|
|
528
|
+
.replace(/\[image\]\s+data:image\/[^\s)]+/g, "[image attachment]")
|
|
529
|
+
.replace(/data:image\/[A-Za-z0-9+/=;:._-]+/g, "[image attachment]");
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function renderInlineMarkdown(value) {
|
|
533
|
+
let html = escapeHtml(value);
|
|
534
|
+
|
|
535
|
+
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label, url) => {
|
|
536
|
+
if (/^https?:\/\//.test(url)) {
|
|
537
|
+
return `<a href="${url}" target="_blank" rel="noreferrer">${label}</a>`;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return `<span class="inline-link">${label}</span>`;
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
544
|
+
|
|
545
|
+
return html;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export function renderTranscriptText(text) {
|
|
549
|
+
return sanitizeTranscriptText(text)
|
|
550
|
+
.trim()
|
|
551
|
+
.split(/\n{2,}/)
|
|
552
|
+
.map((paragraph) => `<p>${renderInlineMarkdown(paragraph).replaceAll("\n", "<br>")}</p>`)
|
|
553
|
+
.join("");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export function displayTranscriptText(entry, { expanded = false } = {}) {
|
|
557
|
+
const source = entry?.role === "tool" ? normalizeToolSource(entry?.text) : normalizeTranscriptSource(entry?.text);
|
|
558
|
+
if (!source) {
|
|
559
|
+
return "";
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (entry?.role !== "tool") {
|
|
563
|
+
return source;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (expanded) {
|
|
567
|
+
return formatExpandedToolText(source);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const compactToolText = compactToolEnvelopeText(source);
|
|
571
|
+
if (!compactToolText) {
|
|
572
|
+
return "";
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (compactToolText.length <= 140) {
|
|
576
|
+
return compactToolText;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return `${compactToolText.slice(0, 137).trimEnd()}...`;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function normalizeTranscriptSource(value) {
|
|
583
|
+
return String(value || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function firstMeaningfulLine(text) {
|
|
587
|
+
return (
|
|
588
|
+
normalizeTranscriptSource(text)
|
|
589
|
+
.split("\n")
|
|
590
|
+
.map((line) => line.trim())
|
|
591
|
+
.find(Boolean) || ""
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function looksLikeInternalContextEnvelope(text) {
|
|
596
|
+
const normalized = normalizeTranscriptSource(text);
|
|
597
|
+
if (!normalized) {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const firstLine = firstMeaningfulLine(normalized);
|
|
602
|
+
if (
|
|
603
|
+
/^<(permissions instructions|app-context|collaboration_mode|skills_instructions|environment_context)>$/i.test(firstLine) ||
|
|
604
|
+
/^<INSTRUCTIONS>$/i.test(firstLine) ||
|
|
605
|
+
/^# AGENTS\.md instructions for /i.test(firstLine) ||
|
|
606
|
+
/^# Internal workflow instructions for /i.test(firstLine) ||
|
|
607
|
+
/^# File: ~\/\.codex\/AGENTS\.md$/i.test(firstLine) ||
|
|
608
|
+
/^Filesystem sandboxing defines which files can be read or written\./i.test(firstLine)
|
|
609
|
+
) {
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function normalizeToolSource(value) {
|
|
617
|
+
if (typeof value === "string") {
|
|
618
|
+
return normalizeTranscriptSource(value);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const flattened = flattenToolEnvelopeValue(value)
|
|
622
|
+
.map((item) => normalizeTranscriptSource(item))
|
|
623
|
+
.filter(Boolean);
|
|
624
|
+
if (flattened.length) {
|
|
625
|
+
return flattened.join("\n").trim();
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
return normalizeTranscriptSource(JSON.stringify(value));
|
|
630
|
+
} catch {
|
|
631
|
+
return normalizeTranscriptSource(String(value || ""));
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function formatExpandedToolText(text) {
|
|
636
|
+
const source = String(text || "").trim();
|
|
637
|
+
if (!source) {
|
|
638
|
+
return "";
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const unwrapped = unwrapToolEnvelopeText(source);
|
|
642
|
+
if (looksLikePlaywrightToolText(unwrapped)) {
|
|
643
|
+
return formatExpandedPlaywrightToolText(unwrapped) || compactToolEnvelopeText(source) || unwrapped;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return unwrapped || source;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function compactToolEnvelopeText(text) {
|
|
650
|
+
const trimmed = String(text || "").trim();
|
|
651
|
+
const unwrapped = unwrapToolEnvelopeText(trimmed);
|
|
652
|
+
const lines = unwrapped
|
|
653
|
+
.replace(/\r\n/g, "\n")
|
|
654
|
+
.replace(/\r/g, "\n")
|
|
655
|
+
.split("\n")
|
|
656
|
+
.map((line) => normalizeToolLine(line))
|
|
657
|
+
.filter(Boolean);
|
|
658
|
+
|
|
659
|
+
const updatedFilesIndex = lines.findIndex((line) => /^success\. updated the following files:?$/i.test(line));
|
|
660
|
+
if (updatedFilesIndex >= 0) {
|
|
661
|
+
const fileLines = lines
|
|
662
|
+
.slice(updatedFilesIndex + 1)
|
|
663
|
+
.filter((line) => /^(A|M|D|R)\s+.+/.test(line))
|
|
664
|
+
.map((line) => line.replace(/^(A|M|D|R)\s+/, "").trim());
|
|
665
|
+
if (fileLines.length) {
|
|
666
|
+
const labels = fileLines.slice(0, 3).map((path) => path.split("/").filter(Boolean).at(-1) || path);
|
|
667
|
+
const suffix = fileLines.length > 3 ? ` (+${fileLines.length - 3} more)` : "";
|
|
668
|
+
return `Updated files: ${labels.join(", ")}${suffix}`;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const playwrightSummary = summarizePlaywrightToolLines(lines);
|
|
673
|
+
if (playwrightSummary) {
|
|
674
|
+
return playwrightSummary;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const outputIndex = lines.findIndex((line) => line.toLowerCase() === "output:");
|
|
678
|
+
if (outputIndex >= 0) {
|
|
679
|
+
const meaningfulOutput = lines
|
|
680
|
+
.slice(outputIndex + 1)
|
|
681
|
+
.find((line) => line && !isToolWrapperLine(line));
|
|
682
|
+
if (meaningfulOutput) {
|
|
683
|
+
return meaningfulOutput;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const firstMeaningfulLine = lines.find((line) => line && !isToolWrapperLine(line));
|
|
688
|
+
if (firstMeaningfulLine) {
|
|
689
|
+
return firstMeaningfulLine;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return unwrapped;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function unwrapToolEnvelopeText(text) {
|
|
696
|
+
const trimmed = String(text || "").trim();
|
|
697
|
+
if (!trimmed) {
|
|
698
|
+
return "";
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
702
|
+
return trimmed;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
try {
|
|
706
|
+
const parsed = JSON.parse(trimmed);
|
|
707
|
+
const flattened = flattenToolEnvelopeValue(parsed).filter((value) => value && value.trim());
|
|
708
|
+
if (flattened.length) {
|
|
709
|
+
const next = flattened.join("\n").trim();
|
|
710
|
+
return next && next !== trimmed ? unwrapToolEnvelopeText(next) : next;
|
|
711
|
+
}
|
|
712
|
+
} catch {}
|
|
713
|
+
|
|
714
|
+
const decoded = decodeEscapedToolText(trimmed);
|
|
715
|
+
if (decoded !== trimmed) {
|
|
716
|
+
return unwrapToolEnvelopeText(decoded);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const partialEnvelopeText = extractPartialToolEnvelopeText(trimmed);
|
|
720
|
+
if (partialEnvelopeText) {
|
|
721
|
+
return partialEnvelopeText;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return trimmed;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function decodeEscapedToolText(text) {
|
|
728
|
+
const value = String(text || "").trim();
|
|
729
|
+
if (!value || (!value.includes("\\n") && !value.includes('\\"') && !value.includes("\\t"))) {
|
|
730
|
+
return value;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
try {
|
|
734
|
+
return value
|
|
735
|
+
.replace(/\\\\/g, "\\")
|
|
736
|
+
.replace(/\\"/g, '"')
|
|
737
|
+
.replace(/\\n/g, "\n")
|
|
738
|
+
.replace(/\\r/g, "\r")
|
|
739
|
+
.replace(/\\t/g, "\t");
|
|
740
|
+
} catch {
|
|
741
|
+
return value;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function flattenToolEnvelopeValue(value) {
|
|
746
|
+
if (typeof value === "string") {
|
|
747
|
+
return [value];
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (Array.isArray(value)) {
|
|
751
|
+
return value.flatMap((item) => flattenToolEnvelopeValue(item));
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (value && typeof value === "object") {
|
|
755
|
+
for (const key of ["text", "output", "stdout", "stderr", "message", "command"]) {
|
|
756
|
+
const next = value[key];
|
|
757
|
+
if (next != null) {
|
|
758
|
+
const flattened = flattenToolEnvelopeValue(next);
|
|
759
|
+
if (flattened.length) {
|
|
760
|
+
return flattened;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return Object.values(value).flatMap((item) => flattenToolEnvelopeValue(item));
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
return [];
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function extractPartialToolEnvelopeText(text) {
|
|
772
|
+
const normalized = String(text || "").trim();
|
|
773
|
+
if (!normalized) {
|
|
774
|
+
return "";
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
for (const key of ["text", "output", "message", "stdout", "stderr"]) {
|
|
778
|
+
const directClosedMatch = normalized.match(new RegExp(`"${key}"\\s*:\\s*"((?:\\\\.|[^"])*)"`, "i"));
|
|
779
|
+
if (directClosedMatch?.[1]) {
|
|
780
|
+
return decodePartialToolString(directClosedMatch[1]);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const escapedClosedMatch = normalized.match(new RegExp(`\\\\"${key}\\\\"\\s*:\\s*\\\\"((?:\\\\\\\\.|[^"])*)\\\\"`, "i"));
|
|
784
|
+
if (escapedClosedMatch?.[1]) {
|
|
785
|
+
return decodePartialToolString(escapedClosedMatch[1]);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const directMatch = normalized.match(new RegExp(`"${key}"\\s*:\\s*"([\\s\\S]*)$`, "i"));
|
|
789
|
+
if (directMatch?.[1]) {
|
|
790
|
+
return decodePartialToolString(directMatch[1]);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const escapedMatch = normalized.match(new RegExp(`\\\\"${key}\\\\"\\s*:\\s*\\\\"([\\s\\S]*)$`, "i"));
|
|
794
|
+
if (escapedMatch?.[1]) {
|
|
795
|
+
return decodePartialToolString(escapedMatch[1]);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return "";
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function decodePartialToolString(value) {
|
|
803
|
+
const decoded = String(value || "")
|
|
804
|
+
.replace(/\\"/g, '"')
|
|
805
|
+
.replace(/\\n/g, "\n")
|
|
806
|
+
.replace(/\\r/g, "\r")
|
|
807
|
+
.replace(/\\t/g, "\t")
|
|
808
|
+
.trim();
|
|
809
|
+
|
|
810
|
+
if (decoded.includes("```") && !/```[\s\S]*```/.test(decoded)) {
|
|
811
|
+
return decoded.replace(/```[\s\S]*$/, "").trim();
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return decoded;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function normalizeToolLine(line) {
|
|
818
|
+
const normalized = String(line || "").trim();
|
|
819
|
+
if (!normalized) {
|
|
820
|
+
return "";
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (normalized === "```" || normalized.startsWith("```")) {
|
|
824
|
+
return "";
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (/^#{1,6}\s+/.test(normalized)) {
|
|
828
|
+
return normalized.replace(/^#{1,6}\s+/, "").trim();
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (/^-\s+/.test(normalized)) {
|
|
832
|
+
return normalized.replace(/^-\s+/, "").trim();
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return normalized;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function isToolWrapperLine(line) {
|
|
839
|
+
const normalized = String(line || "").trim().toLowerCase();
|
|
840
|
+
if (!normalized) {
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return (
|
|
845
|
+
normalized.startsWith("command:") ||
|
|
846
|
+
normalized.startsWith("chunk id:") ||
|
|
847
|
+
normalized.startsWith("wall time:") ||
|
|
848
|
+
normalized.startsWith("process exited") ||
|
|
849
|
+
normalized.startsWith("original token count:") ||
|
|
850
|
+
normalized === "output:" ||
|
|
851
|
+
normalized === "page" ||
|
|
852
|
+
normalized === "snapshot"
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function summarizePlaywrightToolLines(lines) {
|
|
857
|
+
if (!Array.isArray(lines) || lines.length === 0) {
|
|
858
|
+
return "";
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const ranCode = lines.find((line) => /ran playwright code/i.test(line));
|
|
862
|
+
if (ranCode) {
|
|
863
|
+
const pageUrl = lines.find((line) => /^page url:/i.test(line));
|
|
864
|
+
if (pageUrl) {
|
|
865
|
+
return `Ran Playwright code // ${pageUrl.replace(/^page url:\s*/i, "").trim()}`;
|
|
866
|
+
}
|
|
867
|
+
return "Ran Playwright code";
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const resultLine = lines.find((line) => /^result$/i.test(line));
|
|
871
|
+
if (resultLine) {
|
|
872
|
+
return "Playwright result";
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const openTabsLine = lines.find((line) => /^open tabs$/i.test(line));
|
|
876
|
+
if (openTabsLine) {
|
|
877
|
+
return "Open tabs";
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const pageUrl = lines.find((line) => /^page url:/i.test(line));
|
|
881
|
+
if (pageUrl) {
|
|
882
|
+
return `Page URL: ${pageUrl.replace(/^page url:\s*/i, "").trim()}`;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
return "";
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function looksLikePlaywrightToolText(text) {
|
|
889
|
+
const normalized = String(text || "");
|
|
890
|
+
return (
|
|
891
|
+
/ran playwright code/i.test(normalized) ||
|
|
892
|
+
/^page url:/im.test(normalized) ||
|
|
893
|
+
/^###\s+page/im.test(normalized) ||
|
|
894
|
+
/^###\s+open tabs/im.test(normalized) ||
|
|
895
|
+
/^###\s+result/im.test(normalized) ||
|
|
896
|
+
/^open tabs$/im.test(normalized) ||
|
|
897
|
+
/^result$/im.test(normalized)
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function formatExpandedPlaywrightToolText(text) {
|
|
902
|
+
const normalized = String(text || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
|
|
903
|
+
if (!normalized) {
|
|
904
|
+
return "";
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const sections = [];
|
|
908
|
+
const summary = compactToolEnvelopeText(normalized);
|
|
909
|
+
if (summary) {
|
|
910
|
+
sections.push(summary);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const codeBlock = extractFirstMarkdownCodeBlock(normalized);
|
|
914
|
+
if (codeBlock) {
|
|
915
|
+
sections.push(codeBlock);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const pageLines = [];
|
|
919
|
+
const pageUrl = extractPrefixedLine(normalized, "Page URL:");
|
|
920
|
+
if (pageUrl) {
|
|
921
|
+
pageLines.push(`Page URL: ${pageUrl}`);
|
|
922
|
+
}
|
|
923
|
+
const pageTitle = extractPrefixedLine(normalized, "Page Title:");
|
|
924
|
+
if (pageTitle) {
|
|
925
|
+
pageLines.push(`Page Title: ${pageTitle}`);
|
|
926
|
+
}
|
|
927
|
+
if (pageLines.length) {
|
|
928
|
+
sections.push(pageLines.join("\n"));
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
return sections.filter(Boolean).join("\n\n").trim();
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function extractFirstMarkdownCodeBlock(text) {
|
|
935
|
+
const match = decodeEscapedToolText(String(text || "")).match(/```[^\n]*\n([\s\S]*?)```/);
|
|
936
|
+
return match?.[1]?.trim() || "";
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function extractPrefixedLine(text, prefix) {
|
|
940
|
+
const pattern = new RegExp(`^\\s*[-*]?\\s*${escapeRegExp(prefix)}\\s*(.+)$`, "im");
|
|
941
|
+
const match = String(text || "").match(pattern);
|
|
942
|
+
return match?.[1]?.trim() || "";
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function escapeRegExp(value) {
|
|
946
|
+
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export function isTranscriptCardExpandable(entry) {
|
|
950
|
+
const text = String(entry?.text || "").trim();
|
|
951
|
+
if (!text) {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
return text.length > 220 || text.split("\n").length > 5;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function previewClampLines(entry, { expanded = false } = {}) {
|
|
959
|
+
if (expanded || !isTranscriptCardExpandable(entry)) {
|
|
960
|
+
return 0;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (entry?.participant?.role === "advisory") {
|
|
964
|
+
return 1;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (entry?.role === "tool") {
|
|
968
|
+
return 1;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (entry?.role === "assistant" && entry?.kind === "commentary") {
|
|
972
|
+
return 1;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (entry?.kind === "pending" || entry?.kind === "queued") {
|
|
976
|
+
return 1;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return 3;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
export function shouldClampCardPreview(entry, { expanded = false } = {}) {
|
|
983
|
+
return previewClampLines(entry, { expanded }) > 0;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
export function formatCardLabel(entry) {
|
|
987
|
+
if (entry?.participant?.label) {
|
|
988
|
+
return entry.participant.label;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (entry.kind === "pending") {
|
|
992
|
+
return "sending";
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (entry.kind === "queued") {
|
|
996
|
+
return "queued";
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (entry.role === "user") {
|
|
1000
|
+
return "you";
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (entry.role === "assistant" && entry.kind === "commentary") {
|
|
1004
|
+
return "update";
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (entry.role === "assistant") {
|
|
1008
|
+
return "codex";
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
if (entry.kind === "context_compaction") {
|
|
1012
|
+
return "context";
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if (entry.kind === "control_notice") {
|
|
1016
|
+
return "control";
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (entry.kind === "surface_notice") {
|
|
1020
|
+
return "presence";
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (entry.kind === "selection_notice") {
|
|
1024
|
+
return "room";
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (entry.role === "tool") {
|
|
1028
|
+
return "tool";
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
return humanize(entry.kind || entry.role || "event");
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
export function formatOriginLabel(origin) {
|
|
1035
|
+
if (!origin) {
|
|
1036
|
+
return "";
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (origin === "remote") {
|
|
1040
|
+
return "remote lane";
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (origin === "desktop" || origin === "host") {
|
|
1044
|
+
return "desktop lane";
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
if (origin === "external") {
|
|
1048
|
+
return "external client";
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
return `${humanize(origin)} lane`;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function participantCapabilityLabel(participant, { controlLane = "" } = {}) {
|
|
1055
|
+
if (!participant) {
|
|
1056
|
+
return "";
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (controlLane && participant.lane && participant.lane === controlLane) {
|
|
1060
|
+
return "keyboard";
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (participant.metaLabel) {
|
|
1064
|
+
return participant.metaLabel;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (participant.capability === "advisory" || participant.role === "advisory") {
|
|
1068
|
+
return "advisory";
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (participant.canAct || participant.capability === "write") {
|
|
1072
|
+
return "writable";
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
if (participant.role === "tool") {
|
|
1076
|
+
return "tool";
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
return "observe";
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
export function renderParticipantRoster(participants, { controlLane = "" } = {}) {
|
|
1083
|
+
const items = (participants || [])
|
|
1084
|
+
.filter((participant) => participant && participant.role !== "system")
|
|
1085
|
+
.map((participant) => ({
|
|
1086
|
+
capability: participantCapabilityLabel(participant, { controlLane }),
|
|
1087
|
+
id: participant.id || participant.label || "",
|
|
1088
|
+
label: participant.displayLabel || participant.label || participant.id || "voice",
|
|
1089
|
+
state: participant.state || "",
|
|
1090
|
+
token: participant.token || "system"
|
|
1091
|
+
}))
|
|
1092
|
+
.filter((participant) => participant.id);
|
|
1093
|
+
|
|
1094
|
+
const signature = JSON.stringify({
|
|
1095
|
+
controlLane,
|
|
1096
|
+
items
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
if (!items.length) {
|
|
1100
|
+
return {
|
|
1101
|
+
html: "",
|
|
1102
|
+
signature
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const html = items
|
|
1107
|
+
.map((participant) => {
|
|
1108
|
+
const classes = ["participant-pill", `participant-pill-${participant.token}`];
|
|
1109
|
+
if (participant.capability === "keyboard") {
|
|
1110
|
+
classes.push("is-keyboard");
|
|
1111
|
+
}
|
|
1112
|
+
if (participant.state) {
|
|
1113
|
+
classes.push(`is-${participant.state}`);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
return `
|
|
1117
|
+
<span class="${classes.join(" ")}">
|
|
1118
|
+
<span class="participant-pill-label">${escapeHtml(participant.label)}</span>
|
|
1119
|
+
<span class="participant-pill-meta">${escapeHtml(participant.capability)}</span>
|
|
1120
|
+
</span>
|
|
1121
|
+
`;
|
|
1122
|
+
})
|
|
1123
|
+
.join("");
|
|
1124
|
+
|
|
1125
|
+
return {
|
|
1126
|
+
html,
|
|
1127
|
+
signature
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
export function formatCardNote(entry) {
|
|
1132
|
+
if (entry.note) {
|
|
1133
|
+
return entry.note;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (entry.kind === "pending") {
|
|
1137
|
+
return "sending";
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
if (entry.kind === "queued") {
|
|
1141
|
+
return Number.isFinite(entry.queuePosition) ? `slot ${entry.queuePosition}` : "queued";
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
if (entry.role === "assistant" && entry.kind === "commentary") {
|
|
1145
|
+
if (entry.participant?.role === "advisory") {
|
|
1146
|
+
return entry.wakeKind === "review" ? "review" : "recap";
|
|
1147
|
+
}
|
|
1148
|
+
return "";
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
if (entry.role === "assistant" && (entry.lane || entry.origin)) {
|
|
1152
|
+
return `reply via ${formatOriginLabel(entry.lane || entry.origin)}`;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if (entry.role === "assistant" || entry.role === "user") {
|
|
1156
|
+
return "";
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (entry.role === "tool") {
|
|
1160
|
+
return "";
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
if (entry.kind === "context_compaction") {
|
|
1164
|
+
return "history compacted";
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if (entry.kind === "control_notice") {
|
|
1168
|
+
return entry.note || "control handoff";
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
if (entry.kind === "surface_notice") {
|
|
1172
|
+
return entry.note || "surface change";
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
if (entry.kind === "selection_notice") {
|
|
1176
|
+
return entry.note || "room change";
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
return humanize(entry.kind || entry.role || "event");
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
export function isConversationEntry(entry) {
|
|
1183
|
+
if (entry.role === "user") {
|
|
1184
|
+
return true;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
return entry.role === "assistant" && entry.kind !== "commentary";
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function summarySpeakerLabel(entry) {
|
|
1191
|
+
if (!entry) {
|
|
1192
|
+
return "update";
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
if (entry.role === "user") {
|
|
1196
|
+
return "you";
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
if (entry.role === "assistant") {
|
|
1200
|
+
return entry?.participant?.label || "codex";
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
return entry?.participant?.label || entry?.lane || humanize(entry?.role || entry?.kind || "update");
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function normalizeSummaryText(text) {
|
|
1207
|
+
return String(text || "")
|
|
1208
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
1209
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
1210
|
+
.replace(/\s+/g, " ")
|
|
1211
|
+
.trim();
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
export function summarizeRecentTranscript(entries, { limit = 2, maxLength = 180 } = {}) {
|
|
1215
|
+
const allEntries = Array.isArray(entries) ? entries : [];
|
|
1216
|
+
const meaningful = allEntries.filter((entry) => String(entry?.text || "").trim() && !shouldHideTranscriptEntry(entry));
|
|
1217
|
+
const preferredEntries = meaningful.filter((entry) => isConversationEntry(entry));
|
|
1218
|
+
const sourceEntries = preferredEntries.length
|
|
1219
|
+
? preferredEntries
|
|
1220
|
+
: meaningful.filter((entry) => !isSystemNoticeEntry(entry));
|
|
1221
|
+
const recentEntries = sourceEntries.slice(-Math.max(1, limit));
|
|
1222
|
+
|
|
1223
|
+
if (!recentEntries.length) {
|
|
1224
|
+
return "";
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
const summary = recentEntries
|
|
1228
|
+
.map((entry) => {
|
|
1229
|
+
const speaker = summarySpeakerLabel(entry);
|
|
1230
|
+
const text = normalizeSummaryText(entry?.text || "");
|
|
1231
|
+
return `${speaker}: ${text}`;
|
|
1232
|
+
})
|
|
1233
|
+
.join(" | ");
|
|
1234
|
+
|
|
1235
|
+
if (summary.length <= maxLength) {
|
|
1236
|
+
return summary;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
return `${summary.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
export function isAdvisoryEntry(entry) {
|
|
1243
|
+
return entry?.participant?.role === "advisory";
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function isTimelineMarkerEntry(entry) {
|
|
1247
|
+
return (
|
|
1248
|
+
entry?.kind === "context_compaction" ||
|
|
1249
|
+
entry?.kind === "control_notice" ||
|
|
1250
|
+
entry?.kind === "surface_notice" ||
|
|
1251
|
+
entry?.kind === "selection_notice"
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
export function shouldRenderOriginBadge(entry) {
|
|
1256
|
+
if (!entry?.participant?.lane && !entry?.lane && !entry?.origin) {
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (entry.kind === "pending" || entry.kind === "queued") {
|
|
1261
|
+
return true;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
if (entry.role === "assistant" && (entry.lane || entry.origin)) {
|
|
1265
|
+
return true;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
return false;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function normalizeEntryText(value = "") {
|
|
1272
|
+
return String(value || "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function entryTimestampBucket(value = "") {
|
|
1276
|
+
const ms = new Date(value || 0).getTime();
|
|
1277
|
+
if (!Number.isFinite(ms) || ms <= 0) {
|
|
1278
|
+
return "";
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
return String(Math.floor(ms / 1000));
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
export function entryDedupKey(entry) {
|
|
1285
|
+
if (entry?.itemId) {
|
|
1286
|
+
return `item:${entry.itemId}`;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
const text = normalizeEntryText(entry?.text || "");
|
|
1290
|
+
if (text) {
|
|
1291
|
+
return [
|
|
1292
|
+
entry?.role || "",
|
|
1293
|
+
entry?.kind || "",
|
|
1294
|
+
entryTimestampBucket(entry?.timestamp || ""),
|
|
1295
|
+
text
|
|
1296
|
+
].join("|");
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
return [
|
|
1300
|
+
entry?.id || "",
|
|
1301
|
+
entry?.turnId || "",
|
|
1302
|
+
entry?.role || "",
|
|
1303
|
+
entry?.kind || "",
|
|
1304
|
+
entry?.timestamp || ""
|
|
1305
|
+
].join("|");
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function normalizedTranscriptOrder(entry) {
|
|
1309
|
+
const order = Number(entry?.transcriptOrder);
|
|
1310
|
+
return Number.isFinite(order) ? order : null;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function normalizedEntryTimestamp(entry) {
|
|
1314
|
+
const ms = new Date(entry?.timestamp || 0).getTime();
|
|
1315
|
+
return Number.isFinite(ms) && ms > 0 ? ms : null;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
export function compareEntryChronology(left, right) {
|
|
1319
|
+
const leftOrder = normalizedTranscriptOrder(left);
|
|
1320
|
+
const rightOrder = normalizedTranscriptOrder(right);
|
|
1321
|
+
if (leftOrder != null && rightOrder != null && leftOrder !== rightOrder) {
|
|
1322
|
+
return leftOrder - rightOrder;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
const leftTimestamp = normalizedEntryTimestamp(left);
|
|
1326
|
+
const rightTimestamp = normalizedEntryTimestamp(right);
|
|
1327
|
+
if (leftTimestamp != null && rightTimestamp != null && leftTimestamp !== rightTimestamp) {
|
|
1328
|
+
return leftTimestamp - rightTimestamp;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if (leftOrder != null && rightOrder == null) {
|
|
1332
|
+
return -1;
|
|
1333
|
+
}
|
|
1334
|
+
if (leftOrder == null && rightOrder != null) {
|
|
1335
|
+
return 1;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
if (leftTimestamp != null && rightTimestamp == null) {
|
|
1339
|
+
return -1;
|
|
1340
|
+
}
|
|
1341
|
+
if (leftTimestamp == null && rightTimestamp != null) {
|
|
1342
|
+
return 1;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
return 0;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
export function compareEntryChronologyDesc(left, right) {
|
|
1349
|
+
return compareEntryChronology(right, left);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
export function entryKey(entry) {
|
|
1353
|
+
return [
|
|
1354
|
+
entry.id || "",
|
|
1355
|
+
entry.turnId || "",
|
|
1356
|
+
entry.itemId || "",
|
|
1357
|
+
entryDedupKey(entry)
|
|
1358
|
+
].join("|");
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
export function renderTranscriptCard(entry, { expanded = false, isNew = false } = {}) {
|
|
1362
|
+
const classes = ["transcript-card", `transcript-${entry.role}`];
|
|
1363
|
+
if (entry.role === "assistant" && entry.kind === "commentary") {
|
|
1364
|
+
classes.push("transcript-commentary");
|
|
1365
|
+
}
|
|
1366
|
+
if (isTimelineMarkerEntry(entry)) {
|
|
1367
|
+
classes.push("transcript-marker");
|
|
1368
|
+
}
|
|
1369
|
+
if (entry?.participant?.token) {
|
|
1370
|
+
classes.push(`participant-${entry.participant.token}`);
|
|
1371
|
+
}
|
|
1372
|
+
if (entry.kind === "control_notice") {
|
|
1373
|
+
classes.push("transcript-control-marker");
|
|
1374
|
+
}
|
|
1375
|
+
if (entry.kind === "surface_notice") {
|
|
1376
|
+
classes.push("transcript-surface-marker");
|
|
1377
|
+
}
|
|
1378
|
+
if (entry.kind === "selection_notice") {
|
|
1379
|
+
classes.push("transcript-selection-marker");
|
|
1380
|
+
}
|
|
1381
|
+
if (shouldRenderOriginBadge(entry)) {
|
|
1382
|
+
classes.push(`transcript-origin-${entry.lane || entry.origin}`);
|
|
1383
|
+
}
|
|
1384
|
+
const expandable = isTranscriptCardExpandable(entry);
|
|
1385
|
+
if (expandable) {
|
|
1386
|
+
classes.push("card-expandable");
|
|
1387
|
+
}
|
|
1388
|
+
if (expanded) {
|
|
1389
|
+
classes.push("card-expanded");
|
|
1390
|
+
}
|
|
1391
|
+
const clampLines = previewClampLines(entry, { expanded });
|
|
1392
|
+
if (clampLines > 0) {
|
|
1393
|
+
classes.push("card-clamped");
|
|
1394
|
+
classes.push(`card-clamp-${clampLines}`);
|
|
1395
|
+
}
|
|
1396
|
+
if (isNew) {
|
|
1397
|
+
classes.push("card-new");
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
const note = formatCardNote(entry);
|
|
1401
|
+
const timestamp = formatTimestamp(entry.timestamp);
|
|
1402
|
+
const origin = shouldRenderOriginBadge(entry) ? formatOriginLabel(entry.participant?.lane || entry.lane || entry.origin) : "";
|
|
1403
|
+
const key = entryKey(entry);
|
|
1404
|
+
const actions = Array.isArray(entry.actions) ? entry.actions : [];
|
|
1405
|
+
const actionBusy = Boolean(entry.actionState?.busy);
|
|
1406
|
+
const actionButtons = actions.length
|
|
1407
|
+
? `
|
|
1408
|
+
<div class="card-actions">
|
|
1409
|
+
${actions
|
|
1410
|
+
.map((action) => {
|
|
1411
|
+
const isBusy = actionBusy && entry.actionState?.action === action.action;
|
|
1412
|
+
const classes = ["transcript-action"];
|
|
1413
|
+
if (action.tone === "danger") {
|
|
1414
|
+
classes.push("is-danger");
|
|
1415
|
+
} else if (action.tone === "success") {
|
|
1416
|
+
classes.push("is-success");
|
|
1417
|
+
}
|
|
1418
|
+
if (isBusy) {
|
|
1419
|
+
classes.push("is-busy");
|
|
1420
|
+
}
|
|
1421
|
+
const actionDisabled = actionBusy || Boolean(action.disabled);
|
|
1422
|
+
|
|
1423
|
+
return `
|
|
1424
|
+
<button
|
|
1425
|
+
type="button"
|
|
1426
|
+
class="${classes.join(" ")}"
|
|
1427
|
+
data-companion-action="${escapeHtml(action.action)}"
|
|
1428
|
+
data-wake-key="${escapeHtml(entry.key || "")}"
|
|
1429
|
+
data-advisor-id="${escapeHtml(entry.advisorId || "")}"
|
|
1430
|
+
${actionDisabled ? "disabled" : ""}
|
|
1431
|
+
>${escapeHtml(isBusy ? action.busyLabel || action.label : action.label)}</button>
|
|
1432
|
+
`;
|
|
1433
|
+
})
|
|
1434
|
+
.join("")}
|
|
1435
|
+
</div>
|
|
1436
|
+
`
|
|
1437
|
+
: "";
|
|
1438
|
+
|
|
1439
|
+
return `
|
|
1440
|
+
<article
|
|
1441
|
+
class="${classes.join(" ")}"
|
|
1442
|
+
data-entry-key="${escapeHtml(key)}"
|
|
1443
|
+
data-expandable="${expandable ? "true" : "false"}"
|
|
1444
|
+
${expandable ? `tabindex="0" aria-expanded="${expanded ? "true" : "false"}"` : ""}
|
|
1445
|
+
>
|
|
1446
|
+
<div class="card-head">
|
|
1447
|
+
<div class="card-meta">
|
|
1448
|
+
<span class="card-label">${escapeHtml(formatCardLabel(entry))}</span>
|
|
1449
|
+
${origin ? `<span class="card-origin">${escapeHtml(origin)}</span>` : ""}
|
|
1450
|
+
</div>
|
|
1451
|
+
<div class="card-tail">
|
|
1452
|
+
${timestamp ? `<time class="card-time">${escapeHtml(timestamp)}</time>` : ""}
|
|
1453
|
+
${expandable && !expanded ? '<span class="card-expand-hint" aria-hidden="true">...</span>' : ""}
|
|
1454
|
+
</div>
|
|
1455
|
+
</div>
|
|
1456
|
+
${note ? `<div class="card-note">${escapeHtml(note)}</div>` : ""}
|
|
1457
|
+
<div class="transcript-copy">${renderTranscriptText(displayTranscriptText(entry, { expanded }))}</div>
|
|
1458
|
+
${actionButtons}
|
|
1459
|
+
</article>
|
|
1460
|
+
`;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
export function renderChangeCard(change, { open = false } = {}) {
|
|
1464
|
+
const meta = [
|
|
1465
|
+
change.kind ? humanize(change.kind) : null,
|
|
1466
|
+
Number.isFinite(change.additions) ? `+${change.additions}` : null,
|
|
1467
|
+
Number.isFinite(change.deletions) ? `-${change.deletions}` : null
|
|
1468
|
+
]
|
|
1469
|
+
.filter(Boolean)
|
|
1470
|
+
.join(" / ");
|
|
1471
|
+
|
|
1472
|
+
const renameNote =
|
|
1473
|
+
change.fromPath && change.fromPath !== change.path
|
|
1474
|
+
? `<div class="change-note">${escapeHtml(change.fromPath)} -> ${escapeHtml(change.path)}</div>`
|
|
1475
|
+
: "";
|
|
1476
|
+
const relevanceNote = change.relevance
|
|
1477
|
+
? `<div class="change-relevance">${escapeHtml(change.relevance)}</div>`
|
|
1478
|
+
: "";
|
|
1479
|
+
|
|
1480
|
+
return `
|
|
1481
|
+
<details class="change-card"${open ? " open" : ""}>
|
|
1482
|
+
<summary class="change-summary">
|
|
1483
|
+
<span class="change-path">${escapeHtml(change.path || "unknown")}</span>
|
|
1484
|
+
<span class="change-meta">${escapeHtml(meta || change.statusCode || "changed")}</span>
|
|
1485
|
+
</summary>
|
|
1486
|
+
${relevanceNote}
|
|
1487
|
+
${renameNote}
|
|
1488
|
+
<pre class="diff-block">${escapeHtml(change.diffPreview || "No diff preview.")}</pre>
|
|
1489
|
+
</details>
|
|
1490
|
+
`;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function applySelectState(select, normalizedOptions, normalizedSelectedValue, signature) {
|
|
1494
|
+
select.innerHTML = "";
|
|
1495
|
+
|
|
1496
|
+
for (const option of normalizedOptions) {
|
|
1497
|
+
const node = document.createElement("option");
|
|
1498
|
+
node.value = option.value;
|
|
1499
|
+
node.textContent = option.label;
|
|
1500
|
+
node.selected = option.value === normalizedSelectedValue;
|
|
1501
|
+
select.append(node);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
select.dataset.renderSignature = signature;
|
|
1505
|
+
delete select.dataset.pendingRenderSignature;
|
|
1506
|
+
select.__pendingSelectState = null;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function isSelectInteracting(select) {
|
|
1510
|
+
const interactiveUntil = Number(select.dataset.interactingUntil || "0");
|
|
1511
|
+
return document.activeElement === select || interactiveUntil > Date.now();
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function flushPendingSelectState(select) {
|
|
1515
|
+
const pending = select.__pendingSelectState;
|
|
1516
|
+
if (!pending || isSelectInteracting(select)) {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
applySelectState(select, pending.options, pending.selectedValue, pending.signature);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function ensureStableSelect(select) {
|
|
1524
|
+
if (select.__stableSelectInitialized) {
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
select.__stableSelectInitialized = true;
|
|
1529
|
+
|
|
1530
|
+
const markInteractive = (ms = 1400) => {
|
|
1531
|
+
select.dataset.interactingUntil = String(Date.now() + ms);
|
|
1532
|
+
};
|
|
1533
|
+
|
|
1534
|
+
const releaseSoon = (ms = 180) => {
|
|
1535
|
+
markInteractive(ms);
|
|
1536
|
+
window.setTimeout(() => flushPendingSelectState(select), ms + 20);
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1539
|
+
select.addEventListener("pointerdown", () => markInteractive(), true);
|
|
1540
|
+
select.addEventListener("mousedown", () => markInteractive(), true);
|
|
1541
|
+
select.addEventListener("touchstart", () => markInteractive(), true);
|
|
1542
|
+
select.addEventListener("focus", () => markInteractive(), true);
|
|
1543
|
+
select.addEventListener("keydown", () => markInteractive(900), true);
|
|
1544
|
+
select.addEventListener("change", () => releaseSoon(120), true);
|
|
1545
|
+
select.addEventListener("blur", () => releaseSoon(), true);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
export function populateSelect(select, options, selectedValue) {
|
|
1549
|
+
ensureStableSelect(select);
|
|
1550
|
+
const normalizedOptions = (options || []).map((option) => ({
|
|
1551
|
+
label: String(option.label ?? ""),
|
|
1552
|
+
value: String(option.value ?? "")
|
|
1553
|
+
}));
|
|
1554
|
+
const normalizedSelectedValue = String(selectedValue ?? "");
|
|
1555
|
+
const signature = JSON.stringify({
|
|
1556
|
+
options: normalizedOptions,
|
|
1557
|
+
selectedValue: normalizedSelectedValue
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
if (select.dataset.renderSignature === signature) {
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
if (isSelectInteracting(select)) {
|
|
1565
|
+
select.__pendingSelectState = {
|
|
1566
|
+
options: normalizedOptions,
|
|
1567
|
+
selectedValue: normalizedSelectedValue,
|
|
1568
|
+
signature
|
|
1569
|
+
};
|
|
1570
|
+
select.dataset.pendingRenderSignature = signature;
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
applySelectState(select, normalizedOptions, normalizedSelectedValue, signature);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
export function setHtmlIfChanged(node, html, signature = html) {
|
|
1578
|
+
if (!node) {
|
|
1579
|
+
return false;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
if (node.__renderSignature === signature) {
|
|
1583
|
+
return false;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
node.innerHTML = html;
|
|
1587
|
+
node.__renderSignature = signature;
|
|
1588
|
+
return true;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
export function clearHtmlRenderState(node) {
|
|
1592
|
+
if (!node) {
|
|
1593
|
+
return false;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
node.innerHTML = "";
|
|
1597
|
+
node.__renderSignature = null;
|
|
1598
|
+
return true;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
function renderKeySelector(value) {
|
|
1602
|
+
if (window.CSS?.escape) {
|
|
1603
|
+
return `[data-render-key="${window.CSS.escape(value)}"]`;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
return `[data-render-key="${String(value).replaceAll('"', '\\"')}"]`;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
function createRenderedNode(html, key, signature) {
|
|
1610
|
+
const template = document.createElement("template");
|
|
1611
|
+
template.innerHTML = String(html || "").trim();
|
|
1612
|
+
const node = template.content.firstElementChild;
|
|
1613
|
+
if (!node) {
|
|
1614
|
+
return null;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
node.dataset.renderKey = key;
|
|
1618
|
+
node.__renderSignature = signature;
|
|
1619
|
+
return node;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function captureViewportAnchor(container) {
|
|
1623
|
+
if (!container) {
|
|
1624
|
+
return null;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
const children = Array.from(container.children);
|
|
1628
|
+
const anchorNode = children.find((child) => child.getBoundingClientRect().bottom > 0) || children[0] || null;
|
|
1629
|
+
if (!anchorNode?.dataset?.renderKey) {
|
|
1630
|
+
return null;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
return {
|
|
1634
|
+
key: anchorNode.dataset.renderKey,
|
|
1635
|
+
top: anchorNode.getBoundingClientRect().top
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
function restoreViewportAnchor(container, anchor) {
|
|
1640
|
+
if (!container || !anchor?.key) {
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
const node = container.querySelector(renderKeySelector(anchor.key));
|
|
1645
|
+
if (!node) {
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
const nextTop = node.getBoundingClientRect().top;
|
|
1650
|
+
const delta = nextTop - anchor.top;
|
|
1651
|
+
if (delta) {
|
|
1652
|
+
window.scrollBy(0, delta);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
export function reconcileRenderedList(container, items = []) {
|
|
1657
|
+
if (!container) {
|
|
1658
|
+
return false;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
const existing = new Map(
|
|
1662
|
+
Array.from(container.children)
|
|
1663
|
+
.filter((child) => child?.dataset?.renderKey)
|
|
1664
|
+
.map((child) => [child.dataset.renderKey, child])
|
|
1665
|
+
);
|
|
1666
|
+
const anchor = captureViewportAnchor(container);
|
|
1667
|
+
const fragment = document.createDocumentFragment();
|
|
1668
|
+
let changed = false;
|
|
1669
|
+
|
|
1670
|
+
for (const item of items) {
|
|
1671
|
+
const key = String(item?.key || "");
|
|
1672
|
+
if (!key) {
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
let node = existing.get(key) || null;
|
|
1677
|
+
if (!node || node.__renderSignature !== item.signature) {
|
|
1678
|
+
const nextNode = createRenderedNode(item.html, key, item.signature);
|
|
1679
|
+
if (!nextNode) {
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
changed = true;
|
|
1683
|
+
if (node) {
|
|
1684
|
+
node.replaceWith(nextNode);
|
|
1685
|
+
}
|
|
1686
|
+
node = nextNode;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
existing.delete(key);
|
|
1690
|
+
fragment.append(node);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
if (existing.size) {
|
|
1694
|
+
changed = true;
|
|
1695
|
+
for (const node of existing.values()) {
|
|
1696
|
+
node.remove();
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
if (container.childNodes.length !== items.length || Array.from(container.children).some((child, index) => child.dataset.renderKey !== items[index]?.key)) {
|
|
1701
|
+
changed = true;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
if (!changed) {
|
|
1705
|
+
return false;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
container.replaceChildren(fragment);
|
|
1709
|
+
restoreViewportAnchor(container, anchor);
|
|
1710
|
+
return true;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
export function startTicker(
|
|
1714
|
+
node,
|
|
1715
|
+
phrases,
|
|
1716
|
+
{ typeDelay = 34, eraseDelay = 14, holdMs = 1200, loop = false } = {}
|
|
1717
|
+
) {
|
|
1718
|
+
if (!node || !phrases?.length) {
|
|
1719
|
+
return {
|
|
1720
|
+
setText() {}
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
1725
|
+
node.textContent = phrases[0];
|
|
1726
|
+
node.classList.add("is-static");
|
|
1727
|
+
return {
|
|
1728
|
+
setText(text) {
|
|
1729
|
+
node.textContent = text;
|
|
1730
|
+
node.classList.add("is-static");
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
let phraseIndex = 0;
|
|
1736
|
+
let charIndex = 0;
|
|
1737
|
+
let mode = "typing";
|
|
1738
|
+
let settledText = String(node.textContent || "");
|
|
1739
|
+
let timeoutId = null;
|
|
1740
|
+
let stopped = false;
|
|
1741
|
+
let transitionAnimation = null;
|
|
1742
|
+
|
|
1743
|
+
function queue(nextDelay) {
|
|
1744
|
+
timeoutId = window.setTimeout(tick, nextDelay);
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
function settle(text, { animate = false } = {}) {
|
|
1748
|
+
stopped = true;
|
|
1749
|
+
if (timeoutId != null) {
|
|
1750
|
+
window.clearTimeout(timeoutId);
|
|
1751
|
+
timeoutId = null;
|
|
1752
|
+
}
|
|
1753
|
+
const nextText = String(text || "");
|
|
1754
|
+
if (settledText === nextText && node.classList.contains("is-static")) {
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
settledText = nextText;
|
|
1758
|
+
node.textContent = nextText;
|
|
1759
|
+
node.classList.add("is-static");
|
|
1760
|
+
if (animate && typeof node.animate === "function") {
|
|
1761
|
+
try {
|
|
1762
|
+
transitionAnimation?.cancel();
|
|
1763
|
+
transitionAnimation = node.animate(
|
|
1764
|
+
[
|
|
1765
|
+
{ filter: "blur(0.8px)", opacity: 0.58, transform: "translateY(1px)" },
|
|
1766
|
+
{ filter: "blur(0px)", opacity: 1, transform: "translateY(0)" }
|
|
1767
|
+
],
|
|
1768
|
+
{
|
|
1769
|
+
duration: 180,
|
|
1770
|
+
easing: "cubic-bezier(0.22, 1, 0.36, 1)"
|
|
1771
|
+
}
|
|
1772
|
+
);
|
|
1773
|
+
transitionAnimation.addEventListener("finish", () => {
|
|
1774
|
+
transitionAnimation = null;
|
|
1775
|
+
}, { once: true });
|
|
1776
|
+
} catch {
|
|
1777
|
+
transitionAnimation = null;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
function tick() {
|
|
1783
|
+
if (stopped) {
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
const phrase = phrases[phraseIndex];
|
|
1788
|
+
|
|
1789
|
+
if (mode === "typing") {
|
|
1790
|
+
charIndex += 1;
|
|
1791
|
+
node.textContent = phrase.slice(0, charIndex);
|
|
1792
|
+
if (charIndex < phrase.length) {
|
|
1793
|
+
queue(typeDelay);
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
mode = "holding";
|
|
1797
|
+
queue(holdMs);
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
if (mode === "holding") {
|
|
1802
|
+
if (!loop && phraseIndex === phrases.length - 1) {
|
|
1803
|
+
settle(phrase);
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
mode = "erasing";
|
|
1807
|
+
queue(eraseDelay);
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
charIndex -= 1;
|
|
1812
|
+
node.textContent = phrase.slice(0, charIndex);
|
|
1813
|
+
if (charIndex > 0) {
|
|
1814
|
+
queue(eraseDelay);
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
mode = "typing";
|
|
1819
|
+
phraseIndex = loop ? (phraseIndex + 1) % phrases.length : Math.min(phraseIndex + 1, phrases.length - 1);
|
|
1820
|
+
queue(180);
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
node.classList.remove("is-static");
|
|
1824
|
+
tick();
|
|
1825
|
+
|
|
1826
|
+
return {
|
|
1827
|
+
setText(text) {
|
|
1828
|
+
settle(text, { animate: true });
|
|
1829
|
+
}
|
|
1830
|
+
};
|
|
1831
|
+
}
|