browser-debugging-daemon 1.0.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.
@@ -0,0 +1,1139 @@
1
+ const state = {
2
+ runs: [],
3
+ selectedRunId: null,
4
+ pollHandle: null,
5
+ eventFilter: "all",
6
+ messageFilter: "all",
7
+ currentRun: null,
8
+ focusedHistory: null,
9
+ templates: [],
10
+ selectedTemplateId: "",
11
+ };
12
+ const TOKEN_STORAGE_KEY = "browserDaemonToken";
13
+ const urlSearchParams = new URLSearchParams(window.location.search);
14
+ const tokenFromUrl = urlSearchParams.get("token");
15
+ if (tokenFromUrl) {
16
+ localStorage.setItem(TOKEN_STORAGE_KEY, tokenFromUrl);
17
+ }
18
+
19
+ function getAuthToken() {
20
+ return localStorage.getItem(TOKEN_STORAGE_KEY) || "";
21
+ }
22
+
23
+ async function apiFetch(url, options = {}) {
24
+ const headers = new Headers(options.headers || {});
25
+ const token = getAuthToken();
26
+ if (token) {
27
+ headers.set("Authorization", `Bearer ${token}`);
28
+ }
29
+ return fetch(url, { ...options, headers });
30
+ }
31
+
32
+ async function readJsonSafe(response) {
33
+ try {
34
+ return await response.json();
35
+ } catch (error) {
36
+ return {};
37
+ }
38
+ }
39
+
40
+ function withTokenQuery(url) {
41
+ const token = getAuthToken();
42
+ if (!token) {
43
+ return url;
44
+ }
45
+ const [base, query = ""] = url.split("?");
46
+ const params = new URLSearchParams(query);
47
+ params.set("token", token);
48
+ const encoded = params.toString();
49
+ return encoded ? `${base}?${encoded}` : base;
50
+ }
51
+
52
+ const runListEl = document.getElementById("runList");
53
+ const runCountEl = document.getElementById("runCount");
54
+ const emptyStateEl = document.getElementById("emptyState");
55
+ const runDetailEl = document.getElementById("runDetail");
56
+ const detailTitleEl = document.getElementById("detailTitle");
57
+ const detailStatusEl = document.getElementById("detailStatus");
58
+ const detailSummaryEl = document.getElementById("detailSummary");
59
+ const detailTaskEl = document.getElementById("detailTask");
60
+ const detailTimingEl = document.getElementById("detailTiming");
61
+ const detailLinksEl = document.getElementById("detailLinks");
62
+ const detailRuntimeEl = document.getElementById("detailRuntime");
63
+ const detailRuntimeNoteEl = document.getElementById("detailRuntimeNote");
64
+ const detailTemplateEl = document.getElementById("detailTemplate");
65
+ const detailScreenshotEl = document.getElementById("detailScreenshot");
66
+ const detailVideoEl = document.getElementById("detailVideo");
67
+ const videoFrameEl = document.getElementById("videoFrame");
68
+ const detailStepFocusEl = document.getElementById("detailStepFocus");
69
+ const detailWalkthroughEl = document.getElementById("detailWalkthrough");
70
+ const detailHistoryEl = document.getElementById("detailHistory");
71
+ const detailConsoleEl = document.getElementById("detailConsole");
72
+ const detailNetworkEl = document.getElementById("detailNetwork");
73
+ const detailErrorsEl = document.getElementById("detailErrors");
74
+ const detailEventsEl = document.getElementById("detailEvents");
75
+ const clearStepFocusButtonEl = document.getElementById("clearStepFocusButton");
76
+ const coordinationEmptyEl = document.getElementById("coordinationEmpty");
77
+ const pendingInputPanelEl = document.getElementById("pendingInputPanel");
78
+ const pendingInputQuestionEl = document.getElementById("pendingInputQuestion");
79
+ const pendingInputDetailsEl = document.getElementById("pendingInputDetails");
80
+ const pendingInputSuggestionEl = document.getElementById("pendingInputSuggestion");
81
+ const operatorMessagesEl = document.getElementById("operatorMessages");
82
+ const messageFilterEl = document.getElementById("messageFilter");
83
+ const replyFormEl = document.getElementById("replyForm");
84
+ const replyInstructionEl = document.getElementById("replyInstruction");
85
+ const replyMessageEl = document.getElementById("replyMessage");
86
+ const abortRunButtonEl = document.getElementById("abortRunButton");
87
+ const abortRunButtonSecondaryEl = document.getElementById("abortRunButtonSecondary");
88
+ const manualControlButtonEl = document.getElementById("manualControlButton");
89
+ const resumeRunButtonEl = document.getElementById("resumeRunButton");
90
+ const runControlBarEl = document.getElementById("runControlBar");
91
+ const eventFilterEl = document.getElementById("eventFilter");
92
+ const refreshButtonEl = document.getElementById("refreshButton");
93
+ const runFormEl = document.getElementById("runForm");
94
+ const taskInstructionEl = document.getElementById("taskInstruction");
95
+ const templateSelectEl = document.getElementById("templateSelect");
96
+ const templateRefreshButtonEl = document.getElementById("templateRefreshButton");
97
+ const templateSaveButtonEl = document.getElementById("templateSaveButton");
98
+ const templateDeleteButtonEl = document.getElementById("templateDeleteButton");
99
+ const templateNameEl = document.getElementById("templateName");
100
+ const templateStartUrlEl = document.getElementById("templateStartUrl");
101
+ const templateLoginChecksEl = document.getElementById("templateLoginChecks");
102
+ const templateAssertionsEl = document.getElementById("templateAssertions");
103
+ const browserSourceEl = document.getElementById("browserSource");
104
+ const cdpEndpointEl = document.getElementById("cdpEndpoint");
105
+ const maxStepsEl = document.getElementById("maxSteps");
106
+ const handoffTimeoutSecondsEl = document.getElementById("handoffTimeoutSeconds");
107
+ const formMessageEl = document.getElementById("formMessage");
108
+
109
+ function formatDate(value) {
110
+ if (!value) return "n/a";
111
+ return new Date(value).toLocaleString();
112
+ }
113
+
114
+ function setStatusChip(element, status) {
115
+ element.dataset.status = status || "unknown";
116
+ element.textContent = status || "unknown";
117
+ }
118
+
119
+ function durationText(run) {
120
+ if (!run.startedAt) return "Queued";
121
+ const start = new Date(run.startedAt).getTime();
122
+ const end = run.finishedAt ? new Date(run.finishedAt).getTime() : Date.now();
123
+ const seconds = Math.max(0, Math.round((end - start) / 1000));
124
+ return `${seconds}s`;
125
+ }
126
+
127
+ function createLink(label, url) {
128
+ const anchor = document.createElement("a");
129
+ anchor.href = withTokenQuery(url);
130
+ anchor.target = "_blank";
131
+ anchor.rel = "noreferrer";
132
+ anchor.textContent = label;
133
+ return anchor;
134
+ }
135
+
136
+ function createTraceViewerButton(label, artifactUrl) {
137
+ if (!artifactUrl) return null;
138
+ // Convert /artifacts/<session>/traces/<file> to /trace/<session>/<file>
139
+ const match = artifactUrl.match(/^\/artifacts\/([^/]+)\/traces\/([^/?]+)/);
140
+ if (!match) {
141
+ // Fallback to download link if path doesn't match expected pattern
142
+ return createLink(label, artifactUrl);
143
+ }
144
+ const [, sessionId, traceFile] = match;
145
+ const btn = document.createElement("button");
146
+ btn.textContent = label;
147
+ btn.className = "trace-button";
148
+ btn.onclick = () => {
149
+ const traceUrl = `/trace/${sessionId}/${encodeURIComponent(traceFile)}`;
150
+ window.open(withTokenQuery(traceUrl), "_blank");
151
+ };
152
+ return btn;
153
+ }
154
+
155
+ function withTokenAndCacheBust(url) {
156
+ if (!url) return "";
157
+ const tokenized = withTokenQuery(url);
158
+ const [base, query = ""] = tokenized.split("?");
159
+ const params = new URLSearchParams(query);
160
+ params.set("t", String(Date.now()));
161
+ const encoded = params.toString();
162
+ return encoded ? `${base}?${encoded}` : base;
163
+ }
164
+
165
+ function escapeHtml(value) {
166
+ return String(value ?? "")
167
+ .replaceAll("&", "&amp;")
168
+ .replaceAll("<", "&lt;")
169
+ .replaceAll(">", "&gt;")
170
+ .replaceAll('"', "&quot;")
171
+ .replaceAll("'", "&#39;");
172
+ }
173
+
174
+ function renderLogList(container, entries, formatter) {
175
+ container.innerHTML = "";
176
+ if (!entries?.length) {
177
+ const empty = document.createElement("p");
178
+ empty.textContent = "No entries recorded.";
179
+ container.appendChild(empty);
180
+ return;
181
+ }
182
+
183
+ for (const entry of entries) {
184
+ const item = document.createElement("article");
185
+ item.className = "log-entry";
186
+ item.innerHTML = formatter(entry);
187
+ container.appendChild(item);
188
+ }
189
+ }
190
+
191
+ function formatTimestamp(value) {
192
+ if (!value) return "n/a";
193
+ return new Date(value).toLocaleTimeString();
194
+ }
195
+
196
+ function parseTimestampMs(value) {
197
+ const timestampMs = Date.parse(value);
198
+ return Number.isFinite(timestampMs) ? timestampMs : null;
199
+ }
200
+
201
+ function formatDurationMs(durationMs) {
202
+ const numeric = Number(durationMs);
203
+ if (!Number.isFinite(numeric) || numeric < 0) return "n/a";
204
+ if (numeric < 1000) return `${Math.round(numeric)}ms`;
205
+ return `${(numeric / 1000).toFixed(2)}s`;
206
+ }
207
+
208
+ function normalizeNumber(value) {
209
+ const numeric = Number(value);
210
+ return Number.isFinite(numeric) ? numeric : null;
211
+ }
212
+
213
+ function getFocusedHistoryEntry(run, history) {
214
+ if (!state.focusedHistory || state.focusedHistory.runId !== run.id) {
215
+ return null;
216
+ }
217
+ const { index } = state.focusedHistory;
218
+ if (!Number.isInteger(index) || index < 0 || index >= history.length) {
219
+ return null;
220
+ }
221
+ return history[index];
222
+ }
223
+
224
+ function buildFocusWindow(history, focusedEntry) {
225
+ if (!focusedEntry) return null;
226
+ const currentIndex = history.findIndex((entry) => entry._index === focusedEntry._index);
227
+ if (currentIndex < 0) return null;
228
+
229
+ let startMs = parseTimestampMs(focusedEntry.stepStartedAt);
230
+ let endMs = parseTimestampMs(focusedEntry.stepFinishedAt);
231
+ if (!endMs && history[currentIndex + 1]) {
232
+ endMs = parseTimestampMs(history[currentIndex + 1].stepStartedAt);
233
+ }
234
+
235
+ if (!startMs && endMs) {
236
+ startMs = endMs - 15000;
237
+ }
238
+ if (!endMs && startMs) {
239
+ endMs = startMs + 15000;
240
+ }
241
+ if (!startMs && !endMs) {
242
+ return null;
243
+ }
244
+
245
+ return { startMs, endMs };
246
+ }
247
+
248
+ function setScreenshot(url, runId, stepLabel = "Latest run view") {
249
+ if (!url) {
250
+ detailScreenshotEl.removeAttribute("src");
251
+ detailScreenshotEl.alt = "No screenshot available";
252
+ detailScreenshotEl.style.display = "none";
253
+ } else {
254
+ detailScreenshotEl.src = withTokenAndCacheBust(url);
255
+ detailScreenshotEl.alt = `${stepLabel} screenshot for run ${runId}`;
256
+ detailScreenshotEl.style.display = "block";
257
+ }
258
+ }
259
+
260
+ function applyVideoState(videoUrl, focusedEntry) {
261
+ if (!videoUrl) {
262
+ detailVideoEl.pause();
263
+ detailVideoEl.removeAttribute("src");
264
+ delete detailVideoEl.dataset.sourceUrl;
265
+ videoFrameEl.style.display = "none";
266
+ return;
267
+ }
268
+
269
+ videoFrameEl.style.display = "block";
270
+ const sourceUrl = withTokenQuery(videoUrl);
271
+ if (detailVideoEl.dataset.sourceUrl !== sourceUrl) {
272
+ detailVideoEl.dataset.sourceUrl = sourceUrl;
273
+ detailVideoEl.src = sourceUrl;
274
+ }
275
+
276
+ const targetOffset = normalizeNumber(focusedEntry?.videoOffsetSeconds);
277
+ if (targetOffset === null) {
278
+ return;
279
+ }
280
+
281
+ const seekToOffset = () => {
282
+ try {
283
+ detailVideoEl.currentTime = Math.max(0, targetOffset);
284
+ } catch (error) {
285
+ // Ignore seek errors while metadata is still loading.
286
+ }
287
+ };
288
+
289
+ if (detailVideoEl.readyState >= 1) {
290
+ seekToOffset();
291
+ } else {
292
+ detailVideoEl.addEventListener("loadedmetadata", seekToOffset, { once: true });
293
+ }
294
+ }
295
+
296
+ function parseRuleLines(text) {
297
+ return String(text || "")
298
+ .split("\n")
299
+ .map((line) => line.trim())
300
+ .filter(Boolean)
301
+ .map((line, index) => {
302
+ const [kindRaw = "", expectedRaw = "", nameRaw = ""] = line.split("|");
303
+ const kind = kindRaw.trim().toLowerCase();
304
+ const expected = expectedRaw.trim();
305
+ const name = nameRaw.trim() || `Rule ${index + 1}`;
306
+ if (!kind || !expected) return null;
307
+ return { kind, expected, name, required: true };
308
+ })
309
+ .filter(Boolean);
310
+ }
311
+
312
+ function formatRuleLines(rules) {
313
+ if (!Array.isArray(rules) || !rules.length) return "";
314
+ return rules
315
+ .map((rule) => {
316
+ const kind = rule.kind || "text_includes";
317
+ const expected = rule.expected || rule.value || "";
318
+ const name = rule.name || "";
319
+ return [kind, expected, name].join("|");
320
+ })
321
+ .join("\n");
322
+ }
323
+
324
+ function getSelectedTemplate() {
325
+ if (!state.selectedTemplateId) return null;
326
+ return state.templates.find((template) => template.id === state.selectedTemplateId) || null;
327
+ }
328
+
329
+ function applyTemplateToForm(template) {
330
+ if (!template) return;
331
+ templateNameEl.value = template.name || "";
332
+ templateStartUrlEl.value = template.startUrl || "";
333
+ templateLoginChecksEl.value = formatRuleLines(template.preLoginChecks || []);
334
+ templateAssertionsEl.value = formatRuleLines(template.assertionRules || []);
335
+ taskInstructionEl.value = template.taskInstruction || "";
336
+ browserSourceEl.value = template.browserSource || "auto";
337
+ cdpEndpointEl.value = template.cdpEndpoint || cdpEndpointEl.value || "http://127.0.0.1:9222";
338
+ maxStepsEl.value = String(template.timeoutPolicy?.maxSteps || 12);
339
+ const handoffTimeoutSeconds = Math.max(5, Math.round((template.timeoutPolicy?.handoffTimeoutMs || 300000) / 1000));
340
+ handoffTimeoutSecondsEl.value = String(handoffTimeoutSeconds);
341
+ }
342
+
343
+ function renderTemplateOptions() {
344
+ const previousSelection = state.selectedTemplateId || templateSelectEl.value || "";
345
+ templateSelectEl.innerHTML = "";
346
+
347
+ const defaultOption = document.createElement("option");
348
+ defaultOption.value = "";
349
+ defaultOption.textContent = "No Template";
350
+ templateSelectEl.appendChild(defaultOption);
351
+
352
+ for (const template of state.templates) {
353
+ const option = document.createElement("option");
354
+ option.value = template.id;
355
+ option.textContent = template.name;
356
+ templateSelectEl.appendChild(option);
357
+ }
358
+
359
+ const hasSelection = state.templates.some((template) => template.id === previousSelection);
360
+ state.selectedTemplateId = hasSelection ? previousSelection : "";
361
+ templateSelectEl.value = state.selectedTemplateId;
362
+ templateDeleteButtonEl.disabled = !state.selectedTemplateId;
363
+ }
364
+
365
+ async function loadTemplates() {
366
+ const response = await apiFetch("/run-templates?limit=200");
367
+ const payload = await readJsonSafe(response);
368
+ if (!response.ok) {
369
+ throw new Error(payload.error || `Failed to load templates (${response.status}).`);
370
+ }
371
+ state.templates = payload.templates || [];
372
+ renderTemplateOptions();
373
+ const selectedTemplate = getSelectedTemplate();
374
+ if (selectedTemplate) {
375
+ applyTemplateToForm(selectedTemplate);
376
+ }
377
+ }
378
+
379
+ function collectTemplatePayload() {
380
+ const timeoutSeconds = Number.parseInt(handoffTimeoutSecondsEl.value, 10);
381
+ return {
382
+ id: state.selectedTemplateId || undefined,
383
+ name: templateNameEl.value.trim(),
384
+ task_instruction: taskInstructionEl.value.trim(),
385
+ browser_source: browserSourceEl.value || "auto",
386
+ cdp_endpoint: cdpEndpointEl.value.trim() || null,
387
+ start_url: templateStartUrlEl.value.trim(),
388
+ pre_login_checks: parseRuleLines(templateLoginChecksEl.value),
389
+ assertion_rules: parseRuleLines(templateAssertionsEl.value),
390
+ timeout_policy: {
391
+ max_steps: Number.parseInt(maxStepsEl.value, 10) || 12,
392
+ handoff_timeout_ms: Number.isFinite(timeoutSeconds) ? Math.max(5, timeoutSeconds) * 1000 : 300000,
393
+ },
394
+ };
395
+ }
396
+
397
+ function summarizeEvent(event) {
398
+ const payload = event?.payload || {};
399
+
400
+ switch (event?.type) {
401
+ case "goto":
402
+ return `Navigated to ${payload.url || "unknown URL"}.`;
403
+ case "observe":
404
+ return `Observed ${payload.count ?? 0} interactable elements.`;
405
+ case "click":
406
+ return `Clicked SoM target ${payload.id ?? "unknown"}.`;
407
+ case "type":
408
+ return `Typed into SoM target ${payload.id ?? "unknown"}.`;
409
+ case "hover":
410
+ return `Hovered SoM target ${payload.id ?? "unknown"}.`;
411
+ case "scroll":
412
+ return `Scrolled ${payload.direction || "unknown direction"}.`;
413
+ case "keypress":
414
+ return `Pressed ${payload.key || "unknown key"}.`;
415
+ case "subagent_task_started":
416
+ return `Started delegated task with max ${payload.maxSteps ?? "?"} steps.`;
417
+ case "subagent_decision":
418
+ return `Planner produced a ${payload.decision?.next_action?.type || "no-op"} action.`;
419
+ case "subagent_verification":
420
+ return payload.verification?.summary || "Verification finished.";
421
+ case "subagent_input_requested":
422
+ return payload.question || "Subagent requested guidance.";
423
+ case "subagent_input_received":
424
+ return "Main agent guidance received.";
425
+ case "task_runner_abort_requested":
426
+ return payload.reason || "Abort requested.";
427
+ case "task_runner_manual_control_requested":
428
+ return payload.reason || "Manual control requested.";
429
+ case "subagent_task_aborted":
430
+ return payload.summary || "Delegated task aborted.";
431
+ case "subagent_task_completed":
432
+ return payload.summary || "Delegated task completed.";
433
+ case "subagent_task_failed":
434
+ return payload.summary || "Delegated task failed.";
435
+ case "session_started":
436
+ return payload.reusedStorageState ? "Browser session started with saved storage state." : "Browser session started with a fresh storage state.";
437
+ case "session_auto_attached":
438
+ return "Auto source attached to the current Chrome session.";
439
+ case "session_auto_attach_failed":
440
+ return `Auto attach failed, falling back to managed runtime: ${payload.error || "unknown reason"}.`;
441
+ case "session_auto_fallback_started":
442
+ return "Auto source switched to managed runtime fallback.";
443
+ case "session_switch_requested":
444
+ return `Switching browser mode from ${payload.from || "unknown"} to ${payload.to || "unknown"}.`;
445
+ case "session_switched":
446
+ return `Browser relaunched in ${payload.browserMode || "unknown"} mode.`;
447
+ case "session_switch_completed":
448
+ return `Browser mode switch completed: ${payload.from || "unknown"} to ${payload.to || "unknown"}.`;
449
+ case "session_switch_failed":
450
+ return `Browser mode switch failed: ${payload.error || "unknown error"}.`;
451
+ case "session_switch_recovered":
452
+ return `Browser session recovered in ${payload.browserMode || "previous"} mode.`;
453
+ case "session_switch_recovery_failed":
454
+ return `Browser session recovery failed: ${payload.error || "unknown error"}.`;
455
+ case "session_stopping":
456
+ return "Browser session is stopping and flushing artifacts.";
457
+ default:
458
+ return "Event recorded.";
459
+ }
460
+ }
461
+
462
+ function renderEventList(container, entries, options = {}) {
463
+ container.innerHTML = "";
464
+ const focusWindow = options.focusWindow || null;
465
+ if (!entries?.length) {
466
+ const empty = document.createElement("p");
467
+ empty.textContent = "No runtime events recorded yet.";
468
+ container.appendChild(empty);
469
+ return;
470
+ }
471
+
472
+ const filteredEntries = [...entries].reverse().filter((entry) => {
473
+ if (state.eventFilter === "all") return true;
474
+ if (state.eventFilter === "subagent") return String(entry.type || "").startsWith("subagent_");
475
+ if (state.eventFilter === "browser") return ["goto", "observe", "click", "type", "hover", "scroll", "keypress", "session_started", "session_auto_attached", "session_auto_attach_failed", "session_auto_fallback_started", "session_switch_requested", "session_switched", "session_switch_completed", "session_switch_failed", "session_switch_recovered", "session_switch_recovery_failed", "session_stopping"].includes(entry.type);
476
+ if (state.eventFilter === "control") return String(entry.type || "").startsWith("task_runner_");
477
+ if (state.eventFilter === "errors") return ["subagent_task_failed", "subagent_task_aborted", "task_runner_abort_requested", "invalid_event"].includes(entry.type);
478
+ return true;
479
+ }).filter((entry) => {
480
+ if (!focusWindow) {
481
+ return true;
482
+ }
483
+ const timestampMs = parseTimestampMs(entry.timestamp);
484
+ if (!timestampMs) {
485
+ return false;
486
+ }
487
+ if (focusWindow.startMs && timestampMs < (focusWindow.startMs - 1200)) {
488
+ return false;
489
+ }
490
+ if (focusWindow.endMs && timestampMs > (focusWindow.endMs + 1200)) {
491
+ return false;
492
+ }
493
+ return true;
494
+ });
495
+
496
+ if (!filteredEntries.length) {
497
+ const empty = document.createElement("p");
498
+ empty.textContent = focusWindow
499
+ ? "No runtime events match this filter in the selected step window."
500
+ : "No runtime events match this filter.";
501
+ container.appendChild(empty);
502
+ return;
503
+ }
504
+
505
+ for (const entry of filteredEntries) {
506
+ const item = document.createElement("article");
507
+ item.className = "event-entry";
508
+ item.innerHTML = `
509
+ <div class="event-entry-top">
510
+ <span class="event-type">${escapeHtml(entry.type || "event")}</span>
511
+ <time>${escapeHtml(formatTimestamp(entry.timestamp))}</time>
512
+ </div>
513
+ <p class="event-summary">${escapeHtml(summarizeEvent(entry))}</p>
514
+ <details>
515
+ <summary>Payload</summary>
516
+ <pre>${escapeHtml(JSON.stringify(entry.payload || {}, null, 2))}</pre>
517
+ </details>
518
+ `;
519
+ container.appendChild(item);
520
+ }
521
+ }
522
+
523
+ function renderOperatorMessages(container, messages) {
524
+ container.innerHTML = "";
525
+ const filteredMessages = (messages || []).filter((message) => {
526
+ if (state.messageFilter === "all") return true;
527
+ return message.role === state.messageFilter;
528
+ });
529
+
530
+ if (!filteredMessages.length) {
531
+ const empty = document.createElement("p");
532
+ empty.textContent = messages?.length ? "No messages match this filter." : "No operator messages yet.";
533
+ container.appendChild(empty);
534
+ return;
535
+ }
536
+
537
+ for (const message of [...filteredMessages].reverse()) {
538
+ const item = document.createElement("article");
539
+ item.className = "message-entry";
540
+ item.innerHTML = `
541
+ <div class="message-entry-header">
542
+ <span>${escapeHtml(message.role || "operator")}</span>
543
+ <time>${escapeHtml(formatTimestamp(message.timestamp))}</time>
544
+ </div>
545
+ <p>${escapeHtml(message.content || "")}</p>
546
+ `;
547
+ container.appendChild(item);
548
+ }
549
+ }
550
+
551
+ function renderRunList() {
552
+ runCountEl.textContent = String(state.runs.length);
553
+ runListEl.innerHTML = "";
554
+
555
+ if (!state.runs.length) {
556
+ const empty = document.createElement("p");
557
+ empty.className = "empty-list";
558
+ empty.textContent = "No runs yet. Queue one below.";
559
+ runListEl.appendChild(empty);
560
+ return;
561
+ }
562
+
563
+ for (const run of state.runs) {
564
+ const card = document.createElement("button");
565
+ card.type = "button";
566
+ card.className = `run-card${run.id === state.selectedRunId ? " active" : ""}`;
567
+ card.innerHTML = `
568
+ <div class="run-card-top">
569
+ <h3>${run.id.slice(0, 8)}</h3>
570
+ <span class="status-chip" data-status="${run.status}">${run.status}</span>
571
+ </div>
572
+ <p>${run.summary || run.taskInstruction}</p>
573
+ <div class="run-card-bottom">
574
+ <time>${formatDate(run.createdAt)}</time>
575
+ <span>${durationText(run)}</span>
576
+ </div>
577
+ `;
578
+ card.addEventListener("click", () => {
579
+ state.selectedRunId = run.id;
580
+ state.focusedHistory = null;
581
+ renderRunList();
582
+ void loadRunDetail(run.id);
583
+ });
584
+ runListEl.appendChild(card);
585
+ }
586
+ }
587
+
588
+ async function loadRuns() {
589
+ const response = await apiFetch("/runs?limit=30");
590
+ const payload = await readJsonSafe(response);
591
+ if (!response.ok) {
592
+ throw new Error(payload.error || `Failed to load runs (${response.status}).`);
593
+ }
594
+ state.runs = payload.runs || [];
595
+
596
+ if (!state.selectedRunId && state.runs.length) {
597
+ state.selectedRunId = state.runs[0].id;
598
+ }
599
+
600
+ const activeRunStillExists = state.runs.some((run) => run.id === state.selectedRunId);
601
+ if (!activeRunStillExists) {
602
+ state.selectedRunId = state.runs[0]?.id || null;
603
+ }
604
+
605
+ renderRunList();
606
+
607
+ if (state.selectedRunId) {
608
+ await loadRunDetail(state.selectedRunId);
609
+ } else {
610
+ showEmptyState();
611
+ }
612
+ }
613
+
614
+ function showEmptyState() {
615
+ state.currentRun = null;
616
+ state.focusedHistory = null;
617
+ emptyStateEl.classList.remove("hidden");
618
+ runDetailEl.classList.add("hidden");
619
+ }
620
+
621
+ function showRunDetail() {
622
+ emptyStateEl.classList.add("hidden");
623
+ runDetailEl.classList.remove("hidden");
624
+ }
625
+
626
+ function renderDetail(run) {
627
+ const previousRunId = state.currentRun?.id || null;
628
+ state.currentRun = run;
629
+ showRunDetail();
630
+ const effectiveSource = run.result?.debug?.sessionSource || run.browserSource || "auto";
631
+ const fallbackReason = run.result?.debug?.autoFallbackReason || null;
632
+ detailTitleEl.textContent = `Run ${run.id.slice(0, 8)}`;
633
+ setStatusChip(detailStatusEl, run.status);
634
+ detailSummaryEl.textContent = run.summary || "No summary yet.";
635
+ detailTaskEl.textContent = run.taskInstruction || "No task description.";
636
+ detailTimingEl.textContent = [
637
+ `Requested source: ${run.browserSource || "auto"}`,
638
+ `Effective source: ${effectiveSource}`,
639
+ `Created: ${formatDate(run.createdAt)}`,
640
+ `Started: ${formatDate(run.startedAt)}`,
641
+ `Finished: ${formatDate(run.finishedAt)}`,
642
+ `Duration: ${durationText(run)}`,
643
+ ].join("\n");
644
+ const capabilities = run.result?.capabilities || run.result?.debug?.capabilities || null;
645
+ detailRuntimeEl.textContent = capabilities
646
+ ? [
647
+ `Browser mode: ${run.result?.debug?.browserMode || run.browserSource || "unknown"}`,
648
+ `Visible browser: ${capabilities.visibleBrowser ? "yes" : "no"}`,
649
+ `Videos: ${capabilities.videos ? "available" : "not guaranteed"}`,
650
+ `Traces: ${capabilities.traces ? "available" : "limited"}`,
651
+ `Mode switching: ${capabilities.modeSwitching ? "supported" : "not supported"}`,
652
+ ].join("\n")
653
+ : "Runtime capability summary not available yet.";
654
+ if (run.browserSource === "attached") {
655
+ detailRuntimeNoteEl.textContent = "Attached runs operate on your current Chrome session. They usually preserve live login state better, but some artifacts and automation guarantees can be more limited than the managed runtime.";
656
+ } else if (run.browserSource === "auto") {
657
+ detailRuntimeNoteEl.textContent = effectiveSource === "attached"
658
+ ? "Auto mode successfully attached to your current Chrome session for better live login reuse."
659
+ : `Auto mode is running in managed fallback.${fallbackReason ? ` Reason: ${fallbackReason}` : ""}`;
660
+ } else {
661
+ detailRuntimeNoteEl.textContent = "Managed runs launch a dedicated browser owned by the daemon. They provide stronger artifact capture and more predictable automation behavior.";
662
+ }
663
+ const templateEvaluation = run.templateEvaluation || run.result?.templateEvaluation || null;
664
+ if (run.template) {
665
+ const checksSummary = templateEvaluation
666
+ ? `Checks: ${templateEvaluation.passed ? "passed" : "failed"} (${(templateEvaluation.failureMessages || []).length} issues)`
667
+ : "Checks: pending";
668
+ detailTemplateEl.textContent = [
669
+ `Name: ${run.template.name || run.template.id}`,
670
+ `ID: ${run.template.id}`,
671
+ checksSummary,
672
+ ].join("\n");
673
+ } else {
674
+ detailTemplateEl.textContent = "No run template attached.";
675
+ }
676
+
677
+ detailLinksEl.innerHTML = "";
678
+ if (run.reports?.reportJsonUrl) detailLinksEl.appendChild(createLink("task-report.json", run.reports.reportJsonUrl));
679
+ if (run.reports?.walkthroughUrl) detailLinksEl.appendChild(createLink("walkthrough.md", run.reports.walkthroughUrl));
680
+ if (run.reports?.timelineJsonUrl) detailLinksEl.appendChild(createLink("task-timeline.json", run.reports.timelineJsonUrl));
681
+ if (run.artifacts?.traceUrls?.length) {
682
+ run.artifacts.traceUrls.forEach((url, index) => {
683
+ const btn = createTraceViewerButton(`View Trace ${index + 1}`, url);
684
+ if (btn) detailLinksEl.appendChild(btn);
685
+ });
686
+ } else if (run.artifacts?.traceUrl) {
687
+ const btn = createTraceViewerButton("View Trace", run.artifacts.traceUrl);
688
+ if (btn) detailLinksEl.appendChild(btn);
689
+ }
690
+ if (run.artifacts?.eventsUrl) detailLinksEl.appendChild(createLink("events.jsonl", run.artifacts.eventsUrl));
691
+ if (run.artifacts?.videoUrls?.[0]) detailLinksEl.appendChild(createLink("video", run.artifacts.videoUrls[0]));
692
+
693
+ const history = (run.result?.history || []).map((entry, index) => ({
694
+ ...entry,
695
+ _index: index,
696
+ actionDurationMs: normalizeNumber(entry.actionDurationMs),
697
+ elapsedMs: normalizeNumber(entry.elapsedMs),
698
+ videoOffsetSeconds: normalizeNumber(entry.videoOffsetSeconds),
699
+ }));
700
+
701
+ const focusedHistory = getFocusedHistoryEntry(run, history);
702
+ if (!focusedHistory && state.focusedHistory?.runId === run.id) {
703
+ state.focusedHistory = null;
704
+ }
705
+ const focusLabel = focusedHistory ? `Step ${focusedHistory.step}` : "Latest run view";
706
+ detailStepFocusEl.textContent = focusLabel;
707
+ clearStepFocusButtonEl.disabled = !focusedHistory;
708
+
709
+ const screenshotUrl = focusedHistory?.actionResult?.screenshotUrl || run.latestScreenshotUrl || null;
710
+ setScreenshot(screenshotUrl, run.id, focusLabel);
711
+ applyVideoState(run.artifacts?.videoUrls?.[0] || null, focusedHistory);
712
+
713
+ detailWalkthroughEl.textContent = run.walkthroughContent || "Walkthrough not available yet.";
714
+ if (previousRunId !== run.id) {
715
+ replyInstructionEl.value = "";
716
+ replyMessageEl.textContent = "";
717
+ }
718
+
719
+ detailHistoryEl.innerHTML = "";
720
+ if (!history.length) {
721
+ const empty = document.createElement("p");
722
+ empty.textContent = "No browser actions recorded yet.";
723
+ detailHistoryEl.appendChild(empty);
724
+ } else {
725
+ for (const entry of history) {
726
+ const historyScreenshotUrl = entry.actionResult?.screenshotUrl
727
+ ? withTokenQuery(entry.actionResult.screenshotUrl)
728
+ : null;
729
+ const item = document.createElement("article");
730
+ const isFocused = focusedHistory?._index === entry._index;
731
+ item.className = `history-item${isFocused ? " active" : ""}`;
732
+ const timingLine = [
733
+ `start ${formatTimestamp(entry.stepStartedAt)}`,
734
+ `end ${formatTimestamp(entry.stepFinishedAt)}`,
735
+ `action ${formatDurationMs(entry.actionDurationMs)}`,
736
+ `elapsed ${formatDurationMs(entry.elapsedMs)}`,
737
+ `video ${entry.videoOffsetSeconds === null ? "n/a" : `${entry.videoOffsetSeconds.toFixed(1)}s`}`,
738
+ ].join(" • ");
739
+ item.innerHTML = `
740
+ <div class="history-item-header">
741
+ <strong>Step ${escapeHtml(entry.step)}</strong>
742
+ <span>${escapeHtml(entry.action?.type || "unknown action")}</span>
743
+ </div>
744
+ <p>${escapeHtml(entry.summary || entry.thinking || "No planner note.")}</p>
745
+ <p><strong>Verification:</strong> ${escapeHtml(entry.verification?.summary || "No verification summary.")}</p>
746
+ <p class="history-timing">${escapeHtml(timingLine)}</p>
747
+ ${historyScreenshotUrl ? `<div class="history-screenshot"><a href="${escapeHtml(historyScreenshotUrl)}" target="_blank" rel="noreferrer">Open screenshot</a></div>` : ""}
748
+ `;
749
+ item.addEventListener("click", () => {
750
+ state.focusedHistory = { runId: run.id, index: entry._index };
751
+ renderDetail(run);
752
+ });
753
+ detailHistoryEl.appendChild(item);
754
+ }
755
+ }
756
+
757
+ renderLogList(detailConsoleEl, run.result?.debug?.recentConsole || [], (entry) => `
758
+ <strong>${escapeHtml(entry.type || "log")}</strong>
759
+ <div>${escapeHtml(entry.text || "")}</div>
760
+ `);
761
+
762
+ renderLogList(detailNetworkEl, run.result?.debug?.recentNetwork || [], (entry) => `
763
+ <strong>${escapeHtml(entry.method || "GET")} ${escapeHtml(entry.status ?? "")}</strong>
764
+ <div>${escapeHtml(entry.url || "")}</div>
765
+ <div>${escapeHtml(entry.resourceType || "")}${entry.failure ? ` • ${escapeHtml(entry.failure)}` : ""}</div>
766
+ `);
767
+
768
+ renderLogList(detailErrorsEl, run.result?.debug?.recentErrors || [], (entry) => `
769
+ <strong>${escapeHtml(entry.message || "Page error")}</strong>
770
+ <div>${escapeHtml(entry.stack ? "Stack trace available below." : "No stack trace captured.")}</div>
771
+ ${entry.stack ? `<pre>${escapeHtml(entry.stack)}</pre>` : ""}
772
+ `);
773
+
774
+ const focusWindow = buildFocusWindow(history, focusedHistory);
775
+ renderEventList(detailEventsEl, run.recentEvents || [], { focusWindow });
776
+ renderOperatorMessages(operatorMessagesEl, run.result?.operatorMessages || []);
777
+
778
+ const isAbortable = ["queued", "running", "waiting_for_instruction", "manual_control_requested", "manual_control", "aborting"].includes(run.status);
779
+ const canManualControl = ["running", "waiting_for_instruction", "manual_control_requested", "manual_control"].includes(run.status);
780
+ const canResume = ["waiting_for_instruction", "manual_control"].includes(run.status);
781
+ runControlBarEl.classList.toggle("hidden", !isAbortable);
782
+ abortRunButtonEl.disabled = !isAbortable;
783
+ abortRunButtonSecondaryEl.disabled = !isAbortable;
784
+ manualControlButtonEl.disabled = !canManualControl || run.status === "manual_control";
785
+ resumeRunButtonEl.disabled = !canResume;
786
+
787
+ if (run.pendingInput) {
788
+ coordinationEmptyEl.classList.add("hidden");
789
+ pendingInputPanelEl.classList.remove("hidden");
790
+ pendingInputQuestionEl.textContent = run.pendingInput.question || "The subagent is waiting for guidance.";
791
+ pendingInputDetailsEl.textContent = run.pendingInput.details || "No extra details provided.";
792
+ pendingInputSuggestionEl.textContent = run.pendingInput.suggestedReply ? `Suggested reply: ${run.pendingInput.suggestedReply}` : "No suggested reply.";
793
+ } else {
794
+ coordinationEmptyEl.classList.remove("hidden");
795
+ pendingInputPanelEl.classList.add("hidden");
796
+ pendingInputQuestionEl.textContent = "";
797
+ pendingInputDetailsEl.textContent = "";
798
+ pendingInputSuggestionEl.textContent = "";
799
+ }
800
+ }
801
+
802
+ async function loadRunDetail(runId) {
803
+ const response = await apiFetch(`/runs/${runId}`);
804
+ if (!response.ok) {
805
+ showEmptyState();
806
+ return;
807
+ }
808
+
809
+ const payload = await readJsonSafe(response);
810
+ renderDetail(payload.run);
811
+ }
812
+
813
+ async function queueRun(event) {
814
+ event.preventDefault();
815
+ const taskInstruction = taskInstructionEl.value.trim();
816
+ const templateId = state.selectedTemplateId || templateSelectEl.value || "";
817
+ const maxSteps = Number.parseInt(maxStepsEl.value, 10) || 12;
818
+ const handoffTimeoutSeconds = Number.parseInt(handoffTimeoutSecondsEl.value, 10) || 300;
819
+ const browserSource = browserSourceEl.value || "auto";
820
+ const cdpEndpoint = cdpEndpointEl.value.trim() || "http://127.0.0.1:9222";
821
+
822
+ if (!taskInstruction && !templateId) {
823
+ formMessageEl.textContent = "Task instruction is required when no template is selected.";
824
+ return;
825
+ }
826
+
827
+ if (browserSource !== "managed") {
828
+ formMessageEl.textContent = "Running attach diagnostics...";
829
+ try {
830
+ const diagnosticsResponse = await apiFetch(`/attach-diagnostics?${new URLSearchParams({ cdp_endpoint: cdpEndpoint }).toString()}`);
831
+ const diagnosticsPayload = await readJsonSafe(diagnosticsResponse);
832
+ const diagnostics = diagnosticsPayload?.diagnostics;
833
+ const primaryHint = diagnostics?.hints?.[0] || diagnostics?.health?.error || "endpoint unavailable";
834
+ if ((!diagnosticsResponse.ok || !diagnostics?.ok) && browserSource === "attached") {
835
+ formMessageEl.textContent = `Cannot attach to Chrome: ${primaryHint}`;
836
+ return;
837
+ }
838
+ if ((!diagnosticsResponse.ok || !diagnostics?.ok) && browserSource === "auto") {
839
+ formMessageEl.textContent = `Attach diagnostic warning: ${primaryHint} Auto mode will fall back to managed runtime if needed.`;
840
+ }
841
+ } catch (error) {
842
+ if (browserSource === "attached") {
843
+ formMessageEl.textContent = `Cannot attach to Chrome: ${error.message}`;
844
+ return;
845
+ }
846
+ formMessageEl.textContent = `CDP health check skipped: ${error.message}. Auto mode may fall back to managed runtime.`;
847
+ }
848
+ }
849
+
850
+ formMessageEl.textContent = "Queueing run...";
851
+
852
+ const response = await apiFetch("/runs", {
853
+ method: "POST",
854
+ headers: { "Content-Type": "application/json" },
855
+ body: JSON.stringify({
856
+ task_instruction: taskInstruction,
857
+ max_steps: maxSteps,
858
+ browser_source: browserSource,
859
+ cdp_endpoint: cdpEndpoint || null,
860
+ template_id: templateId || null,
861
+ handoff_timeout_ms: Math.max(5, handoffTimeoutSeconds) * 1000,
862
+ }),
863
+ });
864
+
865
+ const payload = await readJsonSafe(response);
866
+ if (!response.ok) {
867
+ formMessageEl.textContent = payload.error || "Failed to queue run.";
868
+ return;
869
+ }
870
+
871
+ state.selectedRunId = payload.run.id;
872
+ if (!templateId) {
873
+ taskInstructionEl.value = "";
874
+ }
875
+ formMessageEl.textContent = `Queued run ${payload.run.id.slice(0, 8)}.`;
876
+ await loadRuns();
877
+ }
878
+
879
+ async function saveTemplateFromForm() {
880
+ const payload = collectTemplatePayload();
881
+ if (!payload.name) {
882
+ formMessageEl.textContent = "Template name is required.";
883
+ return;
884
+ }
885
+
886
+ formMessageEl.textContent = "Saving template...";
887
+ const response = await apiFetch("/run-templates", {
888
+ method: "POST",
889
+ headers: { "Content-Type": "application/json" },
890
+ body: JSON.stringify(payload),
891
+ });
892
+ const data = await readJsonSafe(response);
893
+ if (!response.ok) {
894
+ formMessageEl.textContent = data.error || "Failed to save template.";
895
+ return;
896
+ }
897
+
898
+ state.selectedTemplateId = data.template?.id || "";
899
+ await loadTemplates();
900
+ templateSelectEl.value = state.selectedTemplateId;
901
+ templateDeleteButtonEl.disabled = !state.selectedTemplateId;
902
+ formMessageEl.textContent = `Template saved: ${data.template?.name || "Unnamed"}.`;
903
+ }
904
+
905
+ async function deleteSelectedTemplate() {
906
+ const template = getSelectedTemplate();
907
+ if (!template) {
908
+ formMessageEl.textContent = "Select a template to delete.";
909
+ return;
910
+ }
911
+
912
+ formMessageEl.textContent = `Deleting template ${template.name}...`;
913
+ const response = await apiFetch(`/run-templates/${template.id}`, {
914
+ method: "DELETE",
915
+ });
916
+ const data = await readJsonSafe(response);
917
+ if (!response.ok) {
918
+ formMessageEl.textContent = data.error || "Failed to delete template.";
919
+ return;
920
+ }
921
+
922
+ state.selectedTemplateId = "";
923
+ await loadTemplates();
924
+ formMessageEl.textContent = `Template deleted: ${template.name}.`;
925
+ }
926
+
927
+ async function refreshNow() {
928
+ try {
929
+ await loadRuns();
930
+ } catch (error) {
931
+ formMessageEl.textContent = `Refresh failed: ${error.message}`;
932
+ }
933
+ }
934
+
935
+ async function sendReply(event) {
936
+ event.preventDefault();
937
+ if (!state.selectedRunId) {
938
+ replyMessageEl.textContent = "Select a run first.";
939
+ return;
940
+ }
941
+
942
+ const instruction = replyInstructionEl.value.trim();
943
+ if (!instruction) {
944
+ replyMessageEl.textContent = "Reply text is required.";
945
+ return;
946
+ }
947
+
948
+ replyMessageEl.textContent = "Sending reply...";
949
+
950
+ const response = await apiFetch(`/runs/${state.selectedRunId}/reply`, {
951
+ method: "POST",
952
+ headers: { "Content-Type": "application/json" },
953
+ body: JSON.stringify({ instruction }),
954
+ });
955
+
956
+ const payload = await readJsonSafe(response);
957
+ if (!response.ok) {
958
+ replyMessageEl.textContent = payload.error || "Failed to send reply.";
959
+ return;
960
+ }
961
+
962
+ replyMessageEl.textContent = "Reply sent. The run is resuming.";
963
+ replyInstructionEl.value = "";
964
+ await refreshNow();
965
+ }
966
+
967
+ async function abortRun() {
968
+ if (!state.selectedRunId) {
969
+ replyMessageEl.textContent = "Select a run first.";
970
+ return;
971
+ }
972
+
973
+ replyMessageEl.textContent = "Aborting run...";
974
+ const response = await apiFetch(`/runs/${state.selectedRunId}/abort`, {
975
+ method: "POST",
976
+ headers: { "Content-Type": "application/json" },
977
+ body: JSON.stringify({
978
+ reason: "Run aborted from the dashboard.",
979
+ }),
980
+ });
981
+
982
+ const payload = await readJsonSafe(response);
983
+ if (!response.ok) {
984
+ replyMessageEl.textContent = payload.error || "Failed to abort run.";
985
+ return;
986
+ }
987
+
988
+ replyMessageEl.textContent = "Abort requested.";
989
+ await refreshNow();
990
+ }
991
+
992
+ async function requestManualControl() {
993
+ if (!state.selectedRunId) {
994
+ replyMessageEl.textContent = "Select a run first.";
995
+ return;
996
+ }
997
+
998
+ replyMessageEl.textContent = "Requesting manual control...";
999
+ const response = await apiFetch(`/runs/${state.selectedRunId}/manual-control`, {
1000
+ method: "POST",
1001
+ headers: { "Content-Type": "application/json" },
1002
+ body: JSON.stringify({
1003
+ reason: "Manual control requested from the dashboard.",
1004
+ }),
1005
+ });
1006
+
1007
+ const payload = await readJsonSafe(response);
1008
+ if (!response.ok) {
1009
+ replyMessageEl.textContent = payload.error || "Failed to enter manual control.";
1010
+ return;
1011
+ }
1012
+
1013
+ replyMessageEl.textContent = "Manual control requested.";
1014
+ await refreshNow();
1015
+ }
1016
+
1017
+ async function resumeRun() {
1018
+ if (!state.selectedRunId) {
1019
+ replyMessageEl.textContent = "Select a run first.";
1020
+ return;
1021
+ }
1022
+
1023
+ const instruction = replyInstructionEl.value.trim() || "Manual control complete. Continue from the current page.";
1024
+ replyMessageEl.textContent = "Resuming run...";
1025
+
1026
+ const response = await apiFetch(`/runs/${state.selectedRunId}/resume`, {
1027
+ method: "POST",
1028
+ headers: { "Content-Type": "application/json" },
1029
+ body: JSON.stringify({ instruction }),
1030
+ });
1031
+
1032
+ const payload = await readJsonSafe(response);
1033
+ if (!response.ok) {
1034
+ replyMessageEl.textContent = payload.error || "Failed to resume run.";
1035
+ return;
1036
+ }
1037
+
1038
+ replyInstructionEl.value = "";
1039
+ replyMessageEl.textContent = "Run resumed.";
1040
+ await refreshNow();
1041
+ }
1042
+
1043
+ function startPolling() {
1044
+ if (state.pollHandle) {
1045
+ clearInterval(state.pollHandle);
1046
+ }
1047
+ state.pollHandle = setInterval(() => {
1048
+ void refreshNow();
1049
+ }, 4000);
1050
+ }
1051
+
1052
+ function startEventStream() {
1053
+ const stream = new EventSource(withTokenQuery("/runs/stream"));
1054
+ stream.onmessage = () => {
1055
+ void refreshNow();
1056
+ };
1057
+ stream.onerror = () => {
1058
+ stream.close();
1059
+ setTimeout(startEventStream, 3000);
1060
+ };
1061
+ }
1062
+
1063
+ refreshButtonEl.addEventListener("click", () => {
1064
+ void refreshNow();
1065
+ void loadTemplates().catch(() => {});
1066
+ });
1067
+ templateRefreshButtonEl.addEventListener("click", () => {
1068
+ void loadTemplates().catch((error) => {
1069
+ formMessageEl.textContent = `Template refresh failed: ${error.message}`;
1070
+ });
1071
+ });
1072
+ templateSaveButtonEl.addEventListener("click", () => {
1073
+ void saveTemplateFromForm();
1074
+ });
1075
+ templateDeleteButtonEl.addEventListener("click", () => {
1076
+ void deleteSelectedTemplate();
1077
+ });
1078
+ templateSelectEl.addEventListener("change", () => {
1079
+ state.selectedTemplateId = templateSelectEl.value || "";
1080
+ templateDeleteButtonEl.disabled = !state.selectedTemplateId;
1081
+ const template = getSelectedTemplate();
1082
+ if (template) {
1083
+ applyTemplateToForm(template);
1084
+ }
1085
+ });
1086
+
1087
+ runFormEl.addEventListener("submit", queueRun);
1088
+ replyFormEl.addEventListener("submit", (event) => {
1089
+ void sendReply(event);
1090
+ });
1091
+ manualControlButtonEl.addEventListener("click", () => {
1092
+ void requestManualControl();
1093
+ });
1094
+ resumeRunButtonEl.addEventListener("click", () => {
1095
+ void resumeRun();
1096
+ });
1097
+ abortRunButtonEl.addEventListener("click", () => {
1098
+ void abortRun();
1099
+ });
1100
+ abortRunButtonSecondaryEl.addEventListener("click", () => {
1101
+ void abortRun();
1102
+ });
1103
+ clearStepFocusButtonEl.addEventListener("click", () => {
1104
+ state.focusedHistory = null;
1105
+ if (state.currentRun) {
1106
+ renderDetail(state.currentRun);
1107
+ }
1108
+ });
1109
+ eventFilterEl.addEventListener("change", () => {
1110
+ state.eventFilter = eventFilterEl.value;
1111
+ if (state.currentRun) {
1112
+ renderDetail(state.currentRun);
1113
+ } else if (state.selectedRunId) {
1114
+ void loadRunDetail(state.selectedRunId);
1115
+ }
1116
+ });
1117
+ messageFilterEl.addEventListener("change", () => {
1118
+ state.messageFilter = messageFilterEl.value;
1119
+ if (state.currentRun) {
1120
+ renderDetail(state.currentRun);
1121
+ } else if (state.selectedRunId) {
1122
+ void loadRunDetail(state.selectedRunId);
1123
+ }
1124
+ });
1125
+
1126
+ if (!cdpEndpointEl.value.trim()) {
1127
+ cdpEndpointEl.value = "http://127.0.0.1:9222";
1128
+ }
1129
+ if (!handoffTimeoutSecondsEl.value.trim()) {
1130
+ handoffTimeoutSecondsEl.value = "300";
1131
+ }
1132
+ templateDeleteButtonEl.disabled = true;
1133
+
1134
+ startPolling();
1135
+ startEventStream();
1136
+ void loadTemplates().catch((error) => {
1137
+ formMessageEl.textContent = `Template load failed: ${error.message}`;
1138
+ });
1139
+ void refreshNow();