@post-print/agent-test 0.3.13 → 1.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.
Files changed (78) hide show
  1. package/README.md +23 -7
  2. package/dist/cli.js +2 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/compare-scenario.d.ts +16 -28
  5. package/dist/compare-scenario.d.ts.map +1 -1
  6. package/dist/compare-scenario.js +163 -261
  7. package/dist/compare-scenario.js.map +1 -1
  8. package/dist/context-panel.d.ts +5 -0
  9. package/dist/context-panel.d.ts.map +1 -0
  10. package/dist/context-panel.js +60 -0
  11. package/dist/context-panel.js.map +1 -0
  12. package/dist/debug-bundle.d.ts.map +1 -1
  13. package/dist/debug-bundle.js +2 -0
  14. package/dist/debug-bundle.js.map +1 -1
  15. package/dist/expect.d.ts +1 -0
  16. package/dist/expect.d.ts.map +1 -1
  17. package/dist/expect.js +19 -0
  18. package/dist/expect.js.map +1 -1
  19. package/dist/html-report.d.ts.map +1 -1
  20. package/dist/html-report.js +238 -38
  21. package/dist/html-report.js.map +1 -1
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/live-isolation.d.ts +2 -0
  27. package/dist/live-isolation.d.ts.map +1 -1
  28. package/dist/live-isolation.js +66 -6
  29. package/dist/live-isolation.js.map +1 -1
  30. package/dist/record-trace.d.ts +7 -1
  31. package/dist/record-trace.d.ts.map +1 -1
  32. package/dist/record-trace.js.map +1 -1
  33. package/dist/run-suite.d.ts.map +1 -1
  34. package/dist/run-suite.js +137 -31
  35. package/dist/run-suite.js.map +1 -1
  36. package/dist/scenario-story.d.ts +4 -3
  37. package/dist/scenario-story.d.ts.map +1 -1
  38. package/dist/scenario-story.js +47 -71
  39. package/dist/scenario-story.js.map +1 -1
  40. package/dist/scenario-usage.d.ts.map +1 -1
  41. package/dist/scenario-usage.js +2 -3
  42. package/dist/scenario-usage.js.map +1 -1
  43. package/dist/types.d.ts +61 -24
  44. package/dist/types.d.ts.map +1 -1
  45. package/dist/validate-suite.d.ts.map +1 -1
  46. package/dist/validate-suite.js +100 -38
  47. package/dist/validate-suite.js.map +1 -1
  48. package/dist/viewer/catalog.d.ts +6 -2
  49. package/dist/viewer/catalog.d.ts.map +1 -1
  50. package/dist/viewer/catalog.js +14 -0
  51. package/dist/viewer/catalog.js.map +1 -1
  52. package/dist/viewer/compare-metrics.d.ts +30 -0
  53. package/dist/viewer/compare-metrics.d.ts.map +1 -0
  54. package/dist/viewer/compare-metrics.js +102 -0
  55. package/dist/viewer/compare-metrics.js.map +1 -0
  56. package/dist/viewer/context-files.d.ts +8 -0
  57. package/dist/viewer/context-files.d.ts.map +1 -0
  58. package/dist/viewer/context-files.js +57 -0
  59. package/dist/viewer/context-files.js.map +1 -0
  60. package/dist/viewer/events.d.ts +20 -0
  61. package/dist/viewer/events.d.ts.map +1 -1
  62. package/dist/viewer/events.js +2 -0
  63. package/dist/viewer/events.js.map +1 -1
  64. package/dist/viewer/live-runner.d.ts +6 -0
  65. package/dist/viewer/live-runner.d.ts.map +1 -1
  66. package/dist/viewer/live-runner.js +34 -5
  67. package/dist/viewer/live-runner.js.map +1 -1
  68. package/dist/viewer/page.d.ts.map +1 -1
  69. package/dist/viewer/page.js +1463 -93
  70. package/dist/viewer/page.js.map +1 -1
  71. package/dist/viewer/run-controller.d.ts +5 -0
  72. package/dist/viewer/run-controller.d.ts.map +1 -1
  73. package/dist/viewer/run-controller.js +27 -1
  74. package/dist/viewer/run-controller.js.map +1 -1
  75. package/dist/viewer/server.d.ts.map +1 -1
  76. package/dist/viewer/server.js +18 -11
  77. package/dist/viewer/server.js.map +1 -1
  78. package/package.json +7 -5
@@ -41,23 +41,239 @@ function viewerCss() {
41
41
  button.danger { border-color: color-mix(in srgb, var(--fail) 40%, var(--border)); color: var(--fail); }
42
42
  button:disabled { opacity: 0.5; cursor: not-allowed; }
43
43
  .warn { color: var(--skip); font-size: 0.8rem; }
44
- .matrix { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
45
- .matrix th, .matrix td { border: 1px solid var(--border); padding: 0.5rem 0.55rem; text-align: left; vertical-align: top; }
46
- .matrix th { color: var(--muted); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em; }
44
+ .scenario-list { display: grid; gap: 0.75rem; }
45
+ .scenario-card {
46
+ background: var(--panel);
47
+ border: 1px solid var(--border);
48
+ border-radius: 12px;
49
+ padding: 0.85rem 0.95rem;
50
+ display: grid;
51
+ gap: 0.7rem;
52
+ }
47
53
  .scenario-detail { display: grid; gap: 0.35rem; }
48
54
  .prompt-preview { color: var(--muted); font-size: 0.78rem; white-space: pre-wrap; }
49
55
  .rubric-line { font-size: 0.75rem; color: var(--muted); }
50
56
  .arm-note { font-size: 0.78rem; color: var(--muted); }
51
- .cell-actions { display: flex; flex-wrap: wrap; gap: 0.35rem; }
57
+ .scenario-toolbar { display: flex; flex-wrap: wrap; gap: 0.55rem 0.85rem; align-items: center; }
58
+ .scenario-actions { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }
59
+ .host-tablist { display: flex; flex-wrap: wrap; gap: 0.35rem; }
60
+ .host-tab {
61
+ display: inline-flex;
62
+ align-items: center;
63
+ gap: 0.4rem;
64
+ }
65
+ .host-tab[aria-selected="true"] {
66
+ background: color-mix(in srgb, var(--pass) 16%, var(--panel-2));
67
+ border-color: color-mix(in srgb, var(--pass) 40%, var(--border));
68
+ }
69
+ .host-tab[data-status="running"],
70
+ .host-tab[data-status="cancelling"],
71
+ .host-tab[data-status="cancelled"] { color: var(--skip); }
72
+ .host-tab[data-status="passed"] { color: var(--pass); }
73
+ .host-tab[data-status="failed"] { color: var(--fail); }
74
+ .host-tab[data-status="skipped"], .host-tab[data-status="skip"] { color: var(--muted); }
75
+ .host-status { display: inline-flex; align-items: center; }
52
76
  .cell-status { font-size: 0.72rem; font-weight: 700; text-transform: uppercase; }
53
- .live-dock { margin-top: 1.5rem; display: grid; gap: 0.85rem; }
54
- .live-cell { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 0.75rem; min-width: 0; }
77
+ .scenario-live { display: grid; gap: 0.75rem; }
78
+ .scenario-live[hidden], .host-panel[hidden], .host-empty[hidden] { display: none; }
79
+ .host-panels { display: grid; min-width: 0; }
80
+ .host-panel:not([hidden]) { min-height: 4rem; }
81
+ .host-empty { color: var(--muted); font-size: 0.85rem; margin: 0; }
82
+ .host-panel > .live-cell > h3 { display: none; }
83
+ .live-cell { background: var(--panel-2); border: 1px solid var(--border); border-radius: 12px; padding: 0.75rem; min-width: 0; }
84
+ .cell-result {
85
+ margin-top: 0.65rem;
86
+ border-left: 3px solid var(--border);
87
+ padding: 0.45rem 0.7rem;
88
+ }
89
+ .cell-result.status-passed { border-left-color: var(--pass); }
90
+ .cell-result.status-failed { border-left-color: var(--fail); }
91
+ .cell-result.status-skipped { border-left-color: var(--skip); }
92
+ .cell-result-verdict { font-weight: 650; margin: 0 0 0.25rem; }
93
+ .cell-result.status-passed .cell-result-verdict { color: var(--pass); }
94
+ .cell-result.status-failed .cell-result-verdict { color: var(--fail); }
95
+ .cell-result-metrics { color: var(--muted); font-size: 0.8rem; margin: 0 0 0.35rem; }
96
+ .cell-result-failures, .cell-result-judges {
97
+ list-style: none;
98
+ margin: 0;
99
+ padding: 0;
100
+ display: grid;
101
+ gap: 0.35rem;
102
+ }
103
+ .cell-result-judges { margin-top: 0.45rem; }
104
+ .cell-result-failures li, .cell-result-judges li {
105
+ font-size: 0.85rem;
106
+ white-space: pre-wrap;
107
+ word-break: break-word;
108
+ }
109
+ .judge-pass { color: var(--pass); }
110
+ .judge-fail { color: var(--fail); }
55
111
  .live-cell h3 { margin-bottom: 0.45rem; }
112
+ .live-status {
113
+ list-style: none;
114
+ margin: 0 0 0.55rem;
115
+ padding: 0;
116
+ display: grid;
117
+ gap: 0.2rem;
118
+ max-height: 9rem;
119
+ overflow: auto;
120
+ font-size: 0.75rem;
121
+ color: var(--muted);
122
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
123
+ }
124
+ .live-status li {
125
+ border-left: 2px solid var(--border);
126
+ padding: 0.12rem 0 0.12rem 0.5rem;
127
+ white-space: pre-wrap;
128
+ word-break: break-word;
129
+ }
56
130
  .run-banner { font-size: 0.85rem; color: var(--muted); }
131
+ .run-progress {
132
+ display: grid;
133
+ gap: 0.55rem;
134
+ background: var(--panel);
135
+ border: 1px solid var(--border);
136
+ border-radius: 12px;
137
+ padding: 0.8rem 1rem;
138
+ margin-bottom: 1.25rem;
139
+ }
140
+ .run-progress[hidden] { display: none; }
141
+ .run-progress-head, .run-progress-counts {
142
+ display: flex;
143
+ flex-wrap: wrap;
144
+ align-items: center;
145
+ justify-content: space-between;
146
+ gap: 0.4rem 1rem;
147
+ }
148
+ .run-progress-title { margin: 0; font-weight: 700; }
149
+ .run-progress-counts { justify-content: flex-start; color: var(--muted); font-size: 0.8rem; }
150
+ .run-progress-counts strong { color: var(--text); font-variant-numeric: tabular-nums; }
151
+ .run-progress-counts .progress-passed strong { color: var(--pass); }
152
+ .run-progress-counts .progress-failed strong { color: var(--fail); }
153
+ .run-progress-counts .progress-skipped strong { color: var(--skip); }
154
+ .run-progress-track {
155
+ height: 0.7rem;
156
+ overflow: hidden;
157
+ border-radius: 999px;
158
+ background: var(--panel-2);
159
+ border: 1px solid var(--border);
160
+ }
161
+ .run-progress-fill {
162
+ display: block;
163
+ width: 0;
164
+ height: 100%;
165
+ background: var(--pass);
166
+ transition: width 180ms ease-out;
167
+ }
168
+ .run-progress[data-has-failures="true"] .run-progress-fill {
169
+ background: linear-gradient(90deg, var(--pass), var(--fail));
170
+ }
171
+ .compare-tab[data-status="running"] { color: var(--skip); }
172
+ .compare-tab[data-status="passed"] { color: var(--pass); }
173
+ .compare-tab[data-status="failed"] { color: var(--fail); }
174
+ .compare-tab[data-status="skipped"] { color: var(--muted); }
175
+ .compare-tabs .compare-arms,
176
+ .compare-tabs .compare-arms[data-arm-count] { grid-template-columns: minmax(0, 1fr); }
177
+ .compare-tabs .live-cell[hidden] { display: none; }
178
+ .compare-tabs .live-cell > h3 { display: none; }
179
+ .chat-running-row[hidden] { display: none; }
180
+ .context-path {
181
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
182
+ font-size: 0.82rem;
183
+ display: block;
184
+ }
185
+ .context-why {
186
+ color: var(--muted);
187
+ font-size: 0.8rem;
188
+ display: block;
189
+ margin-top: 0.15rem;
190
+ }
191
+ .context-panel {
192
+ margin: 0 0 0.75rem;
193
+ border: 1px solid var(--border);
194
+ border-radius: 10px;
195
+ background: color-mix(in srgb, var(--skip) 10%, var(--panel-2));
196
+ padding: 0.55rem 0.75rem;
197
+ }
198
+ .context-panel-summary {
199
+ display: flex;
200
+ align-items: baseline;
201
+ justify-content: space-between;
202
+ gap: 0.75rem;
203
+ cursor: pointer;
204
+ font-weight: 700;
205
+ }
206
+ .context-panel-count { color: var(--muted); font-weight: 600; font-size: 0.82rem; }
207
+ .context-flow { display: flex; align-items: center; gap: 0.35rem; margin: 0.55rem 0; flex-wrap: wrap; }
208
+ .context-flow-node { border: 1px solid var(--border); border-radius: 999px; background: var(--panel); padding: 0.2rem 0.55rem; font-size: 0.78rem; font-weight: 650; }
209
+ .context-flow-mode { border-color: var(--skip); }
210
+ .context-flow-arrow { color: var(--muted); }
211
+ .context-host-input { border: 1px solid var(--border); border-radius: 8px; background: var(--panel); padding: 0.4rem 0.55rem; margin-top: 0.45rem; }
212
+ .context-host-input summary { cursor: pointer; font-size: 0.82rem; font-weight: 700; }
213
+ .context-panel-lede, .context-empty { color: var(--muted); font-size: 0.85rem; margin: 0.45rem 0 0; }
214
+ .context-files { list-style: none; margin: 0.5rem 0 0; padding: 0; display: grid; gap: 0.4rem; }
215
+ .context-file {
216
+ border: 1px solid var(--border);
217
+ border-radius: 8px;
218
+ background: var(--panel);
219
+ padding: 0.4rem 0.55rem;
220
+ }
221
+ .context-file[data-reason="contextSources"] { border-left: 3px solid var(--skip); }
222
+ .context-file[data-reason="skills"] { border-left: 3px solid var(--tool); }
223
+ .context-file[data-reason="profile"] { border-left: 3px solid var(--muted); }
224
+ .context-file summary { cursor: pointer; }
225
+ .context-body {
226
+ margin: 0.45rem 0 0;
227
+ white-space: pre-wrap;
228
+ overflow-wrap: break-word;
229
+ font-size: 0.82rem;
230
+ line-height: 1.45;
231
+ max-height: 18rem;
232
+ overflow: auto;
233
+ }
234
+ .chat-running {
235
+ display: inline-flex;
236
+ align-items: center;
237
+ gap: 0.45rem;
238
+ color: var(--muted);
239
+ font-size: 0.85rem;
240
+ padding: 0.2rem 0.1rem;
241
+ }
242
+ .chat-progress {
243
+ display: inline-flex;
244
+ gap: 0.18rem;
245
+ }
246
+ .chat-progress span {
247
+ width: 0.32rem;
248
+ height: 0.32rem;
249
+ border-radius: 999px;
250
+ background: var(--skip);
251
+ animation: chat-progress 1s ease-in-out infinite;
252
+ }
253
+ .chat-progress span:nth-child(2) { animation-delay: 0.15s; }
254
+ .chat-progress span:nth-child(3) { animation-delay: 0.3s; }
255
+ @keyframes chat-progress {
256
+ 0%, 80%, 100% { opacity: 0.2; }
257
+ 40% { opacity: 1; }
258
+ }
259
+ .bubble.is-streaming .bubble-text::after {
260
+ content: "▍";
261
+ margin-left: 0.12rem;
262
+ color: var(--pass);
263
+ animation: chat-caret 1s step-end infinite;
264
+ }
265
+ @keyframes chat-caret {
266
+ 50% { opacity: 0; }
267
+ }
268
+ @media (prefers-reduced-motion: reduce) {
269
+ .chat-progress span, .bubble.is-streaming .bubble-text::after { animation: none; }
270
+ }
57
271
  `;
58
272
  }
59
273
  function rubricSummary(scenario) {
60
- const parts = [];
274
+ const parts = [
275
+ scenario.contextMode === "host-native" ? "host-native context" : "harness preamble",
276
+ ];
61
277
  const rubric = scenario.rubric;
62
278
  if (rubric.must?.length) {
63
279
  parts.push(`must ${rubric.must.join(", ")}`);
@@ -85,43 +301,79 @@ function rubricSummary(scenario) {
85
301
  if (rubric.judge) {
86
302
  parts.push("judge");
87
303
  }
304
+ if (scenario.contextSources?.length) {
305
+ parts.push(`contextSources ${scenario.contextSources.join(", ")}`);
306
+ }
88
307
  return parts.join(" · ");
89
308
  }
90
- function renderScenarioRow(suite, scenario) {
91
- const hosts = suite.hosts
309
+ function defaultScenarioHost(suite, scenario) {
310
+ if (scenario.host && suite.hosts.includes(scenario.host)) {
311
+ return scenario.host;
312
+ }
313
+ if (suite.hosts.includes("cursor")) {
314
+ return "cursor";
315
+ }
316
+ return suite.hosts[0] ?? "cursor";
317
+ }
318
+ function hostIsSkipped(scenario, host) {
319
+ return Boolean(scenario.skip || (scenario.host && scenario.host !== host));
320
+ }
321
+ function renderHostChrome(suite, scenario, selectedHost) {
322
+ if (suite.hosts.length <= 1) {
323
+ const host = suite.hosts[0] ?? selectedHost;
324
+ const skipped = hostIsSkipped(scenario, host);
325
+ return `<div class="host-status" data-cell="${escapeHtml(`${suite.name}::${scenario.name}::${host}`)}">
326
+ <span class="cell-status muted">${skipped ? "skip" : "idle"}</span>
327
+ </div>`;
328
+ }
329
+ const tabs = suite.hosts
330
+ .map((host) => {
331
+ const skipped = hostIsSkipped(scenario, host);
332
+ const selected = host === selectedHost;
333
+ return `<button type="button" class="host-tab" role="tab" aria-label="${escapeHtml(host)}" data-cell="${escapeHtml(`${suite.name}::${scenario.name}::${host}`)}" data-host="${escapeHtml(host)}" aria-selected="${selected ? "true" : "false"}"${skipped ? " disabled" : ""}>${escapeHtml(host)} <span class="cell-status muted" aria-hidden="true">${skipped ? "skip" : "idle"}</span></button>`;
334
+ })
335
+ .join("");
336
+ return `<div class="host-tablist" role="tablist">${tabs}</div>`;
337
+ }
338
+ function renderHostPanels(suite, selectedHost) {
339
+ return suite.hosts
92
340
  .map((host) => {
93
- const pinned = scenario.host && scenario.host !== host;
94
- const skipped = scenario.skip || pinned;
95
- return `<td data-cell="${escapeHtml(`${suite.name}::${scenario.name}::${host}`)}">
96
- <div class="cell-actions">
97
- <button class="primary run-cell" data-suite="${escapeHtml(suite.name)}" data-scenario="${escapeHtml(scenario.name)}" data-host="${escapeHtml(host)}"${skipped ? " disabled" : ""}>Run</button>
98
- <span class="cell-status muted">${skipped ? "skip" : "idle"}</span>
99
- </div>
100
- </td>`;
341
+ const selected = host === selectedHost;
342
+ return `<div class="host-panel" data-host-panel="${escapeHtml(host)}" role="tabpanel"${selected ? "" : " hidden"}><p class="host-empty">No run yet.</p></div>`;
101
343
  })
102
344
  .join("");
345
+ }
346
+ function renderScenarioCard(suite, scenario) {
347
+ const selectedHost = defaultScenarioHost(suite, scenario);
348
+ const runDisabled = hostIsSkipped(scenario, selectedHost);
103
349
  const compare = scenario.compare
104
350
  ? scenario.compare
105
351
  .map((arm) => `<p class="arm-note">${escapeHtml(arm.label)}: ${escapeHtml(arm.description ?? arm.prompt ?? "")}</p>`)
106
352
  .join("")
107
353
  : "";
108
- return `<tr>
109
- <th scope="row">
110
- <div class="scenario-detail">
111
- <strong>${escapeHtml(scenario.name)}</strong>
112
- ${scenario.description ? `<p class="scenario-lede">${escapeHtml(scenario.description)}</p>` : ""}
113
- <p class="prompt-preview">${escapeHtml(scenario.prompt)}</p>
114
- <p class="rubric-line">${escapeHtml(rubricSummary(scenario))}</p>
115
- ${compare}
116
- <button class="run-row" data-suite="${escapeHtml(suite.name)}" data-scenario="${escapeHtml(scenario.name)}">Run row</button>
354
+ const key = escapeHtml(`${suite.name}::${scenario.name}`);
355
+ return `<article class="scenario-card" data-scenario-card="${key}">
356
+ <div class="scenario-detail">
357
+ <strong>${escapeHtml(scenario.name)}</strong>
358
+ ${scenario.description ? `<p class="scenario-lede">${escapeHtml(scenario.description)}</p>` : ""}
359
+ <p class="prompt-preview">${escapeHtml(scenario.prompt)}</p>
360
+ <p class="rubric-line">${escapeHtml(rubricSummary(scenario))}</p>
361
+ ${compare}
362
+ </div>
363
+ <div class="scenario-toolbar">
364
+ ${renderHostChrome(suite, scenario, selectedHost)}
365
+ <div class="scenario-actions">
366
+ <button class="primary run-cell" data-suite="${escapeHtml(suite.name)}" data-scenario="${escapeHtml(scenario.name)}" data-host="${escapeHtml(selectedHost)}"${runDisabled ? " disabled" : ""}>Run</button>
367
+ <button class="run-row" data-suite="${escapeHtml(suite.name)}" data-scenario="${escapeHtml(scenario.name)}"${scenario.skip ? " disabled" : ""}>Run selected hosts</button>
117
368
  </div>
118
- </th>
119
- ${hosts}
120
- </tr>`;
369
+ </div>
370
+ <div class="scenario-live" hidden data-live-row="${key}" data-live-slot="${key}">
371
+ <div class="host-panels">${renderHostPanels(suite, selectedHost)}</div>
372
+ </div>
373
+ </article>`;
121
374
  }
122
375
  function renderSuiteSection(suite) {
123
- const columns = suite.hosts.map((host) => `<th>${escapeHtml(host)}</th>`).join("");
124
- const rows = suite.scenarios.map((scenario) => renderScenarioRow(suite, scenario)).join("");
376
+ const cards = suite.scenarios.map((scenario) => renderScenarioCard(suite, scenario)).join("");
125
377
  return `
126
378
  <section class="suite" data-suite="${escapeHtml(suite.name)}">
127
379
  <header class="suite-header">
@@ -131,10 +383,7 @@ function renderSuiteSection(suite) {
131
383
  </div>
132
384
  <button class="run-suite" data-suite="${escapeHtml(suite.name)}">Run suite</button>
133
385
  </header>
134
- <table class="matrix">
135
- <thead><tr><th>Scenario</th>${columns}</tr></thead>
136
- <tbody>${rows}</tbody>
137
- </table>
386
+ <div class="scenario-list">${cards}</div>
138
387
  </section>`;
139
388
  }
140
389
  function clientScript() {
@@ -143,6 +392,8 @@ function clientScript() {
143
392
  var catalog = JSON.parse(document.getElementById("catalog-data").textContent);
144
393
  var source = null;
145
394
  var runId = null;
395
+ var cancelRequested = false;
396
+ var progress = { total: 0, completed: 0, passed: 0, failed: 0, skipped: 0, seen: {}, experiments: {} };
146
397
 
147
398
  function selectedHosts() {
148
399
  return Array.prototype.map.call(document.querySelectorAll("[data-host-toggle]:checked"), function (box) {
@@ -158,6 +409,134 @@ function clientScript() {
158
409
  document.getElementById("run-banner").textContent = text;
159
410
  }
160
411
 
412
+ function runnableTargets(body) {
413
+ var requestedHosts = body.hosts && body.hosts.length ? body.hosts : catalog.defaultSelectedHosts;
414
+ var total = 0;
415
+ catalog.suites.forEach(function (suite) {
416
+ if (body.suite && suite.name !== body.suite) {
417
+ return;
418
+ }
419
+ suite.scenarios.forEach(function (scenario) {
420
+ if (body.scenario && scenario.name !== body.scenario) {
421
+ return;
422
+ }
423
+ if (scenario.skip) {
424
+ return;
425
+ }
426
+ requestedHosts.forEach(function (host) {
427
+ if (suite.hosts.indexOf(host) === -1 || (scenario.host && scenario.host !== host)) {
428
+ return;
429
+ }
430
+ total += 1;
431
+ });
432
+ });
433
+ });
434
+ return total;
435
+ }
436
+
437
+ function renderProgress() {
438
+ var box = document.getElementById("run-progress");
439
+ var total = Math.max(progress.total, progress.completed);
440
+ var remaining = Math.max(0, total - progress.completed);
441
+ box.hidden = total === 0;
442
+ box.setAttribute("data-has-failures", progress.failed > 0 ? "true" : "false");
443
+ var testLabel = total === 1 ? "test" : "tests";
444
+ box.querySelector(".run-progress-title").textContent = progress.completed + " of " + total + " " + testLabel + " finished";
445
+ box.querySelector(".progress-passed strong").textContent = String(progress.passed);
446
+ box.querySelector(".progress-failed strong").textContent = String(progress.failed);
447
+ box.querySelector(".progress-skipped strong").textContent = String(progress.skipped);
448
+ box.querySelector(".progress-remaining strong").textContent = String(remaining);
449
+ var percent = total > 0 ? Math.round((progress.completed / total) * 100) : 0;
450
+ var fill = box.querySelector(".run-progress-fill");
451
+ fill.style.width = percent + "%";
452
+ fill.parentNode.setAttribute("aria-valuenow", String(progress.completed));
453
+ fill.parentNode.setAttribute("aria-valuemax", String(total));
454
+ }
455
+
456
+ function resetProgress(body) {
457
+ progress = { total: runnableTargets(body), completed: 0, passed: 0, failed: 0, skipped: 0, seen: {}, experiments: {} };
458
+ renderProgress();
459
+ }
460
+
461
+ function recordProgress(event) {
462
+ var key = cellKey(event);
463
+ if (progress.seen[key]) {
464
+ return;
465
+ }
466
+ progress.seen[key] = true;
467
+ var experiment = experimentState(event);
468
+ experiment.cells[event.arm || "_"] = event;
469
+ var scenario = findScenario(event.suite, event.scenario);
470
+ var expectedArms = scenario && scenario.compare && scenario.compare.length ? scenario.compare.length : 1;
471
+ if (Object.keys(experiment.cells).length < expectedArms || experiment.counted) {
472
+ renderProgress();
473
+ return;
474
+ }
475
+ experiment.counted = true;
476
+ progress.completed += 1;
477
+ var outcome = experimentOutcome(scenario, experiment.cells);
478
+ if (outcome === "skipped") progress.skipped += 1;
479
+ else if (outcome === "passed") progress.passed += 1;
480
+ else progress.failed += 1;
481
+ renderProgress();
482
+ }
483
+
484
+ function finishProgress(event) {
485
+ // The stream reports one cell per comparison arm. Progress is based on
486
+ // the completed scenario/host experiment, so expected failed controls do
487
+ // not turn a passing comparison into a red suite result.
488
+ progress.total = Math.max(progress.total, progress.completed);
489
+ renderProgress();
490
+ }
491
+
492
+ function experimentKey(event) {
493
+ return [event.suite, event.scenario, event.host].join("::");
494
+ }
495
+
496
+ function experimentState(event) {
497
+ var key = experimentKey(event);
498
+ if (!progress.experiments[key]) {
499
+ progress.experiments[key] = { cells: {}, counted: false };
500
+ }
501
+ return progress.experiments[key];
502
+ }
503
+
504
+ function experimentOutcome(scenario, cells) {
505
+ var events = Object.keys(cells).map(function (id) { return cells[id]; });
506
+ if (events.some(function (event) { return event.skipped; })) return "skipped";
507
+ var byArm = {};
508
+ events.forEach(function (event) { byArm[event.arm || "_"] = event; });
509
+ if (!scenario || !scenario.compare || !scenario.compare.length) {
510
+ return events.every(function (event) { return event.passed; }) ? "passed" : "failed";
511
+ }
512
+ var gates = scenario.gates || [];
513
+ var behaviorOk = events.every(function (event) {
514
+ return event.passed || (event.failures || []).every(function (failure) {
515
+ return gates.some(function (gate) {
516
+ return gate.metric === "outcome" && gate.arm === event.arm && gate.operator === "equal" && gate.value === "fail";
517
+ });
518
+ });
519
+ });
520
+ var gatesOk = gates.every(function (gate) {
521
+ if (gate.metric === "outcome" && gate.arm) {
522
+ var event = byArm[gate.arm];
523
+ if (!event) return false;
524
+ var actual = event.passed ? "pass" : "fail";
525
+ return gate.operator === "equal" ? actual === gate.value : false;
526
+ }
527
+ if (["turns", "tokens", "tools", "durationMs"].indexOf(gate.metric) !== -1 && gate.winner && gate.loser) {
528
+ var winner = byArm[gate.winner];
529
+ var loser = byArm[gate.loser];
530
+ var field = gate.metric === "durationMs" ? "durationMs" : gate.metric;
531
+ var winnerValue = winner && winner.metrics && winner.metrics[field];
532
+ var loserValue = loser && loser.metrics && loser.metrics[field];
533
+ return typeof winnerValue === "number" && typeof loserValue === "number" && winnerValue < loserValue;
534
+ }
535
+ return true;
536
+ });
537
+ return behaviorOk && gatesOk ? "passed" : "failed";
538
+ }
539
+
161
540
  function cellKey(event) {
162
541
  return [event.suite, event.scenario, event.host, event.arm || "_"].join("::");
163
542
  }
@@ -166,41 +545,376 @@ function clientScript() {
166
545
  return "live-" + cellKey(event).replace(/[^a-z0-9]+/gi, "-");
167
546
  }
168
547
 
548
+ function compareWrapId(event) {
549
+ return ("live-wrap-" + event.suite + "-" + event.scenario + "-" + event.host).replace(/[^a-z0-9]+/gi, "-");
550
+ }
551
+
552
+ function selectCompareTab(wrap, armId) {
553
+ wrap.querySelectorAll(".compare-tab").forEach(function (tab) {
554
+ var on = tab.getAttribute("data-arm") === armId;
555
+ tab.setAttribute("aria-selected", on ? "true" : "false");
556
+ });
557
+ wrap.querySelectorAll(".live-cell").forEach(function (panel) {
558
+ panel.hidden = panel.getAttribute("data-arm") !== armId;
559
+ });
560
+ }
561
+
562
+ function scenarioCard(event) {
563
+ return document.querySelector('[data-scenario-card="' + event.suite + "::" + event.scenario + '"]');
564
+ }
565
+
566
+ function selectHostTab(card, host) {
567
+ if (!card || !host) {
568
+ return;
569
+ }
570
+ card.querySelectorAll(".host-tab").forEach(function (tab) {
571
+ var on = tab.getAttribute("data-host") === host;
572
+ tab.setAttribute("aria-selected", on ? "true" : "false");
573
+ });
574
+ card.querySelectorAll("[data-host-panel]").forEach(function (panel) {
575
+ panel.hidden = panel.getAttribute("data-host-panel") !== host;
576
+ });
577
+ var run = card.querySelector("button.run-cell");
578
+ if (run) {
579
+ run.setAttribute("data-host", host);
580
+ var tab = card.querySelector('.host-tab[data-host="' + host + '"]');
581
+ if (tab) {
582
+ run.disabled = tab.disabled;
583
+ }
584
+ }
585
+ }
586
+
587
+ function revealLive(event) {
588
+ var card = scenarioCard(event);
589
+ if (!card) {
590
+ return;
591
+ }
592
+ var live = card.querySelector(".scenario-live");
593
+ if (live) {
594
+ live.hidden = false;
595
+ }
596
+ if (card.dataset.userPickedHost === "1") {
597
+ return;
598
+ }
599
+ if (card.dataset.followHost && card.dataset.followHost !== event.host) {
600
+ return;
601
+ }
602
+ card.dataset.followHost = event.host;
603
+ selectHostTab(card, event.host);
604
+ }
605
+
606
+ function hostPanel(event) {
607
+ var card = scenarioCard(event);
608
+ return card ? card.querySelector('[data-host-panel="' + event.host + '"]') : null;
609
+ }
610
+
611
+ function markArmTab(wrap, armId, status) {
612
+ var tab = wrap.querySelector('.compare-tab[data-arm="' + armId + '"]');
613
+ if (tab) {
614
+ tab.setAttribute("data-status", status);
615
+ }
616
+ }
617
+
618
+ function createLiveArticle(event, title) {
619
+ var article = document.createElement("article");
620
+ article.className = "live-cell";
621
+ article.id = paneId(event);
622
+ article.dataset.chatKey = cellKey(event);
623
+ article.innerHTML = "<h3></h3><ol class=\\"live-status\\"></ol><div class=\\"chat\\"></div>";
624
+ article.querySelector("h3").textContent = title;
625
+ return article;
626
+ }
627
+
628
+ var openChats = {};
629
+
630
+ function resetArticle(article) {
631
+ if (!article) {
632
+ return;
633
+ }
634
+ var status = article.querySelector(".live-status");
635
+ if (status) {
636
+ status.replaceChildren();
637
+ }
638
+ var chat = article.querySelector(".chat");
639
+ if (chat) {
640
+ chat.replaceChildren();
641
+ }
642
+ var result = article.querySelector(".cell-result");
643
+ if (result) {
644
+ result.remove();
645
+ }
646
+ var contextPanel = article.querySelector(".context-panel");
647
+ if (contextPanel) {
648
+ contextPanel.remove();
649
+ }
650
+ var panel = article.closest("[data-host-panel]");
651
+ var empty = panel ? panel.querySelector(".host-empty") : null;
652
+ if (empty) {
653
+ empty.hidden = false;
654
+ }
655
+ }
656
+
657
+ function chatIsOpen(event) {
658
+ return !!openChats[cellKey(event)];
659
+ }
660
+
661
+ function startChat(event) {
662
+ openChats[cellKey(event)] = true;
663
+ delete assistantBodies[cellKey(event)];
664
+ delete finishedMetrics[cellKey(event)];
665
+ ensurePane(event);
666
+ resetArticle(document.getElementById(paneId(event)));
667
+ var panel = hostPanel(event);
668
+ var empty = panel ? panel.querySelector(".host-empty") : null;
669
+ if (empty) {
670
+ empty.hidden = true;
671
+ }
672
+ }
673
+
674
+ function markRunningIdle() {
675
+ document.querySelectorAll("[data-cell]").forEach(function (node) {
676
+ var status = node.querySelector(".cell-status");
677
+ if (!status) {
678
+ return;
679
+ }
680
+ var label = status.textContent;
681
+ if (label !== "running" && label !== "starting") {
682
+ return;
683
+ }
684
+ status.textContent = "idle";
685
+ status.className = "cell-status muted";
686
+ if (node.classList.contains("host-tab")) {
687
+ node.setAttribute("data-status", "idle");
688
+ }
689
+ });
690
+ document.querySelectorAll(".compare-tab[data-status='running']").forEach(function (tab) {
691
+ tab.setAttribute("data-status", "idle");
692
+ });
693
+ }
694
+
695
+ function closeAllChats() {
696
+ Object.keys(openChats).forEach(function (key) {
697
+ resetArticle(document.getElementById("live-" + key.replace(/[^a-z0-9]+/gi, "-")));
698
+ });
699
+ openChats = {};
700
+ assistantBodies = {};
701
+ hideAllRunning();
702
+ markRunningIdle();
703
+ }
704
+
705
+ function setCancelButton(label, disabled) {
706
+ var button = document.getElementById("cancel-run");
707
+ button.textContent = label;
708
+ button.disabled = disabled;
709
+ }
710
+
711
+ function showCancelledNote(article, text) {
712
+ if (!article) {
713
+ return;
714
+ }
715
+ var existing = article.querySelector(".cell-result");
716
+ if (existing) {
717
+ existing.remove();
718
+ }
719
+ var box = document.createElement("div");
720
+ box.className = "cell-result status-skipped";
721
+ var verdict = document.createElement("p");
722
+ verdict.className = "cell-result-verdict";
723
+ verdict.textContent = text;
724
+ box.appendChild(verdict);
725
+ article.appendChild(box);
726
+ var panel = article.closest("[data-host-panel]");
727
+ var empty = panel ? panel.querySelector(".host-empty") : null;
728
+ if (empty) {
729
+ empty.hidden = true;
730
+ }
731
+ }
732
+
733
+ function cancelOpenChats() {
734
+ document.querySelectorAll(".cell-status").forEach(function (status) {
735
+ if (status.textContent !== "running" && status.textContent !== "starting") {
736
+ return;
737
+ }
738
+ status.textContent = "cancelling";
739
+ status.className = "cell-status status-skipped";
740
+ var tab = status.closest(".host-tab");
741
+ if (tab) {
742
+ tab.setAttribute("data-status", "cancelling");
743
+ }
744
+ });
745
+ document.querySelectorAll(".compare-tab[data-status='running']").forEach(function (tab) {
746
+ tab.setAttribute("data-status", "cancelling");
747
+ });
748
+ hideAllRunning();
749
+ document.querySelectorAll("[data-cell]").forEach(function (node) {
750
+ var status = node.querySelector(".cell-status");
751
+ if (!status || status.textContent !== "cancelling") {
752
+ return;
753
+ }
754
+ var card = node.closest("[data-scenario-card]");
755
+ if (!card) {
756
+ return;
757
+ }
758
+ var live = card.querySelector(".scenario-live");
759
+ if (live) {
760
+ live.hidden = false;
761
+ }
762
+ var host = node.getAttribute("data-host") || (node.getAttribute("data-cell") || "").split("::")[2];
763
+ var panel = card.querySelector('[data-host-panel="' + host + '"]');
764
+ if (!panel) {
765
+ return;
766
+ }
767
+ var articles = panel.querySelectorAll("article.live-cell");
768
+ if (articles.length === 0) {
769
+ showCancelledNote(panel, "Cancelled. Stopping the host agent.");
770
+ return;
771
+ }
772
+ articles.forEach(function (article) {
773
+ var key = article.dataset.chatKey;
774
+ resetArticle(article);
775
+ showCancelledNote(article, "Cancelled. Stopping the host agent.");
776
+ if (key) {
777
+ delete openChats[key];
778
+ delete assistantBodies[key];
779
+ }
780
+ });
781
+ });
782
+ }
783
+
784
+ function markCancelledCells() {
785
+ document.querySelectorAll(".cell-status").forEach(function (status) {
786
+ if (status.textContent === "cancelling") {
787
+ status.textContent = "cancelled";
788
+ status.className = "cell-status status-skipped";
789
+ var tab = status.closest(".host-tab");
790
+ if (tab) {
791
+ tab.setAttribute("data-status", "cancelled");
792
+ }
793
+ }
794
+ });
795
+ document.querySelectorAll(".compare-tab[data-status='cancelling']").forEach(function (tab) {
796
+ tab.setAttribute("data-status", "cancelled");
797
+ });
798
+ document.querySelectorAll(".cell-result-verdict").forEach(function (node) {
799
+ if (node.textContent === "Cancelled. Stopping the host agent.") {
800
+ node.textContent = "Cancelled.";
801
+ }
802
+ });
803
+ }
804
+
805
+ function sealAllChats() {
806
+ openChats = {};
807
+ }
808
+
809
+ function addArmTab(wrap, list, article, armId, label, selected) {
810
+ article.setAttribute("role", "tabpanel");
811
+ article.setAttribute("data-arm", armId);
812
+ article.hidden = !selected;
813
+ var tab = document.createElement("button");
814
+ tab.type = "button";
815
+ tab.className = "compare-tab";
816
+ tab.setAttribute("role", "tab");
817
+ tab.setAttribute("data-arm", armId);
818
+ tab.setAttribute("aria-controls", article.id);
819
+ tab.setAttribute("aria-selected", selected ? "true" : "false");
820
+ tab.textContent = label;
821
+ tab.addEventListener("click", function () {
822
+ wrap.dataset.userPicked = "1";
823
+ selectCompareTab(wrap, armId);
824
+ });
825
+ list.appendChild(tab);
826
+ }
827
+
828
+ function ensureCompareWrap(event, scenario, slot) {
829
+ var wrapId = compareWrapId(event);
830
+ var wrap = document.getElementById(wrapId);
831
+ if (!wrap) {
832
+ wrap = document.createElement("div");
833
+ wrap.className = "compare-layout compare-tabs";
834
+ wrap.id = wrapId;
835
+ var list = document.createElement("div");
836
+ list.className = "compare-tablist";
837
+ list.setAttribute("role", "tablist");
838
+ list.setAttribute("aria-label", "Comparison arms");
839
+ wrap.appendChild(list);
840
+ var arms = document.createElement("div");
841
+ arms.className = "compare-arms";
842
+ if (scenario && scenario.compare) {
843
+ arms.setAttribute("data-arm-count", String(scenario.compare.length));
844
+ }
845
+ wrap.appendChild(arms);
846
+ if (scenario && scenario.compare) {
847
+ scenario.compare.forEach(function (arm, index) {
848
+ var article = createLiveArticle({
849
+ suite: event.suite,
850
+ scenario: event.scenario,
851
+ host: event.host,
852
+ arm: arm.id,
853
+ }, arm.label);
854
+ addArmTab(wrap, list, article, arm.id, arm.label, index === 0);
855
+ arms.appendChild(article);
856
+ });
857
+ }
858
+ }
859
+ if (!wrap.parentNode) {
860
+ slot.appendChild(wrap);
861
+ }
862
+ return wrap;
863
+ }
864
+
169
865
  function ensurePane(event) {
170
- var id = paneId(event);
171
- var existing = document.getElementById(id);
866
+ var existing = document.getElementById(paneId(event));
172
867
  if (existing) {
868
+ revealLive(event);
173
869
  return existing.querySelector(".chat");
174
870
  }
175
- var dock = document.getElementById("live-dock");
176
- var article = document.createElement("article");
177
- article.className = "live-cell";
178
- article.id = id;
179
- var title = event.suite + " / " + event.scenario + " / " + event.host;
180
- if (event.arm) {
181
- title += " / arm " + event.arm;
871
+ var slot = hostPanel(event);
872
+ if (!slot) {
873
+ return null;
182
874
  }
183
- article.innerHTML = "<h3></h3><div class=\\"chat\\"></div>";
184
- article.querySelector("h3").textContent = title;
185
- var parent = dock;
875
+ revealLive(event);
876
+ var empty = slot.querySelector(".host-empty");
877
+ if (empty) {
878
+ empty.hidden = true;
879
+ }
880
+ var leftover = slot.querySelector(":scope > .cell-result");
881
+ if (leftover) {
882
+ leftover.remove();
883
+ }
884
+ var scenario = findScenario(event.suite, event.scenario);
186
885
  if (event.arm) {
187
- var wrapId = ("live-wrap-" + event.suite + "-" + event.scenario + "-" + event.host).replace(/[^a-z0-9]+/gi, "-");
188
- var wrap = document.getElementById(wrapId);
189
- if (!wrap) {
190
- wrap = document.createElement("div");
191
- wrap.className = "compare-layout";
192
- wrap.id = wrapId;
193
- var arms = document.createElement("div");
194
- arms.className = "compare-arms";
195
- wrap.appendChild(arms);
196
- dock.appendChild(wrap);
197
- }
198
- parent = wrap.querySelector(".compare-arms");
199
- }
200
- parent.appendChild(article);
886
+ var wrap = ensureCompareWrap(event, scenario, slot);
887
+ existing = document.getElementById(paneId(event));
888
+ if (existing) {
889
+ if (wrap.dataset.userPicked !== "1") {
890
+ selectCompareTab(wrap, event.arm);
891
+ }
892
+ return existing.querySelector(".chat");
893
+ }
894
+ return null;
895
+ }
896
+ var article = createLiveArticle(event, event.host);
897
+ slot.appendChild(article);
201
898
  return article.querySelector(".chat");
202
899
  }
203
900
 
901
+ function appendStatus(event, text) {
902
+ var article = document.getElementById(paneId(event));
903
+ var list = article ? article.querySelector(".live-status") : null;
904
+ if (!list) {
905
+ ensurePane(event);
906
+ article = document.getElementById(paneId(event));
907
+ list = article ? article.querySelector(".live-status") : null;
908
+ }
909
+ if (!list) {
910
+ return;
911
+ }
912
+ var item = document.createElement("li");
913
+ item.textContent = text;
914
+ list.appendChild(item);
915
+ list.scrollTop = list.scrollHeight;
916
+ }
917
+
204
918
  function appendBubble(chat, role, text) {
205
919
  var row = document.createElement("div");
206
920
  row.className = "chat-row " + (role === "user" ? "side-right" : "side-left");
@@ -219,6 +933,204 @@ function clientScript() {
219
933
  return body;
220
934
  }
221
935
 
936
+ function runningRow(article) {
937
+ return article.querySelector(".chat-running-row");
938
+ }
939
+
940
+ function showRunning(event) {
941
+ var article = document.getElementById(paneId(event));
942
+ if (!article) {
943
+ ensurePane(event);
944
+ article = document.getElementById(paneId(event));
945
+ }
946
+ if (!article) {
947
+ return;
948
+ }
949
+ var chat = article.querySelector(".chat");
950
+ if (!chat) {
951
+ return;
952
+ }
953
+ var row = runningRow(article);
954
+ if (!row) {
955
+ row = document.createElement("div");
956
+ row.className = "chat-row side-left chat-running-row";
957
+ row.innerHTML = '<div class="chat-running" aria-live="polite">Running<span class="chat-progress" aria-hidden="true"><span></span><span></span><span></span></span></div>';
958
+ chat.appendChild(row);
959
+ }
960
+ row.hidden = false;
961
+ chat.appendChild(row);
962
+ }
963
+
964
+ function hideRunning(event) {
965
+ var article = document.getElementById(paneId(event));
966
+ var row = article ? runningRow(article) : null;
967
+ if (row) {
968
+ row.remove();
969
+ }
970
+ }
971
+
972
+ function hideAllRunning() {
973
+ document.querySelectorAll(".chat-running-row").forEach(function (row) {
974
+ row.remove();
975
+ });
976
+ document.querySelectorAll(".bubble.is-streaming").forEach(function (bubble) {
977
+ bubble.classList.remove("is-streaming");
978
+ });
979
+ }
980
+
981
+ function setStreaming(event, on) {
982
+ var body = assistantBodies[cellKey(event)];
983
+ var bubble = body ? body.parentNode : null;
984
+ if (bubble) {
985
+ if (on) {
986
+ bubble.classList.add("is-streaming");
987
+ } else {
988
+ bubble.classList.remove("is-streaming");
989
+ }
990
+ }
991
+ }
992
+
993
+ function appendContextPanel(article, mode, files, hostInput) {
994
+ if (!article) {
995
+ return;
996
+ }
997
+ var existing = article.querySelector(".context-panel");
998
+ if (existing) {
999
+ existing.remove();
1000
+ }
1001
+ var list = files || [];
1002
+ var panel = document.createElement("details");
1003
+ panel.className = "context-panel";
1004
+ panel.open = true;
1005
+ panel.setAttribute("data-context-mode", mode || "harness-preamble");
1006
+ panel.setAttribute("data-file-count", String(list.length));
1007
+ var summary = document.createElement("summary");
1008
+ summary.className = "context-panel-summary";
1009
+ var title = document.createElement("span");
1010
+ title.className = "context-panel-title";
1011
+ title.textContent = "Context delivery";
1012
+ var count = document.createElement("span");
1013
+ count.className = "context-panel-count";
1014
+ count.textContent = mode === "host-native"
1015
+ ? "Host-native"
1016
+ : "Harness preamble · " + (list.length === 1 ? "1 file" : list.length + " files");
1017
+ summary.appendChild(title);
1018
+ summary.appendChild(count);
1019
+ panel.appendChild(summary);
1020
+
1021
+ var flow = document.createElement("div");
1022
+ flow.className = "context-flow";
1023
+ flow.setAttribute("aria-label", "Scenario prompt delivery path");
1024
+ ["Scenario", "→", mode === "host-native" ? "Host discovery" : "Harness preamble", "→", "Host"].forEach(function (label, index) {
1025
+ var item = document.createElement("span");
1026
+ item.className = index === 1 || index === 3
1027
+ ? "context-flow-arrow"
1028
+ : "context-flow-node" + (index === 2 ? " context-flow-mode" : "");
1029
+ item.textContent = label;
1030
+ flow.appendChild(item);
1031
+ });
1032
+ panel.appendChild(flow);
1033
+
1034
+ if (typeof hostInput === "string") {
1035
+ var input = document.createElement("details");
1036
+ input.className = "context-host-input";
1037
+ input.open = true;
1038
+ var inputSummary = document.createElement("summary");
1039
+ inputSummary.textContent = "Exact submitted user input";
1040
+ var inputBody = document.createElement("pre");
1041
+ inputBody.className = "context-body";
1042
+ inputBody.textContent = hostInput;
1043
+ input.appendChild(inputSummary);
1044
+ input.appendChild(inputBody);
1045
+ panel.appendChild(input);
1046
+ }
1047
+
1048
+ if (mode === "host-native") {
1049
+ var nativeLede = document.createElement("p");
1050
+ nativeLede.className = "context-panel-lede";
1051
+ nativeLede.textContent = "No harness preamble. Workspace files remain on disk for the host to discover.";
1052
+ panel.appendChild(nativeLede);
1053
+ var nativeUnknown = document.createElement("p");
1054
+ nativeUnknown.className = "context-empty";
1055
+ nativeUnknown.textContent = "Host-owned system instructions and native discovery are not exposed as one inspectable payload. agent-test does not claim which workspace instructions or skills the host loaded.";
1056
+ panel.appendChild(nativeUnknown);
1057
+ } else if (list.length === 0) {
1058
+ var empty = document.createElement("p");
1059
+ empty.className = "context-empty";
1060
+ empty.textContent = "No preamble files. The agent received the prompt only.";
1061
+ panel.appendChild(empty);
1062
+ } else {
1063
+ var lede = document.createElement("p");
1064
+ lede.className = "context-panel-lede";
1065
+ lede.textContent = "These files were in the host preamble for this run.";
1066
+ panel.appendChild(lede);
1067
+ var ol = document.createElement("ol");
1068
+ ol.className = "context-files";
1069
+ list.forEach(function (file) {
1070
+ var item = document.createElement("li");
1071
+ var details = document.createElement("details");
1072
+ details.className = "context-file";
1073
+ details.open = list.length === 1;
1074
+ details.setAttribute("data-reason", file.reason || "profile");
1075
+ details.setAttribute("data-path", file.path || "context");
1076
+ var fileSummary = document.createElement("summary");
1077
+ var path = document.createElement("span");
1078
+ path.className = "context-path";
1079
+ path.textContent = file.path || "context";
1080
+ var why = document.createElement("span");
1081
+ why.className = "context-why";
1082
+ why.textContent = file.why || "The runner loaded this file into the host preamble.";
1083
+ fileSummary.appendChild(path);
1084
+ fileSummary.appendChild(why);
1085
+ var body = document.createElement("pre");
1086
+ body.className = "context-body";
1087
+ body.textContent = file.text || "";
1088
+ details.appendChild(fileSummary);
1089
+ details.appendChild(body);
1090
+ item.appendChild(details);
1091
+ ol.appendChild(item);
1092
+ });
1093
+ panel.appendChild(ol);
1094
+ }
1095
+ var chat = article.querySelector(".chat");
1096
+ if (chat) {
1097
+ article.insertBefore(panel, chat);
1098
+ } else {
1099
+ article.appendChild(panel);
1100
+ }
1101
+ }
1102
+
1103
+ function appendContextFiles(chat, mode, files, hostInput) {
1104
+ appendContextPanel(chat ? chat.closest("article.live-cell") : null, mode, files, hostInput);
1105
+ }
1106
+
1107
+ function displayToolPath(path) {
1108
+ var stripped = String(path).replace(/^file:\\/\\//, "").replace(/\\/+$/, "");
1109
+ var workspace = stripped.match(/\\/agent-harness-seal-[^/]+(?:\\/(.*))?$/);
1110
+ if (workspace) {
1111
+ return workspace[1] ? workspace[1] : ".";
1112
+ }
1113
+ var sealed = stripped.match(/\\/(?:\\.agents|\\.claude|\\.codex|AGENTS\\.md|CLAUDE\\.md)(?:\\/|$)/);
1114
+ if (sealed && sealed.index !== undefined) {
1115
+ return stripped.slice(sealed.index + 1);
1116
+ }
1117
+ var parts = stripped.split("/").filter(Boolean);
1118
+ if (parts.length > 3) {
1119
+ return ".../" + parts.slice(-3).join("/");
1120
+ }
1121
+ return stripped;
1122
+ }
1123
+
1124
+ function formatArgValue(key, value) {
1125
+ var pathKeys = { path: 1, file_path: 1, filePath: 1, target_file: 1, uri: 1, cwd: 1 };
1126
+ if (typeof value === "string" && (pathKeys[key] || value.indexOf("/") === 0 || value.indexOf("file://") === 0)) {
1127
+ return { text: displayToolPath(value), title: value };
1128
+ }
1129
+ var raw = typeof value === "string" ? value : JSON.stringify(value);
1130
+ var oneLine = raw.replace(/\\n/g, " ↵ ");
1131
+ return { text: oneLine.length > 140 ? oneLine.slice(0, 140) + "…" : oneLine };
1132
+ }
1133
+
222
1134
  function appendTool(chat, name, args) {
223
1135
  var row = document.createElement("div");
224
1136
  row.className = "chat-row side-left";
@@ -241,7 +1153,11 @@ function clientScript() {
241
1153
  keyEl.className = "tool-arg-key";
242
1154
  keyEl.textContent = key;
243
1155
  var value = document.createElement("code");
244
- value.textContent = typeof args[key] === "string" ? args[key] : JSON.stringify(args[key]);
1156
+ var formatted = formatArgValue(key, args[key]);
1157
+ value.textContent = formatted.text;
1158
+ if (formatted.title && formatted.title !== formatted.text) {
1159
+ value.title = formatted.title;
1160
+ }
245
1161
  line.appendChild(keyEl);
246
1162
  line.appendChild(value);
247
1163
  box.appendChild(line);
@@ -253,101 +1169,542 @@ function clientScript() {
253
1169
  }
254
1170
 
255
1171
  var assistantBodies = {};
1172
+ var finishedMetrics = {};
1173
+
1174
+ function findScenario(suiteName, scenarioName) {
1175
+ for (var i = 0; i < catalog.suites.length; i++) {
1176
+ if (catalog.suites[i].name !== suiteName) {
1177
+ continue;
1178
+ }
1179
+ var scenarios = catalog.suites[i].scenarios;
1180
+ for (var j = 0; j < scenarios.length; j++) {
1181
+ if (scenarios[j].name === scenarioName) {
1182
+ return scenarios[j];
1183
+ }
1184
+ }
1185
+ }
1186
+ return null;
1187
+ }
1188
+
1189
+ function joinLabels(labels) {
1190
+ if (labels.length <= 1) {
1191
+ return labels[0] || "";
1192
+ }
1193
+ if (labels.length === 2) {
1194
+ return labels[0] + " and " + labels[1];
1195
+ }
1196
+ return labels.slice(0, -1).join(", ") + ", and " + labels[labels.length - 1];
1197
+ }
1198
+
1199
+ function summarizeMetric(arms, id, label) {
1200
+ var values = arms.map(function (arm) { return arm[id]; });
1201
+ var numeric = values.filter(function (value) { return typeof value === "number"; });
1202
+ if (numeric.length < 2) {
1203
+ return { line: label + ": n/a." };
1204
+ }
1205
+ var lowest = Math.min.apply(null, numeric);
1206
+ var winners = arms.filter(function (arm) { return arm[id] === lowest; });
1207
+ var shown = values.map(function (value) { return typeof value === "number" ? String(value) : "n/a"; }).join(" vs ");
1208
+ if (winners.length > 1) {
1209
+ return {
1210
+ line: arms.length === 2
1211
+ ? label + ": tie (" + shown + ")."
1212
+ : label + ": lowest is a tie between " + joinLabels(winners.map(function (arm) { return arm.label; })) + " (" + shown + ").",
1213
+ };
1214
+ }
1215
+ var winner = winners[0];
1216
+ if (!winner) {
1217
+ return { line: label + ": n/a." };
1218
+ }
1219
+ if (arms.length === 2) {
1220
+ return { line: label + ": " + winner.label + " wins (" + shown + ")." };
1221
+ }
1222
+ return { line: label + ": lowest is " + winner.label + " (" + shown + ")." };
1223
+ }
1224
+
1225
+ function summarizeGates(arms, gates) {
1226
+ var byId = {};
1227
+ arms.forEach(function (arm) { byId[arm.id] = arm; });
1228
+ return (gates || []).map(function (gate) {
1229
+ var field = gate.metric === "durationMs" ? "durationMs" : gate.metric;
1230
+ var winner = byId[gate.winner];
1231
+ var loser = byId[gate.loser];
1232
+ if (!winner || !loser || ["turns", "tokens", "tools", "durationMs"].indexOf(field) === -1) {
1233
+ return { passed: true, line: gate.metric + ": scored in the final result." };
1234
+ }
1235
+ var winnerValue = winner[field];
1236
+ var loserValue = loser[field];
1237
+ if (typeof winnerValue !== "number" || typeof loserValue !== "number") {
1238
+ return { passed: false, line: gate.metric + ": one arm did not report a value. Fail." };
1239
+ }
1240
+ var passed = winnerValue < loserValue;
1241
+ return { passed: passed, line: winner.label + " must beat " + loser.label + " on " + gate.metric + " (" + winnerValue + " vs " + loserValue + "). " + (passed ? "Pass." : "Fail.") };
1242
+ });
1243
+ }
1244
+
1245
+ function renderCompareWinners(event) {
1246
+ var scenario = findScenario(event.suite, event.scenario);
1247
+ if (!scenario || !scenario.compare || !scenario.compare.length) {
1248
+ return;
1249
+ }
1250
+ var wrap = document.getElementById(compareWrapId(event));
1251
+ if (!wrap) {
1252
+ return;
1253
+ }
1254
+ var rows = [];
1255
+ for (var i = 0; i < scenario.compare.length; i++) {
1256
+ var arm = scenario.compare[i];
1257
+ var metrics = finishedMetrics[[event.suite, event.scenario, event.host, arm.id].join("::")];
1258
+ if (!metrics) {
1259
+ return;
1260
+ }
1261
+ rows.push({
1262
+ id: arm.id,
1263
+ label: arm.label,
1264
+ turns: metrics.turns,
1265
+ tokens: metrics.tokens,
1266
+ tools: metrics.tools,
1267
+ durationMs: metrics.durationMs,
1268
+ passed: metrics.passed,
1269
+ });
1270
+ }
1271
+ var metricLines = [
1272
+ summarizeMetric(rows, "turns", "Turns"),
1273
+ summarizeMetric(rows, "tokens", "Tokens"),
1274
+ summarizeMetric(rows, "tools", "Tools"),
1275
+ summarizeMetric(rows, "durationMs", "Duration"),
1276
+ ];
1277
+ var gateLines = summarizeGates(rows, scenario.gates);
1278
+ var existing = wrap.querySelector(".compare-winners");
1279
+ if (existing) {
1280
+ existing.remove();
1281
+ }
1282
+ var section = document.createElement("section");
1283
+ section.className = "compare-winners";
1284
+ var heading = document.createElement("h3");
1285
+ heading.textContent = "Winners";
1286
+ var list = document.createElement("ul");
1287
+ metricLines.forEach(function (metric) {
1288
+ var item = document.createElement("li");
1289
+ item.textContent = metric.line;
1290
+ list.appendChild(item);
1291
+ });
1292
+ gateLines.forEach(function (gate) {
1293
+ var item = document.createElement("li");
1294
+ item.className = gate.passed ? "compare-winner-pass" : "compare-winner-fail";
1295
+ item.textContent = gate.line;
1296
+ list.appendChild(item);
1297
+ });
1298
+ section.appendChild(heading);
1299
+ section.appendChild(list);
1300
+ wrap.appendChild(section);
1301
+ }
256
1302
 
257
1303
  function markCell(event, label, statusClass) {
258
- var selector = '[data-cell="' + event.suite + "::" + event.scenario + "::" + event.host + '"] .cell-status';
1304
+ var selector = '[data-cell="' + event.suite + "::" + event.scenario + "::" + event.host + '"]';
259
1305
  document.querySelectorAll(selector).forEach(function (node) {
260
- node.textContent = label;
261
- node.className = "cell-status " + statusClass;
1306
+ var status = node.querySelector(".cell-status");
1307
+ if (status) {
1308
+ status.textContent = label;
1309
+ status.className = "cell-status " + statusClass;
1310
+ }
1311
+ if (node.classList.contains("host-tab")) {
1312
+ node.setAttribute("data-status", label);
1313
+ }
1314
+ });
1315
+ }
1316
+
1317
+ function metricPart(count, one, many) {
1318
+ if (typeof count !== "number") {
1319
+ return "";
1320
+ }
1321
+ return count + " " + (count === 1 ? one : many);
1322
+ }
1323
+
1324
+ function formatMetrics(metrics) {
1325
+ if (!metrics) {
1326
+ return "";
1327
+ }
1328
+ return [metricPart(metrics.turns, "turn", "turns"), metricPart(metrics.tokens, "token", "tokens"), metricPart(metrics.tools, "tool", "tools")]
1329
+ .filter(Boolean)
1330
+ .join(" · ");
1331
+ }
1332
+
1333
+ function clearCellResult(event) {
1334
+ var article = document.getElementById(paneId(event));
1335
+ var existing = article ? article.querySelector(".cell-result") : null;
1336
+ if (existing) {
1337
+ existing.remove();
1338
+ }
1339
+ }
1340
+
1341
+ function renderCellResult(event) {
1342
+ ensurePane(event);
1343
+ var article = document.getElementById(paneId(event));
1344
+ if (!article) {
1345
+ return;
1346
+ }
1347
+ var existing = article.querySelector(".cell-result");
1348
+ if (existing) {
1349
+ existing.remove();
1350
+ }
1351
+ var skipped = !!event.skipped;
1352
+ var passed = !!event.passed && !skipped;
1353
+ var box = document.createElement("div");
1354
+ box.className = "cell-result " + (skipped ? "status-skipped" : passed ? "status-passed" : "status-failed");
1355
+ var verdict = document.createElement("p");
1356
+ verdict.className = "cell-result-verdict";
1357
+ verdict.textContent = skipped ? "Skipped." : passed ? "Passed." : "Failed.";
1358
+ box.appendChild(verdict);
1359
+ var metrics = formatMetrics(event.metrics);
1360
+ if (metrics) {
1361
+ var line = document.createElement("p");
1362
+ line.className = "cell-result-metrics";
1363
+ line.textContent = metrics;
1364
+ box.appendChild(line);
1365
+ }
1366
+ if (!skipped && !passed) {
1367
+ var list = document.createElement("ul");
1368
+ list.className = "cell-result-failures";
1369
+ var failures = event.failures || [];
1370
+ if (failures.length === 0) {
1371
+ var item = document.createElement("li");
1372
+ item.textContent = "The run failed. The host did not report a reason.";
1373
+ list.appendChild(item);
1374
+ } else {
1375
+ failures.forEach(function (failure) {
1376
+ var item = document.createElement("li");
1377
+ item.textContent = failure.message || failure.matcher || "The run failed.";
1378
+ list.appendChild(item);
1379
+ });
1380
+ }
1381
+ box.appendChild(list);
1382
+ }
1383
+ article.appendChild(box);
1384
+ }
1385
+
1386
+ function appendJudgeVerdicts(event) {
1387
+ ensurePane(event);
1388
+ var article = document.getElementById(paneId(event));
1389
+ if (!article) {
1390
+ return;
1391
+ }
1392
+ var box = article.querySelector(".cell-result");
1393
+ if (!box) {
1394
+ box = document.createElement("div");
1395
+ box.className = "cell-result";
1396
+ article.appendChild(box);
1397
+ }
1398
+ var existing = box.querySelector(".cell-result-judges");
1399
+ if (existing) {
1400
+ existing.remove();
1401
+ }
1402
+ var list = document.createElement("ul");
1403
+ list.className = "cell-result-judges";
1404
+ (event.verdicts || []).forEach(function (verdict) {
1405
+ var item = document.createElement("li");
1406
+ item.className = verdict.pass ? "judge-pass" : "judge-fail";
1407
+ var text = (verdict.pass ? "Pass. " : "Fail. ") + (verdict.question || "");
1408
+ if (verdict.rationale) {
1409
+ text += " " + verdict.rationale;
1410
+ }
1411
+ item.textContent = text;
1412
+ list.appendChild(item);
262
1413
  });
1414
+ box.appendChild(list);
263
1415
  }
264
1416
 
265
1417
  function handleEvent(event) {
266
1418
  if (event.type === "run_started") {
1419
+ if (cancelRequested) {
1420
+ return;
1421
+ }
267
1422
  setRunBanner("Run " + event.runId + " started.");
268
- document.getElementById("cancel-run").disabled = false;
1423
+ setCancelButton("Cancel run", false);
269
1424
  return;
270
1425
  }
271
1426
  if (event.type === "run_finished") {
272
- setRunBanner(
273
- "Run finished. " + event.passed + " passed. " + event.failed + " failed. " + event.skipped + " skipped."
274
- );
275
- document.getElementById("cancel-run").disabled = true;
1427
+ finishProgress(event);
1428
+ hideAllRunning();
1429
+ sealAllChats();
1430
+ if (cancelRequested) {
1431
+ markCancelledCells();
1432
+ setRunBanner("Run cancelled.");
1433
+ } else {
1434
+ setRunBanner(
1435
+ "Run finished. " + progress.passed + " passed. " + progress.failed + " failed. " + progress.skipped + " skipped."
1436
+ );
1437
+ }
1438
+ setCancelButton("Cancel run", true);
1439
+ cancelRequested = false;
276
1440
  runId = null;
1441
+ if (source) {
1442
+ source.close();
1443
+ source = null;
1444
+ }
1445
+ return;
1446
+ }
1447
+ if (cancelRequested) {
277
1448
  return;
278
1449
  }
279
1450
  if (event.type === "error") {
280
1451
  setRunBanner(event.message);
1452
+ if (event.suite && chatIsOpen(event)) {
1453
+ appendStatus(event, event.message);
1454
+ var errorChat = ensurePane(event);
1455
+ if (errorChat) {
1456
+ appendBubble(errorChat, "system", event.message);
1457
+ }
1458
+ }
281
1459
  return;
282
1460
  }
283
1461
  if (event.type === "cell_started") {
284
1462
  markCell(event, "running", "status-skipped");
285
- ensurePane(event);
1463
+ startChat(event);
1464
+ if (event.arm) {
1465
+ var startedWrap = document.getElementById(compareWrapId(event));
1466
+ if (startedWrap) {
1467
+ markArmTab(startedWrap, event.arm, "running");
1468
+ if (startedWrap.classList.contains("compare-tabs") && startedWrap.dataset.userPicked !== "1") {
1469
+ selectCompareTab(startedWrap, event.arm);
1470
+ }
1471
+ }
1472
+ }
1473
+ return;
1474
+ }
1475
+ if (event.type === "status") {
1476
+ if (!chatIsOpen(event)) {
1477
+ return;
1478
+ }
1479
+ appendStatus(event, event.text);
1480
+ return;
1481
+ }
1482
+ if (event.type === "context") {
1483
+ if (!chatIsOpen(event)) {
1484
+ return;
1485
+ }
1486
+ var contextChat = ensurePane(event);
1487
+ if (contextChat) {
1488
+ appendContextFiles(contextChat, event.mode, event.files, event.hostInput);
1489
+ }
286
1490
  return;
287
1491
  }
288
1492
  if (event.type === "prompt") {
289
- appendBubble(ensurePane(event), "user", event.text);
1493
+ if (!chatIsOpen(event)) {
1494
+ return;
1495
+ }
1496
+ var promptChat = ensurePane(event);
1497
+ if (promptChat) {
1498
+ appendBubble(promptChat, "user", event.text);
1499
+ showRunning(event);
1500
+ }
290
1501
  return;
291
1502
  }
292
1503
  if (event.type === "text") {
1504
+ if (!chatIsOpen(event)) {
1505
+ return;
1506
+ }
293
1507
  var key = cellKey(event);
294
1508
  var chat = ensurePane(event);
1509
+ if (!chat) {
1510
+ return;
1511
+ }
1512
+ hideRunning(event);
295
1513
  if (!assistantBodies[key]) {
296
1514
  assistantBodies[key] = appendBubble(chat, "assistant", event.text);
297
1515
  } else {
298
1516
  assistantBodies[key].textContent = event.text;
299
1517
  }
1518
+ setStreaming(event, true);
300
1519
  return;
301
1520
  }
302
1521
  if (event.type === "tool") {
1522
+ if (!chatIsOpen(event)) {
1523
+ return;
1524
+ }
1525
+ var toolChat = ensurePane(event);
1526
+ if (!toolChat) {
1527
+ return;
1528
+ }
1529
+ setStreaming(event, false);
303
1530
  assistantBodies[cellKey(event)] = null;
304
- appendTool(ensurePane(event), event.name, event.args);
1531
+ hideRunning(event);
1532
+ appendTool(toolChat, event.name, event.args);
1533
+ showRunning(event);
305
1534
  return;
306
1535
  }
307
1536
  if (event.type === "cell_finished") {
1537
+ recordProgress(event);
1538
+ if (!chatIsOpen(event)) {
1539
+ return;
1540
+ }
1541
+ hideRunning(event);
1542
+ setStreaming(event, false);
308
1543
  markCell(event, event.skipped ? "skipped" : event.passed ? "passed" : "failed", event.skipped ? "status-skipped" : event.passed ? "status-passed" : "status-failed");
1544
+ finishedMetrics[cellKey(event)] = Object.assign({}, event.metrics || {}, {
1545
+ durationMs: event.durationMs,
1546
+ passed: event.passed,
1547
+ });
1548
+ renderCellResult(event);
1549
+ if (event.arm) {
1550
+ var finishedWrap = document.getElementById(compareWrapId(event));
1551
+ if (finishedWrap) {
1552
+ markArmTab(
1553
+ finishedWrap,
1554
+ event.arm,
1555
+ event.skipped ? "skipped" : event.passed ? "passed" : "failed"
1556
+ );
1557
+ }
1558
+ renderCompareWinners(event);
1559
+ }
1560
+ return;
1561
+ }
1562
+ if (event.type === "judge") {
1563
+ if (!chatIsOpen(event)) {
1564
+ return;
1565
+ }
1566
+ appendJudgeVerdicts(event);
309
1567
  }
310
1568
  }
311
1569
 
1570
+ function markRunTargets(body, label, statusClass) {
1571
+ var hosts = body.hosts && body.hosts.length ? body.hosts : catalog.defaultSelectedHosts;
1572
+ catalog.suites.forEach(function (suite) {
1573
+ if (body.suite && suite.name !== body.suite) {
1574
+ return;
1575
+ }
1576
+ suite.scenarios.forEach(function (scenario) {
1577
+ if (body.scenario && scenario.name !== body.scenario) {
1578
+ return;
1579
+ }
1580
+ if (scenario.skip) {
1581
+ return;
1582
+ }
1583
+ hosts.forEach(function (host) {
1584
+ if (scenario.host && scenario.host !== host) {
1585
+ return;
1586
+ }
1587
+ markCell({ suite: suite.name, scenario: scenario.name, host: host }, label, statusClass);
1588
+ });
1589
+ });
1590
+ });
1591
+ }
1592
+
312
1593
  async function startRun(body) {
313
1594
  if (runId) {
314
- setRunBanner("A run is already in progress.");
1595
+ setRunBanner(
1596
+ cancelRequested
1597
+ ? "Cancelling the run. Wait for it to stop."
1598
+ : "A run is already in progress. Cancel it first."
1599
+ );
315
1600
  return;
316
1601
  }
317
- var response = await fetch("/api/runs", {
318
- method: "POST",
319
- headers: { "content-type": "application/json" },
320
- body: JSON.stringify(body),
1602
+ resetProgress(body);
1603
+ markRunTargets(body, "starting", "status-skipped");
1604
+ var firstCard = document.querySelector(
1605
+ body.suite && body.scenario
1606
+ ? '[data-scenario-card="' + body.suite + "::" + body.scenario + '"]'
1607
+ : body.suite
1608
+ ? '[data-suite="' + body.suite + '"] [data-scenario-card]'
1609
+ : "[data-scenario-card]"
1610
+ );
1611
+ document.querySelectorAll("[data-scenario-card]").forEach(function (card) {
1612
+ if (body.suite && card.getAttribute("data-scenario-card").indexOf(body.suite + "::") !== 0) {
1613
+ return;
1614
+ }
1615
+ if (body.scenario && card.getAttribute("data-scenario-card") !== body.suite + "::" + body.scenario) {
1616
+ return;
1617
+ }
1618
+ delete card.dataset.followHost;
321
1619
  });
322
- var payload = await response.json();
323
- if (!response.ok) {
324
- setRunBanner(payload.error || "Run failed to start.");
325
- return;
1620
+ if (firstCard) {
1621
+ var live = firstCard.querySelector(".scenario-live");
1622
+ if (live) {
1623
+ live.hidden = false;
1624
+ }
1625
+ if (body.hosts && body.hosts.length === 1 && firstCard.dataset.userPickedHost !== "1") {
1626
+ firstCard.dataset.followHost = body.hosts[0];
1627
+ selectHostTab(firstCard, body.hosts[0]);
1628
+ }
1629
+ if (live) {
1630
+ live.scrollIntoView({ block: "nearest" });
1631
+ }
326
1632
  }
327
- runId = payload.runId;
328
- if (source) {
329
- source.close();
1633
+ try {
1634
+ var response = await fetch("/api/runs", {
1635
+ method: "POST",
1636
+ headers: { "content-type": "application/json" },
1637
+ body: JSON.stringify(body),
1638
+ });
1639
+ var payload = await response.json();
1640
+ if (!response.ok) {
1641
+ setRunBanner(payload.error || "Run failed to start.");
1642
+ progress = { total: 0, completed: 0, passed: 0, failed: 0, skipped: 0, seen: {} };
1643
+ renderProgress();
1644
+ markRunTargets(body, "idle", "muted");
1645
+ return;
1646
+ }
1647
+ runId = payload.runId;
1648
+ cancelRequested = false;
1649
+ setRunBanner("Run " + runId + " started.");
1650
+ setCancelButton("Cancel run", false);
1651
+ if (source) {
1652
+ source.close();
1653
+ }
1654
+ source = new EventSource("/api/runs/" + runId + "/events");
1655
+ source.onmessage = function (message) {
1656
+ handleEvent(JSON.parse(message.data));
1657
+ };
1658
+ source.onerror = function () {
1659
+ source.close();
1660
+ if (!runId) {
1661
+ return;
1662
+ }
1663
+ setRunBanner("Live event stream dropped.");
1664
+ };
1665
+ } catch (error) {
1666
+ setRunBanner(error instanceof Error ? error.message : String(error));
1667
+ markRunTargets(body, "idle", "muted");
330
1668
  }
331
- source = new EventSource("/api/runs/" + runId + "/events");
332
- source.onmessage = function (message) {
333
- handleEvent(JSON.parse(message.data));
334
- };
335
1669
  }
336
1670
 
337
1671
  document.getElementById("run-selection").addEventListener("click", function () {
338
1672
  startRun({ hosts: selectedHosts(), parallelHosts: parallelHosts() });
339
1673
  });
340
1674
  document.getElementById("cancel-run").addEventListener("click", function () {
341
- if (!runId) {
1675
+ if (!runId || cancelRequested) {
342
1676
  return;
343
1677
  }
344
- fetch("/api/runs/" + runId + "/cancel", { method: "POST" });
1678
+ cancelRequested = true;
1679
+ setRunBanner("Cancelling the run.");
1680
+ setCancelButton("Cancelling", true);
1681
+ cancelOpenChats();
1682
+ fetch("/api/runs/" + runId + "/cancel", { method: "POST" }).then(function (response) {
1683
+ if (!response.ok && cancelRequested) {
1684
+ setRunBanner("Cancel failed.");
1685
+ setCancelButton("Cancel run", false);
1686
+ cancelRequested = false;
1687
+ }
1688
+ }).catch(function () {
1689
+ if (cancelRequested) {
1690
+ setRunBanner("Cancel failed.");
1691
+ setCancelButton("Cancel run", false);
1692
+ cancelRequested = false;
1693
+ }
1694
+ });
345
1695
  });
346
1696
  document.body.addEventListener("click", function (event) {
347
1697
  var button = event.target.closest("button");
348
1698
  if (!button) {
349
1699
  return;
350
1700
  }
1701
+ if (button.classList.contains("host-tab")) {
1702
+ var card = button.closest("[data-scenario-card]");
1703
+ if (card) {
1704
+ card.dataset.userPickedHost = "1";
1705
+ selectHostTab(card, button.getAttribute("data-host"));
1706
+ }
1707
+ }
351
1708
  if (button.classList.contains("run-cell")) {
352
1709
  startRun({
353
1710
  suite: button.getAttribute("data-suite"),
@@ -408,7 +1765,7 @@ ${viewerCss()}
408
1765
  <div class="host-toggles">${hostToggles}</div>
409
1766
  </div>
410
1767
  <div class="toolbar-block">
411
- <p class="toolbar-label">Matrix</p>
1768
+ <p class="toolbar-label">Run order</p>
412
1769
  <label><input type="checkbox" id="parallel-hosts"> Run hosts together</label>
413
1770
  </div>
414
1771
  <div class="toolbar-actions">
@@ -417,8 +1774,21 @@ ${viewerCss()}
417
1774
  </div>
418
1775
  <p class="run-banner" id="run-banner"></p>
419
1776
  </section>
1777
+ <section class="run-progress" id="run-progress" aria-live="polite" hidden>
1778
+ <div class="run-progress-head">
1779
+ <p class="run-progress-title">0 of 0 tests finished</p>
1780
+ </div>
1781
+ <div class="run-progress-track" role="progressbar" aria-label="Suite run progress" aria-valuemin="0" aria-valuenow="0" aria-valuemax="0">
1782
+ <span class="run-progress-fill"></span>
1783
+ </div>
1784
+ <div class="run-progress-counts">
1785
+ <span class="progress-passed">Passed <strong>0</strong></span>
1786
+ <span class="progress-failed">Failed <strong>0</strong></span>
1787
+ <span class="progress-skipped">Skipped <strong>0</strong></span>
1788
+ <span class="progress-remaining">Remaining <strong>0</strong></span>
1789
+ </div>
1790
+ </section>
420
1791
  ${suites}
421
- <section class="live-dock" id="live-dock"></section>
422
1792
  </main>
423
1793
  <script type="application/json" id="catalog-data">${embedJson(catalog)}</script>
424
1794
  <script>