import { post } from "./api.js"; import { ago, errorText, h, replace } from "./dom.js"; import { findTask, onEach, state } from "./state.js"; import { reportCard } from "./task.js"; const drafts = new Map(); const modes = new Map(); let scope = "all"; export function mountInbox(root) { const tabs = h("nav", { class: "scope-tabs" }); const list = h("div", { class: "inbox-list" }); const submitBar = h("div", { class: "submit-bar" }); const cards = new Map(); const visible = () => { const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined; if (scope === "task" && task) { return state.inbox.filter((i) => i.task_id === task.id); } if (scope === "project" && task) { return state.inbox.filter((i) => i.project_id === task.project_id); } return state.inbox; }; const renderTabs = () => { const hasTask = state.selectedTaskId !== undefined; const tab = (s, label) => { const b = h("button", { class: s === scope ? "active" : "", disabled: s !== "all" && !hasTask }, label); b.onclick = () => { scope = s; render(); }; return b; }; replace(tabs, tab("all", "All"), tab("project", "This project"), tab("task", "This task")); }; const renderSubmit = () => { const ready = [...drafts.values()].filter((d) => state.inbox.some((i) => i.id === d.item_id)); if (ready.length === 0) { replace(submitBar); return; } const button = h("button", { class: "primary" }, `Submit ${ready.length} ${ready.length === 1 ? "response" : "responses"}`); button.onclick = async () => { button.disabled = true; try { await post("/api/inbox/responses", { body: { responses: ready } }); for (const d of ready) { drafts.delete(d.item_id); modes.delete(d.item_id); } } catch (err) { alert(errorText(err)); } button.disabled = false; render(); }; replace(submitBar, button); }; const render = () => { if (scope !== "all" && !state.selectedTaskId) { scope = "all"; } renderTabs(); const items = visible(); const keep = new Set(items.map((i) => i.id)); for (const id of cards.keys()) { if (!keep.has(id)) { cards.delete(id); } } for (const id of drafts.keys()) { if (!state.inbox.some((i) => i.id === id)) { drafts.delete(id); modes.delete(id); } } const els = items.map((item) => { const sig = JSON.stringify(item); const cached = cards.get(item.id); if (cached && cached.sig === sig) { return cached.el; } const el = card(item, () => { renderSubmit(); }); cards.set(item.id, { sig, el }); return el; }); replace(list, ...(els.length > 0 ? els : [h("div", { class: "empty" }, "Nothing waiting on you.")])); renderSubmit(); }; const off = onEach(["inbox", "selection", "tasks"], render); replace(root, h("div", { class: "inbox-view" }, tabs, submitBar, list)); render(); return off; } function card(item, changed) { const task = findTask(item.task_id); 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("a", { href: `#/task/${item.task_id}`, class: "card-task" }, task?.title ?? item.task_id), h("time", { dateTime: item.created_at, title: item.created_at }, ago(item.created_at))), body, thread(item), actions); const allowed = item.kind === "question" ? ["option", "free_form", "question", "redirect", "snooze"] : ["dismiss", "redirect", "snooze"]; const labels = { option: "Pick", free_form: "Other answer", question: "Ask back", redirect: "Redirect", snooze: "Snooze", dismiss: "Dismiss", }; const renderActions = () => { const mode = modes.get(item.id) ?? allowed[0]; const tabs = h("div", { class: "modes" }, ...allowed.map((m) => { const b = h("button", { type: "button", class: m === mode ? "active" : "" }, labels[m]); b.onclick = () => { modes.set(item.id, m); drafts.delete(item.id); if (m === "dismiss") { drafts.set(item.id, { item_id: item.id, type: "dismiss" }); } renderActions(); changed(); }; return b; })); replace(actions, tabs, modeInput(item, mode, changed)); }; switch (item.kind) { case "question": replace(body, h("p", { class: "question" }, String(p["question"])), h("p", { class: "why" }, h("strong", {}, "Blocked on: "), String(p["why_blocked"])), attachments(p["attachments"] ?? [])); break; case "report": replace(body, reportCard(p)); 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 modeInput(item, mode, changed) { const draft = drafts.get(item.id); const set = (d) => { if (d) { drafts.set(item.id, d); } else { drafts.delete(item.id); } changed(); }; switch (mode) { case "option": { const p = item.payload; const options = p["options"] ?? []; const note = h("input", { placeholder: "Optional note", value: draft?.text ?? "" }); const choose = (option) => set({ item_id: item.id, type: "option", option, text: note.value || undefined }); note.oninput = () => { const current = drafts.get(item.id); if (current?.type === "option" && current.option) { choose(current.option); } }; return h("div", { class: "options" }, ...options.map((option) => { const radio = h("input", { type: "radio", name: `opt-${item.id}`, checked: drafts.get(item.id)?.option === option }); radio.onchange = () => choose(option); return h("label", { class: "option" }, radio, " ", option, option === p["recommended_option"] && h("span", { class: "recommended" }, "recommended")); }), note); } case "free_form": case "question": case "redirect": { const placeholder = { free_form: "Your answer", question: "Your question back to the agent", redirect: "New instructions for the agent", }[mode]; const text = h("textarea", { rows: 3, placeholder, value: draft?.type === mode ? (draft.text ?? "") : "" }); text.oninput = () => set(text.value.trim() ? { item_id: item.id, type: mode, text: text.value } : undefined); return text; } case "snooze": { const presets = [ ["1 hour", () => new Date(Date.now() + 3600_000)], ["4 hours", () => new Date(Date.now() + 4 * 3600_000)], [ "Tomorrow 9am", () => { const d = new Date(); d.setDate(d.getDate() + 1); d.setHours(9, 0, 0, 0); return d; }, ], ]; return h("div", { class: "snooze" }, ...presets.map(([label, when]) => { const radio = h("input", { type: "radio", name: `snooze-${item.id}` }); radio.onchange = () => set({ item_id: item.id, type: "snooze", snooze_until: when().toISOString() }); return h("label", {}, radio, " ", label); })); } case "dismiss": return h("p", { class: "hint" }, "Will be dismissed when you submit."); } } 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.option ? `: ${r.option}` : "", r.text ? ` — ${r.text}` : ""))); } function attachments(list) { 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?.startsWith("data:image/") ? h("img", { src: a.image, alt: a.caption ?? "" }) : null, caption); case "diagram": return h("li", {}, h("pre", { class: "mermaid-source" }, 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