--- title: Agent Inbox --- ```js import {API, apiFetch, pollDelay, waitForVisible} from "./components/config.js"; ``` ```js // Live poll: messages list const inboxState = (async function*() { let failures = 0; while (true) { let messages = [], notices = [], ok = false; try { const resp = await apiFetch("/messages/?limit=100"); ok = resp.ok; if (ok) messages = await resp.json(); const nresp = await apiFetch("/messages/notices"); if (nresp.ok) notices = await nresp.json(); } catch {} failures = ok ? 0 : failures + 1; yield {messages, notices, ok, ts: new Date()}; await waitForVisible(pollDelay({ok, failures})); } })(); ``` ```js const messages = inboxState.messages ?? []; const notices = inboxState.notices ?? []; const _ok = inboxState.ok ?? false; const _ts = inboxState.ts; // Broadcast read state is per reader (STATE-WP-0093): the dashboard has no // reader identity, so live broadcasts get their own section. const isBc = m => m.to_agent === "broadcast"; const unread = messages.filter(m => !isBc(m) && !m.read_at && !m.archived_at); const read = messages.filter(m => !isBc(m) && m.read_at && !m.archived_at); const broadcasts = messages.filter(m => isBc(m) && !m.archived_at); const archived = messages.filter(m => m.archived_at); // Group unread by agent for KPI const agentCounts = {}; for (const m of unread) { agentCounts[m.to_agent] = (agentCounts[m.to_agent] ?? 0) + 1; } const topAgents = Object.entries(agentCounts).sort((a, b) => b[1] - a[1]).slice(0, 4); ``` # Agent Inbox ```js import {injectTocTop} from "./components/toc-sidebar.js"; import {withDocHelp} from "./components/doc-overlay.js"; const _kpiBox = html`
Inbox
unread
${unread.length}
standing notices
${notices.length}
total
${messages.length}
${topAgents.map(([agent, count]) => html`
${agent}
${count}
`)}
`; const _liveEl = html`
● ${_ok ? `Live · updated ${_ts?.toLocaleTimeString()}` : html`Offline — run: make api`}
`; injectTocTop("inbox-kpi-box", _kpiBox); injectTocTop("live-indicator", _liveEl); ``` Inter-agent coordination messages. Agents send messages via `send_message()` MCP tool and read them via `get_messages()`. --- ## Standing notices Live `standing` broadcasts stay in every agent's inbox until that agent acknowledges them (`POST /messages/{id}/ack`). Repositories listed as not yet acknowledged are the ones still out of date. ```js function repoList(label, slugs, color) { if (!slugs.length) return ""; return html`
${label} (${slugs.length})
${slugs.join(", ")}
`; } if (notices.length === 0) { display(html`

No live standing notices.

`); } else { display(html`
${notices.map(n => html`
${n.from_agent} standing expires ${n.expires_at ? new Date(n.expires_at).toLocaleDateString() : "never"}
${n.subject}
${n.acked_count} acknowledged · ${n.delivered_only_count} delivered, not acknowledged · ${n.unreached_count} not reached
${repoList("Not yet acknowledged — delivered", n.delivered_only, "#d97706")} ${repoList("Not yet acknowledged — never reached", n.unreached, "#dc2626")} ${repoList("Acknowledged", n.acknowledged, "#059669")}
`)}
`); } ``` --- ## Unread ```js function fmtDate(s) { if (!s) return "—"; const d = new Date(s); return d.toLocaleDateString() + " " + d.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); } function renderMessage(m, showMarkRead = false) { const isBroadcast = m.to_agent === "broadcast"; const borderColor = isBroadcast ? "#7c3aed" : (!m.read_at ? "#d97706" : "#6b7280"); async function onMarkRead() { await fetch(`${API}/messages/${m.id}/read`, {method: "PATCH"}); } async function onArchive() { await fetch(`${API}/messages/${m.id}/archive`, {method: "PATCH"}); } return html`
${m.from_agent} → ${m.to_agent} ${isBroadcast ? html`${m.kind}` : ""} ${fmtDate(m.created_at)}
${showMarkRead && !isBroadcast ? html`` : ""}
${m.subject}
body
${m.body}
${m.thread_id ? html`
thread: ${m.thread_id}
` : ""}
`; } if (unread.length === 0) { display(html`

No unread messages.

`); } else { display(html`
${unread.map(m => renderMessage(m, true))}
`); } ``` --- ## Broadcasts ```js if (broadcasts.length === 0) { display(html`

No live broadcasts.

`); } else { display(html`
${broadcasts.map(m => renderMessage(m, false))}
`); } ``` --- ## Read ```js if (read.length === 0) { display(html`

No read messages.

`); } else { display(html`
${read.map(m => renderMessage(m, false))}
`); } ``` --- ## Archived ```js if (archived.length === 0) { display(html`

No archived messages.

`); } else { display(html`
${archived.map(m => renderMessage(m, false))}
`); } ```