import { post } from "./api.js"; import { mountChanges } from "./changes.js"; import { ago, errorText, h, replace, showAlert } from "./dom.js"; import { image, inlineMarkdown, markdown } from "./markdown.js"; import { diagram } from "./mermaid.js"; import { onEach, state } from "./state.js"; const checked = new Map(); const texts = new Map(); export function mountInbox(root) { const list = h("div", { class: "inbox-list" }); const cards = new Map(); const nothing = h("div", { class: "empty" }, "Nothing waiting on you in this task."); const noTask = h("div", { class: "empty" }, "No task selected."); const render = () => { const taskId = state.selectedTaskId; const items = state.inbox.filter((i) => i.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)); const keep = new Set(items.map((i) => i.id)); for (const id of cards.keys()) { if (!keep.has(id)) { cards.delete(id); } } for (const draft of [checked, texts]) { for (const id of draft.keys()) { if (!state.inbox.some((i) => i.id === id)) { draft.delete(id); } } } const cardFor = (item) => { const sig = JSON.stringify(item); const cached = cards.get(item.id); if (cached && cached.sig === sig) { return cached.el; } const el = card(item); cards.set(item.id, { sig, el }); return el; }; if (!taskId) { reconcile(list, [noTask]); return; } reconcile(list, items.length > 0 ? items.map(cardFor) : [nothing]); }; const off = onEach(["inbox", "selection"], render); const changes = h("section", { class: "inbox-changes" }); replace(root, h("div", { class: "inbox-view" }, changes, list)); const offChanges = mountChanges(changes, true); render(); return () => { off(); offChanges(); }; } function reconcile(parent, nodes) { nodes.forEach((node, i) => { const at = parent.childNodes[i]; if (at !== node) { parent.insertBefore(node, at ?? null); } }); while (parent.childNodes.length > nodes.length) { parent.lastChild.remove(); } } function clickable(el, go) { el.onclick = (e) => { if (!e.target.closest("a")) { go(); } }; el.onkeydown = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); go(); } }; return el; } function card(item) { const p = item.payload; const body = h("div", { class: "card-body" }); const actions = h("div", { class: "card-actions" }); const el = h("article", { class: `card ${item.kind} ${item.priority}` }, h("header", {}, h("span", { class: `priority ${item.priority}` }, item.priority), h("span", { class: "kind" }, item.kind.replace("_", " ")), h("time", { dateTime: item.created_at, title: item.created_at }, ago(item.created_at))), body, thread(item), actions); const busy = () => el.classList.contains("submitting"); const lock = (on) => { el.classList.toggle("submitting", on); for (const c of el.querySelectorAll("button, input, textarea")) { c.disabled = on; } }; const submit = async (response) => { if (busy()) { return; } lock(true); try { await post("/api/inbox/responses", { body: { responses: [response] } }); checked.delete(item.id); texts.delete(item.id); } catch (err) { void showAlert(errorText(err)); lock(false); renderActions(); } }; const options = p["options"] ?? []; const recommended = p["recommended"] ?? []; const multiple = p["multiple"] === true; const seen = new Set(); const choice = (text) => { if (!options.includes(text) || seen.has(text)) { return null; } seen.add(text); const tag = recommended.includes(text) && h("span", { class: "recommended" }, "recommended"); if (!multiple) { return clickable(h("div", { class: "pick", role: "button", tabIndex: 0 }, inlineMarkdown(text), tag), () => { void submit({ item_id: item.id, type: "option", options: [text] }); }); } const input = h("input", { type: "checkbox", checked: (checked.get(item.id) ?? []).includes(text) }); input.onchange = () => { const now = (checked.get(item.id) ?? []).filter((o) => o !== text); if (input.checked) { now.push(text); } checked.set(item.id, options.filter((o) => now.includes(o))); renderActions(); }; return h("label", { class: "option" }, input, " ", inlineMarkdown(text), tag); }; const textEntry = (label, placeholder, shared) => { const open = texts.has(item.id); const text = h("textarea", { rows: 3, placeholder, value: texts.get(item.id) ?? "" }); const send = h("button", { type: "button", class: "primary", disabled: !text.value.trim() }, "Submit"); const editor = h("div", { class: "text-editor", hidden: !open }, text, !shared && h("div", { class: "submit-row" }, send)); const block = clickable(h("div", { class: "pick other", role: "button", tabIndex: 0, hidden: open }, label), () => { texts.set(item.id, ""); block.hidden = true; editor.hidden = false; text.focus(); }); const go = () => { if (shared) { shared.onSubmit(); return; } if (text.value.trim()) { void submit({ item_id: item.id, type: "other", text: text.value }); } }; send.onclick = go; text.oninput = () => { texts.set(item.id, text.value); send.disabled = !text.value.trim(); shared?.onInput(); }; text.onblur = () => { if (!text.value.trim() && !busy()) { texts.delete(item.id); editor.hidden = true; block.hidden = false; shared?.onInput(); } }; text.onkeydown = (e) => { if (e.key === "Enter" && !e.shiftKey && !e.isComposing) { e.preventDefault(); go(); } }; return h("div", { class: "text-entry" }, block, editor); }; const renderActions = () => { switch (item.kind) { case "report": { const ok = h("button", { type: "button", class: "primary" }, "OK"); ok.onclick = () => void submit({ item_id: item.id, type: "dismiss" }); replace(actions, h("div", { class: "ack" }, ok)); return; } case "question": { const placeholder = "Your answer, a question back, or new instructions"; if (!multiple) { replace(actions, textEntry("Other…", placeholder)); return; } const send = h("button", { type: "button", class: "primary" }, "Submit"); const typed = () => (texts.get(item.id) ?? "").trim(); const refresh = () => { send.disabled = !typed() && (checked.get(item.id) ?? []).length === 0; }; const go = () => { const options = checked.get(item.id) ?? []; if (options.length > 0) { void submit({ item_id: item.id, type: "option", options, text: typed() ? texts.get(item.id) : undefined }); return; } if (typed()) { void submit({ item_id: item.id, type: "other", text: texts.get(item.id) }); } }; const other = textEntry("Other…", placeholder, { onInput: refresh, onSubmit: go }); send.onclick = go; refresh(); replace(actions, other, h("div", { class: "submit-row" }, send)); return; } default: { const dismiss = h("button", { type: "button" }, "Dismiss"); dismiss.onclick = () => void submit({ item_id: item.id, type: "dismiss" }); replace(actions, h("div", { class: "reply-row" }, textEntry("Reply…", "Reply to the agent"), dismiss)); } } }; switch (item.kind) { case "question": replace(body, markdown(String(p["context"]), "md context", { choice, taskId: item.task_id }), markdown(String(p["question"]), "md question", { taskId: item.task_id }), h("p", { class: "why" }, h("strong", {}, "Blocked on: "), inlineMarkdown(String(p["why_blocked"]), { taskId: item.task_id })), attachments(p["attachments"] ?? [], item.task_id)); break; case "report": replace(body, markdown(String(p["text"] ?? ""), "md summary", { taskId: item.task_id })); break; case "session_error": replace(body, h("p", { class: "error-text" }, String(p["error"]))); break; case "rate_limited": { const limit = (p["rate_limit"] ?? {}); const resets = typeof limit["resetsAt"] === "number" ? new Date(limit["resetsAt"] * 1000).toLocaleString() : "unknown"; replace(body, h("p", { class: "error-text" }, String(p["error"])), h("p", {}, `${p["vendor"]} ${limit["rateLimitType"] ?? ""} limit resets ${resets}`)); break; } } renderActions(); return el; } function thread(item) { if (item.responses.length === 0) { return null; } return h("ul", { class: "thread" }, ...item.responses.map((r) => h("li", {}, h("strong", {}, r.user), ` ${r.type.replace("_", " ")}`, r.options ? `: ${r.options.join("; ")}` : "", r.text ? ` — ${r.text}` : ""))); } function attachments(list, taskId) { if (list.length === 0) { return null; } return h("ul", { class: "attachments" }, ...list.map((a) => { const caption = a.caption ? h("div", { class: "caption" }, a.caption) : null; switch (a.kind) { case "file": { const lines = a.start_line ? `:${a.start_line}${a.end_line ? `-${a.end_line}` : ""}` : ""; return h("li", {}, h("code", {}, `${a.path ?? ""}${lines}`), caption); } case "diff": return h("li", {}, "diff ", h("code", {}, a.ref ?? ""), caption); case "image": return h("li", {}, a.image || a.path ? image(a.image || a.path, a.caption ?? "", taskId) : null, caption); case "diagram": return h("li", {}, diagram(a.mermaid ?? ""), caption); case "task": return h("li", {}, h("a", { href: `#/task/${a.task_id}` }, "task ", a.task_id ?? ""), caption); case "snapshot": return h("li", {}, "snapshot ", h("code", {}, a.snapshot_id ?? ""), caption); case "url": return h("li", {}, safeLink(a.url ?? ""), caption); } })); } function safeLink(url) { const ok = /^https?:\/\//i.test(url); return ok ? h("a", { href: url, target: "_blank", rel: "noopener noreferrer" }, url) : h("code", {}, url); } //# sourceMappingURL=inbox.js.map