import { post } from "./api.js"; import { ago, errorText, h, replace } from "./dom.js"; import { inlineMarkdown, markdown } from "./markdown.js"; import { currentSession, findTask, onEach, state } from "./state.js"; const models = ["", "opus", "sonnet", "haiku"]; export function mountTranscript(root) { const head = h("header", { class: "task-header" }); const transcript = h("div", { class: "transcript" }); const composer = h("div", { class: "composer" }); const visibleReports = new Map(); const acked = new Set(); const ackVisible = () => { if (document.visibilityState !== "visible") { return; } const ids = [...new Set(visibleReports.values())].filter((id) => !acked.has(id) && state.inbox.some((i) => i.id === id)); if (ids.length === 0) { return; } for (const id of ids) { acked.add(id); } post("/api/inbox/responses", { body: { responses: ids.map((id) => ({ item_id: id, type: "dismiss" })) } }).catch((err) => { for (const id of ids) { acked.delete(id); } console.error("acknowledge reports", err); }); }; const observer = new IntersectionObserver((entries) => { for (const e of entries) { const id = e.target.dataset["inboxItem"]; if (!id) { continue; } if (e.isIntersecting) { visibleReports.set(e.target, id); } else { visibleReports.delete(e.target); } } ackVisible(); }, { root: transcript, threshold: 0.5 }); const newCtx = () => { const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined; return { tools: new Map(), run: undefined, root: task?.worktree_path ?? "", watchReport: (el, itemId) => { el.dataset["inboxItem"] = itemId; observer.observe(el); }, }; }; let ctx = newCtx(); let context = new Map(); const spinner = h("div", { class: "spinner", hidden: true }); const setRunning = (running) => { transcript.classList.toggle("running", running); if (running === !spinner.hidden) { return; } const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80; spinner.hidden = !running; if (pinned) { transcript.scrollTop = transcript.scrollHeight; } }; const put = (el) => { if (spinner.parentNode === transcript) { transcript.insertBefore(el, spinner); } else { transcript.append(el); } }; const renderHead = () => { const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined; setRunning(task !== undefined && currentSession(task.id)?.state === "running"); if (!task) { replace(head); return; } const sess = currentSession(task.id); const complete = h("button", { class: "danger", title: "Remove the worktree and branch; history stays" }, "Complete task"); complete.onclick = async () => { if (!confirm(`Complete "${task.title}"? Its worktree and branch are removed; its history stays.`)) { return; } try { await post("/api/tasks/{taskId}/complete", { path: { taskId: task.id } }); location.hash = "#/"; } catch (err) { alert(errorText(err)); } }; replace(head, h("div", { class: "meta" }, h("code", {}, task.branch)), h("div", { class: "head-actions" }, sess && contextMeter(context.get(sess.id)), complete)); }; const renderComposer = () => { const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined; if (!task) { replace(composer); return; } const sess = currentSession(task.id); if (!sess || sess.state === "completed") { replace(composer, startForm(task.id)); return; } replace(composer, messageForm(task.id, sess.id, sess.state)); }; const addToRun = (el, call) => { const run = ctx.run; if (!run) { ctx.run = { first: el, group: undefined, calls: call ? [call] : [] }; put(el); return; } 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); body.append(run.first); if (run.first.classList.contains("errored")) { group.classList.add("errored"); } run.group = { el: group, summary, body }; } run.group.body.append(el); replace(run.group.summary, runSummary(run.calls)); }; const appendEvents = (events) => { const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80; let contextChanged = false; for (const ev of events) { const p = ev.payload; 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 === "agent.tool_call") { addToRun(renderToolCall(p, ctx), { name: String(p["name"]), target: toolTarget(p["input"], ctx.root) }); continue; } const el = renderEvent(ev, ctx); if (!el) { continue; } if (ev.kind === "agent.thinking") { addToRun(el, undefined); continue; } ctx.run = undefined; put(el); } if (contextChanged) { renderHead(); } if (pinned) { transcript.scrollTop = transcript.scrollHeight; } }; const renderAll = () => { observer.disconnect(); visibleReports.clear(); ctx = newCtx(); context = new Map(); renderHead(); renderComposer(); if (!state.selectedTaskId) { replace(transcript, h("div", { class: "empty" }, "No task selected. Pick one with ⌘K or from the Tasks tab.")); return; } replace(transcript, spinner); appendEvents(state.transcript); }; const offs = [ onEach(["selection"], renderAll), onEach(["tasks"], () => { renderHead(); renderComposer(); }), onEach(["sessions"], (taskId) => { if (taskId === state.selectedTaskId) { renderHead(); renderComposer(); } }), onEach(["transcript"], (fresh) => appendEvents(fresh)), onEach(["inbox"], ackVisible), ]; document.addEventListener("visibilitychange", ackVisible); replace(root, h("div", { class: "transcript-view" }, head, transcript, composer)); renderAll(); return () => { for (const off of offs) { off(); } document.removeEventListener("visibilitychange", ackVisible); observer.disconnect(); }; } const composerDrafts = new Map(); function keepDraft(input, key) { input.value = composerDrafts.get(key) ?? ""; input.addEventListener("input", () => { if (input.value) { composerDrafts.set(key, input.value); } else { composerDrafts.delete(key); } }); return () => composerDrafts.delete(key); } function startForm(taskId) { const prompt = h("textarea", { placeholder: "What should the agent do?", rows: 4, required: true }); const clearDraft = keepDraft(prompt, `start:${taskId}`); const model = h("select", {}, ...models.map((m) => h("option", { value: m }, m || "default model"))); const form = h("form", { class: "compose" }, prompt, h("div", { class: "compose-actions" }, model, h("button", { type: "submit" }, "Start agent"))); form.onsubmit = async (e) => { e.preventDefault(); try { await post("/api/tasks/{taskId}/sessions", { path: { taskId }, body: { prompt: prompt.value, model: model.value || undefined } }); clearDraft(); } catch (err) { alert(errorText(err)); } }; submitOnEnter(prompt, form); return form; } function messageForm(taskId, sessionId, sessionState) { const text = h("textarea", { placeholder: sessionState === "idle" ? "Message the agent" : "Wait for the turn to end, or interrupt it", rows: 3 }); const clearDraft = keepDraft(text, `message:${taskId}`); const send = h("button", { type: "submit", disabled: sessionState !== "idle" }, "Send"); const interrupt = h("button", { type: "button", class: "danger", disabled: sessionState !== "running" && sessionState !== "queued" }, sessionState === "queued" ? "Cancel queued turn" : "Interrupt"); interrupt.onclick = async () => { try { await post("/api/sessions/{sessionId}/interrupt", { path: { sessionId } }); } catch (err) { alert(errorText(err)); } }; const form = h("form", { class: "compose" }, text, h("div", { class: "compose-actions" }, interrupt, send)); form.onsubmit = async (e) => { e.preventDefault(); if (!text.value.trim()) { return; } try { await post("/api/sessions/{sessionId}/messages", { path: { sessionId }, body: { text: text.value } }); text.value = ""; clearDraft(); } catch (err) { alert(errorText(err)); } }; submitOnEnter(text, form); return form; } function submitOnEnter(input, form) { input.onkeydown = (e) => { if (e.key === "Enter" && !e.shiftKey && !e.isComposing) { e.preventDefault(); form.requestSubmit(); } }; } function contextMeter(c) { if (!c || c.window <= 0) { return null; } const pct = Math.min(100, (c.used / c.window) * 100); const level = pct >= 90 ? "danger" : pct >= 70 ? "warn" : "ok"; const title = `context: ${c.used.toLocaleString()} of ${c.window.toLocaleString()} tokens${c.model ? ` (${c.model})` : ""}`; return h("span", { class: `context-meter ${level}`, title }, `${Math.round(pct)}%`); } function renderToolCall(p, ctx) { const result = h("div", { class: "tool-result pending" }, "running…"); ctx.tools.set(String(p["id"]), result); return h("details", { class: "tool" }, h("summary", {}, h("span", { class: "tool-name" }, String(p["name"])), " ", h("span", { class: "tool-brief" }, toolBrief(p["input"], ctx.root))), h("pre", { class: "tool-input" }, JSON.stringify(p["input"], null, 2)), result); } function runSummary(calls) { 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 = []; 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, ctx) { const p = ev.payload; const when = h("time", { dateTime: ev.created_at, title: ev.created_at }, ago(ev.created_at)); switch (ev.kind) { case "user.message": if (ev.actor === "byre") { return h("div", { class: "note" }, String(p["text"]), " ", when); } return h("div", { class: "msg user" }, h("div", { class: "who" }, ev.actor, " ", when), markdown(String(p["text"]), "text md")); case "agent.message": return h("div", { class: "msg agent" }, markdown(String(p["text"]), "text md")); case "agent.thinking": return h("details", { class: "thinking" }, h("summary", {}, "thinking"), markdown(String(p["text"]), "text md")); case "agent.tool_result": { const target = ctx.tools.get(String(p["tool_use_id"])); const body = h("pre", {}, resultText(p["content"])); if (!target) { return h("div", { class: "tool-result orphan" }, body); } target.className = p["is_error"] ? "tool-result error" : "tool-result"; replace(target, body); if (p["is_error"]) { target.closest("details.tool")?.classList.add("errored"); target.closest("details.tool-group")?.classList.add("errored"); } return null; } case "agent.report": { const card = reportCard(p); if (typeof p["inbox_item_id"] === "string" && p["inbox_item_id"]) { ctx.watchReport(card, p["inbox_item_id"]); } return card; } case "turn.ended": 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.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"]; return h("div", { class: "note ask" }, `asked${p["blocking"] ? " (blocking)" : ""}: `, String(q["question"])); } if (kind === "session_error" || kind === "rate_limited") { const q = p["payload"]; return h("div", { class: "note error" }, String(q["error"])); } return null; } case "task.completed": return h("div", { class: "note" }, `${ev.actor} completed the task `, when); } return null; } export function reportCard(p) { const sections = [ ["done", "Done"], ["deviations", "Deviations from plan"], ["risks", "Risks"], ["next_steps", "Next steps"], ]; return h("div", { class: "report" }, ...sections.map(([key, label]) => { const items = p[key] ?? []; if (items.length === 0) { return null; } return h("div", { class: `report-section ${key}` }, h("h4", {}, label), h("ul", {}, ...items.map((i) => h("li", {}, inlineMarkdown(i))))); })); } function toolBrief(input, root) { if (typeof input !== "object" || input === null) { return ""; } const i = input; 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, root) { if (typeof input !== "object" || input === null) { return ""; } const path = input["file_path"]; return typeof path === "string" ? relative(path, root) : ""; } function relative(path, root) { return root && path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path; } function resultText(content) { 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); } //# sourceMappingURL=task.js.map