import { del, get, post, type AgentSession, type ByreEvent, type Participant, type Schemas, type Snapshot } from "./api.js"; import { ago, copyable, errorText, h, replace, showAlert, showChoice, showConfirm } from "./dom.js"; import { markdown } from "./markdown.js"; import { previewURL } from "./preview.js"; import { findingCount, reviewCounts, severityCounts, unignoredFindings } from "./reviews.js"; import { composerText, endSend, flushDrafts, forgetLostImages, lostImageCount, setComposerImages, setComposerText, startSend } from "./drafts.js"; import { imageDrafts, imageTray, releaseImages, uploadImages, type PendingImage } from "./images.js"; import { currentSession, findTask, focusTab, focusTask, markSummaryRead, on, onEach, pickReview, pinSnapshot, setFocus, showReview, state, transcriptLoading, type Topic } from "./state.js"; import { taskStyle } from "./taskcolor.js"; import { fillTodoLinks } from "./todolink.js"; export type Usage = { used: number; window: number; model: string }; type Review = Schemas["Review"]; export type ChatSource = { key: () => string | undefined; events: () => ByreEvent[]; loading: () => boolean; session: () => AgentSession | undefined; root: () => string; taskId: () => string | undefined; resetTopics: Topic[]; refreshTopics: Topic[]; transcriptTopic: Topic; streamTopic: Topic; readyTopic: Topic; older?: { has: () => boolean; load: () => Promise }; empty: string; canSend: (sessionState: string) => boolean; placeholder: string; send: (m: { key: string; sessionId: string | undefined; text: string; images: string[] | undefined; model: string | undefined; firstModel: string }) => Promise; }; export type Head = { render: (usage: (sessionId: string) => Usage | undefined) => (Node | false | null)[]; onEvents?: (events: ByreEvent[]) => void; reset?: () => void; dispose?: () => void; }; export const models = [ "claude-opus-5-5", "claude-sonnet-5-5", "claude-haiku-5-5", "claude-fable-5-1", "gpt-6.1-sol", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", ]; const quietReply = "NO_REPLY"; type Call = { name: string; target: string }; type Run = { first: HTMLElement; group: { el: HTMLDetailsElement; summary: HTMLElement; body: HTMLElement } | undefined; calls: Call[]; parent: string; }; type Subagent = { type: string; description: string; status: HTMLElement; body: HTMLElement; run: Run | undefined; }; type Ctx = { tools: Map; subagents: Map; reviewers: Set; reviewButtons: Map; run: Run | undefined; root: () => string; taskId: string | undefined; held: DocumentFragment | undefined; open: boolean; drawn: HTMLElement[] | undefined; quiet: boolean; handoff: { summary: HTMLElement; body: HTMLElement } | undefined; }; const taskSource: ChatSource = { key: () => state.selectedTaskId, events: () => state.transcript, loading: transcriptLoading, session: () => (state.selectedTaskId ? currentSession(state.selectedTaskId) : undefined), root: () => (state.selectedTaskId ? (findTask(state.selectedTaskId)?.worktree_path ?? "") : ""), taskId: () => state.selectedTaskId, resetTopics: ["selection"], refreshTopics: ["tasks", "sessions"], transcriptTopic: "transcript", streamTopic: "stream", readyTopic: "ready", empty: "No task selected. Ask your lead, or pick one with ⌘K.", canSend: () => true, placeholder: "Message the agent. Paste or drop images to attach them.", send: async ({ key, sessionId, text, images, model, firstModel }) => { if (sessionId) { await post("/api/sessions/{sessionId}/messages", { path: { sessionId }, body: { text, images, model } }); return; } await post("/api/tasks/{taskId}/sessions", { path: { taskId: key }, body: { prompt: text, model: firstModel, images } }); }, }; export function mountTranscript(root: HTMLElement): () => void { return mountChat(root, taskSource, taskHead); } function taskHead(rerender: () => void): Head { let unmerged: { taskId: string; has: boolean } | undefined; let timer: number | undefined; const load = async () => { const taskId = state.selectedTaskId; if (!taskId) { return; } try { const u = await get("/api/tasks/{taskId}/unmerged", { path: { taskId } }); if (state.selectedTaskId !== taskId) { return; } const has = u.commits > 0 || u.files > 0; if (unmerged?.taskId === taskId && unmerged.has === has) { return; } unmerged = { taskId, has }; rerender(); } catch (err) { console.error("load unmerged changes", err); } }; return { reset: () => { clearTimeout(timer); void load(); }, dispose: () => clearTimeout(timer), onEvents: (events) => { if (events.some((ev) => ev.kind === "agent.tool_result" || ev.kind === "turn.ended")) { clearTimeout(timer); timer = setTimeout(() => void load(), 1500); } }, render: (usage) => { const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined; if (!task) { return []; } const sess = currentSession(task.id); if (task.status !== "active") { return [h("div", { class: "head-actions" }, task.status)]; } const target = state.projects.find((p) => p.id === task.project_id)?.default_branch ?? "the default branch"; const busy = sess?.state === "running"; const held = task.merge_queued_at !== undefined; const intoParent = task.merging_into_parent; const merge = h( "button", { class: "merge-button", disabled: busy || held || intoParent || unmerged?.taskId !== task.id || !unmerged.has, title: intoParent ? "Waiting for the parent task's agent to merge this subtask" : held ? "Waiting for the merge in progress in this project to finish" : task.merge_after_review ? "Merges when the running review finishes with no findings; open to merge now instead (⌘S)" : task.parent_task_id !== undefined ? `Merge this subtask into its parent task's branch, or into ${target} (⌘S)` : `The agent merges ${target} in, then byre squashes into one commit on ${target} and pushes it to origin; held while another task in the project is merging (⌘S)`, }, intoParent ? "Merging into parent" : held ? "Merge held" : task.merge_after_review ? "Merge after review" : "Merge", ); merge.onclick = () => confirmMerge(task.id); const mergeIn = h("button", { disabled: busy, title: `Ask the agent to bring ${target} into this branch and resolve any conflicts` }, `Sync from ${target}`); mergeIn.onclick = async () => { mergeIn.disabled = true; try { await post("/api/tasks/{taskId}/merge-in", { path: { taskId: task.id } }); } catch (err) { void showAlert(errorText(err)); mergeIn.disabled = false; } }; const discard = h("button", { class: "danger", disabled: busy, title: "The agent writes the task summary, then byre deletes the worktree and branch without merging" }, "Discard"); discard.onclick = () => confirmThen(`Discard "${task.title}"?`, "Discard", true, discard, async () => { await post("/api/tasks/{taskId}/discard", { path: { taskId: task.id } }); await setFocus({ task_id: null }); }); return [ h("div", { class: "head-actions session-actions" }, compactButton(sess?.id, sess?.state, sess && usage(sess.id)), mergeIn), h("div", { class: "head-actions" }, merge, discard), ]; }, }; } export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerender: () => void) => Head): () => void { const head = h("header", { class: "task-header" }); const transcript = h("div", { class: "transcript" }); const composer = h("div", { class: "composer" }); let stuck = true; const pin = () => { if (stuck) { transcript.scrollTop = transcript.scrollHeight; } }; transcript.addEventListener("scroll", () => { stuck = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80; }); transcript.addEventListener("load", pin, true); const resizes = new ResizeObserver(pin); resizes.observe(transcript); const mutations = new MutationObserver(pin); mutations.observe(transcript, { childList: true, subtree: true, characterData: true }); const shown = new Set(); const markShown = () => { const taskId = ctx.taskId; if (!taskId || document.visibilityState !== "visible") { return; } for (const el of shown) { markSummaryRead(taskId, Number(el.dataset["seq"])); } }; const sightings = new IntersectionObserver( (entries) => { for (const e of entries) { if (e.isIntersecting) { shown.add(e.target as HTMLElement); } else { shown.delete(e.target as HTMLElement); } } markShown(); }, { root: transcript }, ); document.addEventListener("visibilitychange", markShown); const jump = () => { const target = state.jump; if (!target || target.taskId !== ctx.taskId) { return; } const end = transcript.querySelector(`.turn-end[data-seq="${target.seq}"]`); if (!end) { return; } state.jump = undefined; const msg = end.previousElementSibling; const box = transcript.getBoundingClientRect(); if (msg?.classList.contains("agent")) { transcript.scrollTop += msg.getBoundingClientRect().top - box.top - 8; } else { transcript.scrollTop += end.getBoundingClientRect().bottom - box.bottom + 8; } stuck = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80; }; const newCtx = (): Ctx => ({ tools: new Map(), subagents: new Map(), reviewers: new Set(), reviewButtons: new Map(), run: undefined, held: undefined, open: false, drawn: undefined, quiet: false, handoff: undefined, root: src.root, taskId: src.taskId(), }); let ctx = newCtx(); let context = new Map(); const headImpl = makeHead(() => renderHead()); const scanner = h("div", { class: "scanner", hidden: true }); const loading = h("div", { class: "empty" }, "loading…"); const earlier = h("div"); let sink: DocumentFragment | undefined; const setRunning = (running: boolean) => { transcript.classList.toggle("running", running); scanner.hidden = !running; }; const putBefore = (el: Node, anchor: HTMLElement | undefined) => { if (sink) { sink.append(el); return; } if (anchor?.parentNode === transcript) { transcript.insertBefore(el, anchor); } else { transcript.insertBefore(el, scanner); } }; const syncRunning = () => setRunning(src.session()?.state === "running" && ctx.open); const release = () => { const buffered = ctx.held; if (!buffered) { return; } ctx.held = undefined; ctx.open = true; putBefore(buffered, pending[0]?.el); syncRunning(); }; const put = (el: HTMLElement, keepHeld = false) => { if (ctx.handoff) { ctx.handoff.body.append(el); return; } if (!keepHeld) { release(); } ctx.drawn?.push(el); if (ctx.held) { ctx.held.append(el); return; } putBefore(el, pending[0]?.el); }; const fold = () => { for (const el of ctx.drawn ?? []) { el.remove(); } ctx.drawn = undefined; ctx.held = undefined; ctx.open = false; ctx.quiet = false; syncRunning(); }; const renderHead = () => { syncRunning(); if (!src.key()) { replace(head); return; } replace(head, ...headImpl.render((id) => context.get(id))); }; let composerModel = ""; let focusStart = false; const renderComposer = () => { const key = src.key(); const old = composer.querySelector("textarea"); if (old) { releaseInput(old); } if (!key) { replace(composer); return; } const sess = src.session(); const live = sess && sess.state !== "completed" ? sess : undefined; composerModel = live ? live.model || context.get(live.id)?.model || "" : ""; const form = composeForm(src, key, live?.id, live?.state ?? "idle", composerModel, live?.vendor, showPending); const caret = old !== null && document.activeElement === old ? [old.selectionStart, old.selectionEnd] : undefined; replace(composer, form); const text = form.querySelector("textarea")!; if (caret) { text.focus(); text.setSelectionRange(caret[0]!, caret[1]!); } else if (focusStart && !sess) { text.focus(); } focusStart = false; }; let pending: { el: HTMLElement; text: string; imgs: PendingImage[] }[] = []; const showPending = (text: string, imgs: PendingImage[]) => { const el = h( "div", { class: "msg user pending" }, h("div", { class: "who" }, src.session()?.state === "running" ? "queued…" : "sending…"), imgs.length > 0 ? h("div", { class: "msg-images" }, ...imgs.map((img) => h("img", { src: img.url, alt: "attached image" }))) : null, text.trim() ? markdown(text, "text md") : null, ); const entry = { el, text, imgs }; pending.push(entry); ctx.run = undefined; putBefore(el, undefined); stuck = true; pin(); return () => { el.remove(); pending = pending.filter((p) => p !== entry); }; }; const confirmPending = (text: string) => { const entry = pending.find((p) => p.text === text); if (!entry) { return; } entry.el.remove(); releaseImages(entry.imgs); pending = pending.filter((p) => p !== entry); }; const runHead = (run: Run) => [subagentTag(run.parent, ctx), runSummary(run.calls)]; const addToRun = (el: HTMLElement, call: Call | undefined, parent: string) => { const sub = ctx.subagents.get(parent); if (sub) { sub.run = extendRun(sub.run, el, call, parent, (e) => sub.body.append(e)); return; } ctx.run = extendRun(ctx.run, el, call, parent, (e) => put(e, true)); }; const extendRun = (run: Run | undefined, el: HTMLElement, call: Call | undefined, parent: string, place: (el: HTMLElement) => void): Run => { if (!run || run.parent !== parent) { place(el); return { first: el, group: undefined, calls: call ? [call] : [], parent }; } if (call) { run.calls.push(call); } if (!run.group) { const summary = h("summary"); const body = h("div", { class: "tool-group-body" }); const group = h("details", { class: "tool-group" }, summary, body); run.first.replaceWith(group); ctx.drawn?.push(group); body.append(run.first); run.group = { el: group, summary, body }; } run.group.body.append(el); replace(run.group.summary, ...runHead(run)); return run; }; let stream: { el: HTMLElement; text: string; timer: number } | undefined; const dropStream = () => { if (!stream) { return; } clearTimeout(stream.timer); stream.el.remove(); stream = undefined; }; const appendDelta = (ev: ByreEvent) => { if (ctx.handoff || (ev.agent_session_id && ctx.reviewers.has(ev.agent_session_id))) { return; } if (!stream) { stream = { el: h("div", { class: "msg agent", hidden: true }), text: "", timer: 0 }; ctx.run = undefined; put(stream.el, true); } stream.text += String(ev.payload["text"] ?? ""); if (stream.timer) { return; } stream.timer = setTimeout(() => { if (!stream) { return; } stream.timer = 0; stream.el.hidden = ctx.held !== undefined || quietReply.startsWith(stream.text.trim()); replace(stream.el, markdown(stream.text, "text md", { taskId: ctx.taskId })); fillTodoLinks(stream.el, models[0]!); pin(); }, 100); }; const openHandoff = () => { const summary = h("summary", {}, "writing a handoff…"); const body = h("div", { class: "folded-turn" }); put(h("details", { class: "note" }, summary, body)); ctx.handoff = { summary, body }; }; const closeHandoff = (end: HTMLElement | undefined): HTMLElement | undefined => { const handoff = ctx.handoff; if (!handoff) { return undefined; } ctx.handoff = undefined; if (end) { replace(handoff.summary, ...end.childNodes); } else { replace(handoff.summary, "handoff not written"); } return handoff.summary; }; const renderEvents = (events: ByreEvent[]): boolean => { let contextChanged = false; for (const ev of events) { const p = ev.payload as Record; const parent = parentOf(p); if (ev.kind === "session.created" && p["role"] === "reviewer" && ev.agent_session_id) { ctx.reviewers.add(ev.agent_session_id); continue; } if (ev.agent_session_id && ctx.reviewers.has(ev.agent_session_id)) { continue; } if (ev.kind === "agent.vendor") { claudeTaskStatus(p, ctx); continue; } if (!sink && ((ev.kind === "agent.message" && !parent) || ev.kind === "turn.ended" || ev.kind === "user.message")) { dropStream(); } if (!sink && ev.kind === "user.message" && ev.actor !== "byre") { confirmPending(String(p["text"] ?? "")); } if (ev.kind === "session.context" && sink) { continue; } if (ev.kind === "session.context" && ev.agent_session_id) { const prev = context.get(ev.agent_session_id); const window = Number(p["window"]) || prev?.window || 0; context.set(ev.agent_session_id, { used: Number(p["used"]), window, model: String(p["model"] ?? "") }); contextChanged = true; continue; } if (ev.kind === "snapshot.created") { ctx.run = undefined; put(snapRow(p as unknown as Snapshot, ctx)); continue; } if (ev.kind === "review.started" || ev.kind === "review.finished" || ev.kind === "review.finding") { const r = p as unknown as Review; const button = ctx.reviewButtons.get(r.snapshot_n); if (button) { markReview(button, r); } } const sub = ctx.subagents.get(parent); if (ev.kind === "agent.tool_call" && isSubagentCall(p)) { const el = renderSubagent(p, ctx); if (sub) { sub.run = undefined; sub.body.append(el); continue; } ctx.run = undefined; put(el, true); continue; } if (ev.kind === "agent.tool_call") { addToRun(renderToolCall(p, ctx), { name: toolLabel(String(p["name"])), target: toolTarget(p["input"], ctx.root()) }, parent); continue; } const el = renderEvent(ev, ctx); if (!el) { continue; } if (ev.kind === "agent.thinking") { addToRun(el, undefined, parent); continue; } if (sub) { sub.run = undefined; sub.body.append(el); continue; } ctx.run = undefined; if (ev.kind === "agent.message" && !parent) { ctx.drawn = undefined; } if (ev.kind === "turn.ended") { ctx.open = false; } if (ev.kind === "turn.ended" && p["handoff"] === true && ctx.handoff) { closeHandoff(el); continue; } if (ev.kind === "turn.ended" || (ev.kind === "user.message" && p["mid_turn"] !== true)) { closeHandoff(undefined); } if (ev.kind === "user.message" && p["handoff"] === true) { openHandoff(); } if (ev.kind === "turn.ended" && ctx.quiet && ctx.drawn) { fold(); continue; } if (ev.kind === "turn.ended" || ev.kind === "user.message") { ctx.drawn = undefined; } if (ev.kind === "user.message" && ev.actor === "byre" && ev.owner && p["mid_turn"] !== true && p["handoff"] !== true) { ctx.held = document.createDocumentFragment(); ctx.open = false; ctx.drawn = []; continue; } if (ev.kind === "user.message" && p["mid_turn"] !== true) { ctx.open = true; } if (ev.kind === "turn.ended" && p["summary"] === true && ctx.taskId) { el.dataset["seq"] = String(ev.seq); sightings.observe(el); } put(el); } return contextChanged; }; const appendEvents = (events: ByreEvent[]) => { headImpl.onEvents?.(events); const contextChanged = renderEvents(events); syncRunning(); if (contextChanged) { renderHead(); const sess = src.session(); if (sess && !sess.model && (context.get(sess.id)?.model ?? "") !== composerModel) { renderComposer(); } } fillTodoLinks(transcript, models[0]!); syncMergeAfterReview(transcript); pin(); jump(); }; const prependEvents = (events: ByreEvent[]) => { const live = ctx; const fromBottom = transcript.scrollHeight - transcript.scrollTop; const box = document.createDocumentFragment(); ctx = newCtx(); sink = box; renderEvents(events); sink = undefined; ctx = live; syncRunning(); fillTodoLinks(box, models[0]!); earlier.after(box); syncMergeAfterReview(transcript); transcript.scrollTop = transcript.scrollHeight - fromBottom; }; let disposed = false; let fetching = false; const loadOlder = async () => { const older = src.older; if (!older || fetching || !older.has()) { return; } fetching = true; replace(earlier, h("div", { class: "empty" }, "loading earlier…")); try { const events = await older.load(); replace(earlier); if (!disposed) { prependEvents(events); } } finally { fetching = false; replace(earlier); } watchTop(); }; const top = new IntersectionObserver( (entries) => { if (entries.some((e) => e.isIntersecting)) { loadOlder().catch((err: unknown) => console.error("load earlier events", err)); } }, { root: transcript, rootMargin: "400px 0px 0px 0px" }, ); const watchTop = () => { if (disposed) { return; } top.unobserve(earlier); top.observe(earlier); }; const renderAll = () => { stuck = true; dropStream(); pending = []; sightings.disconnect(); shown.clear(); ctx = newCtx(); context = new Map(); focusStart = true; headImpl.reset?.(); renderHead(); if (!src.key()) { renderComposer(); replace(transcript, h("div", { class: "empty" }, src.empty), scanner); return; } replace(transcript, earlier, scanner); if (src.loading()) { transcript.prepend(loading); } appendEvents(src.events()); renderComposer(); watchTop(); }; const offs = [ onEach(src.resetTopics, renderAll), onEach(src.refreshTopics, () => { renderHead(); renderComposer(); }), onEach([src.transcriptTopic], (fresh) => appendEvents(fresh as ByreEvent[])), onEach([src.streamTopic], (ev) => appendDelta(ev as ByreEvent)), onEach([src.readyTopic], () => { loading.remove(); watchTop(); }), onEach(["tasks", "sessions"], () => { fillTodoLinks(transcript, models[0]!); syncMergeAfterReview(transcript); }), on("jump", jump), ]; replace(root, h("div", { class: "transcript-view" }, head, transcript, composer)); renderAll(); return () => { for (const off of offs) { off(); } disposed = true; top.disconnect(); headImpl.dispose?.(); resizes.disconnect(); mutations.disconnect(); sightings.disconnect(); document.removeEventListener("visibilitychange", markShown); dropStream(); const input = composer.querySelector("textarea"); if (input) { releaseInput(input); } flushDrafts(); }; } const composerInputs = new Map(); function releaseInput(input: HTMLTextAreaElement): void { for (const [key, el] of composerInputs) { if (el === input) { composerInputs.delete(key); } } } function keepDraft(input: HTMLTextAreaElement, key: string): void { composerInputs.set(key, input); input.value = composerText(key); input.addEventListener("input", () => setComposerText(key, input.value)); input.addEventListener("blur", flushDrafts); } function lostImagesNote(key: string): HTMLElement | null { const n = lostImageCount(key); if (n === 0) { return null; } const dismiss = h("button", { type: "button", class: "remove", title: "Dismiss" }, "×"); const note = h("div", { class: "compose-note" }, `${n === 1 ? "1 image" : `${n} images`} from before the reload need re-attaching.`, dismiss); dismiss.onclick = () => { forgetLostImages(key); note.remove(); }; return note; } const modelChoices = new Map(); function vendorOf(model: string): string { return model.startsWith("gpt-") ? "codex" : "claude"; } function modelSelect(key: string, current: string, vendor: string | undefined): HTMLSelectElement { const offered = vendor ? models.filter((m) => vendorOf(m) === vendor) : models; const options = current && !offered.includes(current) ? [current, ...offered] : offered; const select = h("select", { title: "Model for the next turn" }, ...options.map((m) => h("option", { value: m }, m))); select.value = modelChoices.get(key) ?? (current || options[0]!); select.onchange = () => { modelChoices.set(key, select.value); }; return select; } function composeForm( src: ChatSource, key: string, sessionId: string | undefined, sessionState: string, currentModel: string, vendor: string | undefined, showPending: (text: string, imgs: PendingImage[]) => () => void, ): HTMLElement { const text = h("textarea", { placeholder: src.placeholder, rows: 3 }); const draftKey = `message:${key}`; keepDraft(text, draftKey); const send = h("button", { type: "submit", disabled: !src.canSend(sessionState) }, "Send"); const interrupt = h("button", { type: "button", class: "danger", disabled: sessionState !== "running" }, "Interrupt"); interrupt.onclick = async () => { if (!sessionId) { return; } try { await post("/api/sessions/{sessionId}/interrupt", { path: { sessionId } }); } catch (err) { void showAlert(errorText(err)); } }; const modelKey = `message:${sessionId ?? key}`; const model = modelSelect(modelKey, currentModel, vendor); const form = h("form", { class: "compose" }); const tray = imageTray(form, text, draftKey, (n) => setComposerImages(draftKey, n)); form.append(lostImagesNote(draftKey) ?? "", tray.el, text, h("div", { class: "compose-actions" }, model, interrupt, send)); form.onsubmit = async (e) => { e.preventDefault(); const value = text.value; if (!value.trim() && !imageDrafts.has(draftKey)) { return; } const imgs = tray.take(); text.value = ""; const sent = startSend(draftKey, value, imgs.length); const cancel = showPending(value, imgs); try { const images = await uploadImages(imgs); await src.send({ key, sessionId, text: value, images, model: model.value !== currentModel ? model.value : undefined, firstModel: model.value }); modelChoices.delete(modelKey); endSend(sent, true); form.querySelector(".compose-note")?.remove(); } catch (err) { cancel(); endSend(sent, false); const live = composerInputs.get(draftKey) ?? text; live.value = composerText(draftKey); tray.restore(imgs); void showAlert(errorText(err)); } }; submitOnEnter(text, form); return form; } function submitOnEnter(input: HTMLTextAreaElement, form: HTMLFormElement): void { input.onkeydown = (e) => { if (e.key === "Enter" && !e.shiftKey && !e.isComposing) { e.preventDefault(); form.requestSubmit(); } }; } export function compactButton(sessionId: string | undefined, sessionState: string | undefined, c: { used: number; window: number; model: string } | undefined): HTMLElement { const pct = c && c.window > 0 ? Math.min(100, (c.used / c.window) * 100) : 0; const level = pct >= 90 ? "danger" : pct >= 70 ? "warn" : "ok"; const usage = c && c.window > 0 ? `\ncontext: ${c.used.toLocaleString()} of ${c.window.toLocaleString()} tokens${c.model ? ` (${c.model})` : ""}` : ""; const b = h( "button", { type: "button", class: `compact ${level}`, style: `--fill: ${pct}%`, disabled: !sessionId || sessionState !== "idle", title: `Ask the agent for a handoff, then continue in a new session with a fresh context${usage}`, }, "Compact", ); b.onclick = async () => { if (!sessionId) { return; } b.disabled = true; try { await post("/api/sessions/{sessionId}/compact", { path: { sessionId } }); } catch (err) { void showAlert(errorText(err)); b.disabled = false; } }; return b; } function parentOf(p: Record): string { return typeof p["parent_tool_use_id"] === "string" ? p["parent_tool_use_id"] : ""; } function subagentTag(parent: string, ctx: Ctx): HTMLElement | null { if (!parent) { return null; } if (ctx.subagents.has(parent)) { return null; } return h("span", { class: "subagent-tag" }, "↳ subagent"); } function renderToolCall(p: Record, ctx: Ctx): HTMLElement { const result = h("div", { class: "tool-result pending" }, "running…"); ctx.tools.set(String(p["id"]), result); return h( "details", { class: "tool" }, h("summary", {}, subagentTag(parentOf(p), ctx), h("span", { class: "tool-name" }, toolLabel(String(p["name"]))), " ", h("span", { class: "tool-brief" }, toolBrief(p["input"], ctx.root()))), copyable(h("pre", { class: "tool-input" }, JSON.stringify(p["input"], null, 2))), result, ); } function toolLabel(name: string): string { const prefix = "mcp__byre__"; if (!name.startsWith(prefix)) { return name; } return name .slice(prefix.length) .split("_") .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(" "); } function isSubagentCall(p: Record): boolean { return p["name"] === "Agent" || p["name"] === "Task"; } function renderSubagent(p: Record, ctx: Ctx): HTMLElement { const input = (typeof p["input"] === "object" && p["input"] !== null ? p["input"] : {}) as Record; const id = String(p["id"]); const type = String(input["subagent_type"] ?? "subagent"); const description = String(input["description"] ?? ""); const status = h("span", { class: "subagent-status" }, "starting…"); const result = h("div", { class: "tool-result pending" }, "running…"); const body = h("div", { class: "subagent-body" }); const tag = subagentTag(parentOf(p), ctx); ctx.tools.set(id, result); ctx.subagents.set(id, { type, description, status, body, run: undefined }); return h( "details", { class: "tool subagent" }, h("summary", {}, tag, h("span", { class: "tool-name" }, `${p["name"]} ${type}`), " ", h("span", { class: "tool-brief" }, description), " ", status), markdown(String(input["prompt"] ?? ""), "text md subagent-prompt", { taskId: ctx.taskId }), body, result, ); } function claudeTaskStatus(p: Record, ctx: Ctx): void { if (p["type"] !== "system") { return; } const sub = ctx.subagents.get(String(p["tool_use_id"])); if (!sub) { return; } switch (p["subtype"]) { case "task_started": replace(sub.status, p["is_backgrounded"] === true ? "running in the background" : "running"); return; case "task_progress": replace(sub.status, String(p["description"] ?? "running")); return; case "task_notification": sub.status.className = `subagent-status ${p["status"] === "completed" ? "done" : "error"}`; replace(sub.status, String(p["status"])); return; } } function markReview(button: HTMLElement, r: Review): void { const n = findingCount(r); button.classList.toggle("attention", n > 0); if (r.state === "running") { replace(button, "review", h("span", { class: "review-mark spinner", title: "review in progress" })); return; } if (r.state === "failed") { replace(button, "review"); return; } if (r.state === "skipped") { replace(button, "review", h("span", { class: "review-mark skipped", title: "skipped: no changes" }, "●")); return; } if (n === 0) { replace(button, "review", h("span", { class: "review-mark clean", title: Object.keys(r.finding_states).length > 0 ? "every finding addressed" : "no findings" }, "✓")); return; } replace(button, "review", h("span", { class: "badge", title: `${n} open findings` }, n)); } function snapRow(snap: Snapshot, ctx: Ctx): HTMLElement { const taskId = ctx.taskId; const changes = h("button", { type: "button", title: `Show snapshot ${snap.n} in the Changes tab` }, "changes"); changes.onclick = () => { pinSnapshot(snap.n); focusTab("changes"); }; const fork = h("button", { type: "button", title: "Start a new task from this snapshot, its agent continuing the conversation as it stood here" }, "fork"); fork.onclick = async () => { if (!taskId) { return; } fork.disabled = true; try { const task = await post("/api/tasks/{taskId}/snapshots/{n}/fork", { path: { taskId, n: snap.n } }); focusTask(task.id); } catch (err) { fork.disabled = false; void showAlert(errorText(err)); } }; const review = h("button", { type: "button", title: `Show the review of snapshot ${snap.n} in the Reviews tab` }, "review"); review.onclick = () => showReview(snap.n); ctx.reviewButtons.set(snap.n, review); const time = new Date(snap.created_at).toLocaleTimeString([], { hour12: false }); return h("div", { class: "snap-row", title: `${snap.trigger.replace("_", " ")} · ${time}` }, `snapshot #${snap.n}`, changes, fork, review); } function runSummary(calls: Call[]): HTMLElement { const counts = new Map(); for (const c of calls) { const key = `${c.name}\0${c.target}`; const entry = counts.get(key); if (entry) { entry.n++; } else { counts.set(key, { call: c, n: 1 }); } } if (counts.size === 0) { return h("span", { class: "tool-brief" }, "thinking"); } const parts: (HTMLElement | string)[] = []; for (const { call, n } of counts.values()) { if (parts.length > 0) { parts.push(", "); } parts.push( h("span", {}, h("span", { class: "tool-name" }, call.name), call.target ? h("span", { class: "tool-brief" }, " ", call.target) : null, n > 1 ? h("span", { class: "tool-count" }, ` ×${n}`) : null), ); } return h("span", {}, ...parts); } function renderEvent(ev: ByreEvent, ctx: Ctx): HTMLElement | null { const p = ev.payload as Record; const md = { taskId: ev.task_id }; const when = h("time", { dateTime: ev.created_at, title: ev.created_at }, ago(ev.created_at)); switch (ev.kind) { case "user.message": ctx.quiet = false; if (ev.actor === "byre") { const text = String(p["text"]); const first = text.split("\n")[0] ?? ""; const brief = first.length > 120 ? `${first.slice(0, 120)}…` : first; return h( "details", { class: "tool byre-prompt", hidden: p["continuation"] === true }, h("summary", {}, h("span", { class: "tool-name" }, "byre"), " ", h("span", { class: "tool-brief" }, brief), " ", when), markdown(text, "text md", md), ); } return h( "div", { class: "msg user" }, h("div", { class: "who" }, ev.actor, " ", when), messageImages(p["images"]), String(p["text"] ?? "").trim() ? markdown(String(p["text"]), "text md", md) : null, ); case "agent.message": if (parentOf(p)) { return h("div", { class: "msg agent subagent" }, h("div", { class: "who" }, subagentTag(parentOf(p), ctx), " ", when), markdown(String(p["text"]), "text md", md)); } if (String(p["text"]).trim().split("\n").pop()?.trim() === quietReply) { ctx.quiet = true; return null; } ctx.quiet = false; return h("div", { class: "msg agent" }, markdown(String(p["text"]), "text md", md)); case "agent.thinking": return h("details", { class: "thinking" }, h("summary", {}, subagentTag(parentOf(p), ctx), "thinking"), markdown(String(p["text"]), "text md", md)); case "agent.tool_result": { const target = ctx.tools.get(String(p["tool_use_id"])); const body = copyable(h("pre", {}, resultText(p["content"]))); if (!target) { return h("div", { class: "tool-result orphan" }, body); } target.className = "tool-result"; replace(target, body); return null; } case "turn.ended": for (const result of ctx.tools.values()) { if (result.classList.contains("pending")) { result.className = "tool-result unfinished"; replace(result, "no result"); } } if (p["handoff"] === true) { return h("div", { class: "note" }, "handoff written; continuing in a new session with a fresh context ", when); } return h("div", { class: `turn-end ${p["is_error"] ? "error" : ""}` }, p["is_error"] ? `turn ended: ${String(p["subtype"]).replaceAll("_", " ")}` : ""); case "session.created": return h("div", { class: "note" }, `${ev.actor} started a ${p["vendor"]} session`, p["model"] ? ` (${p["model"]})` : "", " ", when); case "session.model": return h("div", { class: "note" }, `${ev.actor} switched the model to ${p["model"]} `, when); case "session.state": return p["error"] ? h("div", { class: "note error" }, `session ${p["state"]}: ${p["error"]}`) : null; case "inbox.created": { const kind = p["kind"]; if (kind === "question") { const q = p["payload"] as Record; return h("div", { class: "note ask" }, "asked: ", String(q["question"])); } if (kind === "session_error" || kind === "rate_limited") { const q = p["payload"] as Record; return h("div", { class: "note error" }, String(q["error"])); } return null; } case "task.created": { if (typeof p["forked_from"] === "string") { return h( "div", { class: "note" }, `${ev.actor} forked this task from `, taskLink(p["forked_from"]), ` at snapshot #${String(p["forked_at"])}; its agent continues that task's conversation as it stood there `, when, ); } if (typeof p["parent_task_id"] === "string") { return h("div", { class: "note" }, "subtask of ", taskLink(p["parent_task_id"]), ", created by its agent ", when); } return null; } case "task.parent_merge_started": return h("div", { class: "note" }, `${ev.actor} asked `, taskLink(String(p["parent_task_id"])), "'s agent to merge this subtask into its branch ", when); case "task.parent_merge_stopped": return h("div", { class: "note error" }, "the merge into the parent task didn't start ", when); case "task.merge_title": return h("div", { class: "note" }, `${ev.actor} set the merge commit title to "${String(p["merge_title"])}" `, when); case "review.finished": { const r = p as unknown as Review; const outcome = r.state === "failed" ? `failed: ${r.error}` : r.state === "skipped" ? "skipped: no changes" : reviewCounts(r); return h("div", { class: `note${r.state === "failed" ? " error" : ""}` }, `review of snapshot #${r.snapshot_n}: ${outcome} `, when); } case "task.merge_queued": return h("div", { class: "note" }, `${ev.actor} asked to merge; held until the merge in progress in this project is done `, when); case "task.merge_stopped": return h("div", { class: "note" }, `${ev.actor} stopped the merge for review: ${String(p["reason"])} `, when); case "task.merge_after_review": { const taskId = String(p["id"]); const cancel = h("button", { type: "button", class: "merge-after-review-cancel", hidden: true, title: "Withdraw this request; nothing is merged", data: { taskId } }, "Cancel"); cancel.onclick = async () => { cancel.disabled = true; try { await del("/api/tasks/{taskId}/merge-after-review", { path: { taskId } }); } catch (err) { void showAlert(errorText(err)); } finally { cancel.disabled = false; } }; return h("div", { class: "note" }, `${ev.actor} asked to merge once the review of snapshot #${String(p["snapshot_n"])} finishes with no findings `, when, cancel); } case "task.merge_after_review_cancelled": return h("div", { class: "note" }, `${ev.actor} cancelled the merge after review `, when); case "task.merge_after_review_dropped": return h("div", { class: "note" }, `didn't merge after the review: ${String(p["reason"])} `, when); case "preview.exposed": { const url = previewURL(Number(p["port"])); return h("div", { class: "note" }, `${ev.actor} exposed "${String(p["name"])}" at `, h("a", { href: url, target: "_blank", rel: "noopener" }, url), " ", when); } case "task.merged": if (typeof p["parent_task_id"] === "string") { return h("div", { class: "note" }, `${ev.actor} merged the task into `, taskLink(p["parent_task_id"]), `, now at ${String(p["commit"]).slice(0, 7)} `, when); } return h( "div", { class: "note" }, `${ev.actor} merged the task${p["commit"] ? ` as ${String(p["commit"]).slice(0, 7)}` : ""}${p["pushed"] ? ` and pushed it to ${String(p["pushed"])}` : ""} `, when, ); case "task.summary": return h("div", { class: "note" }, `${ev.actor} set the task summary `, when); case "task.discard_started": return h("div", { class: "note" }, `${ev.actor} asked to discard the task; the agent writes the task summary first `, when); case "task.discarded": return h("div", { class: "note" }, `${ev.actor} discarded the task `, when); } return null; } function syncMergeAfterReview(root: ParentNode): void { const buttons = [...root.querySelectorAll(".merge-after-review-cancel")]; const last = buttons.at(-1); for (const b of buttons) { b.hidden = b !== last || findTask(b.dataset["taskId"] ?? "")?.merge_after_review !== true; } } function taskLink(id: string): Node { const task = findTask(id); if (task?.status !== "active") { return document.createTextNode(task?.title ?? id); } return h( "a", { href: `#/task/${task.id}`, class: "task-link", style: taskStyle(task.id), onclick: (e: MouseEvent) => { e.preventDefault(); focusTask(task.id); }, }, task.title, ); } function messageImages(ids: unknown): HTMLElement | null { if (!Array.isArray(ids) || ids.length === 0) { return null; } return h( "div", { class: "msg-images" }, ...ids.map((id) => { const src = `/api/blobs/${encodeURIComponent(String(id))}`; return h("a", { href: src, target: "_blank", rel: "noopener" }, h("img", { src, alt: "attached image", loading: "lazy" })); }), ); } export async function confirmMerge(taskId: string): Promise { const task = findTask(taskId); const sessionState = currentSession(taskId)?.state; if (!task || task.status !== "active") { return; } if (sessionState === "running") { void showAlert("Agent is mid-turn; merge when it's idle"); return; } if (task.merge_queued_at !== undefined) { void showAlert("Merge already held behind another merge in this project"); return; } if (task.merging_into_parent) { void showAlert("Already waiting for the parent task's agent to merge this subtask"); return; } const target = state.projects.find((p) => p.id === task.project_id)?.default_branch ?? "the default branch"; const parent = task.parent_task_id !== undefined ? findTask(task.parent_task_id) : undefined; let coAuthors; try { const [unmerged, people] = await Promise.all([ get("/api/tasks/{taskId}/unmerged", { path: { taskId } }), get("/api/tasks/{taskId}/co-authors", { path: { taskId } }), ]); if (unmerged.commits === 0 && unmerged.files === 0) { void showAlert(`Nothing to merge: no commits or uncommitted files differ from ${target}`); return; } coAuthors = people; } catch (err) { void showAlert(errorText(err)); return; } const title = mergeDrafts.get(taskId) ?? task.merge_title; mergeDrafts.delete(taskId); let input: HTMLInputElement | undefined; const edit = h("button", { type: "button", class: "merge-title-edit", title: "Edit the commit title", ariaLabel: "Edit the commit title" }, "✎"); const row = h("div", { class: "merge-title" }, h("span", { class: "merge-title-text" }, title), edit); edit.onclick = () => { const field = h("input", { type: "text", value: title, required: true, maxLength: 72, pattern: ".*\\S.*", ariaLabel: "Commit title" }); field.onkeydown = (e) => { if (e.key === "Enter") { e.preventDefault(); row.closest("form")?.querySelector(".modal-actions button:last-child")?.click(); } }; input = field; replace(row, field); field.focus(); field.select(); }; const warnings = h("ul", { class: "merge-warnings" }); const into = h("p", { class: "merge-into" }, `Merge into ${target} as:`); const co = coAuthorsSummary(coAuthors); const toMain: Node[] = co ? [into, row, co] : [into, row]; let toParent = false; const question: Node[] = []; if (parent?.status === "active") { toParent = true; const option = (value: boolean, label: string, hint: string) => { const radio = h("input", { type: "radio", name: "merge-target", checked: value === toParent }); radio.onchange = () => { toParent = value; for (const n of toMain) { (n as HTMLElement).hidden = toParent; } }; return h("label", { class: "merge-target" }, radio, h("span", {}, label, h("span", { class: "merge-target-hint" }, hint))); }; question.push( h( "div", { class: "merge-targets" }, option(true, `Into ${parent.title}`, "Its agent merges this subtask's branch into the parent's branch; the subtask closes once it has."), option(false, `Into ${target}`, "Squashed into one commit on the default branch and pushed, like any task."), ), ); for (const n of toMain) { (n as HTMLElement).hidden = true; } } question.push(...toMain.slice(0, 2), warnings, ...toMain.slice(2)); const leave = (go: () => void) => { mergeDrafts.set(taskId, input?.value ?? title); warnings.closest("dialog")?.close("cancel"); go(); }; const watch = watchMergeWarnings(taskId, warnings, leave); await watch.ready; const anchor = document.querySelector(".merge-button"); const merge = async (afterReview: boolean) => { const body = toParent ? { target: "parent" as const } : { merge_title: input?.value ?? title }; try { await post("/api/tasks/{taskId}/merge", { path: { taskId }, body: afterReview ? { ...body, after_review: true } : body }); } catch (err) { await showAlert(errorText(err)); } }; try { if (!watch.reviewRunning()) { if (await showConfirm(question, "Merge", false, anchor)) { await merge(false); } return; } const choice = await showChoice( question, [ { label: "Merge now", value: "now" }, { label: "After review", value: "after", class: "primary", title: "Merge once the running review finishes with no findings" }, ], anchor, ); if (choice === "now" || choice === "after") { await merge(choice === "after"); } } finally { watch.dispose(); } } const mergeDrafts = new Map(); function watchMergeWarnings(taskId: string, list: HTMLElement, leave: (go: () => void) => void): { ready: Promise; reviewRunning: () => boolean; dispose: () => void } { let reviewItems: HTMLElement[] = []; let running = false; let target = 0; const render = () => { const questions = state.inbox.filter((i) => i.task_id === taskId && i.kind === "question").length; const items = [...reviewItems]; if (questions > 0) { const text = questions === 1 ? "The agent's open question will be dismissed" : `The agent's ${questions} open questions will be dismissed`; items.push(mergeWarning(text, () => leave(() => focusTask(taskId, "inbox")))); } replace(list, ...items); list.hidden = items.length === 0; }; const load = async () => { const started = ++target; let items: HTMLElement[]; let isRunning = false; try { ({ items, running: isRunning } = await reviewWarnings(taskId, leave)); } catch (err) { items = [mergeWarning("Review status unavailable", undefined, errorText(err))]; } if (started === target) { reviewItems = items; running = isRunning; render(); } }; const offs = [ on("event", (detail) => { const ev = detail as ByreEvent; if (ev.task_id === taskId && (ev.kind === "snapshot.created" || ev.kind.startsWith("review."))) { void load(); } }), on("inbox", render), ]; return { ready: load(), reviewRunning: () => running, dispose: () => { target++; for (const off of offs) { off(); } }, }; } async function reviewWarnings(taskId: string, leave: (go: () => void) => void): Promise<{ items: HTMLElement[]; running: boolean }> { const [snaps, reviews] = await Promise.all([get("/api/tasks/{taskId}/snapshots", { path: { taskId } }), get("/api/tasks/{taskId}/reviews", { path: { taskId } })]); const latest = snaps.at(-1); if (!latest) { return { items: [mergeWarning("These changes haven't been reviewed")], running: false }; } const items: HTMLElement[] = []; const open = () => leave(() => { focusTask(taskId, "reviews"); pickReview(latest.n); }); const r = reviews.find((r) => r.snapshot_n === latest.n); if (!r) { items.push(mergeWarning("The latest snapshot hasn't been reviewed", open)); } else if (r.state === "running") { items.push(mergeWarning("Review of the latest snapshot is still running", open)); } else if (r.state === "failed") { items.push(mergeWarning("Review of the latest snapshot failed", open)); } else if (unignoredFindings(r).length > 0) { items.push(mergeWarning(`Review of the latest snapshot found ${severityCounts(unignoredFindings(r))}`, open)); } const changes = await get("/api/tasks/{taskId}/changes", { path: { taskId }, query: { since: latest.commit_sha } }); if (changes.files.length > 0) { items.push(mergeWarning("Changes since the last snapshot haven't been reviewed")); } return { items, running: r?.state === "running" }; } function mergeWarning(text: string, open?: () => void, detail?: string): HTMLElement { if (!open) { return h("li", { class: "merge-warning", title: detail ?? "" }, `⚠ ${text}`); } const link = h("button", { type: "button", class: "merge-warning-link" }, text); link.onclick = open; return h("li", { class: "merge-warning" }, "⚠ ", link); } function coAuthorsSummary(coAuthors: Participant[]): HTMLElement | null { if (coAuthors.length === 0) { return null; } const missing = coAuthors.filter((p) => !p.git_name || !p.git_email).length; const label = `${coAuthors.length} co-author${coAuthors.length === 1 ? "" : "s"}${missing > 0 ? ` (${missing} without git identity)` : ""}`; return h("details", { class: "merge-coauthors" }, h("summary", {}, label), h("p", { class: "modal-text" }, mergeTrailers(coAuthors))); } function mergeTrailers(coAuthors: Participant[]): string { const lines: string[] = []; const credited = coAuthors.filter((p) => p.git_name && p.git_email); const missing = coAuthors.filter((p) => !p.git_name || !p.git_email); if (credited.length > 0) { lines.push("Co-authored-by:", ...credited.map((p) => ` ${p.git_name} <${p.git_email}>`)); } if (missing.length > 0) { if (lines.length > 0) { lines.push(""); } lines.push("No git identity, so no trailer:", ...missing.map((p) => ` ${p.display_name} (${p.login})`)); } return lines.join("\n"); } async function confirmThen(question: string | Node[], action: string, danger: boolean, anchor: Element | null, act: () => Promise) { if (!(await showConfirm(question, action, danger, anchor))) { return; } try { await act(); } catch (err) { await showAlert(errorText(err)); } } function toolBrief(input: unknown, root: string): string { if (typeof input !== "object" || input === null) { return ""; } const i = input as Record; const brief = i["command"] ?? i["file_path"] ?? i["pattern"] ?? i["url"] ?? i["question"] ?? i["description"] ?? ""; const text = relative(String(brief), root); return text.length > 120 ? `${text.slice(0, 120)}…` : text; } function toolTarget(input: unknown, root: string): string { if (typeof input !== "object" || input === null) { return ""; } const path = (input as Record)["file_path"]; return typeof path === "string" ? relative(path, root) : ""; } function relative(path: string, root: string): string { return root && path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path; } function resultText(content: unknown): string { if (typeof content === "string") { return content; } if (Array.isArray(content)) { return content.map((c) => (typeof c === "object" && c !== null && "text" in c ? String(c.text) : JSON.stringify(c))).join("\n"); } return JSON.stringify(content, null, 2); }