@ancha-labs/cli 0.1.0 → 0.1.1

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.
@@ -159,7 +159,7 @@ function seedData(scene, selected) {
159
159
  return `<h2>${h(toolName(selected))}</h2>${body || empty("No data seeded")}${details("Full starting data", s)}${selected === "datadog" ? details("Generated telemetry definitions", { metrics: scene.world_rules?.metrics, logs: scene.world_rules?.logs, monitors: scene.world_rules?.monitors }) : ""}`;
160
160
  }
161
161
  var countLabel = (count, noun, plural = noun + "s") => `${count} ${count === 1 ? noun : plural}`;
162
- var toolNames = { slack: "Slack", datadog: "Datadog", pagerduty: "PagerDuty", github: "GitHub", "linkedin-ads": "LinkedIn Ads", salesforce: "Salesforce", gong: "Gong", "google-ads": "Google Ads", "chatgpt-ads": "ChatGPT Ads", hubspot: "HubSpot", ga4: "Google Analytics" };
162
+ var toolNames = { slack: "Slack", "open-meteo": "Open-Meteo", datadog: "Datadog", pagerduty: "PagerDuty", github: "GitHub", "linkedin-ads": "LinkedIn Ads", salesforce: "Salesforce", gong: "Gong", "google-ads": "Google Ads", "chatgpt-ads": "ChatGPT Ads", hubspot: "HubSpot", ga4: "Google Analytics" };
163
163
  var toolName = (name) => toolNames[name] ?? name;
164
164
  var toolLinks = (names) => names.map((name) => `<a class="tool-chip" href="/tools/${encodeURIComponent(name)}" data-nav>${h(toolName(name))}</a>`).join("");
165
165
  var toolDescription = (name) => ({ slack: "Channels, messages and threads", datadog: "Monitors, metrics and logs", pagerduty: "Incidents, on-call and escalation", github: "Repositories, pull requests and deployments" })[name] ?? "API capabilities";
@@ -325,240 +325,6 @@ function toolLogo(name) {
325
325
  return svg ? `<img class="catalog-brand" src="data:image/svg+xml,${encodeURIComponent(svg)}" alt="" width="36" height="36">` : '<span class="catalog-brand" aria-hidden="true">\u25C7</span>';
326
326
  }
327
327
 
328
- // frontend/src/world-workspace.ts
329
- var enc = encodeURIComponent;
330
- var label = (task, index) => task.title && task.title !== String(task.prompt).split("\n")[0]?.slice(0, 120) ? task.title : "Task " + (index + 1);
331
- function worldSetups(tests, worldId, agentId) {
332
- return tests.filter((test) => !test.archived && test.worldId === worldId && (!agentId || test.agentId === agentId));
333
- }
334
- function setupRuns(runs, test, exactRevision = true) {
335
- return runs.filter((run) => !run.entries?.length && run.test?.id === test.id && (!exactRevision || run.test.revision === test.revision) && run.agentId === test.agentId && run.world?.id === test.worldId && (!exactRevision || run.world.revision === test.worldRevision)).sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
336
- }
337
- function setupHistory(runs, test) {
338
- const singles = new Set(setupRuns(runs, test, false).map((run) => run.id));
339
- return runs.flatMap((run) => {
340
- if (singles.has(run.id)) return [{ run, attemptCount: run.attempts?.length ?? 0, earlier: run.test.revision !== test.revision || run.world.revision !== test.worldRevision, grouped: false }];
341
- const entries = (run.entries ?? []).filter((entry) => entry.test?.id === test.id && entry.agentId === test.agentId && entry.world?.id === test.worldId && entry.test.agentId === test.agentId && entry.test.worldId === test.worldId && entry.test.worldRevision === entry.world.revision);
342
- if (!entries.length) return [];
343
- const ids = new Set(entries.map((entry) => entry.id));
344
- return [{
345
- run,
346
- attemptCount: (run.attempts ?? []).filter((attempt) => ids.has(attempt.entryId)).length,
347
- earlier: entries.some((entry) => entry.test.revision !== test.revision || entry.world.revision !== test.worldRevision),
348
- grouped: true
349
- }];
350
- }).sort((a, b) => String(b.run.createdAt).localeCompare(String(a.run.createdAt)));
351
- }
352
- function worldHistory(runs, worldId, agentId) {
353
- return runs.flatMap((run) => {
354
- if (!run.entries?.length) return run.world?.id === worldId && (!agentId || run.agentId === agentId) ? [{ run, attemptCount: run.attempts?.length ?? 0, grouped: false }] : [];
355
- const entries = run.entries.filter((entry) => entry.world?.id === worldId && (!agentId || entry.agentId === agentId));
356
- if (!entries.length) return [];
357
- const ids = new Set(entries.map((entry) => entry.id));
358
- return [{ run, attemptCount: (run.attempts ?? []).filter((attempt) => ids.has(attempt.entryId)).length, grouped: true }];
359
- }).sort((a, b) => String(b.run.createdAt).localeCompare(String(a.run.createdAt)));
360
- }
361
- function workspaceDefinition(draft) {
362
- return {
363
- name: draft.name,
364
- agentId: draft.agentId,
365
- worldId: draft.worldId,
366
- worldRevision: Number(draft.worldRevision),
367
- tasks: draft.tasks.map((task) => ({ id: task.id, ...String(task.title ?? "").trim() ? { title: String(task.title).trim() } : {}, prompt: task.prompt, attempts: Number(task.attempts), timeoutMs: Number(task.timeoutMs) })),
368
- concurrency: Number(draft.concurrency),
369
- seed: draft.seed ?? "playground-1"
370
- };
371
- }
372
- function mountWorldWorkspace(node, options) {
373
- const { world, agents, api: api2, go: go2 } = options, base = "/worlds/" + enc(world.id), q = new URLSearchParams(location.search);
374
- let tests = [], runs = [], errors = [], selected, agentId = q.get("agent") ?? "", configure = q.has("configure"), showHistory = false, saving = false, disposed = false, error = "", loaded = false;
375
- const revisions = /* @__PURE__ */ new Map([[world.revision, world]]);
376
- let draft, timer;
377
- const launchPending = () => {
378
- if (!selected) return false;
379
- try {
380
- return sessionStorage.getItem("ancha-saved-test-run:" + selected.id) !== null;
381
- } catch {
382
- return true;
383
- }
384
- };
385
- const key = () => "ancha-world-setup:" + world.id + ":" + (selected?.id ?? "new:" + agentId);
386
- const remember = () => {
387
- if (draft) try {
388
- sessionStorage.setItem(key(), JSON.stringify(draft));
389
- } catch {
390
- }
391
- };
392
- function getDraft() {
393
- if (draft) return draft;
394
- try {
395
- const stored = JSON.parse(sessionStorage.getItem(key()) ?? "null");
396
- if (stored && Array.isArray(stored.tasks)) draft = stored;
397
- } catch {
398
- }
399
- draft ??= selected ? { ...selected, tasks: structuredClone(selected.tasks) } : { name: world.name, agentId, worldId: world.id, worldRevision: world.revision, concurrency: 1, seed: "playground-1", tasks: [{ id: "task_" + crypto.randomUUID(), title: "", prompt: "", attempts: 1, timeoutMs: 3e5 }] };
400
- return draft;
401
- }
402
- function capture() {
403
- const form = node.querySelector(".ww-form");
404
- if (!form) return;
405
- const d = getDraft(), values = new FormData(form);
406
- d.name = String(values.get("name") ?? "");
407
- d.agentId = String(values.get("agentId") ?? "");
408
- d.concurrency = Number(values.get("concurrency"));
409
- d.worldRevision = Number(values.get("worldRevision"));
410
- d.tasks = [...form.querySelectorAll(".ww-task-editor")].map((row) => ({ id: row.dataset.taskId, title: row.querySelector("[name=title]").value, prompt: row.querySelector("[name=prompt]").value, attempts: Number(row.querySelector("[name=attempts]").value), timeoutMs: Math.round(Number(row.querySelector("[name=minutes]").value) * 6e4) }));
411
- remember();
412
- }
413
- function href2(test, agent = agentId, extra = "") {
414
- return base + "?" + new URLSearchParams({ ...agent ? { agent } : {}, ...test ? { test: test.id } : {}, ...extra ? { [extra]: "1" } : {} });
415
- }
416
- function configuration2() {
417
- const d = getDraft(), pinned = Number(d.worldRevision) !== world.revision, startingWorld = revisions.get(Number(d.worldRevision));
418
- return `<section class="ww-configure"><div class="ww-section-heading"><h2>Configure</h2><button type="button" data-ww="close">Close</button></div><form class="ww-form"><div class="form-grid"><label>Agent<select name="agentId" required><option value="">Choose agent</option>${agents.map((a) => `<option value="${h(a.id)}" ${d.agentId === a.id ? "selected" : ""}>${h(a.name)}</option>`).join("")}</select></label><label>Configuration name<input name="name" required maxlength="120" value="${h(d.name)}"></label></div><section class="ww-tools"><div class="ww-section-heading"><h3>Tools</h3><a data-nav href="${base}?edit=1">Edit world</a></div><div class="ww-tool-list">${(startingWorld?.configuration.preset.datasets ?? []).map((item) => `<a data-nav href="/tools/${enc(item.tool)}">${toolLogo(item.tool)}${h(toolName(item.tool))}</a>`).join("")}</div>${pinned ? `<label>Starting data<select name="worldRevision"><option value="${h(d.worldRevision)}">Saved revision ${h(d.worldRevision)}</option><option value="${world.revision}">Current revision ${world.revision}</option></select></label>` : `<input type="hidden" name="worldRevision" value="${h(d.worldRevision)}">`}</section><div class="ww-section-heading"><h3>Tasks</h3><button type="button" data-ww="add-task" ${d.tasks.length >= 20 ? "disabled" : ""}>Add task</button></div><div class="ww-task-fields">${d.tasks.map((task, index) => `<article class="ww-task-editor" data-task-id="${h(task.id)}"><div class="ww-section-heading"><h3>Task ${index + 1}</h3>${d.tasks.length > 1 ? `<button type="button" data-ww="remove-task" data-value="${h(task.id)}">Remove</button>` : ""}</div><label>Task name <span class="muted">optional</span><input name="title" maxlength="120" value="${h(task.title ?? "")}" placeholder="Task ${index + 1}"></label><label>Instructions<textarea name="prompt" required maxlength="20000" rows="4">${h(task.prompt)}</textarea></label><div class="ww-task-options"><label>Repeat<input type="number" name="attempts" min="1" max="100" value="${h(task.attempts)}" required><span>times</span></label><label>Time limit <span class="muted">minutes</span><input type="number" name="minutes" min="0.016666666666666666" max="60" step="any" value="${h(task.timeoutMs / 6e4)}" required></label></div></article>`).join("")}</div><div class="ww-footer"><label>Run at once<input name="concurrency" type="number" min="1" max="10" value="${h(d.concurrency)}" required></label><button type="submit" class="primary" ${saving ? "disabled" : ""}>${saving ? "Saving\u2026" : "Save configuration"}</button></div></form></section>`;
419
- }
420
- function render() {
421
- if (disposed) return;
422
- const currentRuns = selected ? setupRuns(runs, selected) : [], run = currentRuns[0], attempts = run?.attempts ?? [], tasks = selected?.tasks ?? [], choices = worldSetups(tests, world.id, agentId);
423
- node.innerHTML = `<div class="ww-workspace"><header class="ww-header"><h1>${h(world.name)}</h1><div class="ww-world-actions"><label class="ww-agent-select">Agent<select data-ww-select="agent"><option value="" disabled>Choose agent</option>${agents.map((a) => `<option value="${h(a.id)}" ${a.id === agentId ? "selected" : ""}>${h(a.name)}</option>`).join("")}</select></label><button type="button" data-ww="configure">Configure</button>${selected ? options.runActions?.(selected.id) ?? button("Run tasks", "run-saved-test", selected.id, "primary") : ""}</div></header><p class="ww-error" role="alert" ${error ? "" : "hidden"}>${h(error)}</p>${errors.map((e) => `<p class="ww-error" role="alert">${h(e.message)}</p>`).join("")}${!loaded ? '<p role="status">Loading tasks\u2026</p>' : ""}${choices.length > 1 ? `<label class="ww-setup-select">Configuration<select data-ww-select="test">${!selected ? '<option value="" selected disabled>New configuration</option>' : ""}${choices.map((t) => `<option value="${h(t.id)}" ${t.id === selected?.id ? "selected" : ""}>${h(t.name)}</option>`).join("")}</select></label>` : ""}${loaded && configure ? configuration2() : loaded ? `<section class="ww-summary-widgets"><article><span>Success rate</span><strong class="muted">\u2014</strong></article><article><span>Attempts</span><strong>${attempts.length}</strong></article><article><span>Ungraded</span><strong>${attempts.length}</strong></article></section><div class="ww-section-heading"><h2>Tasks${tasks.length ? ` <span class="ww-task-count">${tasks.length}</span>` : ""}</h2><div class="actions">${worldHistory(runs, world.id, agentId || void 0).length || selected ? `<button type="button" data-ww="history" aria-expanded="${showHistory}">History</button>` : ""}<button type="button" data-ww="configure">${tasks.length ? "Edit tasks" : "Add tasks"}</button></div></div>${tasks.length ? `<section class="ww-task-table"><div class="ww-table-head"><span>Task</span><span>Success rate</span><span>Attempts</span><span></span></div>${tasks.map((task, index) => {
424
- const list2 = attempts.filter((a) => a.taskId === task.id), taskURL = run ? `/playgrounds/${enc(run.id)}?task=${enc(task.id)}${list2[0] ? "&attempt=" + enc(list2[0].id) : ""}` : "";
425
- return `<article class="ww-task-row"><div class="ww-task-identity">${taskURL ? `<a data-nav class="ww-task-name" href="${taskURL}"><span class="ww-ordinal">${index + 1}</span><strong>${h(label(task, index))}</strong></a>` : `<strong class="ww-task-name"><span class="ww-ordinal">${index + 1}</span>${h(label(task, index))}</strong>`}<p class="ww-task-prompt" title="${h(task.prompt)}">${h(task.prompt)}</p></div><div class="ww-rate">${list2.length ? "Ungraded" : "\u2014"}</div><div class="ww-attempt-marks" role="group" aria-label="Attempts for ${h(label(task, index))}">${list2.map((a) => `<a data-nav class="ww-attempt-mark ${h(a.status)}" href="/playgrounds/${enc(run.id)}?task=${enc(task.id)}&attempt=${enc(a.id)}" title="Attempt ${a.number}: ${h(attemptLabel(a.status))} \xB7 Ungraded" aria-label="Attempt ${a.number}: ${h(attemptLabel(a.status))} \xB7 Ungraded">${a.status === "error" ? "!" : ["running", "starting"].includes(a.status) ? "\u25CC" : "\u2212"}</a>`).join("") || `<span class="muted">Repeat ${h(task.attempts)} times</span>`}</div>${taskURL ? `<a data-nav class="ww-row-inspect" href="${taskURL}" aria-label="Inspect ${h(label(task, index))}">\u2192</a>` : "<span></span>"}</article>`;
426
- }).join("")}</section>` : `<div class="quiet-empty">${agents.length ? "Add tasks to this world." : '<a data-nav href="/agents">Connect an agent</a>'}</div>`}${selected && !currentRuns.length && setupHistory(runs, selected).some((item) => item.earlier) ? '<p class="ww-snapshot-notice">This configuration has changed. Earlier attempts are in History.</p>' : ""}${showHistory ? history2() : ""}` : ""}</div>`;
427
- }
428
- function history2() {
429
- const all = worldHistory(runs, world.id, agentId || void 0);
430
- return `<section class="ww-history"><div class="ww-section-heading"><h2>History</h2><button type="button" data-ww="new">New configuration</button></div>${all.length ? all.map(({ run, attemptCount, grouped }) => `<a data-nav class="ww-history-row" href="/playgrounds/${enc(run.id)}"><time>${h(new Date(run.createdAt).toLocaleString())}</time><span>${h(attemptLabel(run.status))}</span><span>${attemptCount} attempts</span>${grouped ? "<span>Group</span>" : ""}${run.test?.name || run.groupName ? `<span>${h(run.groupName ?? run.test.name)}</span>` : ""}<span>\u2192</span></a>`).join("") : '<p class="quiet-empty">No attempts yet.</p>'}</section>`;
431
- }
432
- async function refresh(initial = false) {
433
- try {
434
- const [definitions, batches] = await Promise.all([api2("/playground-tests"), api2("/playgrounds")]);
435
- if (disposed) return;
436
- tests = definitions.tests ?? [];
437
- runs = batches.runs ?? [];
438
- errors = [...definitions.errors ?? [], ...batches.errors ?? []];
439
- if (initial) {
440
- const requested = q.get("test");
441
- if (requested) {
442
- selected = worldSetups(tests, world.id).find((test) => test.id === requested);
443
- if (!selected || agentId && selected.agentId !== agentId) {
444
- selected = void 0;
445
- error = "This configuration is unavailable for this world and agent.";
446
- } else agentId = selected.agentId;
447
- } else if (q.has("new")) {
448
- selected = void 0;
449
- agentId ||= agents[0]?.id ?? "";
450
- } else {
451
- selected = worldSetups(tests, world.id, agentId || void 0)[0];
452
- const historical = worldHistory(runs, world.id)[0]?.run;
453
- agentId ||= selected?.agentId ?? historical?.entries?.find((entry) => entry.world?.id === world.id)?.agentId ?? historical?.agentId ?? agents[0]?.id ?? "";
454
- }
455
- } else if (selected) selected = tests.find((test) => test.id === selected.id);
456
- if (initial && !selected && worldHistory(runs, world.id, agentId || void 0).length) showHistory = true;
457
- if (configure) getDraft();
458
- options.onContext?.(selected);
459
- const pinnedRevision = Number(draft?.worldRevision ?? selected?.worldRevision ?? world.revision);
460
- if (!revisions.has(pinnedRevision)) {
461
- const savedWorld = await api2(`/worlds/${enc(world.id)}?revision=${pinnedRevision}`);
462
- if (disposed) return;
463
- revisions.set(pinnedRevision, savedWorld);
464
- }
465
- loaded = true;
466
- if (!configure) render();
467
- else if (initial) render();
468
- if (runs.some((run) => !terminal(run.status))) timer = setTimeout(() => {
469
- void refresh();
470
- }, 2e3);
471
- } catch (e) {
472
- if (!disposed) {
473
- error = e instanceof Error ? e.message : String(e);
474
- loaded = true;
475
- render();
476
- }
477
- }
478
- }
479
- node.addEventListener("input", capture);
480
- node.addEventListener("change", (event) => {
481
- const control = event.target;
482
- if (control.dataset.wwSelect === "agent") {
483
- capture();
484
- const match = worldSetups(tests, world.id, control.value)[0];
485
- go2(href2(match, control.value));
486
- } else if (control.dataset.wwSelect === "test") go2(href2(tests.find((t) => t.id === control.value)));
487
- else {
488
- capture();
489
- if (control.name === "worldRevision") render();
490
- }
491
- });
492
- node.addEventListener("click", (event) => {
493
- const control = event.target.closest("[data-ww]");
494
- if (!control) return;
495
- event.preventDefault();
496
- event.stopPropagation();
497
- if (saving) return;
498
- capture();
499
- error = "";
500
- switch (control.dataset.ww) {
501
- case "configure":
502
- if (launchPending()) {
503
- error = "Resolve the pending run before editing this configuration.";
504
- break;
505
- }
506
- configure = true;
507
- break;
508
- case "close":
509
- configure = false;
510
- break;
511
- case "history":
512
- showHistory = !showHistory;
513
- break;
514
- case "new":
515
- go2(href2(void 0, agentId, "new") + "&configure=1");
516
- return;
517
- case "add-task":
518
- if (getDraft().tasks.length < 20) getDraft().tasks.push({ id: "task_" + crypto.randomUUID(), title: "", prompt: "", attempts: 1, timeoutMs: 3e5 });
519
- break;
520
- case "remove-task":
521
- if (getDraft().tasks.length > 1) getDraft().tasks = getDraft().tasks.filter((t) => t.id !== control.dataset.value);
522
- break;
523
- }
524
- remember();
525
- render();
526
- });
527
- node.addEventListener("submit", (event) => {
528
- const form = event.target;
529
- if (!form.matches(".ww-form")) return;
530
- event.preventDefault();
531
- event.stopPropagation();
532
- if (saving) return;
533
- if (launchPending()) {
534
- error = "Resolve the pending run before editing this configuration.";
535
- render();
536
- return;
537
- }
538
- capture();
539
- saving = true;
540
- error = "";
541
- render();
542
- const d = getDraft(), body = workspaceDefinition(d), oldKey = key();
543
- void api2(selected ? `/playground-tests/${enc(selected.id)}/update` : "/playground-tests", { ...body, ...selected ? { revision: d.revision } : {} }).then((saved) => {
544
- sessionStorage.removeItem(oldKey);
545
- if (!disposed) go2(href2(saved, saved.agentId));
546
- }).catch((e) => {
547
- if (!disposed) {
548
- error = e instanceof Error ? e.message : String(e);
549
- saving = false;
550
- render();
551
- }
552
- });
553
- });
554
- render();
555
- void refresh(true);
556
- return () => {
557
- disposed = true;
558
- clearTimeout(timer);
559
- };
560
- }
561
-
562
328
  // frontend/src/experience.ts
563
329
  var list = (value) => Array.isArray(value) ? value.filter((v) => v && typeof v === "object") : [];
564
330
  var text = (value) => typeof value === "string" ? value : typeof value === "number" || typeof value === "boolean" ? String(value) : "";
@@ -915,7 +681,15 @@ function taskDisplayName(task, index) {
915
681
  const title2 = typeof task.title === "string" ? task.title.trim().replace(/\s+/g, " ") : "";
916
682
  return title2 && title2.length <= 80 ? title2 : `Task ${index + 1}`;
917
683
  }
684
+ function attemptOutcome(attempt) {
685
+ if (attempt.outcome === "passed") return { key: "passed", label: "Passed", tone: "pass", mark: "\u2713" };
686
+ if (attempt.outcome === "failed") return { key: "failed", label: "Failed", tone: "fail", mark: "\u2717" };
687
+ return { key: "ungraded", label: "Ungraded", tone: "neutral", mark: "\u2013" };
688
+ }
918
689
  function executionSummary(attempts) {
690
+ const finished = attempts.filter((a) => ["completed", "error", "stopped"].includes(a.status));
691
+ const durations = finished.map((a) => Date.parse(a.finishedAt) - Date.parse(a.startedAt)).filter((ms) => Number.isFinite(ms) && ms >= 0).sort((x, y) => x - y);
692
+ const passed = attempts.filter((a) => a.outcome === "passed").length, failed = attempts.filter((a) => a.outcome === "failed").length;
919
693
  return {
920
694
  total: attempts.length,
921
695
  completed: attempts.filter((a) => a.status === "completed").length,
@@ -923,17 +697,61 @@ function executionSummary(attempts) {
923
697
  queued: attempts.filter((a) => a.status === "queued").length,
924
698
  error: attempts.filter((a) => a.status === "error").length,
925
699
  stopped: attempts.filter((a) => a.status === "stopped").length,
926
- ungraded: attempts.length
700
+ finished: finished.length,
701
+ passed,
702
+ failed,
703
+ graded: passed + failed,
704
+ ungraded: attempts.length - passed - failed,
705
+ toolCalls: attempts.reduce((sum, a) => sum + (Number(a.toolCalls) || 0), 0),
706
+ medianMs: durations.length ? durations[Math.floor((durations.length - 1) / 2)] : void 0
927
707
  };
928
708
  }
709
+ var percent = (part, whole) => whole ? `${Math.round(part / whole * 100)}%` : "\u2014";
710
+ var duration = (ms) => ms === void 0 ? "\u2014" : ms < 1e4 ? `${(ms / 1e3).toFixed(1)}s` : ms < 12e4 ? `${Math.round(ms / 1e3)}s` : `${Math.round(ms / 6e4)}m`;
711
+ function summaryWidgets(attempts, wrapper = "aw-widgets", item = 'section class="aw-widget"') {
712
+ const s = executionSummary(attempts), tag = item.split(" ")[0];
713
+ const cell = (label3, value, note) => `<${item}><span>${h(label3)}</span><strong>${h(value)}</strong><small>${h(note)}</small></${tag}>`;
714
+ const progress = s.active || s.queued ? [s.active && `${s.active} running`, s.queued && `${s.queued} queued`].filter(Boolean).join(" \xB7 ") : s.error ? `${s.error} ended with an error` : s.total ? "All finished" : "No attempts yet";
715
+ const rate = s.graded ? `${s.passed} of ${s.graded} passed` : s.total ? "Add checks to a task to grade it" : "No attempts yet";
716
+ return `<div class="${wrapper} four">${cell("Success rate", percent(s.passed, s.graded), rate)}${cell("Finished", `${s.finished}/${s.total}`, progress)}${cell("Tool calls", String(s.toolCalls), s.finished ? `${(s.toolCalls / s.finished).toFixed(1)} per attempt` : "\u2014")}${cell("Median time", duration(s.medianMs), "per attempt")}</div>${progressBar(attempts)}`;
717
+ }
718
+ function attemptState(attempt) {
719
+ const outcome = attemptOutcome(attempt);
720
+ if (outcome.key !== "ungraded") return outcome.key;
721
+ return ["queued", "starting", "running", "completed", "stopped", "error"].includes(attempt.status) ? attempt.status === "starting" ? "running" : attempt.status : "queued";
722
+ }
723
+ function progressBar(attempts) {
724
+ if (!attempts.length) return "";
725
+ const s = executionSummary(attempts), label3 = `${s.finished} of ${s.total} attempts finished${s.graded ? `, ${s.passed} passed, ${s.failed} failed` : ""}`;
726
+ return `<div class="run-progress" role="img" aria-label="${h(label3)}" title="${h(label3)}">${attempts.map((a) => `<span class="seg ${attemptState(a)}"></span>`).join("")}</div>`;
727
+ }
728
+ function taskOutcome(attempts) {
729
+ const s = executionSummary(attempts);
730
+ if (s.graded) return { value: percent(s.passed, s.graded), note: `${s.passed} of ${s.graded} passed`, tone: s.passed === s.graded ? "pass" : s.passed ? "mixed" : "fail" };
731
+ if (!s.total) return { value: "\u2014", note: "No attempts", tone: "neutral" };
732
+ return { value: `${s.finished}/${s.total}`, note: s.active || s.queued ? "running" : "finished \xB7 no checks", tone: "neutral" };
733
+ }
929
734
  function executionBadge(attempt) {
930
735
  const status = String(attempt.status ?? "unavailable");
931
736
  const tone = status === "error" ? "error" : ["running", "starting"].includes(status) ? "active" : "neutral";
932
737
  return `<span class="aw-execution ${tone}">${h(attemptLabel(status))}</span>`;
933
738
  }
739
+ function outcomeBadge(attempt) {
740
+ const outcome = attemptOutcome(attempt);
741
+ return `<span class="aw-outcome ${outcome.tone}">${h(outcome.label)}</span>`;
742
+ }
743
+ function railMark(attempt) {
744
+ const outcome = attemptOutcome(attempt);
745
+ return `<span class="aw-rail-mark ${outcome.tone}" title="${h(outcome.label)}" aria-label="${h(outcome.label)}">${outcome.mark}</span>`;
746
+ }
747
+ function checksView(attempt) {
748
+ const checks2 = records(attempt.checks);
749
+ if (!checks2.length) return "";
750
+ const passed = checks2.filter((c) => c.passed).length;
751
+ return `<section class="aw-checks"><h3>Checks <span>${passed} of ${checks2.length} passed</span></h3><ul>${checks2.map((c) => `<li class="${c.passed ? "pass" : "fail"}"><span class="aw-check-mark" aria-hidden="true">${c.passed ? "\u2713" : "\u2717"}</span><div><strong>${h(c.id)}</strong><p>${h(c.expected)}</p><p class="muted">${h(c.error ? `Error: ${c.error}` : c.actual)}</p></div></li>`).join("")}</ul></section>`;
752
+ }
934
753
  function widgets(attempts) {
935
- const summary = executionSummary(attempts);
936
- return `<div class="aw-widgets"><section class="aw-widget"><span>Success rate</span><strong>\u2014</strong></section><section class="aw-widget"><span>Attempts</span><strong>${summary.total}</strong></section><section class="aw-widget"><span>Ungraded</span><strong>${summary.ungraded}</strong></section></div>`;
754
+ return summaryWidgets(attempts);
937
755
  }
938
756
  function errorMessage(value) {
939
757
  if (!value) return "";
@@ -947,8 +765,9 @@ function entrySections(batch) {
947
765
  function markers(batch, task, attempts) {
948
766
  return `<div class="aw-attempt-strip" aria-label="${h(task.title || "Task")} attempts">${attempts.map((attempt) => {
949
767
  const state = ["queued", "starting", "running", "completed", "stopped", "error"].includes(attempt.status) ? attempt.status : "unavailable";
950
- const label3 = `Attempt ${attempt.number} \xB7 ${attemptLabel(state)} \xB7 Ungraded`;
951
- return `<a data-nav class="aw-attempt-marker execution-${h(state)}" href="${h(taskHref(batch, task, { attempt: attempt.id }))}" title="${h(label3)}" aria-label="${h(label3)}"><span>${h(attempt.number)}</span></a>`;
768
+ const outcome = attemptOutcome(attempt);
769
+ const label3 = `Attempt ${attempt.number} \xB7 ${attemptLabel(state)} \xB7 ${outcome.label}`;
770
+ return `<a data-nav class="aw-attempt-marker execution-${h(state)} outcome-${outcome.key}" href="${h(taskHref(batch, task, { attempt: attempt.id }))}" title="${h(label3)}" aria-label="${h(label3)}"><span>${h(attempt.number)}</span></a>`;
952
771
  }).join("")}</div>`;
953
772
  }
954
773
  function comparisonForm(batch, task, attempts, selected) {
@@ -964,8 +783,8 @@ function renderRunOverview(batch, actionsHtml = "", options = {}) {
964
783
  const shown = selected ? tasks.filter((t) => t.id === selected.id) : tasks;
965
784
  if (!shown.length) return "";
966
785
  return `<section class="aw-task-section">${groups.length > 1 ? `<header class="aw-entry-header"><h2>${h(entry?.agentName ?? entry?.name ?? "Agent")}</h2><span>${h(entry?.world?.name ?? entry?.test?.world?.name ?? "")}</span></header>` : ""}<div class="aw-section-heading"><h2>${selected ? "Attempts" : "Tasks"}</h2>${!selected ? `<span>${tasks.length}</span>` : ""}</div>${selected ? `<section class="aw-task-description"><p>${h(selected.prompt)}</p></section>` : ""}<div class="aw-task-table">${shown.map((task) => {
967
- const taskAttempts = attempts.filter((a) => a.taskId === task.id), index = tasks.indexOf(task), first = taskAttempts[0];
968
- return `<article class="aw-task-row"><div class="aw-task-name">${navLink(taskDisplayName(task, index), taskHref(batch, task, first ? { attempt: first.id } : {}))}${!selected ? `<p class="aw-prompt-excerpt">${h(task.prompt)}</p>` : ""}</div><div class="aw-task-outcome"><strong>\u2014</strong><span>Ungraded</span></div>${markers(batch, task, taskAttempts)}${taskAttempts.length ? "" : '<p class="quiet-empty">No attempts recorded.</p>'}${comparisonForm(batch, task, taskAttempts)}</article>`;
786
+ const taskAttempts = attempts.filter((a) => a.taskId === task.id), index = tasks.indexOf(task), first = taskAttempts[0], outcome = taskOutcome(taskAttempts);
787
+ return `<article class="aw-task-row"><div class="aw-task-name">${navLink(taskDisplayName(task, index), taskHref(batch, task, first ? { attempt: first.id } : {}))}${!selected ? `<p class="aw-prompt-excerpt">${h(task.prompt)}</p>` : ""}</div><div class="aw-task-outcome ${outcome.tone}"><strong>${h(outcome.value)}</strong><span>${h(outcome.note)}</span></div>${markers(batch, task, taskAttempts)}${taskAttempts.length ? "" : '<p class="quiet-empty">No attempts recorded.</p>'}${comparisonForm(batch, task, taskAttempts)}</article>`;
969
788
  }).join("")}</div></section>`;
970
789
  }).join("") || '<p class="quiet-empty">No tasks recorded.</p>'}</div>`;
971
790
  }
@@ -1082,7 +901,7 @@ function renderAttemptWorkspace(batch, attempt, evidence, options = {}) {
1082
901
  const query = options.query ?? {};
1083
902
  const toolsNav = `<nav class="tool-nav" aria-label="Attempt tools">${tools.map((t) => navLink(toolName(t), taskHref(batch, task, { attempt: attempt.id, view: "tools", tool: t }), t === tool ? "active" : "")).join("")}</nav>`;
1084
903
  const content = view === "tools" ? toolsNav + (tool ? capturedToolView(observation2, tool, { ...query, baseHref: attemptBase }) : '<p class="quiet-empty">No captured tool data.</p>') : managedResponse(fullEvidence) + activityView(observation2, { baseHref: attemptBase });
1085
- return `<div class="aw-task-detail">${navLink("\u2190 Tasks", basePath(batch), "back")}<header class="aw-header"><h1>${h(taskDisplayName(task, Math.max(index, 0)))}</h1><div class="aw-actions">${options.actionsHtml ?? ""}</div></header><section class="aw-task-description"><p>${h(task.prompt)}</p></section>${errorMessage(batch.error)}<div class="aw-workspace"><aside class="aw-attempt-rail"><h2>Attempts</h2><nav class="aw-attempt-list" aria-label="Task attempts">${attempts.map((a) => `<a data-nav class="aw-attempt-item ${a.id === attempt.id ? "selected" : ""}" ${a.id === attempt.id ? 'aria-current="page"' : ""} href="${h(taskHref(batch, task, { attempt: a.id, view }))}"><span class="aw-attempt-number">Attempt ${h(a.number)}</span>${executionBadge(a)}</a>`).join("")}</nav></aside><section class="aw-evidence"><header class="aw-evidence-header"><div><h2>Attempt ${h(attempt.number)}</h2>${executionBadge(attempt)}<span class="aw-outcome">Ungraded</span></div>${comparisonForm(batch, task, attempts, attempt)}</header><nav class="aw-tabs" aria-label="Attempt evidence">${navLink("Activity", attemptBase, view === "activity" ? "active" : "").replace("<a ", `<a ${view === "activity" ? 'aria-current="page" ' : ""}`)}${navLink("Tools", taskHref(batch, task, { attempt: attempt.id, view: "tools" }), view === "tools" ? "active" : "").replace("<a ", `<a ${view === "tools" ? 'aria-current="page" ' : ""}`)}</nav><div id="attempt-evidence" class="aw-evidence-body">${errorMessage(fullEvidence.error)}${content}</div></section></div></div>`;
904
+ return `<div class="aw-task-detail">${navLink("\u2190 Tasks", basePath(batch), "back")}<header class="aw-header"><h1>${h(taskDisplayName(task, Math.max(index, 0)))}</h1><div class="aw-actions">${options.actionsHtml ?? ""}</div></header><section class="aw-task-description"><p>${h(task.prompt)}</p></section>${errorMessage(batch.error)}<div class="aw-workspace"><aside class="aw-attempt-rail"><h2>Attempts</h2><nav class="aw-attempt-list" aria-label="Task attempts">${attempts.map((a) => `<a data-nav class="aw-attempt-item ${a.id === attempt.id ? "selected" : ""}" ${a.id === attempt.id ? 'aria-current="page"' : ""} href="${h(taskHref(batch, task, { attempt: a.id, view }))}"><span class="aw-attempt-number">Attempt ${h(a.number)}</span>${executionBadge(a)}${a.outcome ? railMark(a) : ""}</a>`).join("")}</nav></aside><section class="aw-evidence"><header class="aw-evidence-header"><div><h2>Attempt ${h(attempt.number)}</h2>${executionBadge(attempt)}${outcomeBadge(attempt)}</div>${comparisonForm(batch, task, attempts, attempt)}</header>${checksView(attempt)}<nav class="aw-tabs" aria-label="Attempt evidence">${navLink("Activity", attemptBase, view === "activity" ? "active" : "").replace("<a ", `<a ${view === "activity" ? 'aria-current="page" ' : ""}`)}${navLink("Tools", taskHref(batch, task, { attempt: attempt.id, view: "tools" }), view === "tools" ? "active" : "").replace("<a ", `<a ${view === "tools" ? 'aria-current="page" ' : ""}`)}</nav><div id="attempt-evidence" class="aw-evidence-body">${errorMessage(fullEvidence.error)}${content}</div></section></div></div>`;
1086
905
  }
1087
906
  function renderManagedComparison(data) {
1088
907
  const sides = records(data.attempts ?? [data.left, data.right]);
@@ -1091,7 +910,279 @@ function renderManagedComparison(data) {
1091
910
  const summary = data.stateComparison;
1092
911
  const stateSummary = !summary?.available ? "Final tool data is unavailable for one or both attempts." : !records(summary.differences).length ? "Same final tool data" : `Different final data: ${[...new Set(records(summary.differences).map((d) => String(d.path).split("/")[1]).filter(Boolean))].map(toolName).join(" \xB7 ")}${summary.truncated ? " \xB7 More differences in saved evidence" : ""}`;
1093
912
  const cells = (render, extra = "") => sides.map((side) => `<section class="aw-compare-cell ${extra}">${render(side)}</section>`).join("");
1094
- return `<div class="aw-managed-comparison"><p class="aw-comparison-summary">${h(stateSummary)}</p><div class="aw-comparison">${cells((side) => `<h2>Attempt ${h(side.number)}</h2>${executionBadge(side)}<span class="aw-outcome">Ungraded</span>${navLink("Open attempt", url("/playgrounds/" + encodeURIComponent(String(data.runId)), { task: data.taskId ?? side.taskId, attempt: side.id }))}`, "aw-compare-head")}${cells((side) => errorMessage(side.error) + managedResponse(side), "aw-compare-response")}${cells((side) => activityView({ ...side, trace: side.toolActivity ?? side.trace ?? [], state: side.state ?? {} }, { baseHref: url("/playgrounds/" + encodeURIComponent(String(data.runId)), { task: data.taskId ?? side.taskId, attempt: side.id }) }), "aw-compare-activity")}</div></div>`;
913
+ return `<div class="aw-managed-comparison"><p class="aw-comparison-summary">${h(stateSummary)}</p><div class="aw-comparison">${cells((side) => `<h2>Attempt ${h(side.number)}</h2>${executionBadge(side)}${outcomeBadge(side)}${navLink("Open attempt", url("/playgrounds/" + encodeURIComponent(String(data.runId)), { task: data.taskId ?? side.taskId, attempt: side.id }))}`, "aw-compare-head")}${cells((side) => errorMessage(side.error) + managedResponse(side), "aw-compare-response")}${cells((side) => activityView({ ...side, trace: side.toolActivity ?? side.trace ?? [], state: side.state ?? {} }, { baseHref: url("/playgrounds/" + encodeURIComponent(String(data.runId)), { task: data.taskId ?? side.taskId, attempt: side.id }) }), "aw-compare-activity")}</div></div>`;
914
+ }
915
+
916
+ // frontend/src/world-workspace.ts
917
+ var enc = encodeURIComponent;
918
+ var label = (task, index) => task.title && task.title !== String(task.prompt).split("\n")[0]?.slice(0, 120) ? task.title : "Task " + (index + 1);
919
+ function worldSetups(tests, worldId, agentId) {
920
+ return tests.filter((test) => !test.archived && test.worldId === worldId && (!agentId || test.agentId === agentId));
921
+ }
922
+ function setupRuns(runs, test, exactRevision = true) {
923
+ return runs.filter((run) => !run.entries?.length && run.test?.id === test.id && (!exactRevision || run.test.revision === test.revision) && run.agentId === test.agentId && run.world?.id === test.worldId && (!exactRevision || run.world.revision === test.worldRevision)).sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
924
+ }
925
+ function setupHistory(runs, test) {
926
+ const singles = new Set(setupRuns(runs, test, false).map((run) => run.id));
927
+ return runs.flatMap((run) => {
928
+ if (singles.has(run.id)) return [{ run, attemptCount: run.attempts?.length ?? 0, earlier: run.test.revision !== test.revision || run.world.revision !== test.worldRevision, grouped: false }];
929
+ const entries = (run.entries ?? []).filter((entry) => entry.test?.id === test.id && entry.agentId === test.agentId && entry.world?.id === test.worldId && entry.test.agentId === test.agentId && entry.test.worldId === test.worldId && entry.test.worldRevision === entry.world.revision);
930
+ if (!entries.length) return [];
931
+ const ids = new Set(entries.map((entry) => entry.id));
932
+ return [{
933
+ run,
934
+ attemptCount: (run.attempts ?? []).filter((attempt) => ids.has(attempt.entryId)).length,
935
+ earlier: entries.some((entry) => entry.test.revision !== test.revision || entry.world.revision !== test.worldRevision),
936
+ grouped: true
937
+ }];
938
+ }).sort((a, b) => String(b.run.createdAt).localeCompare(String(a.run.createdAt)));
939
+ }
940
+ function worldHistory(runs, worldId, agentId) {
941
+ return runs.flatMap((run) => {
942
+ if (!run.entries?.length) return run.world?.id === worldId && (!agentId || run.agentId === agentId) ? [{ run, attemptCount: run.attempts?.length ?? 0, grouped: false }] : [];
943
+ const entries = run.entries.filter((entry) => entry.world?.id === worldId && (!agentId || entry.agentId === agentId));
944
+ if (!entries.length) return [];
945
+ const ids = new Set(entries.map((entry) => entry.id));
946
+ return [{ run, attemptCount: (run.attempts ?? []).filter((attempt) => ids.has(attempt.entryId)).length, grouped: true }];
947
+ }).sort((a, b) => String(b.run.createdAt).localeCompare(String(a.run.createdAt)));
948
+ }
949
+ function checksText(task) {
950
+ if (typeof task.checksText === "string") return task.checksText;
951
+ return Array.isArray(task.checks) && task.checks.length ? `[
952
+ ${task.checks.map((check) => " " + JSON.stringify(check)).join(",\n")}
953
+ ]` : "";
954
+ }
955
+ function taskChecks(task, index) {
956
+ if (typeof task.checksText !== "string") return Array.isArray(task.checks) && task.checks.length ? task.checks : void 0;
957
+ const text2 = task.checksText.trim();
958
+ if (!text2) return void 0;
959
+ let parsed;
960
+ try {
961
+ parsed = JSON.parse(text2);
962
+ } catch {
963
+ throw new Error(`Task ${index + 1}: checks must be valid JSON, a list like [{"id": "posted", "trace": {"called": "POST /api/chat.postMessage"}}]`);
964
+ }
965
+ const list2 = Array.isArray(parsed) ? parsed : [parsed];
966
+ if (list2.some((item) => !item || typeof item !== "object" || Array.isArray(item))) throw new Error(`Task ${index + 1}: each check must be an object with an id and a state or trace check`);
967
+ return list2;
968
+ }
969
+ function workspaceDefinition(draft) {
970
+ return {
971
+ name: draft.name,
972
+ agentId: draft.agentId,
973
+ worldId: draft.worldId,
974
+ worldRevision: Number(draft.worldRevision),
975
+ tasks: draft.tasks.map((task, index) => {
976
+ const checks2 = taskChecks(task, index);
977
+ return { id: task.id, ...String(task.title ?? "").trim() ? { title: String(task.title).trim() } : {}, prompt: task.prompt, attempts: Number(task.attempts), timeoutMs: Number(task.timeoutMs), ...checks2?.length ? { checks: checks2 } : {} };
978
+ }),
979
+ concurrency: Number(draft.concurrency),
980
+ seed: draft.seed ?? "playground-1"
981
+ };
982
+ }
983
+ function mountWorldWorkspace(node, options) {
984
+ const { world, agents, api: api2, go: go2 } = options, base = "/worlds/" + enc(world.id), q = new URLSearchParams(location.search);
985
+ let tests = [], runs = [], errors = [], selected, agentId = q.get("agent") ?? "", configure = q.has("configure"), showHistory = false, saving = false, disposed = false, error = "", loaded = false;
986
+ const revisions = /* @__PURE__ */ new Map([[world.revision, world]]);
987
+ let draft, timer;
988
+ const launchPending = () => {
989
+ if (!selected) return false;
990
+ try {
991
+ return sessionStorage.getItem("ancha-saved-test-run:" + selected.id) !== null;
992
+ } catch {
993
+ return true;
994
+ }
995
+ };
996
+ const key = () => "ancha-world-setup:" + world.id + ":" + (selected?.id ?? "new:" + agentId);
997
+ const remember = () => {
998
+ if (draft) try {
999
+ sessionStorage.setItem(key(), JSON.stringify(draft));
1000
+ } catch {
1001
+ }
1002
+ };
1003
+ function getDraft() {
1004
+ if (draft) return draft;
1005
+ try {
1006
+ const stored = JSON.parse(sessionStorage.getItem(key()) ?? "null");
1007
+ if (stored && Array.isArray(stored.tasks)) draft = stored;
1008
+ } catch {
1009
+ }
1010
+ draft ??= selected ? { ...selected, tasks: structuredClone(selected.tasks) } : { name: world.name, agentId, worldId: world.id, worldRevision: world.revision, concurrency: 1, seed: "playground-1", tasks: [{ id: "task_" + crypto.randomUUID(), title: "", prompt: "", attempts: 1, timeoutMs: 3e5 }] };
1011
+ return draft;
1012
+ }
1013
+ function capture() {
1014
+ const form = node.querySelector(".ww-form");
1015
+ if (!form) return;
1016
+ const d = getDraft(), values = new FormData(form);
1017
+ d.name = String(values.get("name") ?? "");
1018
+ d.agentId = String(values.get("agentId") ?? "");
1019
+ d.concurrency = Number(values.get("concurrency"));
1020
+ d.worldRevision = Number(values.get("worldRevision"));
1021
+ d.tasks = [...form.querySelectorAll(".ww-task-editor")].map((row) => ({ id: row.dataset.taskId, title: row.querySelector("[name=title]").value, prompt: row.querySelector("[name=prompt]").value, attempts: Number(row.querySelector("[name=attempts]").value), timeoutMs: Math.round(Number(row.querySelector("[name=minutes]").value) * 6e4), checksText: row.querySelector("[name=checks]")?.value ?? "" }));
1022
+ remember();
1023
+ }
1024
+ function href2(test, agent = agentId, extra = "") {
1025
+ return base + "?" + new URLSearchParams({ ...agent ? { agent } : {}, ...test ? { test: test.id } : {}, ...extra ? { [extra]: "1" } : {} });
1026
+ }
1027
+ function configuration2() {
1028
+ const d = getDraft(), pinned = Number(d.worldRevision) !== world.revision, startingWorld = revisions.get(Number(d.worldRevision));
1029
+ return `<section class="ww-configure"><div class="ww-section-heading"><h2>Configure</h2><button type="button" data-ww="close">Close</button></div><form class="ww-form"><div class="form-grid"><label>Agent<select name="agentId" required><option value="">Choose agent</option>${agents.map((a) => `<option value="${h(a.id)}" ${d.agentId === a.id ? "selected" : ""}>${h(a.name)}</option>`).join("")}</select></label><label>Configuration name<input name="name" required maxlength="120" value="${h(d.name)}"></label></div><section class="ww-tools"><div class="ww-section-heading"><h3>Tools</h3><a data-nav href="${base}?edit=1">Edit world</a></div><div class="ww-tool-list">${(startingWorld?.configuration.preset.datasets ?? []).map((item) => `<a data-nav href="/tools/${enc(item.tool)}">${toolLogo(item.tool)}${h(toolName(item.tool))}</a>`).join("")}</div>${pinned ? `<label>Starting data<select name="worldRevision"><option value="${h(d.worldRevision)}">Saved revision ${h(d.worldRevision)}</option><option value="${world.revision}">Current revision ${world.revision}</option></select></label>` : `<input type="hidden" name="worldRevision" value="${h(d.worldRevision)}">`}</section><div class="ww-section-heading"><h3>Tasks</h3><button type="button" data-ww="add-task" ${d.tasks.length >= 20 ? "disabled" : ""}>Add task</button></div><div class="ww-task-fields">${d.tasks.map((task, index) => `<article class="ww-task-editor" data-task-id="${h(task.id)}"><div class="ww-section-heading"><h3>Task ${index + 1}</h3>${d.tasks.length > 1 ? `<button type="button" data-ww="remove-task" data-value="${h(task.id)}">Remove</button>` : ""}</div><label>Task name <span class="muted">optional</span><input name="title" maxlength="120" value="${h(task.title ?? "")}" placeholder="Task ${index + 1}"></label><label>Instructions<textarea name="prompt" required maxlength="20000" rows="4">${h(task.prompt)}</textarea></label><div class="ww-task-options"><label>Repeat<input type="number" name="attempts" min="1" max="100" value="${h(task.attempts)}" required><span>times</span></label><label>Time limit <span class="muted">minutes</span><input type="number" name="minutes" min="0.016666666666666666" max="60" step="any" value="${h(task.timeoutMs / 6e4)}" required></label></div><label class="ww-checks-field">Checks <span class="muted">optional \xB7 JSON list, see <a href="/docs#checks" target="_blank" rel="noopener">Docs</a></span><textarea name="checks" rows="${Math.min(12, Math.max(3, checksText(task).split("\n").length))}" spellcheck="false" maxlength="20000" placeholder='[{"id": "posted", "trace": {"called": "POST /api/chat.postMessage"}}]'>${h(checksText(task))}</textarea></label></article>`).join("")}</div><div class="ww-footer"><label>Run at once<input name="concurrency" type="number" min="1" max="10" value="${h(d.concurrency)}" required></label><button type="submit" class="primary" ${saving ? "disabled" : ""}>${saving ? "Saving\u2026" : "Save configuration"}</button></div></form></section>`;
1030
+ }
1031
+ function render() {
1032
+ if (disposed) return;
1033
+ const currentRuns = selected ? setupRuns(runs, selected) : [], run = currentRuns[0], attempts = run?.attempts ?? [], tasks = selected?.tasks ?? [], choices = worldSetups(tests, world.id, agentId);
1034
+ node.innerHTML = `<div class="ww-workspace"><header class="ww-header"><h1>${h(world.name)}</h1><div class="ww-world-actions"><label class="ww-agent-select">Agent<select data-ww-select="agent"><option value="" disabled>Choose agent</option>${agents.map((a) => `<option value="${h(a.id)}" ${a.id === agentId ? "selected" : ""}>${h(a.name)}</option>`).join("")}</select></label><button type="button" data-ww="configure">Configure</button>${selected ? options.runActions?.(selected.id) ?? button("Run tasks", "run-saved-test", selected.id, "primary") : ""}</div></header><p class="ww-error" role="alert" ${error ? "" : "hidden"}>${h(error)}</p>${errors.map((e) => `<p class="ww-error" role="alert">${h(e.message)}</p>`).join("")}${!loaded ? '<p role="status">Loading tasks\u2026</p>' : ""}${choices.length > 1 ? `<label class="ww-setup-select">Configuration<select data-ww-select="test">${!selected ? '<option value="" selected disabled>New configuration</option>' : ""}${choices.map((t) => `<option value="${h(t.id)}" ${t.id === selected?.id ? "selected" : ""}>${h(t.name)}</option>`).join("")}</select></label>` : ""}${loaded && configure ? configuration2() : loaded ? `${summaryWidgets(attempts, "ww-summary-widgets", "article")}<div class="ww-section-heading"><h2>Tasks${tasks.length ? ` <span class="ww-task-count">${tasks.length}</span>` : ""}</h2><div class="actions">${worldHistory(runs, world.id, agentId || void 0).length || selected ? `<button type="button" data-ww="history" aria-expanded="${showHistory}">History</button>` : ""}<button type="button" data-ww="configure">${tasks.length ? "Edit tasks" : "Add tasks"}</button></div></div>${tasks.length ? `<section class="ww-task-table"><div class="ww-table-head"><span>Task</span><span>Success rate</span><span>Attempts</span><span></span></div>${tasks.map((task, index) => {
1035
+ const list2 = attempts.filter((a) => a.taskId === task.id), taskURL = run ? `/playgrounds/${enc(run.id)}?task=${enc(task.id)}${list2[0] ? "&attempt=" + enc(list2[0].id) : ""}` : "";
1036
+ return `<article class="ww-task-row"><div class="ww-task-identity">${taskURL ? `<a data-nav class="ww-task-name" href="${taskURL}"><span class="ww-ordinal">${index + 1}</span><strong>${h(label(task, index))}</strong></a>` : `<strong class="ww-task-name"><span class="ww-ordinal">${index + 1}</span>${h(label(task, index))}</strong>`}<p class="ww-task-prompt" title="${h(task.prompt)}">${h(task.prompt)}</p></div>${(() => {
1037
+ const o = taskOutcome(list2);
1038
+ return `<div class="ww-rate ${o.tone}"><strong>${h(o.value)}</strong><span>${h(o.note)}</span></div>`;
1039
+ })()}<div class="ww-attempt-marks" role="group" aria-label="Attempts for ${h(label(task, index))}">${list2.map((a) => {
1040
+ const o = attemptOutcome(a);
1041
+ return `<a data-nav class="ww-attempt-mark ${h(a.status)} outcome-${o.key}" href="/playgrounds/${enc(run.id)}?task=${enc(task.id)}&attempt=${enc(a.id)}" title="Attempt ${a.number}: ${h(attemptLabel(a.status))} \xB7 ${o.label}" aria-label="Attempt ${a.number}: ${h(attemptLabel(a.status))} \xB7 ${o.label}">${o.key !== "ungraded" ? o.mark : a.status === "error" ? "!" : ["running", "starting"].includes(a.status) ? "\u25CC" : "\u2212"}</a>`;
1042
+ }).join("") || `<span class="muted">Repeat ${h(task.attempts)} times</span>`}</div>${taskURL ? `<a data-nav class="ww-row-inspect" href="${taskURL}" aria-label="Inspect ${h(label(task, index))}">\u2192</a>` : "<span></span>"}</article>`;
1043
+ }).join("")}</section>` : `<div class="quiet-empty">${agents.length ? "Add tasks to this world." : '<a data-nav href="/agents">Connect an agent</a>'}</div>`}${selected && !currentRuns.length && setupHistory(runs, selected).some((item) => item.earlier) ? '<p class="ww-snapshot-notice">This configuration has changed. Earlier attempts are in History.</p>' : ""}${showHistory ? history2() : ""}` : ""}</div>`;
1044
+ }
1045
+ function history2() {
1046
+ const all = worldHistory(runs, world.id, agentId || void 0);
1047
+ return `<section class="ww-history"><div class="ww-section-heading"><h2>History</h2><button type="button" data-ww="new">New configuration</button></div>${all.length ? all.map(({ run, attemptCount, grouped }) => `<a data-nav class="ww-history-row" href="/playgrounds/${enc(run.id)}"><time>${h(new Date(run.createdAt).toLocaleString())}</time><span>${h(attemptLabel(run.status))}</span><span>${attemptCount} attempts</span>${grouped ? "<span>Group</span>" : ""}${run.test?.name || run.groupName ? `<span>${h(run.groupName ?? run.test.name)}</span>` : ""}<span>\u2192</span></a>`).join("") : '<p class="quiet-empty">No attempts yet.</p>'}</section>`;
1048
+ }
1049
+ async function refresh(initial = false) {
1050
+ try {
1051
+ const [definitions, batches] = await Promise.all([api2("/playground-tests"), api2("/playgrounds")]);
1052
+ if (disposed) return;
1053
+ tests = definitions.tests ?? [];
1054
+ runs = batches.runs ?? [];
1055
+ errors = [...definitions.errors ?? [], ...batches.errors ?? []];
1056
+ if (initial) {
1057
+ const requested = q.get("test");
1058
+ if (requested) {
1059
+ selected = worldSetups(tests, world.id).find((test) => test.id === requested);
1060
+ if (!selected || agentId && selected.agentId !== agentId) {
1061
+ selected = void 0;
1062
+ error = "This configuration is unavailable for this world and agent.";
1063
+ } else agentId = selected.agentId;
1064
+ } else if (q.has("new")) {
1065
+ selected = void 0;
1066
+ agentId ||= agents[0]?.id ?? "";
1067
+ } else {
1068
+ selected = worldSetups(tests, world.id, agentId || void 0)[0];
1069
+ const historical = worldHistory(runs, world.id)[0]?.run;
1070
+ agentId ||= selected?.agentId ?? historical?.entries?.find((entry) => entry.world?.id === world.id)?.agentId ?? historical?.agentId ?? agents[0]?.id ?? "";
1071
+ }
1072
+ } else if (selected) selected = tests.find((test) => test.id === selected.id);
1073
+ if (initial && !selected && worldHistory(runs, world.id, agentId || void 0).length) showHistory = true;
1074
+ if (configure) getDraft();
1075
+ options.onContext?.(selected);
1076
+ const pinnedRevision = Number(draft?.worldRevision ?? selected?.worldRevision ?? world.revision);
1077
+ if (!revisions.has(pinnedRevision)) {
1078
+ const savedWorld = await api2(`/worlds/${enc(world.id)}?revision=${pinnedRevision}`);
1079
+ if (disposed) return;
1080
+ revisions.set(pinnedRevision, savedWorld);
1081
+ }
1082
+ loaded = true;
1083
+ if (!configure) render();
1084
+ else if (initial) render();
1085
+ if (runs.some((run) => !terminal(run.status))) timer = setTimeout(() => {
1086
+ void refresh();
1087
+ }, 2e3);
1088
+ } catch (e) {
1089
+ if (!disposed) {
1090
+ error = e instanceof Error ? e.message : String(e);
1091
+ loaded = true;
1092
+ render();
1093
+ }
1094
+ }
1095
+ }
1096
+ node.addEventListener("input", capture);
1097
+ node.addEventListener("change", (event) => {
1098
+ const control = event.target;
1099
+ if (control.dataset.wwSelect === "agent") {
1100
+ capture();
1101
+ const match = worldSetups(tests, world.id, control.value)[0];
1102
+ go2(href2(match, control.value));
1103
+ } else if (control.dataset.wwSelect === "test") go2(href2(tests.find((t) => t.id === control.value)));
1104
+ else {
1105
+ capture();
1106
+ if (control.name === "worldRevision") render();
1107
+ }
1108
+ });
1109
+ node.addEventListener("click", (event) => {
1110
+ const control = event.target.closest("[data-ww]");
1111
+ if (!control) return;
1112
+ event.preventDefault();
1113
+ event.stopPropagation();
1114
+ if (saving) return;
1115
+ capture();
1116
+ error = "";
1117
+ switch (control.dataset.ww) {
1118
+ case "configure":
1119
+ if (launchPending()) {
1120
+ error = "Resolve the pending run before editing this configuration.";
1121
+ break;
1122
+ }
1123
+ configure = true;
1124
+ break;
1125
+ case "close":
1126
+ configure = false;
1127
+ break;
1128
+ case "history":
1129
+ showHistory = !showHistory;
1130
+ break;
1131
+ case "new":
1132
+ go2(href2(void 0, agentId, "new") + "&configure=1");
1133
+ return;
1134
+ case "add-task":
1135
+ if (getDraft().tasks.length < 20) getDraft().tasks.push({ id: "task_" + crypto.randomUUID(), title: "", prompt: "", attempts: 1, timeoutMs: 3e5 });
1136
+ break;
1137
+ case "remove-task":
1138
+ if (getDraft().tasks.length > 1) getDraft().tasks = getDraft().tasks.filter((t) => t.id !== control.dataset.value);
1139
+ break;
1140
+ }
1141
+ remember();
1142
+ render();
1143
+ });
1144
+ node.addEventListener("submit", (event) => {
1145
+ const form = event.target;
1146
+ if (!form.matches(".ww-form")) return;
1147
+ event.preventDefault();
1148
+ event.stopPropagation();
1149
+ if (saving) return;
1150
+ if (launchPending()) {
1151
+ error = "Resolve the pending run before editing this configuration.";
1152
+ render();
1153
+ return;
1154
+ }
1155
+ capture();
1156
+ saving = true;
1157
+ error = "";
1158
+ render();
1159
+ const d = getDraft(), oldKey = key();
1160
+ let body;
1161
+ try {
1162
+ body = workspaceDefinition(d);
1163
+ } catch (e) {
1164
+ error = e instanceof Error ? e.message : String(e);
1165
+ saving = false;
1166
+ render();
1167
+ return;
1168
+ }
1169
+ void api2(selected ? `/playground-tests/${enc(selected.id)}/update` : "/playground-tests", { ...body, ...selected ? { revision: d.revision } : {} }).then((saved) => {
1170
+ sessionStorage.removeItem(oldKey);
1171
+ if (!disposed) go2(href2(saved, saved.agentId));
1172
+ }).catch((e) => {
1173
+ if (!disposed) {
1174
+ error = e instanceof Error ? e.message : String(e);
1175
+ saving = false;
1176
+ render();
1177
+ }
1178
+ });
1179
+ });
1180
+ render();
1181
+ void refresh(true);
1182
+ return () => {
1183
+ disposed = true;
1184
+ clearTimeout(timer);
1185
+ };
1095
1186
  }
1096
1187
 
1097
1188
  // frontend/src/workspace-pages.ts
@@ -1210,7 +1301,8 @@ function mountTestFlow(node, options) {
1210
1301
  if (form) {
1211
1302
  draft.name = form.elements.namedItem("name").value;
1212
1303
  draft.concurrency = Number(form.elements.namedItem("concurrency").value);
1213
- draft.tasks = [...form.querySelectorAll(".task-editor")].map((row) => ({ id: row.dataset.taskId, prompt: row.querySelector("[name=prompt]").value, attempts: Number(row.querySelector("[name=attempts]").value), timeoutMs: Math.round(Number(row.querySelector("[name=minutes]").value) * 6e4) }));
1304
+ const kept = new Map((draft.tasks ?? []).map((t) => [t.id, t]));
1305
+ draft.tasks = [...form.querySelectorAll(".task-editor")].map((row) => ({ ...kept.get(row.dataset.taskId) ?? {}, id: row.dataset.taskId, prompt: row.querySelector("[name=prompt]").value, attempts: Number(row.querySelector("[name=attempts]").value), timeoutMs: Math.round(Number(row.querySelector("[name=minutes]").value) * 6e4) }));
1214
1306
  }
1215
1307
  remember();
1216
1308
  }
@@ -1474,7 +1566,8 @@ function mountGroupFlow(node, options) {
1474
1566
  const world = options.worlds.find((value) => value.id === draft.worldId);
1475
1567
  draft.worldRevision = world?.revision ?? 0;
1476
1568
  draft.concurrency = Number(values.get("concurrency") ?? 1);
1477
- draft.tasks = [...form.querySelectorAll(".task-editor")].map((row) => ({ id: row.dataset.taskId, prompt: row.querySelector("[name=prompt]").value, attempts: Number(row.querySelector("[name=attempts]").value), timeoutMs: Math.round(Number(row.querySelector("[name=minutes]").value) * 6e4) }));
1569
+ const kept = new Map((draft.tasks ?? []).map((t) => [t.id, t]));
1570
+ draft.tasks = [...form.querySelectorAll(".task-editor")].map((row) => ({ ...kept.get(row.dataset.taskId) ?? {}, id: row.dataset.taskId, prompt: row.querySelector("[name=prompt]").value, attempts: Number(row.querySelector("[name=attempts]").value), timeoutMs: Math.round(Number(row.querySelector("[name=minutes]").value) * 6e4) }));
1478
1571
  remember();
1479
1572
  }
1480
1573
  function showError(message) {
@@ -1634,10 +1727,10 @@ function comparisonSetup(runId, runs) {
1634
1727
  return `<p class="hint">Choose a completed run with matching scenarios and test data.</p><form id="compare-form"><input type="hidden" name="current" value="${h(runId)}"><label>Baseline<select name="baseline" required>${baselines.map((r) => `<option value="${h(r.runId)}">${h(r.metadata?.label ?? "CLI test")} \xB7 ${h(r.runId)}</option>`).join("")}</select></label><button class="primary">Compare</button></form>`;
1635
1728
  }
1636
1729
  function comparisonResults(result, labels) {
1637
- const percent = (rate) => `${(rate * 100).toLocaleString("en-US", { maximumFractionDigits: 1 })}%`;
1730
+ const percent2 = (rate) => `${(rate * 100).toLocaleString("en-US", { maximumFractionDigits: 1 })}%`;
1638
1731
  const change = (delta) => delta === 0 ? "No change" : Math.abs(delta * 100) < 0.1 ? `${delta > 0 ? "Increase" : "Decrease"} under 0.1 percentage points` : `${delta > 0 ? "+" : "\u2212"}${Math.abs(delta * 100).toLocaleString("en-US", { maximumFractionDigits: 1 })} percentage points`;
1639
1732
  const attempts = (count) => `${count} ${count === 1 ? "attempt" : "attempts"}`;
1640
- return `<p>${result.regressed ? "Pass rate decreased in one or more scenarios." : "No decrease in pass rate."}</p><div class="table-wrap comparison-table"><table><thead><tr><th scope="col">Scenario</th><th scope="col">Before</th><th scope="col">After</th><th scope="col">Change</th></tr></thead><tbody>${result.scenes.map((s, index) => `<tr><th scope="row">${h(labels[s.sceneId] ?? `Scenario ${index + 1}`)}</th><td><strong>${percent(s.before)}</strong><span class="comparison-attempts">${attempts(s.beforeTrials)}</span></td><td><strong>${percent(s.after)}</strong><span class="comparison-attempts">${attempts(s.afterTrials)}</span></td><td>${badge(change(s.delta), s.delta > 0 ? "good" : s.delta < 0 ? "bad" : "")}</td></tr>`).join("")}</tbody></table></div><p class="hint">Observed pass rates; no claim of statistical significance.</p>${details("Comparison details", result)}`;
1733
+ return `<p>${result.regressed ? "Pass rate decreased in one or more scenarios." : "No decrease in pass rate."}</p><div class="table-wrap comparison-table"><table><thead><tr><th scope="col">Scenario</th><th scope="col">Before</th><th scope="col">After</th><th scope="col">Change</th></tr></thead><tbody>${result.scenes.map((s, index) => `<tr><th scope="row">${h(labels[s.sceneId] ?? `Scenario ${index + 1}`)}</th><td><strong>${percent2(s.before)}</strong><span class="comparison-attempts">${attempts(s.beforeTrials)}</span></td><td><strong>${percent2(s.after)}</strong><span class="comparison-attempts">${attempts(s.afterTrials)}</span></td><td>${badge(change(s.delta), s.delta > 0 ? "good" : s.delta < 0 ? "bad" : "")}</td></tr>`).join("")}</tbody></table></div><p class="hint">Observed pass rates; no claim of statistical significance.</p>${details("Comparison details", result)}`;
1641
1734
  }
1642
1735
  function dataSelection(item) {
1643
1736
  if (!item) return "";