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`,
questions: "/api/questions",
answer: (id) => `/api/questions/${encodeURIComponent(id)}/answer`,
panel: (id) => `/api/questions/${encodeURIComponent(id)}/panel/index.html`,
chats: "/api/chats",
chat: (id) => `/api/chats/${encodeURIComponent(id)}`,
say: (id) => `/api/chats/${encodeURIComponent(id)}/say`,
file: (id) => `/api/chats/${encodeURIComponent(id)}/file`,
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 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 CHAT_STATUS = {
open: { glyph: "\u25cc", tone: "blue" },
filed: { glyph: "\u25c6", tone: "gold" },
abandoned: { 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) {
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}`,
});
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 state = {
route: { name: "runs", id: null },
health: null,
loop: null,
stopAskedAt: 0,
runs: null,
queue: null,
detail: { id: null, run: null, report: null },
questions: null,
chats: null,
repos: null,
chatDetail: { id: null, chat: null },
chatBusy: null,
busyTurns: 0,
openingChat: false,
planFocus: false,
prevTurnCount: 0,
pending: null,
waitFrom: 0,
waitTimer: null,
chatProblems: { id: null, list: [] },
panelOk: new Map(),
rev: { queue: null, runs: null, questions: null, chats: null, loop: null },
streamOpen: false,
wrap: false,
draftRaw: false,
};
let fallbackTimer = null;
async function request(url, init) {
const res = await fetch(url, init);
if (!res.ok) {
let message = `${res.status} ${res.statusText || "request failed"}`;
let problems = null;
try {
const body = await res.json();
if (body && typeof body.error === "string") message = body.error;
if (body && Array.isArray(body.problems) && body.problems.length) problems = body.problems;
} catch {
}
const error = new Error(message);
error.status = res.status;
if (problems) error.problems = problems;
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" });
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.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 renderLoop() {
const box = $("daemon");
const text = box.querySelector(".daemon-text");
const why = $("loop-why");
const button = $("loop-toggle");
const quiet = (note) => {
setText(why, note || "");
show(why, Boolean(note));
show(button, false);
button.onclick = null;
};
const park = $("loop-park");
show(park, false);
park.disabled = false;
const upgradeBtn = $("loop-upgrade");
show(upgradeBtn, false);
const control = (kind, label, note) => {
setText(why, note);
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");
quiet(null);
return;
}
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);
show(upgradeBtn, !foreign);
if (!foreign && upgradeBtn.dataset.armed !== "yes") {
setText(upgradeBtn, "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.run) {
setAttr(box, "data-state", "working");
text.append(
el("b", { text: "Working" }),
" on ",
currentRunLink(daemon),
daemon.current.task
? el("span", { class: "daemon-run", text: ` \u2190 task ${shortId(daemon.current.task)}` })
: 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.run
? "It finishes the run 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);
}
function renderRuns() {
const list = $("runs-list");
const runs = state.runs;
if (runs === null) {
setText($("runs-count"), "Loading\u2026");
if (!list.dataset.skeleton) {
clear(list);
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%" }),
));
}
list.dataset.skeleton = "1";
}
return;
}
if (list.dataset.skeleton) {
clear(list);
delete list.dataset.skeleton;
}
const moving = runs.filter((r) => !r.done).length;
const unreadable = Number(state.health && state.health.runs_unreadable) || 0;
const unreadableNote = unreadable
? `${unreadable} unreadable`
: "";
const counts = runs.length === 0
? (unreadable ? `no readable runs, ${unreadableNote}` : "Nothing has run yet")
: [`${plural(runs.length, "run", "runs")}, ${moving} in flight`, unreadableNote]
.filter(Boolean)
.join(", ");
setText($("runs-count"), counts);
show($("runs-empty"), runs.length === 0 && unreadable === 0);
show($("runs-unreadable"), runs.length === 0 && unreadable > 0);
syncList(list, runs, (r) => r.id, createRunCard, updateRunCard);
}
function createTaskCard() {
const chipSlot = el("span");
const priority = el("span", { class: "tag", "data-tone": "ink" });
const whenSlot = el("time", { class: "card-when" });
const title = el("h2", { class: "card-title" });
const source = el("span");
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 hold = el("button", { class: "btn btn-quiet", type: "button" });
const deleteBox = el("span", { class: "task-delete-box" });
const actions = el("div", { class: "card-actions" }, runLink, hold, deleteBox);
const card = el("li", { class: "card" },
el("div", { class: "card-top" }, chipSlot, priority, whenSlot),
title, meta, note, error, instruction, actions,
);
card.refs = { card, chipSlot, priority, whenSlot, title, source, repo, attempts, outcome, note, error, instruction, runLink, hold, 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);
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 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);
setText(r.note, meta.note || "");
show(r.note, Boolean(meta.note));
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 held = status === "held";
setText(r.hold, held ? "Release" : "Hold");
setAttr(r.hold, "aria-label", `${held ? "Release" : "Hold"} task ${task.title || task.id}`);
r.hold.disabled = status === "running" || status === "done";
r.hold.onclick = () => mutateTask(task.id, held ? "release" : "hold", r.hold);
show(r.hold, status !== "done");
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(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);
}
}
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}`);
}
}
function renderQueue() {
const list = $("queue-list");
const tasks = state.queue;
if (tasks === null) {
setText($("queue-count"), "Loading\u2026");
return;
}
const waiting = tasks.filter((t) => (t.status_str || t.status) === "queued").length;
const held = tasks.filter((t) => (t.status_str || t.status) === "held").length;
const parts = [`${plural(tasks.length, "task", "tasks")}`];
if (waiting) parts.push(`${waiting} runnable`);
if (held) parts.push(`${held} held`);
setText($("queue-count"), tasks.length === 0 ? "Nothing waiting" : parts.join(", "));
show($("queue-empty"), tasks.length === 0);
syncList(list, tasks, (t) => t.id, createTaskCard, updateTaskCard);
renderLoop();
}
const openQuestions = () => (state.questions || []).filter((q) => q.status === "open");
const openFor = (runId) => openQuestions().filter((q) => q.run === runId);
function openCount() {
return state.questions === null
? Number(state.health && state.health.questions_open) || 0
: openQuestions().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 row = el("li", { class: "ask" },
band,
el("div", { class: "ask-top" }, chipSlot, whenSlot),
summary, where, panelBox, detail, hint, stakes, choices, free, 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 };
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 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
? ""
: 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);
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 !== "");
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;
}
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 = openCount();
const open = openQuestions();
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 = openCount();
const base = state.route.name === "queue"
? "Backlog \u2014 magi"
: state.route.name === "questions"
? "Questions \u2014 magi"
: state.route.name === "chats"
? "Planning \u2014 magi"
: state.route.name === "chat"
? `Planning ${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 chatTurns = (chat) => (chat && Array.isArray(chat.turns) ? chat.turns : []);
const chatTurnsMd = (chat) => (chat && Array.isArray(chat.turn_bodies_md) ? chat.turn_bodies_md : []);
function chatOpener(chat) {
const first = chatTurns(chat).find((turn) => turn.who === "operator");
return first ? String(first.body || "") : "";
}
const chatDraft = (chat) => (chat && typeof chat.draft === "string" ? chat.draft : "");
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 sortChats(list) {
return list.slice().sort((a, b) => {
const rank = (a.status === "open" ? 0 : 1) - (b.status === "open" ? 0 : 1);
const started = (chat) => Date.parse(chat.created_at) || 0;
return rank || started(b) - started(a);
});
}
function createChatCard() {
const chipSlot = el("span");
const ready = el("span", { class: "tag chat-ready", "data-tone": "gold", text: "draft ready" });
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 task = el("span", { class: "win" });
const meta = el("div", { class: "card-meta" }, agent, turns, task);
const last = el("p", { class: "card-event" });
const card = el("a", { class: "card" },
el("div", { class: "card-top" }, chipSlot, ready, whenSlot),
title, meta, last,
);
const row = el("li", {}, card);
row.refs = { card, chipSlot, ready, whenSlot, title, agent, turns, task, last };
return row;
}
function updateChatCard(row, chat) {
const r = row.refs;
const status = String(chat.status || "open");
const turns = chatTurns(chat);
const tone = toneOf(status, CHAT_STATUS);
r.card.setAttribute("href", `#/plan/${chat.id}`);
setAttr(r.card, "data-tone", tone);
setAttr(row, "data-tone", tone);
const next = chip(status, CHAT_STATUS);
if (r.chipSlot.firstChild) r.chipSlot.firstChild.replaceWith(next);
else r.chipSlot.append(next);
show(r.ready, status === "open" && chatDraft(chat).trim() !== "");
const at = when(chat.updated_at || chat.created_at);
setText(r.whenSlot, at.text);
setAttr(r.whenSlot, "datetime", chat.updated_at || chat.created_at);
setAttr(r.whenSlot, "title", `updated ${at.title}`);
setText(r.title, firstLine(chatOpener(chat)) || `conversation ${shortId(chat.id)}`);
setText(r.agent, chat.agent || "");
show(r.agent, Boolean(chat.agent));
setText(r.turns, plural(turns.length, "turn", "turns"));
setText(r.task, chat.task ? `task ${shortId(chat.task)}` : "");
show(r.task, Boolean(chat.task));
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 renderChats() {
const list = $("chats-list");
const chats = state.chats;
if (chats === null) {
const open = Number(state.health && state.health.chats_open) || 0;
setText($("chats-count"), open ? `${plural(open, "conversation open", "conversations open")}` : "Loading\u2026");
return;
}
const open = chats.filter((c) => c.status === "open").length;
const filed = chats.filter((c) => c.status === "filed").length;
setText($("chats-count"), chats.length === 0
? "No interviews yet"
: [open ? `${plural(open, "conversation open", "conversations open")}` : "nothing open",
filed ? `${plural(filed, "task filed from here", "tasks filed from here")}` : null]
.filter(Boolean).join(" \u00b7 "));
show($("chats-empty"), chats.length === 0);
syncList(list, sortChats(chats), (c) => c.id, createChatCard, updateChatCard);
}
function createTurnRow() {
const who = el("span", { class: "turn-who" });
const body = el("div", { class: "turn-body" });
const at = el("time", { class: "turn-at" });
const row = el("li", { class: "turn" }, who, body, at);
row.refs = { who, body, at };
return row;
}
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 at = when(turn.at);
setText(r.at, at.text);
setAttr(r.at, "datetime", turn.at || null);
setAttr(r.at, "title", at.title);
}
function renderProblems(problems) {
const box = $("chat-problems");
clear(box);
if (!problems || problems.length === 0) {
show(box, false);
return;
}
box.append(
el("h3", { text: `Not fileable yet \u2014 ${plural(problems.length, "problem", "problems")}` }),
el("ul", {}, problems.map((problem) => el("li", { text: String(problem) }))),
);
show(box, true);
}
function chatError(message) {
const box = $("chat-error");
setText(box, message || "");
show(box, Boolean(message));
}
function applyDraftView() {
const raw = state.draftRaw;
show($("chat-draft-rendered"), !raw);
show($("chat-draft"), raw);
setText($("chat-draft-raw-toggle"), raw ? "Show formatted" : "Show raw");
setAttr($("chat-draft-raw-toggle"), "aria-pressed", String(raw));
}
function scrollToLastTurn() {
const turns = $("chat-turns");
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" });
}
function renderChat() {
const chat = state.chatDetail.chat;
const busy = state.chatBusy !== null && state.chatBusy === state.chatDetail.id;
if (!chat) {
setText($("chat-h"), "Loading conversation\u2026");
setText($("chat-meta"), "");
clear($("chat-status"));
clear($("chat-turns"));
show($("chat-draft-panel"), false);
show($("chat-filed-panel"), false);
show($("chat-say"), false);
show($("chat-closed"), false);
show($("chat-wait"), false);
show($("chat-problems"), false);
show($("chat-derived-from"), false);
return;
}
const status = String(chat.status || "open");
const pending = busy && state.pending && state.pending.id === chat.id
&& chatTurns(chat).length <= state.busyTurns
? [{ who: "operator", body: state.pending.body, at: state.pending.at }]
: [];
const turns = [...chatTurns(chat), ...pending];
const head = $("chat-status");
clear(head);
head.append(chip(status, CHAT_STATUS));
setText($("chat-h"), firstLine(chatOpener(chat)) || `Conversation ${shortId(chat.id)}`);
const started = when(chat.created_at);
setText($("chat-meta"),
`${shortId(chat.id)} \u00b7 ${chat.agent || "agent"} \u00b7 ${plural(turns.length, "turn", "turns")} \u00b7 started ${started.text}`);
setAttr($("chat-meta"), "title", `${chat.id}\nstarted ${started.title}`);
show($("chat-derived-from"), Boolean(chat.from));
if (chat.from) {
const link = $("chat-derived-from-link");
setText(link, shortId(chat.from));
setAttr(link, "href", `#/plan/${chat.from}`);
}
const turnsMd = chatTurnsMd(chat);
syncList($("chat-turns"), turns.map((turn, i) => ({ turn, md: turnsMd[i], key: String(i) })),
(item) => item.key, createTurnRow, updateTurnRow);
const turnCount = turns.length;
const lastIsPending = busy && state.pending && state.pending.id === chat.id
&& turns.length > 0 && turns[turns.length - 1].who === "operator"
&& turns[turns.length - 1].body === state.pending.body;
if (state.openingChat) {
state.openingChat = false;
if (turnCount > 0) requestAnimationFrame(scrollToLastTurn);
} else if (turnCount > state.prevTurnCount && !lastIsPending) {
requestAnimationFrame(scrollToLastTurn);
}
state.prevTurnCount = turnCount;
const draft = chatDraft(chat);
const hasDraft = draft.trim() !== "";
show($("chat-draft-panel"), hasDraft);
if (hasDraft) {
setText($("chat-draft"), draft);
if ($("chat-draft-rendered").dataset.forDraft !== draft) {
$("chat-draft-rendered").dataset.forDraft = draft;
renderMd($("chat-draft-rendered"), chat.draft_md);
}
}
applyDraftView();
setText($("chat-draft-tag"), status === "filed" ? "filed" : "draft");
setAttr($("chat-draft-tag"), "data-tone", status === "filed" ? "teal" : "gold");
show($("chat-file"), hasDraft && status === "open");
renderProblems(state.chatProblems.id === chat.id ? state.chatProblems.list : []);
show($("chat-filed-panel"), status === "filed");
setText($("chat-filed-note"), chat.task
? `Filed as task ${shortId(chat.task)}. It is in the backlog now, and the loop claims it in priority order.`
: "Filed into the backlog.");
const canSay = status === "open";
show($("chat-say"), canSay);
show($("chat-closed"), !canSay);
$("f-say").disabled = busy;
$("chat-send").disabled = busy;
setText($("chat-send"), busy ? "Thinking\u2026" : "Send");
show($("chat-wait"), busy);
}
function tickWait() {
const box = $("chat-wait");
if (state.chatBusy === null) {
show(box, false);
return;
}
const secs = Math.max(Math.round((Date.now() - state.waitFrom) / 1000), 0);
setText(box.querySelector(".waiting-text"), secs >= 90
? "The agent is still thinking. Long, but not stuck \u2014 it is allowed to take its time, and the reply will appear here."
: "The agent is thinking about your message. A turn usually takes under a minute.");
setText(box.querySelector(".waiting-secs"), `${secs}s`);
show(box, state.chatBusy === state.chatDetail.id);
if (secs > 0 && secs % 10 === 0) loadChat(state.chatBusy);
}
function beginTurn(id, before) {
state.chatBusy = id;
state.busyTurns = before;
state.waitFrom = Date.now();
tickWait();
if (!state.waitTimer) state.waitTimer = setInterval(tickWait, 1000);
}
function endTurn(id) {
if (state.chatBusy !== id) return;
state.chatBusy = null;
state.pending = null;
if (state.waitTimer) {
clearInterval(state.waitTimer);
state.waitTimer = null;
}
show($("chat-wait"), false);
}
function renderRepoOptions(select) {
const current = select.value;
clear(select);
select.append(el("option", { value: "", text: select.dataset.placeholder || "" }));
for (const repo of state.repos || []) {
select.append(el("option", { value: repo.path, text: repo.name }));
}
if ([...select.options].some((option) => option.value === current)) select.value = current;
}
async function loadRepos(refresh) {
try {
state.repos = await getJson(refresh ? API.reposRefresh : API.repos);
} catch {
state.repos = state.repos || [];
}
renderRepoOptions($("chat-start-repo-select"));
renderRepoOptions($("chat-derive-repo-select"));
}
async function loadChats() {
try {
const list = await getJson(API.chats);
state.chats = Array.isArray(list) ? list : [];
renderChats();
ok();
} catch (error) {
fail(`Could not load conversations: ${error.message}`);
}
}
async function loadChat(id) {
try {
const chat = await getJson(API.chat(id));
if (state.chatBusy === id && chatTurns(chat).length >= state.busyTurns + 2) endTurn(id);
if (state.chatDetail.id !== id) return;
state.chatDetail.chat = chat;
renderChat();
ok();
} catch (error) {
if (state.chatDetail.id === id) {
fail(`Could not load conversation ${shortId(id)}: ${error.message}`);
}
}
}
async function startChat() {
const box = $("f-idea");
const error = $("chat-start-error");
const go = $("chat-start-go");
const idea = box.value;
if (!idea.trim()) {
setText(error, "Describe the idea first \u2014 a sentence is enough.");
show(error, true);
box.focus();
return;
}
show(error, false);
go.disabled = true;
setText(go, "Starting\u2026");
show($("chat-start-wait"), true);
const repo = $("chat-start-repo").value.trim();
try {
const chat = await postJson(API.chats, { idea, agent: null, repo: repo || null });
state.chats = sortChats([chat, ...(state.chats || []).filter((c) => c.id !== chat.id)]);
state.chatDetail = { id: chat.id, chat };
box.value = "";
renderChats();
announce("The interview has started.");
location.hash = `#/plan/${chat.id}`;
ok();
} catch (failure) {
setText(error, failure.message);
show(error, true);
} finally {
go.disabled = false;
setText(go, "Start the interview");
show($("chat-start-wait"), false);
}
}
async function deriveChat() {
const chat = state.chatDetail.chat;
const error = $("chat-derive-error");
const go = $("chat-derive-go");
if (!chat) return;
const repo = $("chat-derive-repo").value.trim();
if (!repo) {
setText(error, "Pick a repository, or type a path, first.");
show(error, true);
return;
}
show(error, false);
go.disabled = true;
setText(go, "Starting\u2026");
try {
const derived = await postJson(API.chats, {
idea: "Continue this conversation in a different repository.",
repo,
from: chat.id,
});
state.chats = sortChats([derived, ...(state.chats || []).filter((c) => c.id !== derived.id)]);
renderChats();
announce("Started a new conversation in the other repository.");
location.hash = `#/plan/${derived.id}`;
} catch (failure) {
setText(error, failure.message);
show(error, true);
} finally {
go.disabled = false;
setText(go, "Continue here");
}
}
async function sendTurn(event) {
event.preventDefault();
const id = state.chatDetail.id;
const box = $("f-say");
const text = box.value;
if (!id || state.chatBusy !== null) return;
if (!text.trim()) {
chatError("Say something first.");
box.focus();
return;
}
chatError("");
const before = chatTurns(state.chatDetail.chat).length;
beginTurn(id, before);
state.pending = { id, body: text, at: new Date().toISOString() };
box.value = "";
renderChat();
$("chat-wait").scrollIntoView({ block: "nearest" });
try {
const queued = await postJson(API.say(id), { text });
if (state.chatDetail.id === id) {
state.chatDetail.chat = queued;
renderChat();
announce("Sent. The agent is answering.");
}
loadChats();
ok();
} catch (error) {
if (error.status === 409) {
announce("A turn is already running on this conversation. Waiting for it.");
return;
}
endTurn(id);
chatError(`The message may not have been sent: ${error.message}`);
await loadChat(id);
}
}
async function fileDraft() {
const id = state.chatDetail.id;
const button = $("chat-file");
if (!id) return;
button.disabled = true;
setText(button, "Filing\u2026");
state.chatProblems = { id, list: [] };
renderProblems([]);
chatError("");
try {
const body = await postJson(API.file(id), {});
const task = body && typeof body.task === "string" ? body.task : null;
announce(task ? `Filed as task ${shortId(task)}.` : "Filed.");
await Promise.allSettled([loadChat(id), loadChats(), loadQueue()]);
ok();
} catch (error) {
const list = Array.isArray(error.problems) && error.problems.length
? error.problems
: [error.message];
state.chatProblems = { id, list };
renderProblems(list);
announce(`The draft was not filed: ${plural(list.length, "problem", "problems")} to fix.`);
$("chat-problems").scrollIntoView({ block: "nearest" });
} finally {
button.disabled = false;
setText(button, "File this task");
}
}
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);
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) : 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);
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) {
const terminal = ["merged", "ready", "stalled", "blocked", "failed"].includes(String(run.status || ""));
if (!terminal) {
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"],
["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 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.head ? el("span", { class: "head-sha", text: String(round.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.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 e2e = Array.isArray(round.e2e) ? round.e2e : [];
if (e2e.length) node.append(commandList("Verification", e2e));
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 || ""}` })),
numbers([
`${addressed.length} addressed`,
`${rejected.length} declined`,
fix.committed ? "committed" : "no commit",
]),
fix.failed ? el("p", { class: "card-note", text: 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 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}`);
}
}
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) {
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 chatsRev = source.chats_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 (chatsRev !== state.rev.chats) {
state.rev.chats = chatsRev;
jobs.push(loadChats());
if (state.route.name === "chat" && state.chatDetail.id) jobs.push(loadChat(state.chatDetail.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] === "plan" && parts[1]) return { name: "chat", id: decodeURIComponent(parts[1]) };
if (parts[0] === "plan") return { name: "chats", 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-chats"), route.name === "chats");
show($("view-chat"), route.name === "chat");
show($("run-actions-fab"), route.name === "run");
if (route.name !== "run") closeRunActions();
const section = route.name === "run" ? "runs" : route.name === "chat" ? "chats" : 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 === "chat") {
if (changed) state.openingChat = true;
if (state.chatDetail.id !== route.id) {
state.chatDetail = { id: route.id, chat: null };
loadChat(route.id);
}
renderChat();
} else if (state.chatDetail.id) {
state.chatDetail = { id: null, chat: null };
}
if (changed) window.scrollTo({ top: 0 });
if (changed && route.name === "questions") focusFirstAsk();
if (changed && route.name === "chats" && state.planFocus) {
state.planFocus = false;
requestAnimationFrame(() => $("f-idea").focus());
}
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();
}
function openPlan() {
if (location.hash === "#/plan") {
requestAnimationFrame(() => $("f-idea").focus());
return;
}
state.planFocus = true;
location.hash = "#/plan";
}
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 wire() {
for (const entry of document.querySelectorAll("[data-plan]")) {
entry.addEventListener("click", openPlan);
}
$("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 });
});
$("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";
});
$("chat-draft-raw-toggle").addEventListener("click", () => {
state.draftRaw = !state.draftRaw;
applyDraftView();
});
$("alert-retry").addEventListener("click", () => {
ok();
loadHealth({ applyRevisions: true });
loadQuestions();
loadChats();
if (state.route.name === "run" && state.detail.id) loadRun(state.detail.id);
if (state.route.name === "chat" && state.chatDetail.id) loadChat(state.chatDetail.id);
});
$("chat-start-go").addEventListener("click", startChat);
$("chat-say").addEventListener("submit", sendTurn);
$("chat-file").addEventListener("click", fileDraft);
$("chat-derive-go").addEventListener("click", deriveChat);
$("chat-start-repo-select").addEventListener("change", (event) => {
$("chat-start-repo").value = event.target.value;
});
$("chat-derive-repo-select").addEventListener("change", (event) => {
$("chat-derive-repo").value = event.target.value;
});
$("chat-start-repo-refresh").addEventListener("click", () => loadRepos(true));
$("f-say").addEventListener("keydown", (event) => {
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
$("chat-say").requestSubmit();
}
});
$("panel-full-close").addEventListener("click", closePanel);
$("panel-full").addEventListener("close", () => clear($("panel-full-body")));
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.chats = state.health.chats_rev;
state.rev.loop = state.health.loop_rev;
if (state.health.loop) state.loop = state.health.loop;
}
await Promise.allSettled([loadRuns(), loadQueue(), loadQuestions(), loadChats(), loadRepos(false)]);
subscribe();
setInterval(() => {
if (document.hidden) return;
loadHealth({ applyRevisions: !state.streamOpen });
}, HEALTH_MS);
}
boot();