const API = {
health: "/api/health",
runs: (limit) => `/api/runs?limit=${limit}`,
run: (id) => `/api/runs/${encodeURIComponent(id)}`,
deleteRun: (id) => `/api/runs/${encodeURIComponent(id)}`,
foldRun: (id) => `/api/runs/${encodeURIComponent(id)}/fold`,
resumeRun: (id) => `/api/runs/${encodeURIComponent(id)}/resume`,
report: (id) => `/api/runs/${encodeURIComponent(id)}/report`,
queue: "/api/queue",
deleteTask: (id) => `/api/queue/${encodeURIComponent(id)}`,
hold: (id) => `/api/queue/${encodeURIComponent(id)}/hold`,
release: (id) => `/api/queue/${encodeURIComponent(id)}/release`,
priority: (id) => `/api/queue/${encodeURIComponent(id)}/priority`,
editTask: (id) => `/api/queue/${encodeURIComponent(id)}/edit`,
doneTask: (id) => `/api/queue/${encodeURIComponent(id)}/done`,
questions: "/api/questions",
answer: (id) => `/api/questions/${encodeURIComponent(id)}/answer`,
questionSay: (id) => `/api/questions/${encodeURIComponent(id)}/say`,
panel: (id) => `/api/questions/${encodeURIComponent(id)}/panel/index.html`,
talks: "/api/talks",
talk: (id) => `/api/talks/${encodeURIComponent(id)}`,
talkSay: (id) => `/api/talks/${encodeURIComponent(id)}/say`,
talkPending: (id) => `/api/talks/${encodeURIComponent(id)}/pending`,
talkPendingResume: (id) => `/api/talks/${encodeURIComponent(id)}/pending/resume`,
talkPendingClear: (id) => `/api/talks/${encodeURIComponent(id)}/pending/clear`,
talkPendingEdit: (id) => `/api/talks/${encodeURIComponent(id)}/pending/edit`,
talkClose: (id) => `/api/talks/${encodeURIComponent(id)}/close`,
talkReopen: (id) => `/api/talks/${encodeURIComponent(id)}/reopen`,
talkDelete: (id) => `/api/talks/${encodeURIComponent(id)}`,
talkAttachmentPost: (id) => `/api/talks/${encodeURIComponent(id)}/attachments`,
talkAttachment: (id, att) => `/api/talks/${encodeURIComponent(id)}/attachments/${encodeURIComponent(att)}`,
repos: "/api/repos",
reposRefresh: "/api/repos?refresh=1",
loop: "/api/loop",
upgrade: "/api/upgrade",
events: "/api/events",
};
const RUN_LIMIT = 50;
const HEALTH_MS = 10000;
const STOP_ASK_MS = 20000;
const QUIET_HOLD_MS = 4000;
const UPGRADE_BUSY_STAGES = new Set(["downloading", "replaced", "parking", "restarting"]);
const UPGRADE_WAIT_LIMIT_MS = 70 * 60 * 1000;
const PHASES = ["prep", "implementing", "judging", "deliberating", "voting", "reviewing", "gating"];
const RUN_STATUS = {
prep: { glyph: "\u25cc", tone: "ink" },
implementing: { glyph: "\u25b8", tone: "blue", flight: true },
judging: { glyph: "\u25b8", tone: "blue", flight: true },
deliberating: { glyph: "\u25b8", tone: "blue", flight: true },
voting: { glyph: "\u25b8", tone: "blue", flight: true },
reviewing: { glyph: "\u25b8", tone: "blue", flight: true },
gating: { glyph: "\u25b8", tone: "blue", flight: true },
merged: { glyph: "\u25c6", tone: "gold", note: "Winner merged." },
ready: { glyph: "\u25c7", tone: "teal", note: "Winner passed the gate. Merge was not requested." },
stalled: { glyph: "\u26a0", tone: "rust", note: "The judging panel never reached a quorum, so no verdict was recorded. The work is kept." },
blocked: { glyph: "\u2298", tone: "rust", note: "magi stopped short of merging." },
failed: { glyph: "\u2715", tone: "ink", note: "The graph could not complete." },
waiting: { glyph: "?", tone: "wait", note: "An agent stopped to ask you something. Nothing in this run moves until it is answered." },
};
const TASK_STATUS = {
queued: { glyph: "\u25cc", tone: "ink" },
running: { glyph: "\u25b8", tone: "blue", flight: true },
done: { glyph: "\u25c6", tone: "gold" },
failed: { glyph: "\u2715", tone: "rust" },
held: { glyph: "\u2016", tone: "rust", note: "Held. This task will not be claimed until it is released." },
};
const QUESTION_STATUS = {
open: { glyph: "?", tone: "wait" },
answered: { glyph: "\u2713", tone: "teal" },
abandoned: { glyph: "\u2296", tone: "ink" },
};
const TALK_STATUS = {
open: { glyph: "\u25cc", tone: "blue" },
closed: { glyph: "\u2296", tone: "ink" },
};
const MERGE_NODE = "land-approval";
const CHECKS = {
pending: { glyph: "", word: "checks running" },
green: { glyph: "\u2713", word: "checks green" },
red: { glyph: "\u2715", word: "checks red" },
unknown: { glyph: "\u2013", word: "checks unknown" },
};
const PR_TONE = { open: "ink", merged: "gold", closed: "rust" };
const ASK_ORDER = { open: 0, answered: 1, abandoned: 2 };
const SEV_RANK = { blocker: 3, major: 2, minor: 1, nit: 0 };
const $ = (id) => document.getElementById(id);
function el(tag, props, ...kids) {
const node = document.createElement(tag);
if (props) {
for (const [key, value] of Object.entries(props)) {
if (value === null || value === undefined || value === false) continue;
if (key === "class") node.className = value;
else if (key === "text") node.textContent = value;
else if (key.startsWith("on")) node.addEventListener(key.slice(2), value);
else node.setAttribute(key, value === true ? "" : String(value));
}
}
append(node, kids);
return node;
}
function svg(tag, props, ...kids) {
const node = document.createElementNS("http://www.w3.org/2000/svg", tag);
if (props) {
for (const [key, value] of Object.entries(props)) {
if (value === null || value === undefined || value === false) continue;
if (key === "text") node.textContent = value;
else node.setAttribute(key, String(value));
}
}
append(node, kids);
return node;
}
function append(node, kids) {
for (const kid of kids.flat(4)) {
if (kid === null || kid === undefined || kid === false || kid === "") continue;
node.append(kid);
}
}
function setText(node, value) {
const next = value === null || value === undefined ? "" : String(value);
if (node.textContent !== next) node.textContent = next;
}
function setAttr(node, name, value) {
if (value === null || value === undefined || value === false) {
if (node.hasAttribute(name)) node.removeAttribute(name);
} else if (node.getAttribute(name) !== String(value)) {
node.setAttribute(name, String(value));
}
}
function show(node, visible) {
if (node.hidden === !visible) return;
node.hidden = !visible;
}
function clear(node) {
node.replaceChildren();
}
function separate(container) {
const visible = [...container.children].filter((child) => !child.hidden);
visible.forEach((child, i) => setAttr(child, "data-sep", i < visible.length - 1 ? "1" : null));
}
function numbers(parts) {
const row = el("div", { class: "cand-nums" }, parts.filter(Boolean).map((part) => el("span", { text: part })));
separate(row);
return row;
}
function syncList(parent, items, keyOf, create, update) {
const existing = new Map();
for (const child of parent.children) existing.set(child.dataset.key, child);
let previous = null;
for (const item of items) {
const key = keyOf(item);
let node = existing.get(key);
if (node) {
existing.delete(key);
} else {
node = create(item);
node.dataset.key = key;
}
update(node, item);
const wanted = previous ? previous.nextSibling : parent.firstChild;
if (node !== wanted) parent.insertBefore(node, wanted);
previous = node;
}
for (const stale of existing.values()) stale.remove();
}
const RELATIVE = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" });
const ABSOLUTE = new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" });
const CLOCK = new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit", hour12: false });
function when(iso) {
const at = Date.parse(iso);
if (Number.isNaN(at)) return { text: "\u2014", title: "" };
const seconds = (at - Date.now()) / 1000;
const size = Math.abs(seconds);
let text;
if (size < 45) text = "just now";
else if (size < 3600) text = RELATIVE.format(Math.round(seconds / 60), "minute");
else if (size < 86400) text = RELATIVE.format(Math.round(seconds / 3600), "hour");
else if (size < 6 * 86400) text = RELATIVE.format(Math.round(seconds / 86400), "day");
else text = ABSOLUTE.format(at);
return { text, title: ABSOLUTE.format(at) };
}
function clock(iso) {
const at = Date.parse(iso);
return Number.isNaN(at) ? "\u2014" : CLOCK.format(at);
}
const shortId = (id) => (typeof id === "string" && id.includes("-") ? id.split("-").pop() : id || "");
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
function forgeUrl(value) {
if (typeof value !== "string") return null;
try {
const url = new URL(value, location.origin);
return url.protocol === "https:" || url.protocol === "http:" ? url.href : null;
} catch {
return null;
}
}
const candTone = (index) => `var(--cand-${"abcde"[index % 5]})`;
function seconds(ms) {
if (!ms) return null;
return ms < 1000 ? `${ms}ms` : ms < 60000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms / 60000)}m`;
}
function chip(status, table) {
const meta = table[status] || { glyph: "\u25cc", tone: "ink" };
return el("span", {
class: "chip",
"data-status": status,
"data-glyph": meta.glyph,
"data-flight": meta.flight ? "1" : null,
text: status,
});
}
function toneOf(status, table) {
return (table[status] || { tone: "ink" }).tone;
}
function phaseOf(node) {
if (!node) return null;
return PHASES.find((phase) => phase === node || phase.startsWith(node)) || null;
}
function phaseRail(status, node, note) {
const parked = phaseOf(node);
const at = PHASES.indexOf(parked || status);
if (at < 0) return null;
const rail = el("div", {
class: "phases",
role: "img",
"aria-label": parked
? `Stopped at phase ${at + 1} of ${PHASES.length}, ${parked}, waiting for your answer`
: `Phase ${at + 1} of ${PHASES.length}: ${status}${note ? ` — ${note}` : ""}`,
});
for (let i = 0; i < PHASES.length; i += 1) {
const here = i === at;
rail.append(el("span", {
class: "phase",
"data-on": parked ? (i < at ? "1" : null) : (i <= at ? "1" : null),
"data-now": here && !parked ? "1" : null,
"data-parked": here && parked ? "1" : null,
}));
}
return rail;
}
function roundRail(pr) {
const rounds = Number(pr.rounds) || 0;
const round = Number(pr.round) || 0;
if (rounds <= 0) return null;
const settled = pr.state !== "open";
const rail = el("div", {
class: "phases",
role: "img",
"aria-label": `Land round ${round} of ${rounds}`,
});
for (let i = 1; i <= rounds; i += 1) {
rail.append(el("span", {
class: "phase",
"data-round": i < round || (i === round && settled) ? "1" : null,
"data-now": i === round && !settled ? "1" : null,
}));
}
return rail;
}
const RUNS_COLLAPSE_KEY = "magi-runs-sections";
const QUEUE_COLLAPSE_KEY = "magi-queue-sections";
const state = {
route: { name: "runs", id: null },
health: null,
loop: null,
stopAskedAt: 0,
runs: null,
runsFilter: { section: null, repo: null },
runsStateFilter: "active",
runsCollapsed: loadCollapsed(RUNS_COLLAPSE_KEY),
queue: null,
queueCollapsed: loadCollapsed(QUEUE_COLLAPSE_KEY),
detail: { id: null, run: null, report: null },
questions: null,
panelOk: new Map(),
talks: null,
talkDetail: { id: null, talk: null },
talkAttachments: { id: null, items: [] },
talkWaits: new Map(),
talkWaitTimer: null,
openingTalk: false,
prevTalkTurnCount: 0,
rev: { queue: null, runs: null, questions: null, talks: null, loop: null },
streamOpen: false,
wrap: false,
lastUpgradeStage: null,
};
let fallbackTimer = null;
let nextTalkWaitGeneration = 1;
async function request(url, init) {
const res = await fetch(url, init);
if (!res.ok) {
let message = `${res.status} ${res.statusText || "request failed"}`;
try {
const body = await res.json();
if (body && typeof body.error === "string") message = body.error;
} catch {
}
const error = new Error(message);
error.status = res.status;
throw error;
}
return res;
}
const getJson = (url) => request(url).then((r) => r.json());
const getText = (url) => request(url).then((r) => r.text());
const postJson = (url, body) =>
request(url, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body === undefined ? {} : body),
}).then((r) => r.json());
const deleteReq = (url) => request(url, { method: "DELETE" });
const postBytes = (url, file, filename) =>
request(url, {
method: "POST",
headers: {
"content-type": file.type || "application/octet-stream",
"x-filename": filename || file.name || "attachment",
},
body: file,
}).then((r) => r.json());
function fail(message) {
const box = $("alert");
setText(box.querySelector(".alert-text"), message);
show(box, true);
}
function ok() {
show($("alert"), false);
}
let saidAt = 0;
function announce(message) {
saidAt = Date.now();
setText($("live"), message);
}
function announceQuietly(message) {
if (Date.now() - saidAt < QUIET_HOLD_MS) return;
setText($("live"), message);
}
function runnableTasks() {
if (state.queue === null) return null;
return state.queue.filter((task) => (task.status_str || task.status) === "queued").length;
}
function currentRunLink(daemon) {
const id = daemon.current && daemon.current[0] && daemon.current[0].run;
if (!id) return null;
return el("a", {
class: "daemon-run",
href: `#/runs/${id}`,
text: shortId(id),
title: `run ${id}`,
});
}
function startCost(loop) {
const mode = typeof loop.merge === "string" && loop.merge
? ` Merges as \u2018${loop.merge}\u2019.`
: "";
return `It claims the highest-priority task and runs an implementation competition, which spends agent calls.${mode}`;
}
function upgradeOverdue(upgrade) {
const startedAt = Date.parse(upgrade.started_at);
return Number.isFinite(startedAt) && Date.now() - startedAt > UPGRADE_WAIT_LIMIT_MS;
}
function upgradeStageLabel(stage) {
switch (stage) {
case "downloading": return "Replacing the binary.";
case "replaced": return "Binary replaced.";
case "parking": return "Parking before it restarts.";
case "restarting": return "Restarting.";
default: return "Upgrading.";
}
}
function upgradeStageDetail(stage) {
switch (stage) {
case "downloading": return "Fetching and installing the new binary. This takes a few seconds.";
case "replaced": return "About to hand the address to the successor.";
case "parking": return "Nothing was in flight; handing the address to the successor next.";
case "restarting": return "The address is released and the successor is starting. This page reconnects on its own.";
default: return "";
}
}
function renderLoop() {
const box = $("daemon");
const text = box.querySelector(".daemon-text");
const why = $("loop-why");
const button = $("loop-toggle");
let upgradeFailNote = "";
const quiet = (note) => {
const full = [note, upgradeFailNote].filter(Boolean).join(" ");
setText(why, full);
show(why, Boolean(full));
show(button, false);
button.onclick = null;
};
const park = $("loop-park");
show(park, false);
park.disabled = false;
const upgradeBtn = $("loop-upgrade");
show(upgradeBtn, false);
const versionChip = $("loop-version");
const version = state.health && typeof state.health.version === "string"
? state.health.version.trim()
: "";
setText(versionChip, version ? `v${version}` : "");
setAttr(versionChip, "title", version ? `magi ${version} is serving this page` : null);
show(versionChip, Boolean(version));
const control = (kind, label, note) => {
setText(why, [note, upgradeFailNote].filter(Boolean).join(" "));
show(why, true);
setAttr(button, "data-kind", kind);
setText(button, label);
show(button, true);
button.disabled = false;
button.onclick = () => setLoop(kind === "start");
};
const parkControl = (label, note) => {
setText(park, label);
setAttr(park, "title", note);
show(park, true);
park.onclick = () => setLoop(false, true);
};
if (!state.health) {
setAttr(box, "data-state", null);
setAttr(box, "data-owned", null);
setText(text, "Connecting\u2026");
setText(versionChip, "");
show(versionChip, false);
quiet(null);
return;
}
const upgradeInfo = state.health.upgrade || null;
const upgradeStage = upgradeInfo ? upgradeInfo.stage : null;
if (upgradeStage && UPGRADE_BUSY_STAGES.has(upgradeStage)) {
const overdue = upgradeOverdue(upgradeInfo);
setAttr(box, "data-state", overdue ? "failed" : "upgrading");
setAttr(box, "data-owned", null);
clear(text);
text.append(el("b", {
text: overdue ? "The upgrade is taking longer than expected." : upgradeStageLabel(upgradeStage),
}));
quiet(overdue
? `Asked for ${upgradeInfo.to || "an update"} more than an hour ago and has not come back. Check on it by hand.`
: (upgradeInfo.waiting_on || upgradeStageDetail(upgradeStage)));
state.lastUpgradeStage = upgradeStage;
return;
}
if (upgradeStage === "done" && UPGRADE_BUSY_STAGES.has(state.lastUpgradeStage)) {
announce(`Updated to ${upgradeInfo.to || "the new build"} \u2014 back and running.`);
}
if (upgradeStage === "failed" && state.lastUpgradeStage !== "failed") {
announce(`The upgrade to ${upgradeInfo.to || "a new release"} did not complete.${upgradeInfo.detail ? ` ${upgradeInfo.detail}` : ""} The loop itself is unaffected.`);
}
state.lastUpgradeStage = upgradeStage;
setAttr(box, "data-upgrade-failed", upgradeStage === "failed" ? "yes" : null);
if (upgradeStage === "failed") {
upgradeFailNote = `The last upgrade to ${upgradeInfo.to || "a new release"} did not complete${upgradeInfo.detail ? ` (${upgradeInfo.detail})` : ""} \u2014 check on it by hand.`;
}
const loop = state.health.loop || state.loop || {};
const daemon = loop.daemon || state.health.daemon || {};
clear(text);
const done = Number(daemon.completed);
const tail = Number.isFinite(done) ? ` \u00b7 ${plural(done, "task done", "tasks done")}` : "";
const running = Boolean(loop.running) || Boolean(daemon.running);
const foreign = Boolean(daemon.running) && loop.owned === false && !loop.running;
setAttr(box, "data-owned", foreign ? "no" : null);
const update = state.health.update || { available: false, to: null };
show(upgradeBtn, !foreign && update.available);
if (!foreign && update.available && upgradeBtn.dataset.armed !== "yes") {
setText(upgradeBtn, update.to ? `Update to ${update.to}` : "Update & restart");
upgradeBtn.disabled = false;
upgradeBtn.onclick = upgrade;
}
const asked = !foreign && state.stopAskedAt > 0 && Date.now() - state.stopAskedAt < STOP_ASK_MS;
if (running && (loop.stopping || asked)) {
setAttr(box, "data-state", "stopping");
const link = loop.stopping ? currentRunLink(daemon) : null;
text.append(
el("b", { text: "Stopping." }),
loop.stopping
? [link ? " It is finishing run " : " It is finishing the run it is on", link, " first, and will not abandon it."]
: " It stops as soon as it finishes the poll it is on.",
);
quiet(loop.stopping
? `Nothing new will be claimed after it${tail}. You can start it again once it has stopped.`
: `Nothing new will be claimed${tail}. This takes a few seconds when no run is in flight.`);
if (loop.parking) {
text.append(" Parking at the next step.");
} else if (loop.stopping) {
parkControl("Park at the next step",
"Stops the run after the step it is on and leaves it resumable, instead of waiting for the whole competition. Use this when you want to replace the binary.");
}
return;
}
if (!running) {
const waiting = runnableTasks();
const error = typeof loop.last_error === "string" && loop.last_error.trim() ? loop.last_error : null;
if (error) {
setAttr(box, "data-state", "failed");
text.append(el("b", { text: "The loop stopped on an error." }), " ", error);
control("start", "Start the loop again", `${waiting ? `${plural(waiting, "task", "tasks")} still waiting. ` : ""}Starting it clears this error. ${startCost(loop)}`);
return;
}
if (waiting) {
setAttr(box, "data-state", "waiting");
text.append(
el("b", { text: `${plural(waiting, "task", "tasks")} waiting.` }),
" The loop is off, so nothing will be claimed until you start it.",
);
control("start", "Start the loop", startCost(loop));
return;
}
setAttr(box, "data-state", "off");
text.append(
el("b", { text: "Loop is off." }),
waiting === null
? " Nothing has been claimed."
: " Nothing is queued, so nothing is waiting.",
);
control("start", "Start the loop", `${startCost(loop)} Until one is filed it just watches the queue.`);
return;
}
if (daemon.current && daemon.current.length > 0) {
setAttr(box, "data-state", "working");
const first = daemon.current[0];
const rest = daemon.current.length - 1;
text.append(
el("b", { text: "Working" }),
" on ",
currentRunLink(daemon),
first.task
? el("span", { class: "daemon-run", text: ` \u2190 task ${shortId(first.task)}` })
: null,
rest > 0 ? ` (and ${plural(rest, "other run", "other runs")})` : null,
tail,
);
} else if (daemon.idle) {
setAttr(box, "data-state", "idle");
text.append(el("b", { text: "Loop idle." }), ` Nothing runnable in the queue${tail}.`);
} else if (daemon.running) {
setAttr(box, "data-state", "working");
text.append(el("b", { text: "Working." }), ` Claiming a task${tail}.`);
} else {
setAttr(box, "data-state", "working");
text.append(el("b", { text: "Started." }), " Waiting for the loop\u2019s first heartbeat.");
}
if (foreign) {
const pid = Number(daemon.pid);
quiet(`${Number.isFinite(pid) && pid ? `Process ${pid} owns` : "Another process owns"} this loop, so this page can watch it but not stop it. The queue is being drained regardless \u2014 nothing is waiting on you.`);
return;
}
control("stop", "Stop the loop", daemon.current && daemon.current.length > 0
? "It finishes the run(s) it is on first, then stops claiming. Nothing in flight is abandoned."
: "It stops claiming new tasks. Nothing is in flight, so nothing is interrupted.");
}
function quietNote(text) {
const why = $("loop-why");
if (!why) return;
setText(why, text);
show(why, Boolean(text));
}
async function upgrade() {
const btn = $("loop-upgrade");
if (!confirmed(btn, "Replace the binary and restart?")) return;
btn.disabled = true;
setText(btn, "Upgrading\u2026");
try {
const out = await postJson(API.upgrade);
ok();
const detail = out.detail || "The deck is replacing itself and will come back.";
announce(detail);
if (!out.to) {
setText(btn, "Update & restart");
btn.disabled = false;
quietNote(detail);
return;
}
setText(btn, "Parking, then restarting\u2026");
quietNote(detail);
} catch (error) {
setText(btn, "Update & restart");
btn.disabled = false;
fail(`Could not upgrade: ${error.message}`);
}
}
function confirmed(btn, question) {
if (btn.dataset.armed === "yes") {
btn.dataset.armed = "";
return true;
}
btn.dataset.armed = "yes";
setText(btn, question);
setTimeout(() => {
if (btn.dataset.armed === "yes") {
btn.dataset.armed = "";
setText(btn, "Update & restart");
}
}, 6000);
return false;
}
async function setLoop(running, park = false) {
const button = $("loop-toggle");
const parkBtn = $("loop-park");
button.disabled = true;
if (park) {
parkBtn.disabled = true;
setText(parkBtn, "Parking\u2026");
} else {
setText(button, running ? "Starting\u2026" : "Stopping\u2026");
}
try {
const view = await postJson(API.loop, { running, park });
state.stopAskedAt = running ? 0 : Date.now();
applyLoop(view);
ok();
announce(running
? "Loop started. It claims the highest-priority task next."
: view.stopping
? "Loop asked to stop. It finishes the run it is on first."
: "Loop asked to stop. It goes quiet within a few seconds.");
} catch (error) {
fail(error.status === 409
? `The loop did not change: ${error.message}`
: `Could not ${running ? "start" : "stop"} the loop: ${error.message}`);
await loadLoop();
} finally {
renderLoop();
}
}
function applyLoop(view) {
const stopped = state.stopAskedAt > 0 && view && !view.running && !view.stopping;
state.loop = view;
if (state.health) state.health.loop = view;
if (stopped) {
state.stopAskedAt = 0;
announce("Loop stopped.");
}
renderLoop();
}
async function loadLoop() {
try {
applyLoop(await getJson(API.loop));
} catch (error) {
fail(`Could not read the loop: ${error.message}`);
}
}
function createRunCard() {
const chipSlot = el("span");
const whenSlot = el("time", { class: "card-when" });
const title = el("h2", { class: "card-title" });
const repo = el("span", { class: "repo" });
const counts = el("span");
const winner = el("span", { class: "win" });
const reviews = el("span");
const meta = el("div", { class: "card-meta" }, repo, counts, winner, reviews);
const note = el("p", { class: "card-note" });
const superseded = el("p", { class: "card-note card-superseded" });
const event = el("p", { class: "card-event" });
const rail = el("div");
const card = el("a", { class: "card" },
el("div", { class: "card-top" }, chipSlot, whenSlot),
title, meta, note, superseded, event, rail,
);
const prLink = el("a", { class: "pr-link", target: "_blank", rel: "noopener noreferrer" });
const checks = el("span");
const prRound = el("span", { class: "pr-round" });
const tailGo = el("a", { class: "btn btn-gold tail-go" });
const tailNote = el("p", { class: "tail-note" });
const tail = el("div", { class: "card-tail" }, prLink, checks, prRound, tailGo, tailNote);
const row = el("li", {}, card, tail);
row.refs = { card, chipSlot, whenSlot, title, repo, counts, winner, reviews, note, superseded,
event, rail, tail, prLink, checks, prRound, tailGo, tailNote };
return row;
}
function updateRunCard(row, run) {
const r = row.refs;
const status = run.waiting ? "waiting" : String(run.status || "");
const meta = RUN_STATUS[status] || {};
const parked = isWaiting(run);
const tone = toneOf(status, RUN_STATUS);
r.card.setAttribute("href", `#/runs/${run.id}`);
setAttr(r.card, "data-tone", tone);
setAttr(row, "data-tone", tone);
const next = chip(status, RUN_STATUS);
if (r.chipSlot.firstChild) r.chipSlot.firstChild.replaceWith(next);
else r.chipSlot.append(next);
const at = when(run.updated_at || run.created_at);
setText(r.whenSlot, at.text);
setAttr(r.whenSlot, "datetime", run.updated_at || run.created_at);
setAttr(r.whenSlot, "title", `updated ${at.title}`);
setText(r.title, run.title || run.instruction || run.id);
setText(r.repo, run.repo_name || "");
setAttr(r.repo, "title", run.repo || "");
const cands = Number(run.candidates) || 0;
const viable = Number(run.viable) || 0;
const judges = Number(run.judges) || 0;
const bits = [];
if (cands) bits.push(viable === cands ? plural(cands, "candidate", "candidates") : `${viable}/${cands} viable`);
if (judges) bits.push(plural(judges, "judge", "judges"));
setText(r.counts, bits.join(", "));
show(r.counts, bits.length > 0);
setText(r.winner, run.winner ? `winner ${run.winner}` : "");
show(r.winner, Boolean(run.winner));
const rounds = Number(run.reviews) || 0;
const losses = Number(run.quota_losses) || 0;
const extra = [];
if (rounds) extra.push(plural(rounds, "review round", "review rounds"));
if (losses) extra.push(`${plural(losses, "seat", "seats")} lost to quota`);
setText(r.reviews, extra.join(", "));
show(r.reviews, extra.length > 0);
separate(r.reviews.parentNode);
const spell = Boolean(meta.note) && status !== "merged" && status !== "ready" && status !== "waiting";
setText(r.note, spell ? meta.note : "");
show(r.note, spell);
const later = typeof run.superseded_by === "string" ? run.superseded_by : null;
setText(r.superseded, later ? `Superseded by ${later} \u2014 a later attempt at the same task.` : "");
show(r.superseded, Boolean(later));
const moving = !run.done;
setText(r.event, run.event || "");
show(r.event, Boolean(run.event));
const ask = parked ? openFor(run.id)[0] : null;
const rail = moving ? phaseRail(status, ask ? ask.node : null) : null;
clear(r.rail);
if (rail) r.rail.append(rail);
updateRunTail(row, run, { parked, ask });
}
function updateRunTail(row, run, { parked, ask }) {
const r = row.refs;
const pr = run.pr && typeof run.pr === "object" ? run.pr : null;
const prHref = pr ? forgeUrl(pr.url) : null;
if (prHref) {
setAttr(r.prLink, "href", prHref);
setAttr(r.prLink, "title", prHref);
setText(r.prLink, `PR #${pr.number}`);
}
show(r.prLink, Boolean(prHref));
if (pr) r.checks.replaceChildren(checksChip(pr));
show(r.checks, Boolean(pr));
const rounds = pr ? Number(pr.rounds) || 0 : 0;
setText(r.prRound, rounds ? `land round ${Number(pr.round) || 0} of ${rounds}` : "");
show(r.prRound, rounds > 0);
if (ask) {
setAttr(r.tailGo, "href", "#/questions");
setText(r.tailGo, "Answer");
setAttr(r.tailGo, "aria-label", `Answer: ${ask.summary || "the open question"}`);
}
show(r.tailGo, Boolean(ask));
const note = parked
? `Waiting on you: ${(ask && ask.summary) || "an agent asked for a decision."}`
: run.waiting
? "Answered. The loop picks this up on its next tick."
: pr
? landNote(pr)
: "";
setText(r.tailNote, note);
show(r.tailNote, note !== "");
const tailed = Boolean(pr) || Boolean(run.waiting);
show(r.tail, tailed);
setAttr(row, "data-tail", tailed ? "1" : null);
}
const RUN_SECTIONS = [
{ key: "waiting", label: "Waiting on you", defaultOpen: true },
{ key: "flight", label: "In flight", defaultOpen: true },
{ key: "landed", label: "Landed", defaultOpen: true },
{ key: "ended", label: "Ended", defaultOpen: true },
];
function runSection(run) {
if (run.waiting) return "waiting";
const status = String(run.status || "");
if (status === "merged" || status === "ready") return "landed";
if (status === "stalled" || status === "blocked" || status === "failed") return "ended";
return "flight";
}
const RUN_STATE_FILTERS = [
{ key: "active", label: "Active", countNoun: "active", match: (run) => !run.done },
{ key: "flight", label: "In flight", countNoun: "in flight", match: (run) => !run.done && !run.waiting },
{ key: "waiting", label: "Waiting", countNoun: "waiting", match: (run) => Boolean(run.waiting) },
{ key: "done", label: "Done", countNoun: "done", match: (run) => Boolean(run.done) },
{ key: "all", label: "All", countNoun: "runs", match: () => true },
];
function activeRunStateFilter() {
return RUN_STATE_FILTERS.find((f) => f.key === state.runsStateFilter) || RUN_STATE_FILTERS[0];
}
function matchesRunState(run) {
return activeRunStateFilter().match(run);
}
function isOrphanSuperseded(run) {
return typeof run.superseded_by === "string" && run.superseded_by !== "";
}
function selectRunStateFilter(key) {
if (state.runsStateFilter === key) return;
state.runsStateFilter = key;
renderRuns();
}
function renderRunStateChips(runs) {
const bar = $("runs-state-chips");
if (!bar.childElementCount) {
for (const def of RUN_STATE_FILTERS) {
bar.append(el("button", {
class: "state-chip",
type: "button",
role: "radio",
"data-key": def.key,
onclick: () => selectRunStateFilter(def.key),
},
el("span", { class: "state-chip-label", text: def.label }),
el("span", { class: "state-chip-count" }),
));
}
}
for (const node of bar.children) {
const def = RUN_STATE_FILTERS.find((f) => f.key === node.dataset.key);
setText(node.querySelector(".state-chip-count"), String(runs.filter(def.match).length));
setAttr(node, "aria-checked", state.runsStateFilter === def.key ? "true" : "false");
}
}
function foldRuns(runs) {
const byShort = new Map();
for (const run of runs) if (run.short) byShort.set(run.short, run);
const nextOf = (run) => (run.superseded_by && byShort.get(run.superseded_by)) || null;
const headOf = new Map();
for (const run of runs) {
if (headOf.has(run.id)) continue;
const path = [];
const atIndex = new Map();
let cur = run;
while (!headOf.has(cur.id) && !atIndex.has(cur.id)) {
atIndex.set(cur.id, path.length);
path.push(cur);
const next = nextOf(cur);
if (!next) break;
cur = next;
}
const head = headOf.get(cur.id) || cur;
for (const node of path) headOf.set(node.id, head);
}
const heads = [];
const childrenOf = new Map();
for (const run of runs) {
const head = headOf.get(run.id);
if (head.id === run.id) {
heads.push(run);
} else {
if (!childrenOf.has(head.id)) childrenOf.set(head.id, []);
childrenOf.get(head.id).push(run);
}
}
return { heads, childrenOf };
}
function groupBySection(heads) {
const bySection = new Map(RUN_SECTIONS.map((s) => [s.key, []]));
for (const run of heads) bySection.get(runSection(run)).push(run);
return bySection;
}
const repoLabel = (run) => run.repo_name || run.repo || "Unknown repository";
function buildRunsTree(bySection) {
const sections = [];
for (const { key, label } of RUN_SECTIONS) {
const heads = bySection.get(key);
if (heads.length === 0) continue;
const byRepo = new Map();
for (const run of heads) {
const repo = repoLabel(run);
if (!byRepo.has(repo)) byRepo.set(repo, 0);
byRepo.set(repo, byRepo.get(repo) + 1);
}
const repos = [...byRepo.entries()]
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([repo, count]) => ({ repo, count }));
sections.push({ key, label, count: heads.length, repos });
}
return sections;
}
function matchesFilter(run) {
const { section, repo } = state.runsFilter;
if (!section) return true;
if (runSection(run) !== section) return false;
return !repo || repoLabel(run) === repo;
}
function selectRunsFilter(section, repo) {
const same = state.runsFilter.section === section && state.runsFilter.repo === (repo || null);
state.runsFilter = same ? { section: null, repo: null } : { section, repo: repo || null };
renderRuns();
}
function clearRunsFilter() {
state.runsFilter = { section: null, repo: null };
renderRuns();
}
function renderRunsTree(sections) {
const nav = $("runs-tree");
show(nav, sections.length > 0);
const active = document.activeElement;
const focused = nav.contains(active)
? { section: active.dataset.section, repo: active.dataset.repo || null }
: null;
if (sections.length === 0) {
clear(nav);
return;
}
const root = el("ul", { class: "runs-tree-list" });
for (const section of sections) {
const on = state.runsFilter.section === section.key && !state.runsFilter.repo;
const sub = el("ul", { class: "runs-tree-sub" });
for (const r of section.repos) {
const repoOn = state.runsFilter.section === section.key && state.runsFilter.repo === r.repo;
sub.append(el("li", {},
el("button", {
class: "runs-tree-node runs-tree-repo",
type: "button",
"data-section": section.key,
"data-repo": r.repo,
"aria-current": repoOn ? "true" : null,
onclick: () => selectRunsFilter(section.key, r.repo),
},
el("span", { class: "runs-tree-label", text: r.repo }),
el("span", { class: "runs-tree-count", text: String(r.count) }),
),
));
}
root.append(el("li", {},
el("button", {
class: "runs-tree-node",
type: "button",
"data-section": section.key,
"aria-current": on ? "true" : null,
onclick: () => selectRunsFilter(section.key, null),
},
el("span", { class: "runs-tree-label", text: section.label }),
el("span", { class: "runs-tree-count", text: String(section.count) }),
),
sub,
));
}
clear(nav);
nav.append(root);
if (focused) {
const match = [...nav.querySelectorAll(".runs-tree-node")].find((node) =>
node.dataset.section === focused.section && (node.dataset.repo || null) === focused.repo);
if (match) match.focus();
}
}
function renderRunsFilterBar() {
const bar = $("runs-filter");
const { section, repo } = state.runsFilter;
if (!section) {
show(bar, false);
return;
}
const label = (RUN_SECTIONS.find((s) => s.key === section) || {}).label || section;
setText($("runs-filter-text"), `Showing ${label}${repo ? ` \u203a ${repo}` : ""}.`);
show(bar, true);
}
function loadCollapsed(storageKey) {
try {
const raw = localStorage.getItem(storageKey);
const parsed = raw ? JSON.parse(raw) : null;
return parsed && typeof parsed === "object" ? parsed : {};
} catch {
return {};
}
}
function saveCollapsed(storageKey, collapsed) {
try {
localStorage.setItem(storageKey, JSON.stringify(collapsed));
} catch {
}
}
function isSectionOpen(collapsed, key, defaultOpen) {
const saved = collapsed[key];
return typeof saved === "boolean" ? saved : defaultOpen;
}
function createSection(def, collapsed, storageKey) {
const count = el("span", { class: "list-section-count" });
const summary = el("summary", { class: "list-section-head" },
el("h2", { class: "list-section-title", text: def.label }), count);
const list = el("ol", { class: "cards" });
const details = el("details", {
class: "list-section",
open: isSectionOpen(collapsed, def.key, def.defaultOpen),
}, summary, list);
details.dataset.key = def.key;
details.addEventListener("toggle", () => {
collapsed[def.key] = details.open;
saveCollapsed(storageKey, collapsed);
});
details.refs = { summary, count, list };
return details;
}
function syncSections(root, sectionDefs, itemsByKey, createFn, updateFn) {
const existing = new Map();
for (const child of root.children) existing.set(child.dataset.key, child);
let previous = null;
for (const def of sectionDefs) {
const items = itemsByKey.get(def.key) || [];
if (items.length === 0) continue;
let node = existing.get(def.key);
if (node) existing.delete(def.key);
else node = createFn(def);
updateFn(node, items);
const wanted = previous ? previous.nextSibling : root.firstChild;
if (node !== wanted) root.insertBefore(node, wanted);
previous = node;
}
for (const stale of existing.values()) stale.remove();
}
function createRunSection(def) {
const section = createSection(def, state.runsCollapsed, RUNS_COLLAPSE_KEY);
const folded = el("span", { class: "runs-section-folded" });
section.refs.summary.append(folded);
section.refs.folded = folded;
return section;
}
function updateRunSection(node, heads, childrenOf) {
const foldedTotal = heads.reduce((sum, run) => sum + (childrenOf.get(run.id) || []).length, 0);
setText(node.refs.count, plural(heads.length, "run", "runs"));
setText(node.refs.folded, foldedTotal
? `, ${plural(foldedTotal, "earlier attempt", "earlier attempts")} folded`
: "");
syncList(node.refs.list, heads, (r) => r.id, createRunRow,
(row, run) => updateRunRow(row, run, childrenOf.get(run.id) || []));
}
function syncRunSections(root, bySection, childrenOf) {
syncSections(root, RUN_SECTIONS, bySection, createRunSection,
(node, heads) => updateRunSection(node, heads, childrenOf));
}
function createRunRow() {
const row = createRunCard();
const summary = el("summary", { class: "run-folded-summary" });
const list = el("ul", { class: "run-folded-list" });
const folded = el("details", { class: "run-folded advanced" }, summary, list);
row.append(folded);
row.refs.folded = folded;
row.refs.foldedSummary = summary;
row.refs.foldedList = list;
return row;
}
function updateRunRow(row, run, children) {
updateRunCard(row, run);
const list = row.refs.foldedList;
clear(list);
for (const child of children) {
const at = when(child.updated_at || child.created_at);
list.append(el("li", {},
el("a", { class: "run-folded-link", href: `#/runs/${child.id}` },
el("span", { class: "run-folded-id", text: child.short || shortId(child.id) }),
el("span", { class: "run-folded-status", text: child.waiting ? "waiting" : String(child.status || "") }),
el("time", { class: "run-folded-when", text: at.text, title: at.title }),
),
));
}
setText(row.refs.foldedSummary, children.length
? plural(children.length, "earlier attempt", "earlier attempts")
: "");
show(row.refs.folded, children.length > 0);
}
function renderRuns() {
const runs = state.runs;
const sectionsRoot = $("runs-sections");
if (runs === null) {
setText($("runs-count"), "Loading\u2026");
show($("runs-tree"), false);
show($("runs-filter"), false);
show($("runs-state-chips"), false);
if (!sectionsRoot.dataset.skeleton) {
clear(sectionsRoot);
const list = el("ol", { class: "cards" });
for (let i = 0; i < 3; i += 1) {
list.append(el("li", { class: "card skeleton" },
el("div", { class: "bar", style: "width:34%" }),
el("div", { class: "bar", style: "width:88%;height:18px" }),
el("div", { class: "bar", style: "width:56%" }),
));
}
sectionsRoot.append(list);
sectionsRoot.dataset.skeleton = "1";
}
return;
}
if (sectionsRoot.dataset.skeleton) {
clear(sectionsRoot);
delete sectionsRoot.dataset.skeleton;
}
const unreadable = Number(state.health && state.health.runs_unreadable) || 0;
const unreadableNote = unreadable
? `${unreadable} unreadable`
: "";
const { heads, childrenOf } = foldRuns(runs);
show($("runs-state-chips"), runs.length > 0);
if (runs.length > 0) renderRunStateChips(runs);
const passingState = heads.filter(matchesRunState);
const stateFiltered = state.runsStateFilter === "all"
? passingState
: passingState.filter((r) => !isOrphanSuperseded(r));
const orphanHidden = passingState.length - stateFiltered.length;
renderRunsTree(buildRunsTree(groupBySection(heads)));
renderRunsFilterBar();
const visible = stateFiltered.filter(matchesFilter);
const foldedHidden = state.runsStateFilter === "all"
? 0
: visible.reduce((sum, run) => sum + (childrenOf.get(run.id) || []).length, 0);
const childrenForRender = state.runsStateFilter === "all" ? childrenOf : new Map();
syncRunSections(sectionsRoot, groupBySection(visible), childrenForRender);
const supersededHidden = orphanHidden + foldedHidden;
const counts = runs.length === 0
? (unreadable ? `no readable runs, ${unreadableNote}` : "Nothing has run yet")
: (() => {
const countNoun = activeRunStateFilter().countNoun;
const headline = countNoun === "runs"
? plural(visible.length, "run", "runs")
: `${visible.length} ${countNoun}`;
return [headline, supersededHidden ? `${supersededHidden} superseded hidden` : "", unreadableNote]
.filter(Boolean)
.join(", ");
})();
setText($("runs-count"), counts);
show($("runs-empty"), runs.length === 0 && unreadable === 0);
show($("runs-unreadable"), runs.length === 0 && unreadable > 0);
show($("runs-state-empty"), runs.length > 0 && stateFiltered.length === 0);
show($("runs-filter-empty"), stateFiltered.length > 0 && Boolean(state.runsFilter.section) && visible.length === 0);
}
function createTaskCard() {
const chipSlot = el("span");
const priority = el("span", { class: "tag", "data-tone": "ink" });
const solo = el("span", { class: "tag", "data-tone": "teal", text: "solo" });
const whenSlot = el("time", { class: "card-when" });
const title = el("h2", { class: "card-title" });
const source = el("a", { class: "task-source" });
const repo = el("span", { class: "repo" });
const attempts = el("span");
const outcome = el("span");
const meta = el("div", { class: "card-meta" }, source, repo, attempts, outcome);
const note = el("p", { class: "card-note" });
const error = el("pre", { class: "err" });
const instruction = el("details", { class: "advanced" },
el("summary", { text: "Full instruction" }),
el("div", { class: "instruction md" }));
const runLink = el("a", { class: "btn btn-quiet" });
const priorityDown = el("button", { class: "btn btn-quiet btn-step", type: "button", text: "−" });
const priorityUp = el("button", { class: "btn btn-quiet btn-step", type: "button", text: "+" });
const priorityBox = el("span", { class: "task-priority-box" }, priorityDown, priorityUp);
const editBtn = el("button", { class: "btn btn-quiet", type: "button", text: "Edit" });
const holdBox = el("span", { class: "task-hold-box" });
const doneBox = el("span", { class: "task-done-box" });
const deleteBox = el("span", { class: "task-delete-box" });
const actions = el("div", { class: "card-actions" },
runLink, priorityBox, editBtn, holdBox, doneBox, deleteBox);
const card = el("li", { class: "card" },
el("div", { class: "card-top" }, chipSlot, priority, solo, whenSlot),
title, meta, note, error, instruction, actions,
);
card.refs = {
card, chipSlot, priority, solo, whenSlot, title, source, repo, attempts,
outcome, note, error, instruction, runLink, priorityDown, priorityUp,
editBtn, holdBox, doneBox, deleteBox,
};
return card;
}
function updateTaskCard(row, task) {
const r = row.refs;
const status = String(task.status_str || task.status || "");
const meta = TASK_STATUS[status] || {};
setAttr(r.card, "data-tone", toneOf(status, TASK_STATUS));
const next = chip(status, TASK_STATUS);
if (r.chipSlot.firstChild) r.chipSlot.firstChild.replaceWith(next);
else r.chipSlot.append(next);
const priority = Number(task.priority) || 0;
setText(r.priority, priority > 0 ? `priority +${priority}` : `priority ${priority}`);
setAttr(r.priority, "data-tone", priority > 0 ? "rust" : "ink");
show(r.priority, priority !== 0);
show(r.solo, Boolean(task.solo));
const at = when(task.updated_at || task.created_at);
setText(r.whenSlot, at.text);
setAttr(r.whenSlot, "datetime", task.updated_at || task.created_at);
setAttr(r.whenSlot, "title", `updated ${at.title}`);
setText(r.title, task.title || task.instruction || task.id);
setText(r.source, task.source_label || "");
const src = task.source || {};
const sourceHref = src.kind === "agent"
? (src.node === "chat" ? `#/chat/${src.run}` : `#/runs/${src.run}`)
: null;
setAttr(r.source, "href", sourceHref);
const repoName = typeof task.repo === "string" ? task.repo.split(/[\\/]/).filter(Boolean).pop() : "";
setText(r.repo, repoName || "");
setAttr(r.repo, "title", task.repo || "");
const attempts = Number(task.attempts) || 0;
setText(r.attempts, attempts ? plural(attempts, "attempt", "attempts") : "");
show(r.attempts, attempts > 0);
separate(r.attempts.parentNode);
const noteText = task.hold_reason && meta.note
? `${meta.note} Waiting on: ${task.hold_reason}`
: meta.note || (task.hold_reason ? `Waiting on: ${task.hold_reason}` : "");
setText(r.note, noteText);
show(r.note, Boolean(noteText));
setText(r.error, task.last_error || "");
show(r.error, Boolean(task.last_error));
const full = task.instruction || "";
const instructionBox = r.instruction.querySelector(".instruction");
if (instructionBox.dataset.forTask !== task.id) {
instructionBox.dataset.forTask = task.id;
renderMd(instructionBox, task.instruction_md);
}
show(r.instruction, full.trim() !== (task.title || "").trim() && full !== "");
const runs = Array.isArray(task.runs) ? task.runs : [];
const latest = runs.length ? runs[runs.length - 1] : null;
if (latest) {
setAttr(r.runLink, "href", `#/runs/${latest}`);
setText(r.runLink, `Run ${shortId(latest)}`);
}
show(r.runLink, Boolean(latest));
const run = latest ? (state.runs || []).find((x) => x.id === latest) : null;
const outcome = run && status === "done" && run.status !== "merged"
? `run ended ${run.status} — nothing merged it`
: "";
setText(r.outcome, outcome);
show(r.outcome, Boolean(outcome));
separate(r.outcome.parentNode);
const priorityNow = Number(task.priority) || 0;
r.priorityDown.disabled = status === "running";
r.priorityUp.disabled = status === "running";
setAttr(r.priorityDown, "aria-label", `Lower priority of ${task.title || task.id}`);
setAttr(r.priorityUp, "aria-label", `Raise priority of ${task.title || task.id}`);
r.priorityDown.onclick = () => changePriority(task.id, priorityNow - 1);
r.priorityUp.onclick = () => changePriority(task.id, priorityNow + 1);
const editable = status === "queued" || status === "held";
r.editBtn.disabled = !editable;
setAttr(
r.editBtn,
"title",
editable ? "" : "Only a queued or held task's instruction can be edited.",
);
r.editBtn.onclick = () => openTaskEdit(task);
show(r.editBtn, status !== "done");
renderTaskHoldBox(row, task);
renderTaskDoneBox(row, task);
clear(r.deleteBox);
const armed = row.dataset.armedDelete === "1";
if (armed) {
const cancel = el("button", {
class: "btn btn-quiet",
type: "button",
text: "Cancel",
onclick: () => {
row.dataset.armedDelete = "";
updateTaskCard(row, task);
},
});
const confirm = el("button", {
class: "btn btn-quiet",
type: "button",
text: "Delete now",
onclick: () => deleteTask(task.id, row),
});
r.deleteBox.append(
el("div", { class: "stakes-confirm" },
el("p", { class: "stakes-warn", text: "Deletes the task file. Its id, who filed it, and any run history go with it and cannot be recovered." }),
el("div", { class: "stakes-row" }, cancel, confirm),
),
);
requestAnimationFrame(() => cancel.focus({ preventScroll: true }));
} else {
const del = el("button", {
class: "btn btn-quiet",
type: "button",
text: "Delete…",
disabled: status === "running",
onclick: () => {
row.dataset.armedDelete = "1";
updateTaskCard(row, task);
},
});
setAttr(del, "aria-label", `Delete task ${task.title || task.id}`);
r.deleteBox.append(del);
}
}
function renderTaskHoldBox(row, task) {
const r = row.refs;
const status = String(task.status_str || task.status || "");
clear(r.holdBox);
if (status === "done") return;
if (status === "held") {
const release = el("button", {
class: "btn btn-quiet",
type: "button",
text: "Release",
onclick: () => mutateTask(task.id, "release", release),
});
setAttr(release, "aria-label", `Release task ${task.title || task.id}`);
r.holdBox.append(release);
return;
}
if (row.dataset.armedHold === "1") {
const reasonInput = el("input", {
type: "text",
placeholder: "What is this waiting on? (optional)",
});
const cancel = el("button", {
class: "btn btn-quiet",
type: "button",
text: "Cancel",
onclick: () => {
row.dataset.armedHold = "";
updateTaskCard(row, task);
},
});
const confirm = el("button", {
class: "btn btn-quiet",
type: "button",
text: "Hold",
onclick: () => holdTask(task.id, reasonInput.value, row, confirm),
});
r.holdBox.append(
el("div", { class: "stakes-confirm" },
reasonInput,
el("div", { class: "stakes-row" }, cancel, confirm),
),
);
requestAnimationFrame(() => reasonInput.focus({ preventScroll: true }));
} else {
const hold = el("button", {
class: "btn btn-quiet",
type: "button",
text: "Hold…",
disabled: status === "running",
onclick: () => {
row.dataset.armedHold = "1";
updateTaskCard(row, task);
},
});
setAttr(hold, "aria-label", `Hold task ${task.title || task.id}`);
r.holdBox.append(hold);
}
}
async function holdTask(id, reason, row, button) {
const label = button.textContent;
button.disabled = true;
setText(button, "…");
try {
await postJson(API.hold(id), reason.trim() ? { reason: reason.trim() } : undefined);
ok();
announce(`Task ${shortId(id)} held.`);
row.dataset.armedHold = "";
await loadQueue();
} catch (error) {
setText(button, label);
button.disabled = false;
fail(`Could not hold task ${shortId(id)}: ${error.message}`);
}
}
function renderTaskDoneBox(row, task) {
const r = row.refs;
const status = String(task.status_str || task.status || "");
clear(r.doneBox);
if (status === "done") return;
if (row.dataset.armedDone === "1") {
const cancel = el("button", {
class: "btn btn-quiet",
type: "button",
text: "Cancel",
onclick: () => {
row.dataset.armedDone = "";
updateTaskCard(row, task);
},
});
const confirm = el("button", {
class: "btn btn-quiet",
type: "button",
text: "Yes, mark done",
onclick: () => doneTask(task.id, row, confirm),
});
r.doneBox.append(
el("div", { class: "stakes-confirm" },
el("p", { class: "hint", text: "Marks the task finished. Its id, who filed it, and its run history are kept — nothing is deleted." }),
el("div", { class: "stakes-row" }, cancel, confirm),
),
);
requestAnimationFrame(() => cancel.focus({ preventScroll: true }));
} else {
const done = el("button", {
class: "btn btn-quiet",
type: "button",
text: "Mark done…",
onclick: () => {
row.dataset.armedDone = "1";
updateTaskCard(row, task);
},
});
setAttr(done, "aria-label", `Mark task ${task.title || task.id} done`);
r.doneBox.append(done);
}
}
async function doneTask(id, row, button) {
const label = button.textContent;
button.disabled = true;
setText(button, "…");
try {
await postJson(API.doneTask(id));
ok();
announce(`Task ${shortId(id)} marked done.`);
row.dataset.armedDone = "";
await loadQueue();
} catch (error) {
setText(button, label);
button.disabled = false;
fail(`Could not mark task ${shortId(id)} done: ${error.message}`);
}
}
async function changePriority(id, priority) {
try {
await postJson(API.priority(id), { priority });
ok();
announce(`Task ${shortId(id)} priority set to ${priority}.`);
await loadQueue();
} catch (error) {
fail(`Could not change priority of task ${shortId(id)}: ${error.message}`);
}
}
async function deleteTask(id, row) {
try {
await deleteReq(API.deleteTask(id));
ok();
announce(`Task ${shortId(id)} removed.`);
await loadQueue();
} catch (error) {
if (row) {
row.dataset.armedDelete = "";
const task = (state.queue || []).find((t) => t.id === id);
if (task) updateTaskCard(row, task);
}
fail(`Could not delete task ${shortId(id)}: ${error.message}`);
}
}
async function mutateTask(id, action, button) {
const label = button.textContent;
button.disabled = true;
setText(button, "\u2026");
try {
await postJson(action === "hold" ? API.hold(id) : API.release(id));
ok();
announce(`Task ${shortId(id)} ${action === "hold" ? "held" : "released"}.`);
await loadQueue();
} catch (error) {
setText(button, label);
button.disabled = false;
fail(`Could not ${action} task ${shortId(id)}: ${error.message}`);
}
}
const QUEUE_SECTIONS = [
{ key: "running", label: "Running", defaultOpen: true },
{ key: "upnext", label: "Up next", defaultOpen: true },
{ key: "held", label: "Held", defaultOpen: false },
{ key: "done", label: "Done", defaultOpen: false },
];
function queueSection(task) {
const status = String(task.status_str || task.status || "");
if (status === "running") return "running";
if (status === "queued" || status === "failed") return "upnext";
if (status === "held") return "held";
return "done";
}
function groupQueueBySection(tasks) {
const bySection = new Map(QUEUE_SECTIONS.map((s) => [s.key, []]));
for (const task of tasks) bySection.get(queueSection(task)).push(task);
return bySection;
}
function createQueueSection(def) {
return createSection(def, state.queueCollapsed, QUEUE_COLLAPSE_KEY);
}
function updateQueueSection(node, tasks) {
setText(node.refs.count, plural(tasks.length, "task", "tasks"));
syncList(node.refs.list, tasks, (t) => t.id, createTaskCard, updateTaskCard);
}
function syncQueueSections(root, bySection) {
syncSections(root, QUEUE_SECTIONS, bySection, createQueueSection, updateQueueSection);
}
function renderQueue() {
const sectionsRoot = $("queue-sections");
const tasks = state.queue;
if (tasks === null) {
setText($("queue-count"), "Loading\u2026");
return;
}
const runnable = tasks.filter((t) => {
const status = t.status_str || t.status;
return status === "queued" || status === "failed";
}).length;
const held = tasks.filter((t) => (t.status_str || t.status) === "held").length;
const parts = [`${plural(tasks.length, "task", "tasks")}`];
if (runnable) parts.push(`${runnable} runnable`);
if (held) parts.push(`${held} held`);
setText($("queue-count"), tasks.length === 0 ? "Nothing waiting" : parts.join(", "));
show($("queue-empty"), tasks.length === 0);
syncQueueSections(sectionsRoot, groupQueueBySection(tasks));
renderLoop();
}
const openQuestions = () => (state.questions || []).filter((q) => q.status === "open");
const openFor = (runId) => openQuestions().filter((q) => q.run === runId);
const needsOwnerQuestions = () => openQuestions().filter((q) => q.waiting_on_agent !== true);
function needsOwnerCount() {
return state.questions === null
? Number(state.health && state.health.questions_needs_owner) || 0
: needsOwnerQuestions().length;
}
function sortQuestions(list) {
return list.slice().sort((a, b) => {
const rank = (ASK_ORDER[a.status] ?? 3) - (ASK_ORDER[b.status] ?? 3);
return rank || (Date.parse(b.asked_at) || 0) - (Date.parse(a.asked_at) || 0);
});
}
function isWaiting(run) {
if (!run.waiting) return false;
return state.questions === null || openFor(run.id).length > 0;
}
function buildMd(node) {
switch (node && node.type) {
case "paragraph":
return el("p", {}, (node.children || []).map(buildMd));
case "heading": {
const level = Math.min(6, Math.max(1, Number(node.level) || 1));
return el(`h${level}`, {}, (node.children || []).map(buildMd));
}
case "bullet_list":
return el("ul", {}, (node.items || []).map(buildMd));
case "ordered_list":
return el("ol", { start: node.start && node.start !== 1 ? node.start : null },
(node.items || []).map(buildMd));
case "list_item":
if (node.checked === null || node.checked === undefined) {
return el("li", {}, (node.children || []).map(buildMd));
}
return el("li", { class: "task" },
el("input", { type: "checkbox", checked: Boolean(node.checked), disabled: true }),
(node.children || []).map(buildMd));
case "block_quote":
return el("blockquote", {}, (node.children || []).map(buildMd));
case "thematic_break":
return el("hr");
case "code_block":
return el("pre", { "data-lang": node.lang || null }, el("code", { text: node.code || "" }));
case "code":
return el("code", { text: node.code || "" });
case "emphasis":
return el("em", {}, (node.children || []).map(buildMd));
case "strong":
return el("strong", {}, (node.children || []).map(buildMd));
case "strikethrough":
return el("s", {}, (node.children || []).map(buildMd));
case "link":
return el("a", { href: node.href, target: "_blank", rel: "noopener noreferrer" },
(node.children || []).map(buildMd));
case "image":
return el("img", { src: node.src, alt: node.alt || "", loading: "lazy" });
case "table":
return buildMdTable(node);
case "soft_break":
return document.createTextNode(" ");
case "line_break":
return el("br");
case "text":
return document.createTextNode(node.value ?? "");
default:
return document.createTextNode("");
}
}
function buildMdTable(node) {
const rows = Array.isArray(node.rows) ? node.rows : [];
const row = (cells) => el("tr", {}, (Array.isArray(cells) ? cells : []).map((cell) =>
el(cell.header ? "th" : "td",
{ "data-align": cell.align && cell.align !== "none" ? cell.align : null },
(cell.children || []).map(buildMd))));
const head = rows.length ? el("thead", {}, row(rows[0])) : null;
const body = el("tbody", {}, rows.slice(1).map(row));
return el("div", { class: "table-scroll" }, el("table", {}, head, body));
}
function renderMd(container, nodes) {
clear(container);
append(container, (Array.isArray(nodes) ? nodes : []).map(buildMd));
}
async function panelReachable(id) {
if (state.panelOk.has(id)) return state.panelOk.get(id);
let reachable = false;
try {
const res = await fetch(API.panel(id), { method: "HEAD", cache: "no-store" });
reachable = res.ok;
} catch {
reachable = false;
}
state.panelOk.set(id, reachable);
return reachable;
}
function panelFrame(question, label) {
return el("iframe", {
src: API.panel(question.id),
sandbox: "",
referrerpolicy: "no-referrer",
title: `${label}: ${question.summary || shortId(question.id)}`,
});
}
function mountPanel(row, question) {
const r = row.refs;
clear(r.panelBox);
const assets = Array.isArray(question.assets) ? question.assets : [];
const full = el("button", {
class: "btn btn-quiet", type: "button", text: "Full screen",
"aria-label": `Open the panel full screen: ${question.summary || shortId(question.id)}`,
onclick: () => openPanel(question),
});
const pending = el("p", { class: "frame-note", text: "Loading the panel\u2026" });
r.panelBox.append(
el("div", { class: "ask-panel-bar" },
el("span", { class: "ask-panel-label", text: "Panel from the agent" }),
full),
pending,
);
panelReachable(question.id).then((reachable) => {
if (row.dataset.panel !== question.id) return;
pending.remove();
if (!reachable) {
full.disabled = true;
r.panelBox.append(el("div", { class: "frame-fail" },
el("span", { text: "The agent attached a panel, but this server cannot serve it." }),
el("span", { class: "hint", text: "The summary and the context above are all of it that survived \u2014 and the question is still answerable below." }),
));
return;
}
r.panelBox.append(
el("div", { class: "frame-wrap" }, panelFrame(question, "Panel for"), el("div", { class: "frame-more" })),
el("p", { class: "frame-note", text: `${assets.length ? `${plural(assets.length, "attachment", "attachments")} \u00b7 ` : ""}The panel scrolls inside this window. Full screen shows all of it.` }),
);
});
}
function renderPanel(row, question) {
const r = row.refs;
const wanted = question.panel === true;
show(r.panelBox, wanted);
if (!wanted) {
if (row.dataset.panel) {
row.dataset.panel = "";
clear(r.panelBox);
}
return;
}
if (row.dataset.panel === question.id) return;
row.dataset.panel = question.id;
mountPanel(row, question);
}
function openPanel(question) {
const dialog = $("panel-full");
setText($("panel-full-h"), question.summary || `Panel ${shortId(question.id)}`);
const body = $("panel-full-body");
clear(body);
body.append(panelFrame(question, "Panel, full screen, for"));
if (!dialog.open) dialog.showModal();
requestAnimationFrame(() => $("panel-full-close").focus());
}
function closePanel() {
const dialog = $("panel-full");
if (dialog.open) dialog.close();
clear($("panel-full-body"));
}
function isMergeQuestion(question) {
const choices = (Array.isArray(question.choices) ? question.choices : []).map((c) => String(c).toLowerCase());
const pair = choices.includes("merge") && choices.includes("hold");
return pair && (question.node === MERGE_NODE || choices.length === 2);
}
function choiceNamed(question, want) {
const choices = Array.isArray(question.choices) ? question.choices : [];
return choices.find((choice) => String(choice).toLowerCase() === want) || want;
}
function renderStakes(row, question) {
const r = row.refs;
const armed = row.dataset.armed === "1";
clear(r.stakes);
r.stakes.append(el("p", { class: "stakes-what" },
"Merging closes this run: the branch goes into ",
el("span", { class: "ask-seat", text: "the base branch" }),
" and magi has no undo for it.",
));
if (armed) {
const cancel = el("button", {
class: "btn btn-quiet", type: "button", text: "Cancel",
onclick: () => { row.dataset.armed = ""; renderStakes(row, question); },
});
r.stakes.append(el("div", { class: "stakes-confirm" },
el("p", { class: "stakes-warn", text: "Tapping merge now merges it." }),
el("div", { class: "stakes-row" },
cancel,
el("button", {
class: "btn btn-gold", type: "button", text: "Yes, merge now",
onclick: () => answerQuestion(question.id, { choice: choiceNamed(question, "merge") }, row),
}),
),
));
requestAnimationFrame(() => cancel.focus({ preventScroll: true }));
} else {
r.stakes.append(el("button", {
class: "btn btn-gold stakes-arm", type: "button", text: "Merge this pull request\u2026",
onclick: () => { row.dataset.armed = "1"; renderStakes(row, question); },
}));
}
r.stakes.append(el("button", {
class: "btn btn-quiet stakes-hold", type: "button", text: "Hold \u2014 do not merge",
onclick: () => answerQuestion(question.id, { choice: choiceNamed(question, "hold") }, row),
}));
for (const choice of Array.isArray(question.choices) ? question.choices : []) {
const name = String(choice).toLowerCase();
if (name === "merge" || name === "hold") continue;
r.stakes.append(el("button", {
class: "btn", type: "button", text: choice,
onclick: () => answerQuestion(question.id, { choice }, row),
}));
}
}
function createAskCard() {
const chipSlot = el("span");
const whenSlot = el("time", { class: "ask-when" });
const summary = el("h2", { class: "ask-summary", tabindex: "-1" });
const runLink = el("a", { class: "ask-seat" });
const node = el("span");
const seat = el("span", { class: "ask-seat" });
const where = el("div", { class: "ask-where" }, runLink, node, seat);
const detail = el("div");
const hint = el("p", { class: "hint" });
const choices = el("div", { class: "choices" });
const text = el("textarea", { rows: "4", "aria-label": "Your answer" });
const send = el("button", { class: "btn btn-gold", type: "button", text: "Send answer" });
const free = el("div", { class: "ask-free" }, text, send);
const error = el("p", { class: "form-error", role: "alert" });
const answerLabel = el("span", { class: "answer-label" });
const answerText = el("p", { class: "answer-text" });
const answer = el("div", { class: "answer" }, answerLabel, answerText);
const note = el("p", { class: "panel-note" });
const band = el("p", { class: "stakes-band" });
const panelBox = el("div", { class: "ask-panel" });
const stakes = el("div", { class: "stakes" });
const thread = el("ol", { class: "ask-thread" });
const waitingNote = el("p", { class: "ask-waiting" });
const sayText = el("textarea", { rows: "3", "aria-label": "Ask the agent back" });
const saySend = el("button", { class: "btn", type: "button", text: "Ask back" });
const sayBox = el("div", { class: "ask-say" },
el("label", { class: "ask-say-label", text: "Not ready to decide? Ask back instead:" }),
sayText, saySend);
const row = el("li", { class: "ask" },
band,
el("div", { class: "ask-top" }, chipSlot, whenSlot),
summary, where, panelBox, detail, thread, hint, waitingNote, stakes, choices, free, sayBox, error, answer, note,
);
row.refs = { chipSlot, whenSlot, summary, runLink, node, seat, where, detail,
hint, choices, text, send, free, error, answerLabel, answerText, answer, note,
band, panelBox, stakes, thread, waitingNote, sayText, saySend, sayBox };
return row;
}
function updateAskCard(row, question, { compact = false } = {}) {
const r = row.refs;
const status = String(question.status || "open");
const open = status === "open";
const choices = Array.isArray(question.choices) ? question.choices : [];
const merge = isMergeQuestion(question);
setAttr(row, "data-state", status);
setAttr(row, "data-stakes", merge ? "merge" : null);
setText(r.band, merge
? (open ? "Irreversible \u00b7 this merges the pull request" : "Merge decision")
: "");
show(r.band, merge);
const next = chip(status, QUESTION_STATUS);
if (r.chipSlot.firstChild) r.chipSlot.firstChild.replaceWith(next);
else r.chipSlot.append(next);
const settledAt = !open && question.answered_at ? question.answered_at : question.asked_at;
const at = when(settledAt);
setText(r.whenSlot, `${!open && question.answered_at ? "answered" : "asked"} ${at.text}`);
setAttr(r.whenSlot, "datetime", settledAt || null);
setAttr(r.whenSlot, "title", at.title);
setText(r.summary, question.summary || firstLine(question.detail) || `question ${shortId(question.id)}`);
setAttr(r.runLink, "href", `#/runs/${question.run}`);
setText(r.runLink, `run ${shortId(question.run)}`);
show(r.runLink, Boolean(question.run) && !compact);
setText(r.node, question.node ? `node ${question.node}` : "");
show(r.node, Boolean(question.node));
setText(r.seat, question.seat ? `seat ${question.seat}` : "");
show(r.seat, Boolean(question.seat));
separate(r.where);
renderPanel(row, question);
const waitingOnAgent = open && question.waiting_on_agent === true;
setAttr(row, "data-waiting-agent", waitingOnAgent ? "1" : null);
const turns = Array.isArray(question.thread) ? question.thread : [];
const threadKey = String(turns.length);
if (row.dataset.threadKey !== threadKey) {
row.dataset.threadKey = threadKey;
clear(r.thread);
for (const turn of turns) {
const isAgent = turn.who === "agent";
const at = when(turn.at);
r.thread.append(el("li", { class: "ask-turn", "data-who": isAgent ? "agent" : "operator" },
el("span", { class: "ask-turn-who", text: isAgent ? "Agent" : "You" }),
el("time", { class: "ask-turn-when", datetime: turn.at, title: at.title, text: at.text }),
el("p", { class: "ask-turn-body", text: turn.body || "" }),
));
}
}
show(r.thread, turns.length > 0);
setText(r.waitingNote, waitingOnAgent
? "Waiting for the agent to reply. There is nothing to decide until it does."
: "");
show(r.waitingNote, waitingOnAgent);
r.saySend.onclick = () => sayToQuestion(question.id, r.sayText.value, row);
show(r.sayBox, open);
r.sayText.disabled = waitingOnAgent;
r.saySend.disabled = waitingOnAgent;
const detail = typeof question.detail === "string" ? question.detail.trim() : "";
const key = `${open ? "open" : "settled"}:${detail.length}`;
if (row.dataset.detailKey !== key) {
row.dataset.detailKey = key;
clear(r.detail);
if (detail) {
const body = el("div", { class: "md" });
renderMd(body, question.detail_md);
r.detail.append(open
? body
: el("details", { class: "advanced" }, el("summary", { text: "Context" }), body));
}
}
show(r.detail, detail !== "");
const choiceKey = `${merge ? "merge" : "plain"}:${choices.join("\u0000")}`;
if (row.dataset.choiceKey !== choiceKey) {
row.dataset.choiceKey = choiceKey;
row.dataset.armed = "";
clear(r.choices);
clear(r.stakes);
if (merge) {
renderStakes(row, question);
} else {
for (const choice of choices) {
r.choices.append(el("button", {
class: "btn", type: "button", text: choice,
onclick: () => answerQuestion(question.id, { choice }, row),
}));
}
}
}
r.send.onclick = () => answerQuestion(question.id, { text: r.text.value }, row);
setText(r.hint, !open || waitingOnAgent
? ""
: merge
? "Read the panel, then decide. Nothing merges until you say so twice."
: choices.length
? "Pick one. The run resumes as soon as you do."
: "No options were offered \u2014 answer in your own words.");
show(r.hint, open && !waitingOnAgent);
show(r.stakes, open && merge);
show(r.choices, open && !merge && choices.length > 0);
show(r.free, open && choices.length === 0);
show(r.error, open && !r.error.hidden && r.error.textContent !== "");
r.text.disabled = waitingOnAgent;
r.send.disabled = waitingOnAgent;
for (const btn of r.choices.querySelectorAll("button")) btn.disabled = waitingOnAgent;
for (const btn of r.stakes.querySelectorAll("button")) btn.disabled = waitingOnAgent;
const given = question.answer && typeof question.answer === "object" ? question.answer : null;
const value = given
? typeof given.choice === "string" ? given.choice : typeof given.text === "string" ? given.text : ""
: "";
if (value) {
const decided = when(question.answered_at);
setText(r.answerLabel, `Decided ${decided.text}`);
setAttr(r.answerLabel, "title", decided.title);
setText(r.answerText, value);
}
show(r.answer, Boolean(value));
setText(r.note, status === "abandoned"
? "The run ended before this was answered, so nothing acted on it."
: row.dataset.raced === "1"
? "This was answered elsewhere while you had it open. The recorded answer is above."
: "");
show(r.note, r.note.textContent !== "");
}
async function answerQuestion(id, body, row) {
const r = row.refs;
const buttons = [...row.querySelectorAll("button")].filter((b) => !b.closest(".ask-panel"));
const value = typeof body.choice === "string" ? body.choice : String(body.text || "");
if (!value.trim()) {
setText(r.error, "An answer cannot be empty.");
show(r.error, true);
r.text.focus();
return;
}
show(r.error, false);
for (const button of buttons) button.disabled = true;
try {
reflectQuestion(await postJson(API.answer(id), body));
announce(`Answered: ${value.trim()}`);
ok();
} catch (error) {
if (error.status === 409) {
row.dataset.raced = "1";
announce("That question had already been answered.");
await loadQuestions();
return;
}
setText(r.error, error.message);
show(r.error, true);
}
for (const button of buttons) button.disabled = false;
}
async function sayToQuestion(id, text, row) {
const r = row.refs;
const value = String(text || "").trim();
if (!value) {
r.sayText.focus();
return;
}
r.sayText.disabled = true;
r.saySend.disabled = true;
try {
reflectQuestion(await postJson(API.questionSay(id), { body: value }));
r.sayText.value = "";
announce("Sent. Waiting for the agent to reply.");
ok();
} catch (error) {
if (error.status === 409) {
row.dataset.raced = "1";
announce("That question was already settled.");
await loadQuestions();
return;
}
setText(r.error, error.message);
show(r.error, true);
r.sayText.disabled = false;
r.saySend.disabled = false;
}
}
function reflectQuestion(question) {
if (!question || typeof question !== "object" || !question.id) return;
state.questions = sortQuestions([
question,
...(state.questions || []).filter((q) => q.id !== question.id),
]);
renderQuestions();
renderAskBar();
renderRuns();
if (state.route.name === "run" && state.detail.run) renderRunDetail();
}
function renderAskBar() {
const bar = $("ask-bar");
const count = needsOwnerCount();
const open = needsOwnerQuestions();
show(bar, count > 0);
renderIndicators(count);
if (count === 0) return;
setText(bar.querySelector(".ask-bar-count"), count === 1
? "An agent is waiting on your decision"
: `${count} agents are waiting on your decision`);
const oldest = open.length ? open[open.length - 1] : null;
const line = bar.querySelector(".ask-bar-summary");
setText(line, oldest ? oldest.summary || "" : "");
show(line, Boolean(oldest && oldest.summary));
}
function renderIndicators(count) {
for (const id of ["ask-badge-rail", "ask-badge-dock"]) {
const badge = $(id);
setText(badge, count > 99 ? "99+" : String(count));
show(badge, count > 0);
}
for (const link of document.querySelectorAll('[data-nav="questions"]')) {
setAttr(link, "aria-label", count > 0 ? `Questions, ${count} unanswered` : "Questions");
}
renderTitle();
}
function renderTitle() {
const count = needsOwnerCount();
const base = state.route.name === "queue"
? "Backlog \u2014 magi"
: state.route.name === "questions"
? "Questions \u2014 magi"
: state.route.name === "talks"
? "Chat \u2014 magi"
: state.route.name === "talk"
? `Chat ${shortId(state.route.id)} \u2014 magi`
: state.route.name === "run"
? `Run ${shortId(state.route.id)} \u2014 magi`
: "magi \u2014 observation deck";
document.title = count > 0 ? `(${count}) ${base}` : base;
}
function renderQuestions() {
const list = $("questions-list");
const questions = state.questions;
if (questions === null) {
setText($("questions-count"), "Loading\u2026");
return;
}
const open = openQuestions().length;
const settled = questions.length - open;
setText($("questions-count"), questions.length === 0
? "Nothing asked yet"
: open === 0
? `nothing open \u00b7 ${plural(settled, "decision on record", "decisions on record")}`
: [`${plural(open, "question is blocking a run", "questions are blocking runs")}`,
settled ? `${settled} on record` : null].filter(Boolean).join(" \u00b7 "));
show($("questions-empty"), questions.length === 0);
syncList(list, questions, (q) => q.id, createAskCard, (row, q) => updateAskCard(row, q));
}
function focusFirstAsk() {
requestAnimationFrame(() => {
const first = $("questions-list").querySelector('.ask[data-state="open"] .ask-summary');
if (first) first.focus({ preventScroll: true });
});
}
const MAGI_PREFIX = "magi: ";
function turnWho(turn) {
if (turn.who === "agent" && String(turn.body || "").startsWith(MAGI_PREFIX)) return "system";
return turn.who === "operator" ? "operator" : "agent";
}
function createTurnRow() {
const who = el("span", { class: "turn-who" });
const body = el("div", { class: "turn-body" });
const attachments = el("div", { class: "turn-attachments" });
const at = el("time", { class: "turn-at" });
const row = el("li", { class: "turn" }, who, body, attachments, at);
row.refs = { who, body, attachments, at };
return row;
}
function attachmentUrl(conversationId, att) {
return API.talkAttachment(conversationId, att.id);
}
function updateTurnRow(row, item) {
const r = row.refs;
const turn = item.turn;
const kind = turnWho(turn);
const body = String(turn.body || "");
setAttr(row, "data-who", kind);
setText(r.who, kind === "operator" ? "You" : kind === "system" ? "magi" : "Agent");
const key = `${kind}:${body.length}`;
if (row.dataset.turnKey !== key) {
row.dataset.turnKey = key;
clear(r.body);
if (kind === "agent") {
const div = el("div", { class: "md" });
renderMd(div, item.md);
r.body.append(div);
} else {
r.body.append(el("p", {
class: "turn-text",
text: kind === "system" ? body.slice(MAGI_PREFIX.length) : body,
}));
}
}
const atts = Array.isArray(turn.attachments) ? turn.attachments : [];
const attKey = atts.map((a) => a.id).join(",");
if (r.attachments.dataset.attKey !== attKey) {
r.attachments.dataset.attKey = attKey;
clear(r.attachments);
for (const att of atts) {
const url = attachmentUrl(item.conversationId, att);
const name = String(att.name || "attachment");
const thumb = el("button", {
class: "turn-thumb", type: "button",
"aria-label": `Open ${name} at full size`,
onclick: () => showAttachment(url, name),
}, el("img", { src: url, alt: "", loading: "lazy" }));
r.attachments.append(thumb);
}
}
const at = when(turn.at);
setText(r.at, at.text);
setAttr(r.at, "datetime", turn.at || null);
setAttr(r.at, "title", at.title);
}
function showAttachment(url, name) {
const dialog = $("attachment-view");
const img = $("attachment-view-img");
setText($("attachment-view-h"), name || "Attachment");
img.src = url;
img.alt = name || "";
if (!dialog.open) dialog.showModal();
}
function closeAttachmentView() {
const dialog = $("attachment-view");
if (dialog.open) dialog.close();
}
let nextLocalAttachmentId = 1;
function scrollToLastTurn(containerId) {
const turns = $(containerId);
if (!turns.children.length) return;
const last = turns.lastElementChild;
const header = document.querySelector(".top");
const gap = header ? Math.ceil(header.getBoundingClientRect().height) + 4 : 0;
last.style.scrollMarginTop = `${gap}px`;
const motion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
last.scrollIntoView({ behavior: motion ? "auto" : "smooth", block: "start" });
}
const talkTurns = (talk) => (talk && Array.isArray(talk.turns) ? talk.turns : []);
const talkTurnsMd = (talk) => (talk && Array.isArray(talk.turn_bodies_md) ? talk.turn_bodies_md : []);
function talkOpener(talk) {
const first = talkTurns(talk).find((turn) => turn.who === "operator");
return first ? String(first.body || "") : "";
}
function sortTalks(list) {
return list.slice().sort((a, b) => {
const rank = (a.status === "open" ? 0 : 1) - (b.status === "open" ? 0 : 1);
const started = (talk) => Date.parse(talk.created_at) || 0;
return rank || started(b) - started(a);
});
}
function createTalkCard() {
const chipSlot = el("span");
const thinking = el("span", { class: "tag", "data-tone": "blue", text: "thinking…" });
const whenSlot = el("time", { class: "card-when" });
const title = el("h2", { class: "card-title" });
const agent = el("span", { class: "repo" });
const turns = el("span");
const tasks = el("span", { class: "win" });
const meta = el("div", { class: "card-meta" }, agent, turns, tasks);
const last = el("p", { class: "card-event" });
const card = el("a", { class: "card" },
el("div", { class: "card-top" }, chipSlot, thinking, whenSlot),
title, meta, last,
);
const row = el("li", {}, card);
row.refs = { card, chipSlot, thinking, whenSlot, title, agent, turns, tasks, last };
return row;
}
function updateTalkCard(row, talk) {
const r = row.refs;
const status = String(talk.status || "open");
const turns = talkTurns(talk);
const tone = toneOf(status, TALK_STATUS);
r.card.setAttribute("href", `#/chat/${talk.id}`);
setAttr(r.card, "data-tone", tone);
setAttr(row, "data-tone", tone);
const next = chip(status, TALK_STATUS);
if (r.chipSlot.firstChild) r.chipSlot.firstChild.replaceWith(next);
else r.chipSlot.append(next);
show(r.thinking, talkIsThinking(talk));
const at = when(talk.updated_at || talk.created_at);
setText(r.whenSlot, at.text);
setAttr(r.whenSlot, "datetime", talk.updated_at || talk.created_at);
setAttr(r.whenSlot, "title", `updated ${at.title}`);
setText(r.title, firstLine(talkOpener(talk)) || `conversation ${shortId(talk.id)}`);
setText(r.agent, talk.agent || "");
show(r.agent, Boolean(talk.agent));
setText(r.turns, plural(turns.length, "turn", "turns"));
const tasks = Array.isArray(talk.tasks) ? talk.tasks.length : 0;
setText(r.tasks, tasks ? plural(tasks, "task filed", "tasks filed") : "");
show(r.tasks, tasks > 0);
separate(r.turns.parentNode);
const tail = turns.length ? turns[turns.length - 1] : null;
setText(r.last, tail && tail.who === "agent" ? firstLine(tail.body) : "");
show(r.last, Boolean(tail && tail.who === "agent"));
}
function renderTalks() {
const list = $("talks-list");
const talks = state.talks;
renderTalkIndicators();
if (talks === null) {
setText($("talks-count"), "Loading…");
return;
}
const open = talks.filter((t) => t.status === "open").length;
setText($("talks-count"), talks.length === 0
? "No conversations yet"
: open ? `${plural(open, "conversation open", "conversations open")}` : "nothing open");
show($("talks-empty"), talks.length === 0);
syncList(list, sortTalks(talks), (t) => t.id, createTalkCard, updateTalkCard);
}
function renderTalkIndicators() {
const count = (state.talks || []).filter(talkIsThinking).length;
for (const id of ["talk-badge-rail", "talk-badge-dock"]) {
const badge = $(id);
setText(badge, count > 99 ? "99+" : String(count));
show(badge, count > 0);
}
for (const link of document.querySelectorAll('[data-nav="talks"]')) {
setAttr(link, "aria-label", count > 0 ? `Chat, ${count} conversations thinking` : "Chat");
}
}
function talkIsThinking(talk) {
return Boolean(talk && (talk.thinking || state.talkWaits.has(talk.id)));
}
async function loadTalks() {
const observedAt = Date.now();
const observed = new Map([...state.talkWaits].map(([id, wait]) => [id, {
generation: wait.generation, startedAt: observedAt,
}]));
try {
const list = await getJson(API.talks);
state.talks = Array.isArray(list) ? list : [];
for (const talk of state.talks) trackTalkThinking(talk, observed.get(talk.id));
renderTalks();
ok();
} catch (error) {
fail(`Could not load conversations: ${error.message}`);
}
}
async function loadTalk(id) {
const wait = state.talkWaits.get(id);
const observed = wait && { generation: wait.generation, startedAt: Date.now() };
try {
const talk = await getJson(API.talk(id));
trackTalkThinking(talk, observed);
if (state.talkDetail.id !== id) return;
state.talkDetail.talk = talk;
renderTalk();
ok();
} catch (error) {
if (state.talkDetail.id === id) {
fail(`Could not load conversation ${shortId(id)}: ${error.message}`);
}
}
}
function talkTasksSummary(tasks) {
const counts = { running: 0, held: 0, failed: 0, queued: 0, done: 0 };
for (const task of tasks) {
const status = String(task.status_str || task.status || "");
if (status in counts) counts[status] += 1;
}
const parts = [`${tasks.length} filed`];
for (const key of ["running", "held", "failed", "queued", "done"]) {
if (counts[key]) parts.push(`${counts[key]} ${key}`);
}
return parts.join(" · ");
}
const TALK_TASKS_STORAGE_KEY = "magi.talkTasksOpen";
function renderTalkTasks(talk) {
const panel = $("talk-tasks-panel");
const tasks = Array.isArray(talk && talk.tasks) ? talk.tasks : [];
show(panel, tasks.length > 0);
if (tasks.length === 0) return;
setText($("talk-tasks-count"), talkTasksSummary(tasks));
syncList($("talk-tasks"), tasks, (t) => t.id, createTalkTaskRow, updateTalkTaskRow);
const talkId = String(talk.id || "");
if (panel.dataset.talkId !== talkId) {
panel.dataset.talkId = talkId;
panel.open = isSectionOpen(loadCollapsed(TALK_TASKS_STORAGE_KEY), talkId, false);
}
}
function createTalkTaskRow() {
const chipSlot = el("span");
const title = el("span");
const row = el("li", {}, chipSlot, title);
row.refs = { chipSlot, title };
return row;
}
function updateTalkTaskRow(row, task) {
const r = row.refs;
const status = String(task.status_str || task.status || "");
const next = chip(status, TASK_STATUS);
if (r.chipSlot.firstChild) r.chipSlot.firstChild.replaceWith(next);
else r.chipSlot.append(next);
setText(r.title, `${task.title || task.id} · ${shortId(task.id)}`);
}
function renderTalk() {
const talk = state.talkDetail.talk;
const wait = talk ? state.talkWaits.get(talk.id) : undefined;
const busy = Boolean(wait) || Boolean(talk && talk.thinking);
if (!talk) {
setText($("talk-h"), "Loading conversation…");
setText($("talk-meta"), "");
clear($("talk-status"));
clear($("talk-turns"));
show($("talk-tasks-panel"), false);
show($("talk-say"), false);
show($("talk-closed"), false);
show($("talk-close-go"), false);
show($("talk-reopen-go"), false);
show($("talk-wait"), false);
clear($("talk-delete-box"));
renderTalkThumbs();
return;
}
const status = String(talk.status || "open");
const pending = wait && wait.pending && talkTurns(talk).length <= wait.since
? [{ who: "operator", body: wait.pending.body, at: wait.pending.at }]
: [];
const turns = [...talkTurns(talk), ...pending];
const head = $("talk-status");
clear(head);
head.append(chip(status, TALK_STATUS));
setText($("talk-h"), firstLine(talkOpener(talk)) || `Conversation ${shortId(talk.id)}`);
const started = when(talk.created_at);
setText($("talk-meta"),
`${shortId(talk.id)} · ${talk.agent || "agent"} · ${plural(turns.length, "turn", "turns")} · started ${started.text}`);
setAttr($("talk-meta"), "title", `${talk.id}\nstarted ${started.title}`);
const turnsMd = talkTurnsMd(talk);
syncList(
$("talk-turns"),
turns.map((turn, i) => ({ turn, md: turnsMd[i], key: String(i), conversationId: talk.id })),
(item) => item.key, createTurnRow, updateTurnRow,
);
const turnCount = turns.length;
const lastIsPending = wait && wait.pending
&& turns.length > 0 && turns[turns.length - 1].who === "operator"
&& turns[turns.length - 1].body === wait.pending.body;
if (state.openingTalk) {
state.openingTalk = false;
if (turnCount > 0) requestAnimationFrame(() => scrollToLastTurn("talk-turns"));
} else if (turnCount > state.prevTalkTurnCount && !lastIsPending) {
requestAnimationFrame(() => scrollToLastTurn("talk-turns"));
}
state.prevTalkTurnCount = turnCount;
renderTalkTasks(talk);
const canSay = status === "open";
show($("talk-say"), canSay);
show($("talk-closed"), !canSay);
show($("talk-close-go"), canSay);
show($("talk-reopen-go"), !canSay);
renderTalkThumbs();
const uploading = talkAttachmentsBusy();
$("f-talk-say").disabled = false;
$("talk-send").disabled = uploading;
setText($("talk-send"), uploading ? "Uploading…" : busy ? "Queue next" : "Send");
renderTalkPending(talk);
show($("talk-wait"), busy);
renderTalkDelete(talk);
}
function renderTalkPending(talk) {
const box = $("talk-pending");
const text = String(talk.pending || "");
const attachments = Array.isArray(talk.pending_attachments) ? talk.pending_attachments : [];
const busy = Boolean(talk.thinking) || state.talkWaits.has(talk.id);
clear(box);
show(box, Boolean(text || attachments.length));
if (!text && attachments.length === 0) return;
append(box, [
el("p", { class: "panel-note", text: "Queued for the next reply" }),
text ? el("pre", { class: "talk-pending-text", text }) : null,
attachments.length ? el("p", { class: "frame-note", text: `${plural(attachments.length, "attachment", "attachments")} queued` }) : null,
!busy ? el("button", { class: "btn", type: "button", text: "Resume queued draft", onclick: resumeTalkPending }) : null,
el("button", { class: "btn btn-quiet", type: "button", text: "Clear", onclick: clearTalkPending }),
el("button", { class: "btn btn-quiet", type: "button", text: "Edit text", onclick: editTalkPending }),
]);
}
async function resumeTalkPending() {
const id = state.talkDetail.id;
const talk = state.talkDetail.talk;
if (!id || !talk) return;
try {
const next = await postJson(API.talkPendingResume(id), {});
if (state.talkDetail.id === id) {
state.talkDetail.talk = next;
trackTalkThinking(next);
renderTalk();
}
announce("Queued draft resumed.");
loadTalks();
} catch (error) {
if (error.status === 409) await loadTalk(id);
talkError(`Could not resume the queued draft: ${error.message}`);
}
}
async function editTalkPending() {
const id = state.talkDetail.id;
const talk = state.talkDetail.talk;
if (!id || !talk) return;
const expectedText = String(talk.pending || "");
const expectedAttachments = Array.isArray(talk.pending_attachments)
? talk.pending_attachments.map((attachment) => attachment.id)
: [];
const text = window.prompt("Edit queued text", expectedText);
if (text === null || text === expectedText) return;
try {
const next = await postJson(API.talkPendingEdit(id), { text, expected_text: expectedText, expected_attachments: expectedAttachments });
if (state.talkDetail.id === id) {
state.talkDetail.talk = next;
trackTalkThinking(next);
renderTalk();
}
announce("Queued text updated. Attachments are preserved.");
loadTalks();
} catch (error) {
if (error.status === 409) await loadTalk(id);
talkError(`Could not edit the queued message: ${error.message}`);
}
}
async function clearTalkPending() {
const id = state.talkDetail.id;
const talk = state.talkDetail.talk;
if (!id || !talk) return;
const expectedText = String(talk.pending || "");
const expectedAttachments = Array.isArray(talk.pending_attachments)
? talk.pending_attachments.map((attachment) => attachment.id)
: [];
try {
const next = await postJson(API.talkPendingClear(id), { expected_text: expectedText, expected_attachments: expectedAttachments });
if (state.talkDetail.id === id) {
state.talkDetail.talk = next;
renderTalk();
}
announce("Queued message cleared.");
loadTalks();
} catch (error) {
if (error.status === 409) await loadTalk(id);
talkError(`Could not clear the queued message: ${error.message}`);
}
}
function tickTalkWaits() {
const now = Date.now();
for (const [id, wait] of state.talkWaits) {
if (now - wait.lastPoll >= 10000) {
wait.lastPoll = now;
loadTalk(id);
}
}
const box = $("talk-wait");
const wait = state.talkDetail.id ? state.talkWaits.get(state.talkDetail.id) : undefined;
if (!wait) {
show(box, false);
return;
}
const secs = Math.max(Math.round((now - wait.waitFrom) / 1000), 0);
setText(box.querySelector(".waiting-text"), secs >= 90
? "Still working — a standing chat turn can run for several minutes while the agent investigates. Long, but not stuck."
: "The agent is looking into it.");
setText(box.querySelector(".waiting-secs"), `${secs}s`);
show(box, true);
}
function beginTalkTurn(id, since, target, pending = null) {
state.talkWaits.set(id, {
since, target, pending, waitFrom: Date.now(), lastPoll: Date.now(),
generation: nextTalkWaitGeneration++, confirmed: target === null, missingClaimSince: null,
});
if (!state.talkWaitTimer) state.talkWaitTimer = setInterval(tickTalkWaits, 1000);
tickTalkWaits();
}
function endTalkTurn(id) {
if (!state.talkWaits.has(id)) return;
state.talkWaits.delete(id);
if (state.talkDetail.id === id) show($("talk-wait"), false);
if (state.talkWaits.size === 0 && state.talkWaitTimer) {
clearInterval(state.talkWaitTimer);
state.talkWaitTimer = null;
}
}
function trackTalkThinking(talk, observed) {
const wait = state.talkWaits.get(talk.id);
if (wait) {
if (wait.target !== null && talkTurns(talk).length >= wait.target) endTalkTurn(talk.id);
else if (observed && observed.generation === wait.generation && talk.thinking) {
wait.confirmed = true;
wait.missingClaimSince = null;
} else if (!talk.thinking && observed && observed.generation === wait.generation
&& wait.confirmed) {
if (wait.missingClaimSince !== null && observed.startedAt > wait.missingClaimSince) {
endTalkTurn(talk.id);
} else {
wait.missingClaimSince = Date.now();
}
}
return;
}
if (talk.thinking) beginTalkTurn(talk.id, talkTurns(talk).length, null);
}
function talkError(message) {
const box = $("talk-error");
setText(box, message || "");
show(box, Boolean(message));
}
function talkAttachmentsBusy() {
return state.talkAttachments.id === state.talkDetail.id
&& state.talkAttachments.items.some((item) => item.status === "uploading");
}
function resetTalkAttachments(id) {
for (const item of state.talkAttachments.items) {
if (item.previewUrl) URL.revokeObjectURL(item.previewUrl);
}
state.talkAttachments = { id, items: [] };
}
function takeTalkAttachments(id) {
if (state.talkAttachments.id !== id) return [];
const taken = state.talkAttachments.items.filter((item) => item.status === "done");
state.talkAttachments.items = state.talkAttachments.items.filter((item) => item.status !== "done");
return taken;
}
function releaseTalkAttachments(items) {
for (const item of items) {
if (item.previewUrl) URL.revokeObjectURL(item.previewUrl);
}
}
function restoreTalkSubmission(id, text, attachments) {
if (state.talkAttachments.id === id) {
const known = new Set(state.talkAttachments.items.map((item) => item.localId));
state.talkAttachments.items.unshift(...attachments.filter((item) => !known.has(item.localId)));
}
if (state.talkDetail.id !== id) return;
const box = $("f-talk-say");
if (!box.value) box.value = text;
else if (text && box.value !== text) box.value = `${text}\n\n${box.value}`;
renderTalk();
}
function renderTalkThumbs() {
const box = $("talk-say-thumbs");
const items = state.talkAttachments.id === state.talkDetail.id ? state.talkAttachments.items : [];
clear(box);
show(box, items.length > 0);
for (const item of items) {
const thumb = el("div", { class: "say-thumb" });
if (item.status === "uploading") thumb.classList.add("is-uploading");
if (item.status === "error") thumb.classList.add("is-failed");
thumb.append(el("img", { src: item.previewUrl || "", alt: "" }));
if (item.status === "uploading") {
thumb.append(el("div", { class: "say-thumb-spinner" }, el("i", {})));
}
thumb.append(el("button", {
class: "say-thumb-remove", type: "button",
"aria-label": `Remove ${item.name || "image"}`,
onclick: () => removeTalkAttachment(item.localId),
}, svg(
"svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2.5", "stroke-linecap": "round" },
svg("path", { d: "M6 6l12 12M18 6L6 18" }),
)));
box.append(thumb);
}
const uploading = items.filter((item) => item.status === "uploading").length;
const status = $("talk-say-upload-status");
setText(status, uploading > 0 ? `Uploading ${plural(uploading, "image", "images")}…` : "");
show(status, uploading > 0);
}
function removeTalkAttachment(localId) {
const items = state.talkAttachments.items;
const at = items.findIndex((item) => item.localId === localId);
if (at < 0) return;
const [item] = items.splice(at, 1);
if (item.previewUrl) URL.revokeObjectURL(item.previewUrl);
renderTalk();
}
async function attachTalkFiles(files) {
const id = state.talkDetail.id;
if (!id) return;
if (state.talkAttachments.id !== id) resetTalkAttachments(id);
const images = [...files].filter((file) => file.type.startsWith("image/"));
if (images.length === 0) return;
for (const file of images) {
if (state.talkAttachments.id !== id) break;
const localId = nextLocalAttachmentId++;
const item = {
localId,
previewUrl: URL.createObjectURL(file),
name: file.name || "image",
status: "uploading",
serverId: null,
};
state.talkAttachments.items.push(item);
renderTalk();
try {
const att = await postBytes(API.talkAttachmentPost(id), file, file.name);
item.status = "done";
item.serverId = att.id;
} catch (error) {
item.status = "error";
if (state.talkAttachments.id === id) {
talkError(`Could not attach ${item.name}: ${error.message}`);
}
}
if (state.talkAttachments.id === id) renderTalk();
}
}
async function startTalk() {
const go = $("talk-start-go");
go.disabled = true;
setText(go, "Opening…");
try {
const talk = await postJson(API.talks, {});
state.talks = sortTalks([talk, ...(state.talks || []).filter((t) => t.id !== talk.id)]);
state.talkDetail = { id: talk.id, talk };
trackTalkThinking(talk);
renderTalks();
announce("Conversation opened.");
location.hash = `#/chat/${talk.id}`;
ok();
} catch (failure) {
fail(`Could not open a conversation: ${failure.message}`);
} finally {
go.disabled = false;
setText(go, "Start a conversation");
}
}
async function sendTalkTurn(event) {
event.preventDefault();
const id = state.talkDetail.id;
const box = $("f-talk-say");
const text = box.value;
if (!id || talkAttachmentsBusy()) return;
const attachments = state.talkAttachments.id === id
? state.talkAttachments.items.filter((item) => item.status === "done")
: [];
if (!text.trim() && attachments.length === 0) {
talkError("Say something, or attach an image, first.");
box.focus();
return;
}
talkError("");
const submissionAttachments = takeTalkAttachments(id);
const before = talkTurns(state.talkDetail.talk).length;
const ownsTurn = !state.talkWaits.has(id) && !(state.talkDetail.talk && state.talkDetail.talk.thinking);
if (ownsTurn) beginTalkTurn(id, before, before + 2, { body: text, at: new Date().toISOString() });
box.value = "";
renderTalk();
$("talk-wait").scrollIntoView({ block: "nearest" });
try {
const queued = await postJson(API.talkSay(id), {
text,
attachments: submissionAttachments.map((item) => item.serverId),
});
const wait = state.talkWaits.get(id);
trackTalkThinking(queued, wait && { generation: wait.generation, startedAt: Date.now() });
releaseTalkAttachments(submissionAttachments);
if (state.talkDetail.id === id) {
state.talkDetail.talk = queued;
renderTalk();
announce("Sent. The agent is answering.");
}
loadTalks();
ok();
} catch (error) {
restoreTalkSubmission(id, text, submissionAttachments);
if (ownsTurn) endTalkTurn(id);
talkError(`The message may not have been sent: ${error.message}`);
await loadTalk(id);
}
}
async function closeTalk() {
const id = state.talkDetail.id;
const button = $("talk-close-go");
if (!id) return;
button.disabled = true;
try {
const talk = await postJson(API.talkClose(id), {});
state.talkDetail.talk = talk;
renderTalk();
await loadTalks();
announce("Conversation closed.");
ok();
} catch (error) {
fail(`Could not close the conversation: ${error.message}`);
} finally {
button.disabled = false;
}
}
async function reopenTalk() {
const id = state.talkDetail.id;
const button = $("talk-reopen-go");
if (!id) return;
button.disabled = true;
try {
const talk = await postJson(API.talkReopen(id), {});
state.talkDetail.talk = talk;
renderTalk();
await loadTalks();
announce("Conversation reopened.");
ok();
} catch (error) {
fail(`Could not reopen the conversation: ${error.message}`);
} finally {
button.disabled = false;
}
}
let armedTalkDelete = null;
let armedTalkDeleteFocused = null;
function renderTalkDelete(talk) {
const box = $("talk-delete-box");
if (!box) return;
clear(box);
const armed = armedTalkDelete === talk.id;
if (!armed) armedTalkDeleteFocused = null;
if (armed) {
const cancel = el("button", {
class: "btn btn-quiet",
type: "button",
text: "Cancel",
onclick: () => {
armedTalkDelete = null;
renderTalkDelete(talk);
},
});
const confirm = el("button", {
class: "btn btn-quiet",
type: "button",
text: "Yes, delete conversation",
onclick: () => deleteTalk(talk.id),
});
box.append(
el("div", { class: "stakes-confirm" },
el("p", { class: "stakes-warn", text: "Deleting removes the whole conversation and its artifacts. This cannot be undone." }),
el("div", { class: "stakes-row" }, cancel, confirm),
),
);
if (armedTalkDeleteFocused !== talk.id) {
armedTalkDeleteFocused = talk.id;
requestAnimationFrame(() => cancel.focus({ preventScroll: true }));
}
} else {
box.append(
el("button", {
class: "btn btn-quiet",
type: "button",
text: "Delete conversation…",
onclick: () => {
armedTalkDelete = talk.id;
renderTalkDelete(talk);
},
}),
);
}
}
async function deleteTalk(id) {
try {
await deleteReq(API.talkDelete(id));
ok();
announce("Conversation deleted.");
armedTalkDelete = null;
await loadTalks();
location.hash = "#/chat";
} catch (error) {
armedTalkDelete = null;
fail(`Could not delete the conversation: ${error.message}`);
renderTalk();
}
}
function landOf(run) {
if (run.pr && typeof run.pr === "object") return run.pr;
const summary = (state.runs || []).find((r) => r.id === run.id);
return summary && summary.pr && typeof summary.pr === "object" ? summary.pr : null;
}
function landNote(pr) {
const rounds = Number(pr.rounds) || 0;
const left = Math.max(rounds - (Number(pr.round) || 0), 0);
if (pr.state === "merged") return "Merged. The land loop is finished with this run.";
if (pr.state === "closed") return "The pull request was closed without merging. This one needs you.";
if (pr.checks === "red") {
return left > 0
? `Checks failed, so a fixer round is coming \u2014 ${plural(left, "round", "rounds")} of ${rounds} left. Nothing is needed from you.`
: `Checks failed and all ${rounds} fix rounds are spent. This one needs you.`;
}
if (pr.checks === "pending") return "Waiting on the checks. Nothing is needed from you.";
if (pr.checks === "green") return "Checks are green; the loop is taking it to merge.";
return "The check state could not be read from the forge.";
}
function checksChip(pr) {
const level = String(pr.checks || "unknown");
const check = CHECKS[level] || CHECKS.unknown;
return el("span", {
class: "checks",
"data-checks": level,
"data-glyph": check.glyph,
text: check.word,
});
}
function viable(candidate) {
return !candidate.failed && !candidate.empty;
}
function renderRunDetail() {
const run = state.detail.run;
const report = state.detail.report;
$("run-report").dataset.wrap = state.wrap ? "1" : "0";
if (!run) {
setText($("run-h"), "Loading run\u2026");
setText($("run-meta"), "");
clear($("run-status"));
show($("run-ask-panel"), false);
show($("run-land-panel"), false);
show($("run-active-panel"), false);
clear($("run-actions-box"));
clear($("run-delete-box"));
setText($("run-report"), report === null ? "Loading\u2026" : report);
return;
}
const summary = (state.runs || []).find((r) => r.id === run.id);
const parkedNow = Boolean(summary && isWaiting(summary)) || openFor(run.id).length > 0;
const status = parkedNow ? "waiting" : String(run.status || "");
const meta = RUN_STATUS[status] || {};
const head = $("run-status");
clear(head);
head.append(chip(status, RUN_STATUS));
const parkedAt = parkedNow ? (openFor(run.id)[0] || {}).node || null : null;
const rail = PHASES.includes(status) || parkedAt
? phaseRail(status, parkedAt, activeNote(run))
: null;
if (rail) head.append(rail);
if (meta.note) head.append(el("p", { class: "card-note", text: meta.note }));
setText($("run-h"), firstLine(run.instruction) || shortId(run.id));
const created = when(run.created_at);
const updated = when(run.updated_at);
const repoName = typeof run.repo === "string" ? run.repo.split(/[\\/]/).filter(Boolean).pop() : "";
setText($("run-meta"),
`${shortId(run.id)} \u00b7 ${repoName} \u00b7 ${run.base_branch || ""} \u00b7 started ${created.text} \u00b7 updated ${updated.text}`);
setAttr($("run-meta"), "title", `${run.id}\n${run.repo || ""}\nstarted ${created.title}\nupdated ${updated.title}`);
const instructionEl = $("run-instruction");
if (instructionEl.dataset.forRun !== run.id) {
instructionEl.dataset.forRun = run.id;
renderMd(instructionEl, run.instruction_md);
}
renderAsks(run);
renderLand(run);
renderActive(run);
renderVerdict(run);
renderCandidates(run);
renderReviews(run);
renderQuota(run);
renderTimeline(run);
renderRunActions(run);
renderRunDelete(run);
setText($("run-report"), report === null ? "Loading\u2026" : report);
}
let armedRunDelete = null;
let armedRunDeleteFocused = null;
function runDeleteReason(run) {
if (run.live) {
return "This run is still in flight and cannot be deleted.";
}
if (unfolded(run)) {
return "Fold the candidate worktrees first \u2014 the button below does it.";
}
return null;
}
function unfolded(run) {
const candidates = Array.isArray(run.candidates) ? run.candidates : [];
return candidates.some((c) => !c.folded);
}
let armedFold = null;
let armedFoldFocused = null;
let foldBusy = null;
let resumeBusy = null;
function renderRunActions(run) {
const box = $("run-actions-box");
if (!box) return;
clear(box);
const status = String(run.status || "");
if (["stalled", "blocked"].includes(status)) {
const busy = resumeBusy === run.id;
const gone = !unfolded(run);
box.append(
el("div", { class: "stakes-confirm" },
el("button", {
class: "btn",
type: "button",
text: busy ? "Resuming\u2026" : "Resume this run",
disabled: busy || gone,
onclick: () => resumeRun(run.id),
}),
el("p", { class: "card-note", text: gone
? "The candidate worktrees are gone, so there is nothing left to continue from. File the task again instead."
: "Carries on from where it stopped, re-asking only the seats that went missing. It spends agent calls." }),
),
);
}
if (!unfolded(run)) return;
if (armedFold === run.id) {
const cancel = el("button", {
class: "btn btn-quiet",
type: "button",
text: "Cancel",
onclick: () => { armedFold = null; renderRunActions(run); },
});
box.append(
el("div", { class: "stakes-confirm" },
el("p", { class: "stakes-warn", text: "Folding removes this run's worktrees and branches. Anything not committed goes with them, and the run can no longer be resumed." }),
el("div", { class: "stakes-row" },
cancel,
el("button", {
class: "btn btn-quiet",
type: "button",
text: "Yes, fold worktrees",
onclick: () => foldRun(run.id),
}),
),
),
);
if (armedFoldFocused !== run.id) {
armedFoldFocused = run.id;
requestAnimationFrame(() => cancel.focus({ preventScroll: true }));
}
} else {
armedFoldFocused = null;
box.append(
el("div", { class: "stakes-confirm" },
el("button", {
class: "btn btn-quiet",
type: "button",
text: foldBusy === run.id ? "Folding\u2026" : "Fold worktrees\u2026",
disabled: foldBusy === run.id,
onclick: () => { armedFold = run.id; renderRunActions(run); },
}),
el("p", { class: "card-note", text: "Frees the disk this run is holding, and is what the delete button is waiting for." }),
),
);
}
}
async function foldRun(id) {
armedFold = null;
foldBusy = id;
try {
const out = await postJson(API.foldRun(id));
ok();
const n = Number(out.removed_count || 0);
announce(n > 0
? `Folded ${shortId(id)}: ${n} worktree${n === 1 ? "" : "s"} and branches removed.`
: `Run ${shortId(id)} had nothing left to fold.`);
closeRunActions();
await loadRun(id);
} catch (error) {
closeRunActions();
fail(`Could not fold run ${shortId(id)}: ${error.message}`);
} finally {
foldBusy = null;
}
}
async function resumeRun(id) {
resumeBusy = id;
try {
await postJson(API.resumeRun(id));
ok();
announce(`Run ${shortId(id)} is being resumed. The card will follow it.`);
closeRunActions();
await loadRun(id);
} catch (error) {
closeRunActions();
fail(`Could not resume run ${shortId(id)}: ${error.message}`);
} finally {
resumeBusy = null;
}
}
function renderRunDelete(run) {
const box = $("run-delete-box");
if (!box) return;
clear(box);
const reason = runDeleteReason(run);
if (reason) {
const disabledBtn = el("button", {
class: "btn btn-quiet",
type: "button",
text: "Delete run\u2026",
disabled: true,
});
box.append(
el("div", { class: "stakes-confirm" },
disabledBtn,
el("p", { class: "card-note", text: reason }),
),
);
return;
}
const armed = armedRunDelete === run.id;
if (!armed) armedRunDeleteFocused = null;
if (armed) {
const cancel = el("button", {
class: "btn btn-quiet",
type: "button",
text: "Cancel",
onclick: () => {
armedRunDelete = null;
renderRunDelete(run);
},
});
const confirm = el("button", {
class: "btn btn-quiet",
type: "button",
text: "Yes, delete run now",
onclick: () => deleteRun(run.id),
});
box.append(
el("div", { class: "stakes-confirm" },
el("p", { class: "stakes-warn", text: "Deleting removes all recorded state and artifacts. This cannot be undone." }),
el("div", { class: "stakes-row" },
cancel,
confirm,
),
),
);
if (armedRunDeleteFocused !== run.id) {
armedRunDeleteFocused = run.id;
requestAnimationFrame(() => cancel.focus({ preventScroll: true }));
}
} else {
box.append(
el("button", {
class: "btn btn-quiet",
type: "button",
text: "Delete run\u2026",
onclick: () => {
armedRunDelete = run.id;
renderRunDelete(run);
},
}),
);
}
}
async function deleteRun(id) {
try {
await deleteReq(API.deleteRun(id));
ok();
announce(`Run ${shortId(id)} removed.`);
armedRunDelete = null;
closeRunActions();
location.hash = "#/runs";
} catch (error) {
armedRunDelete = null;
closeRunActions();
fail(`Could not delete run ${shortId(id)}: ${error.message}`);
}
}
function renderAsks(run) {
const mine = (state.questions || []).filter((q) => q.run === run.id);
show($("run-ask-panel"), mine.length > 0);
if (mine.length === 0) return;
const open = mine.filter((q) => q.status === "open").length;
setText($("run-ask-title"), open > 0 ? "Waiting on you" : "Decisions");
setText($("run-ask-count"), open > 0 ? `${open} open` : plural(mine.length, "on record", "on record"));
syncList($("run-asks"), sortQuestions(mine), (q) => q.id, createAskCard,
(row, q) => updateAskCard(row, q, { compact: true }));
}
function renderLand(run) {
const pr = landOf(run);
show($("run-land-panel"), Boolean(pr));
if (!pr) return;
const box = $("run-land");
clear(box);
const prHref = forgeUrl(pr.url);
box.append(
el("div", { class: "land-top" },
prHref
? el("a", {
class: "pr-link", href: prHref, title: prHref,
target: "_blank", rel: "noopener noreferrer",
text: `PR #${pr.number}`,
})
: el("span", { class: "ask-seat", text: `PR #${pr.number}` }),
el("span", { class: "tag", "data-tone": PR_TONE[pr.state] || "ink", text: pr.state || "unknown" }),
checksChip(pr),
),
Number(pr.rounds) ? el("p", { class: "land-note", text: `Land round ${Number(pr.round) || 0} of ${pr.rounds}.` }) : null,
roundRail(pr),
el("p", { class: "land-note", text: landNote(pr) }),
);
}
function firstLine(text) {
if (typeof text !== "string") return "";
for (const line of text.split("\n")) {
const trimmed = line.trim();
if (trimmed) return trimmed.length > 96 ? `${trimmed.slice(0, 95)}\u2026` : trimmed;
}
return "";
}
function renderVerdict(run) {
const tally = run.tally;
const panel = $("run-verdict");
const candidates = Array.isArray(run.candidates) ? run.candidates : [];
show(panel, Boolean(tally) || candidates.length > 0);
if (!tally && candidates.length === 0) return;
const converge = $("converge");
clear(converge);
const decided = Boolean(tally && tally.met_quorum);
converge.append(convergeDiagram(candidates, tally ? tally.winner : null, decided));
const facts = $("tally-facts");
clear(facts);
if (!tally) {
facts.append(
el("dt", { text: "Verdict" }),
el("dd", { text: "Not reached yet." }),
);
return;
}
const first = tally.first_choice || {};
const votes = Object.keys(first)
.sort()
.map((label) => `${label}: ${first[label]}`)
.join(" \u00b7 ");
const rows = [
["Winner", tally.winner
? `Candidate ${tally.winner}${decided ? "" : " \u2014 provisional only"}`
: "\u2014"],
];
if (tally.uncontested) {
rows.push(["Judging", `Not needed \u2014 ${tally.uncontested}`]);
} else {
rows.push(
["First choices", votes || "\u2014"],
["Panel", `${Number(tally.present) || 0} of ${Number(tally.judges) || 0} present, quorum ${Number(tally.quorum) || 0}`],
["Quorum", tally.met_quorum ? "Met" : "NOT MET \u2014 the verdict is not trustworthy"],
["Agreement", tally.unanimous_final
? "Unanimous final vote"
: `Split; ${plural(Number(tally.changed_votes) || 0, "judge", "judges")} moved`],
["Deliberated", tally.deliberated ? "Yes" : "No"],
);
if (tally.tie_break) rows.push(["Tie break", tally.tie_break]);
}
for (const [term, value] of rows) {
facts.append(el("dt", { text: term }), el("dd", { text: value }));
}
}
function convergeDiagram(candidates, winner, decided) {
const width = 320;
const height = 132;
const midX = width / 2;
const knot = 96;
const count = Math.max(candidates.length, 1);
const labels = candidates.map((c) => c.label).filter(Boolean).join(", ");
const root = svg("svg", {
viewBox: `0 0 ${width} ${height}`,
role: "img",
"aria-label": candidates.length
? `${plural(candidates.length, "candidate", "candidates")} ${labels}${winner && decided ? `; ${winner} won` : winner ? `; ${winner} leads but the panel reached no quorum` : "; no verdict yet"}`
: "No candidates yet",
});
const span = Math.min(96, (width - 68) / Math.max(count - 1, 1));
const xs = candidates.map((_, i) => midX + (i - (count - 1) / 2) * span);
candidates.forEach((candidate, i) => {
const x = xs[i];
const won = winner && candidate.label === winner && decided;
const dead = !viable(candidate);
const tone = candTone(i);
const path = x === midX
? `M ${x} 44 L ${x} ${knot}`
: `M ${x} 44 C ${x} ${knot - 22}, ${(x + midX) / 2} ${knot - 8}, ${midX} ${knot}`;
root.append(svg("path", {
d: path,
fill: "none",
stroke: tone,
"stroke-width": won ? 5 : 2.5,
"stroke-linecap": "round",
"stroke-dasharray": dead ? "3 5" : null,
opacity: won ? 1 : dead ? 0.35 : 0.55,
}));
root.append(svg("circle", {
cx: x, cy: 26, r: 13,
fill: dead ? "var(--sunk)" : tone,
stroke: tone,
"stroke-width": 2,
"stroke-dasharray": dead ? "3 3" : null,
}));
root.append(svg("text", {
x, y: 31,
"text-anchor": "middle",
fill: dead ? tone : "var(--surface)",
text: candidate.label || "?",
}));
});
if (winner && decided) {
root.append(svg("rect", {
x: midX - 11, y: knot - 11, width: 22, height: 22,
transform: `rotate(45 ${midX} ${knot})`,
fill: "var(--gold-line)",
}));
root.append(svg("path", {
d: `M ${midX} ${knot + 16} L ${midX} ${height - 8}`,
stroke: "var(--gold-line)", "stroke-width": 5, "stroke-linecap": "round",
}));
} else {
root.append(svg("rect", {
x: midX - 10, y: knot - 10, width: 20, height: 20,
transform: `rotate(45 ${midX} ${knot})`,
fill: "none", stroke: "var(--line-2)", "stroke-width": 2, "stroke-dasharray": "3 3",
}));
}
return root;
}
function renderCandidates(run) {
const candidates = Array.isArray(run.candidates) ? run.candidates : [];
show($("run-cands-panel"), candidates.length > 0);
if (candidates.length === 0) return;
const winner = run.tally ? run.tally.winner : null;
const decided = Boolean(run.tally && run.tally.met_quorum);
setText($("cand-count"), `${candidates.filter(viable).length} viable of ${candidates.length}`);
const list = $("run-cands");
clear(list);
candidates.forEach((candidate, i) => {
const dead = !viable(candidate);
const facts = [];
if (candidate.commits) facts.push(plural(candidate.commits, "commit", "commits"));
if (candidate.files) facts.push(plural(candidate.files, "file", "files"));
const took = seconds(candidate.duration_ms);
if (took) facts.push(took);
if (candidate.branch) facts.push(candidate.branch);
list.append(el("li", {
class: "cand",
"data-winner": winner && candidate.label === winner && decided ? "1" : null,
style: `--cand-tone: ${candTone(i)}`,
},
el("div", { class: "cand-head" },
el("span", { class: "cand-label", text: candidate.label || "?" }),
el("span", { class: "cand-agent", text: candidate.agent || "" }),
winner && candidate.label === winner
? el("span", {
class: "crown",
"data-provisional": decided ? null : "1",
text: decided ? "winner" : "provisional",
})
: null,
),
facts.length ? numbers(facts) : null,
dead
? el("p", { class: "card-note", text: candidate.failed || "Produced no change at all." })
: null,
candidate.summary ? el("p", { class: "cand-summary", text: candidate.summary }) : null,
candidate.stat ? el("pre", { class: "stat", text: candidate.stat }) : null,
));
});
}
function voteTone(vote) {
switch (vote) {
case "approve": return "teal";
case "approve_with_findings": return "gold";
case "reject": return "rust";
default: return null;
}
}
function voteLabel(vote) {
switch (vote) {
case "approve": return "approve";
case "approve_with_findings": return "approve w/ findings";
case "reject": return "reject";
default: return String(vote || "");
}
}
function renderReviews(run) {
const rounds = Array.isArray(run.reviews) ? run.reviews : [];
const gate = Array.isArray(run.gate) ? run.gate : [];
show($("run-reviews-panel"), rounds.length > 0 || gate.length > 0);
if (rounds.length === 0 && gate.length === 0) return;
setText($("review-count"), rounds.length ? plural(rounds.length, "round", "rounds") : "gate only");
const list = $("run-reviews");
clear(list);
for (const round of rounds) {
const blocking = Number(round.blocking) || 0;
const records = Array.isArray(round.reviews) ? round.reviews : [];
const node = el("li", { class: "round" },
el("div", { class: "round-head" },
el("span", { class: "round-n", text: `Round ${round.round}` }),
round.clean
? el("span", { class: "tag", "data-tone": "teal", text: "clean" })
: el("span", { class: "tag", "data-tone": "rust", text: `${plural(blocking, "blocker", "blockers")}` }),
round.verify_retried
? el("span", { class: "tag", "data-tone": "gold", text: "verify retried" })
: null,
round.e2e_deferred
? el("span", { class: "tag", "data-tone": "gold", text: "e2e deferred" })
: null,
round.verdict
? el("span", { class: "tag", "data-tone": voteTone(round.verdict), text: `verdict: ${voteLabel(round.verdict)}` })
: null,
round.vote_split
? el("span", { class: "tag", "data-tone": "gold", text: "votes split" })
: null,
round.head ? el("span", { class: "head-sha", title: "reviewed HEAD", text: String(round.head).slice(0, 7) }) : null,
round.verified_head
? el("span", { class: "head-sha", title: "verified HEAD", text: `verified ${String(round.verified_head).slice(0, 7)}` })
: null,
),
);
for (const record of records) {
const findings = Array.isArray(record.findings) ? record.findings : [];
node.append(el("div", { class: "reviewer" },
el("p", {},
el("span", { class: "reviewer-name", text: `reviewer ${record.reviewer} \u00b7 ${record.agent || ""}` }),
record.vote
? el("span", { class: "tag", "data-tone": voteTone(record.vote), text: voteLabel(record.vote) })
: null,
),
record.failed ? el("p", { class: "card-note", text: record.failed }) : null,
record.summary ? el("p", { class: "cand-summary", text: record.summary }) : null,
findings.length ? el("div", { class: "findings" }, findings
.slice()
.sort((a, b) => (SEV_RANK[b.severity] || 0) - (SEV_RANK[a.severity] || 0))
.map((finding) => el("div", { class: "finding", "data-sev": finding.severity },
el("div", { class: "finding-top" },
el("span", { class: "finding-sev", text: finding.severity || "" }),
el("span", { class: "finding-title", text: finding.title || "" }),
finding.id ? el("span", { class: "finding-id", text: finding.id }) : null,
),
finding.file
? el("p", { class: "finding-where", text: `${finding.file}${finding.line ? `:${finding.line}` : ""}` })
: null,
finding.detail ? el("p", { class: "finding-detail", text: finding.detail }) : null,
))) : null,
));
}
const reconsideration = Array.isArray(round.reconsideration) ? round.reconsideration : [];
if (reconsideration.length) {
node.append(el("div", { class: "reviewer" },
el("p", {}, el("span", { class: "reviewer-name", text: "reconsideration" })),
el("div", { class: "findings" }, reconsideration.map((rv) =>
el("div", { class: "finding" },
el("div", { class: "finding-top" },
el("span", { class: "finding-id", text: `reviewer ${rv.reviewer}` }),
rv.vote
? el("span", { class: "tag", "data-tone": voteTone(rv.vote), text: voteLabel(rv.vote) })
: el("span", { class: "tag", "data-tone": "rust", text: "no revote" }),
),
rv.reason ? el("p", { class: "finding-detail", text: rv.reason }) : null,
rv.failed ? el("p", { class: "card-note", text: rv.failed }) : null,
))),
));
}
const e2e = Array.isArray(round.e2e) ? round.e2e : [];
if (e2e.length) {
node.append(commandList("Verification", e2e));
} else if (round.e2e_deferred) {
node.append(el("p", { class: "card-note", text: round.e2e_defer_reason
? `Verification deferred to the fixer: ${round.e2e_defer_reason}`
: "Verification deferred to the fixer" }));
}
if (round.fix) {
const fix = round.fix;
const addressed = Array.isArray(fix.addressed) ? fix.addressed : [];
const rejected = Array.isArray(fix.rejected) ? fix.rejected : [];
node.append(el("div", { class: "reviewer" },
el("p", {}, el("span", { class: "reviewer-name", text: `fix \u00b7 ${fix.agent || ""}` })),
fix.failed
? numbers([fix.committed ? "committed" : "no commit"])
: numbers([
`${addressed.length} addressed`,
`${rejected.length} declined`,
fix.committed ? "committed" : "no commit",
]),
fix.failed ? el("p", { class: "card-note", text: `adoption report lost: ${fix.failed}` }) : null,
fix.notes ? el("p", { class: "cand-summary", text: fix.notes }) : null,
rejected.length ? el("div", { class: "findings" }, rejected.map((r) =>
el("div", { class: "finding" },
el("div", { class: "finding-top" },
el("span", { class: "finding-id", text: r.id || "" }),
el("span", { class: "finding-title", text: "declined" }),
),
r.why ? el("p", { class: "finding-detail", text: r.why }) : null,
))) : null,
));
}
list.append(node);
}
if (gate.length) list.append(el("li", { class: "round" }, commandList("Gate", gate)));
}
function commandList(heading, commands) {
return el("div", { class: "reviewer" },
el("p", {}, el("span", { class: "reviewer-name", text: heading })),
el("div", { class: "findings" }, commands.map((command) => {
const passed = command.code === 0;
return el("div", { class: "finding", "data-sev": passed ? null : "blocker" },
el("div", { class: "finding-top" },
el("span", { class: "tag", "data-tone": passed ? "teal" : "rust", text: passed ? "pass" : "fail" }),
el("span", { class: "finding-where", text: command.command || "" }),
el("span", { class: "finding-id", text: command.code === null || command.code === undefined ? "timeout" : `exit ${command.code}` }),
),
!passed && command.output_tail
? el("pre", { class: "stat", text: command.output_tail })
: null,
);
})),
);
}
function activeNote(run) {
const active = run.active && typeof run.active === "object" ? run.active : {};
const seats = Object.keys(active).sort();
if (seats.length === 0) return null;
if (!run.live) return `${plural(seats.length, "seat", "seats")} left mid-answer by a dead process`;
return seats.length === 1
? `${seats[0]} has not answered yet`
: `${plural(seats.length, "seat", "seats")} have not answered yet (${seats.join(", ")})`;
}
function renderActive(run) {
const active = run.active && typeof run.active === "object" ? run.active : {};
const seats = Object.keys(active).sort();
show($("run-active-panel"), seats.length > 0);
if (seats.length === 0) return;
setText($("run-active-count"), String(seats.length));
const note = $("run-active-note");
show(note, !run.live);
if (!run.live) {
setText(note, "No live daemon claims this run right now — likely left behind by a killed process, not a seat that is actually still working.");
}
const list = $("run-active");
clear(list);
const now = Date.now();
for (const key of seats) {
const a = active[key];
const startedMs = Date.parse(a.started_at || "");
const elapsed = Number.isFinite(startedMs) ? Math.max(Math.round((now - startedMs) / 1000), 0) : null;
const budget = Number(a.timeout_secs) || 0;
const remaining = elapsed === null ? null : Math.max(budget - elapsed, 0);
const retry = Number(a.attempt) > 0 ? ` · retry ${a.attempt}` : "";
list.append(el("li", {},
el("span", { class: "seat", text: key }),
el("span", { text: `${a.node || "?"}${retry}` }),
el("span", {
text: elapsed === null
? "in progress"
: `${elapsed}s elapsed · ${remaining}s left of ${budget}s`,
}),
));
}
}
function tickActive() {
if (state.route.name === "run" && state.detail.run) renderActive(state.detail.run);
}
function renderQuota(run) {
const losses = Array.isArray(run.quota) ? run.quota : [];
show($("run-quota-panel"), losses.length > 0);
if (losses.length === 0) return;
const list = $("run-quota");
clear(list);
for (const loss of losses) {
list.append(el("li", {},
el("span", { class: "seat", text: loss.seat || "" }),
el("span", { text: `during ${loss.node || "?"}` }),
el("span", { text: clock(loss.at) }),
loss.reset ? el("span", { text: `resets ${loss.reset}` }) : null,
));
}
}
function renderTimeline(run) {
const events = Array.isArray(run.events) ? run.events : [];
show($("run-events-panel"), events.length > 0);
if (events.length === 0) return;
const list = $("run-events");
clear(list);
for (const event of events) {
list.append(el("li", {},
el("span", { class: "event-at", text: clock(event.at) }),
el("div", { class: "event-body" },
el("p", { class: "event-node", text: event.node || "" }),
el("p", { class: "event-msg", text: event.message || "" }),
),
));
}
}
async function loadRuns() {
try {
state.runs = await getJson(API.runs(RUN_LIMIT));
renderRuns();
ok();
} catch (error) {
fail(`Could not load runs: ${error.message}`);
}
}
async function loadQueue() {
try {
state.queue = await getJson(API.queue);
renderQueue();
ok();
} catch (error) {
fail(`Could not load the queue: ${error.message}`);
}
}
async function loadQuestions() {
try {
const list = await getJson(API.questions);
state.questions = sortQuestions(Array.isArray(list) ? list : []);
renderQuestions();
renderAskBar();
renderRuns();
if (state.route.name === "run" && state.detail.run) renderRunDetail();
ok();
} catch (error) {
fail(`Could not load questions: ${error.message}`);
}
}
function reportUnreachableDuringUpgrade(error) {
const upgradeInfo = state.health && state.health.upgrade;
if (!upgradeInfo || !UPGRADE_BUSY_STAGES.has(upgradeInfo.stage)) return false;
if (upgradeOverdue(upgradeInfo)) {
fail(`Cannot reach magi: ${error.message}. It was replacing itself with ${upgradeInfo.to || "a new release"} and has not come back in over an hour — check on it by hand.`);
return true;
}
setAttr($("daemon"), "data-state", "upgrading");
setAttr($("daemon"), "data-owned", null);
const why = $("loop-why");
setText(why, "The deck is restarting on the new build. This page reconnects on its own.");
show(why, true);
show($("loop-toggle"), false);
return true;
}
async function loadHealth({ applyRevisions = false } = {}) {
try {
state.health = await getJson(API.health);
if (state.health.loop) applyLoop(state.health.loop);
else renderLoop();
renderAskBar();
if (applyRevisions) await applyRevisions_(state.health);
ok();
} catch (error) {
if (!reportUnreachableDuringUpgrade(error)) fail(`Cannot reach magi: ${error.message}`);
}
}
async function loadRun(id) {
const fresh = state.detail.id !== id;
if (fresh) state.detail = { id, run: null, report: null };
renderRunDetail();
const [run, report] = await Promise.allSettled([getJson(API.run(id)), getText(API.report(id))]);
if (state.detail.id !== id) return;
if (run.status === "fulfilled") {
state.detail.run = run.value;
ok();
} else {
fail(`Could not load run ${shortId(id)}: ${run.reason.message}`);
}
state.detail.report = report.status === "fulfilled"
? report.value
: `The report could not be rendered: ${report.reason.message}`;
renderRunDetail();
}
async function applyRevisions_(source) {
const queueRev = source.queue_rev;
const runsRev = source.runs_rev;
const questionsRev = source.questions_rev;
const talksRev = source.talks_rev;
const jobs = [];
if (queueRev !== state.rev.queue) {
state.rev.queue = queueRev;
jobs.push(loadQueue());
}
if (runsRev !== state.rev.runs) {
state.rev.runs = runsRev;
jobs.push(loadRuns());
if (state.route.name === "run" && state.detail.id) jobs.push(loadRun(state.detail.id));
}
if (questionsRev !== state.rev.questions) {
state.rev.questions = questionsRev;
jobs.push(loadQuestions());
}
if (talksRev !== state.rev.talks) {
state.rev.talks = talksRev;
jobs.push(loadTalks());
if (state.route.name === "talk" && state.talkDetail.id) jobs.push(loadTalk(state.talkDetail.id));
}
if (source.loop_rev !== undefined && source.loop_rev !== state.rev.loop) {
state.rev.loop = source.loop_rev;
jobs.push(loadLoop());
}
if (jobs.length) {
const saidBefore = saidCount;
await Promise.allSettled(jobs);
if (saidCount === saidBefore) announce("Updated.");
}
}
function subscribe() {
let stream;
try {
stream = new EventSource(API.events);
} catch {
return;
}
stream.addEventListener("open", () => {
state.streamOpen = true;
ok();
});
stream.addEventListener("change", (message) => {
let payload;
try {
payload = JSON.parse(message.data);
} catch {
return;
}
state.streamOpen = true;
applyRevisions_(payload);
});
stream.addEventListener("error", () => {
state.streamOpen = false;
if (fallbackTimer) return;
fallbackTimer = setTimeout(() => {
fallbackTimer = null;
loadHealth({ applyRevisions: true });
}, 3000);
});
}
function parseRoute() {
const parts = location.hash.replace(/^#\/?/, "").split("/").filter(Boolean);
if (parts[0] === "queue") return { name: "queue", id: null };
if (parts[0] === "questions") return { name: "questions", id: null };
if (parts[0] === "chat" && parts[1]) return { name: "talk", id: decodeURIComponent(parts[1]) };
if (parts[0] === "chat") return { name: "talks", id: null };
if (parts[0] === "runs" && parts[1]) return { name: "run", id: decodeURIComponent(parts[1]) };
return { name: "runs", id: null };
}
function applyRoute() {
const route = parseRoute();
const changed = route.name !== state.route.name || route.id !== state.route.id;
state.route = route;
show($("view-runs"), route.name === "runs");
show($("view-run"), route.name === "run");
show($("view-queue"), route.name === "queue");
show($("view-questions"), route.name === "questions");
show($("view-talks"), route.name === "talks");
show($("view-talk"), route.name === "talk");
show($("run-actions-fab"), route.name === "run");
if (route.name !== "run") closeRunActions();
const section = route.name === "run" ? "runs"
: route.name === "talk" ? "talks"
: route.name;
for (const link of document.querySelectorAll("[data-nav]")) {
setAttr(link, "aria-current", link.dataset.nav === section ? "page" : null);
}
if (route.name === "run") {
if (state.detail.id !== route.id) loadRun(route.id);
} else {
state.detail = { id: null, run: null, report: null };
}
if (route.name === "talk") {
if (changed) state.openingTalk = true;
if (state.talkDetail.id !== route.id) {
resetTalkAttachments(route.id);
state.talkDetail = { id: route.id, talk: null };
loadTalk(route.id);
}
renderTalk();
} else if (state.talkDetail.id) {
resetTalkAttachments(null);
state.talkDetail = { id: null, talk: null };
}
if (changed) window.scrollTo({ top: 0 });
if (changed && route.name === "questions") focusFirstAsk();
renderTitle();
}
function openRunActions() {
const dialog = $("run-actions-sheet");
if (!dialog.open) dialog.showModal();
}
function closeRunActions() {
const dialog = $("run-actions-sheet");
if (dialog.open) dialog.close();
}
let editingTaskId = null;
function openTaskEdit(task) {
editingTaskId = task.id;
$("task-edit-title").value = task.title || "";
$("task-edit-instruction").value = task.instruction || "";
show($("task-edit-error"), false);
setText($("task-edit-error"), "");
const dialog = $("task-edit-sheet");
if (!dialog.open) dialog.showModal();
requestAnimationFrame(() => $("task-edit-title").focus({ preventScroll: true }));
}
function closeTaskEdit() {
const dialog = $("task-edit-sheet");
if (dialog.open) dialog.close();
editingTaskId = null;
}
async function saveTaskEdit() {
if (!editingTaskId) return;
const id = editingTaskId;
const title = $("task-edit-title").value.trim();
const instruction = $("task-edit-instruction").value;
if (!title || !instruction.trim()) {
setText($("task-edit-error"), "Give both a title and an instruction.");
show($("task-edit-error"), true);
return;
}
const button = $("task-edit-save");
const label = button.textContent;
button.disabled = true;
setText(button, "Saving…");
try {
await postJson(API.editTask(id), { title, instruction });
ok();
announce(`Task ${shortId(id)} edited.`);
closeTaskEdit();
await loadQueue();
} catch (error) {
setText($("task-edit-error"), error.message);
show($("task-edit-error"), true);
} finally {
button.disabled = false;
setText(button, label);
}
}
const THEMES = ["auto", "light", "dark"];
const THEME_LABEL = {
auto: "Colour theme: follow system",
light: "Colour theme: light",
dark: "Colour theme: dark",
};
function currentTheme() {
const value = document.documentElement.dataset.theme;
return THEMES.includes(value) ? value : "auto";
}
function applyTheme(theme) {
if (theme === "auto") delete document.documentElement.dataset.theme;
else document.documentElement.dataset.theme = theme;
setAttr($("theme-toggle"), "aria-label", THEME_LABEL[theme]);
setAttr($("theme-toggle"), "title", THEME_LABEL[theme]);
try {
if (theme === "auto") localStorage.removeItem("magi-theme");
else localStorage.setItem("magi-theme", theme);
} catch {
}
}
function wireAttachments({ fileInput, say, turns, box, attach }) {
const input = $(fileInput);
input.addEventListener("change", () => {
if (input.files && input.files.length) attach(input.files);
input.value = "";
});
$(box).addEventListener("paste", (event) => {
const items = event.clipboardData && event.clipboardData.items;
if (!items) return;
const files = [...items]
.filter((item) => item.kind === "file")
.map((item) => item.getAsFile())
.filter(Boolean);
if (files.length === 0) return;
event.preventDefault();
attach(files);
});
for (const id of [say, turns]) {
const zone = $(id);
zone.addEventListener("dragover", (event) => {
if (!event.dataTransfer || ![...event.dataTransfer.types].includes("Files")) return;
event.preventDefault();
zone.classList.add("is-drop-target");
});
zone.addEventListener("dragleave", (event) => {
if (event.relatedTarget && zone.contains(event.relatedTarget)) return;
zone.classList.remove("is-drop-target");
});
zone.addEventListener("drop", (event) => {
zone.classList.remove("is-drop-target");
if (!event.dataTransfer || event.dataTransfer.files.length === 0) return;
event.preventDefault();
attach(event.dataTransfer.files);
});
}
}
function wire() {
$("run-actions-fab").addEventListener("click", openRunActions);
$("run-actions-close").addEventListener("click", closeRunActions);
$("run-actions-sheet").addEventListener("click", (event) => {
if (event.target === event.currentTarget) closeRunActions();
});
$("run-actions-sheet").addEventListener("close", () => {
$("run-actions-fab").focus({ preventScroll: true });
});
$("task-edit-close").addEventListener("click", closeTaskEdit);
$("task-edit-save").addEventListener("click", saveTaskEdit);
$("task-edit-sheet").addEventListener("click", (event) => {
if (event.target === event.currentTarget) closeTaskEdit();
});
$("task-edit-sheet").addEventListener("close", () => {
editingTaskId = null;
});
$("runs-filter-clear").addEventListener("click", clearRunsFilter);
$("theme-toggle").addEventListener("click", () => {
const next = THEMES[(THEMES.indexOf(currentTheme()) + 1) % THEMES.length];
applyTheme(next);
});
$("wrap-toggle").addEventListener("click", (event) => {
state.wrap = !state.wrap;
event.currentTarget.setAttribute("aria-pressed", String(state.wrap));
$("run-report").dataset.wrap = state.wrap ? "1" : "0";
});
$("alert-retry").addEventListener("click", () => {
ok();
loadHealth({ applyRevisions: true });
loadQuestions();
if (state.route.name === "run" && state.detail.id) loadRun(state.detail.id);
if (state.route.name === "talk" && state.talkDetail.id) loadTalk(state.talkDetail.id);
});
$("talk-start-go").addEventListener("click", startTalk);
$("talk-say").addEventListener("submit", sendTalkTurn);
$("talk-close-go").addEventListener("click", closeTalk);
$("talk-reopen-go").addEventListener("click", reopenTalk);
$("talk-tasks-panel").addEventListener("toggle", () => {
const panel = $("talk-tasks-panel");
const talkId = panel.dataset.talkId;
if (!talkId) return;
const collapsed = loadCollapsed(TALK_TASKS_STORAGE_KEY);
collapsed[talkId] = panel.open;
saveCollapsed(TALK_TASKS_STORAGE_KEY, collapsed);
});
wireAttachments({
fileInput: "talk-file-input", say: "talk-say", turns: "talk-turns", box: "f-talk-say",
attach: attachTalkFiles,
});
$("f-talk-say").addEventListener("keydown", (event) => {
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
$("talk-say").requestSubmit();
}
});
$("panel-full-close").addEventListener("click", closePanel);
$("panel-full").addEventListener("close", () => clear($("panel-full-body")));
$("attachment-view-close").addEventListener("click", closeAttachmentView);
$("attachment-view").addEventListener("click", (event) => {
if (event.target === event.currentTarget) closeAttachmentView();
});
$("attachment-view").addEventListener("close", () => {
$("attachment-view-img").src = "";
});
window.addEventListener("hashchange", applyRoute);
document.addEventListener("visibilitychange", () => {
if (!document.hidden) loadHealth({ applyRevisions: true });
});
}
async function boot() {
applyTheme(currentTheme());
wire();
applyRoute();
await loadHealth();
if (state.health) {
state.rev.queue = state.health.queue_rev;
state.rev.runs = state.health.runs_rev;
state.rev.questions = state.health.questions_rev;
state.rev.talks = state.health.talks_rev;
state.rev.loop = state.health.loop_rev;
if (state.health.loop) state.loop = state.health.loop;
}
await Promise.allSettled([
loadRuns(), loadQueue(), loadQuestions(), loadTalks(),
]);
subscribe();
setInterval(() => {
if (document.hidden) return;
loadHealth({ applyRevisions: !state.streamOpen });
}, HEALTH_MS);
setInterval(tickActive, 1000);
}
boot();