generated from coulomb/repo-seed
T4: workstreams.md and dependencies.md now call /state/deps instead of the
full /state/summary — removes 2 heavy 10-table queries per 60 s cycle.
T5: index.md's 4 independent polling loops (summaryState, sbomSnapState,
regsState, wsChartState) consolidated into a single pageState generator
with one Promise.all batch and a shared backoff counter.
T6: config.js gains waitForVisible(ms) — pauses polling entirely while the
tab is hidden and fires immediately on visibilitychange. pollDelay()
simplified (hidden-tab POLL_HIDDEN logic removed). All 16 polling pages
migrated from await sleep(pollDelay(...)) to await waitForVisible(pollDelay(...)).
CUST-WP-0039 complete — all 6 tasks done.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.7 KiB
6.7 KiB
title
| title |
|---|
| Agent Inbox |
import {API, apiFetch, pollDelay, waitForVisible} from "./components/config.js";
// Live poll: messages list
const inboxState = (async function*() {
let failures = 0;
while (true) {
let messages = [], ok = false;
try {
const resp = await apiFetch("/messages/?limit=100");
ok = resp.ok;
if (ok) messages = await resp.json();
} catch {}
failures = ok ? 0 : failures + 1;
yield {messages, ok, ts: new Date()};
await waitForVisible(pollDelay({ok, failures}));
}
})();
const messages = inboxState.messages ?? [];
const _ok = inboxState.ok ?? false;
const _ts = inboxState.ts;
const unread = messages.filter(m => !m.read_at && !m.archived_at);
const read = messages.filter(m => m.read_at && !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
import {injectTocTop} from "./components/toc-sidebar.js";
import {withDocHelp} from "./components/doc-overlay.js";
const _kpiBox = html`<div class="kpi-infobox">
<div class="kpi-infobox-title">Inbox</div>
<div class="kpi-row">
<span class="kpi-row-label">unread</span>
<div class="kpi-row-right">
<div class="kpi-row-value" style="color:${unread.length > 0 ? '#d97706' : 'inherit'}">${unread.length}</div>
</div>
</div>
<div class="kpi-row">
<span class="kpi-row-label">total</span>
<div class="kpi-row-right">
<div class="kpi-row-value" style="font-size:1rem">${messages.length}</div>
</div>
</div>
${topAgents.map(([agent, count]) => html`
<div class="kpi-row">
<span class="kpi-row-label">${agent}</span>
<div class="kpi-row-right">
<div class="kpi-row-value" style="font-size:0.9rem">${count}</div>
</div>
</div>`)}
</div>`;
const _liveEl = html`<div class="live-indicator">
<span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
${_ok
? `Live · updated ${_ts?.toLocaleTimeString()}`
: html`<span style="color:red">Offline — run: <code>make api</code></span>`}
</div>`;
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().
Unread
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 = !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`<div class="msg-card" style="border-left-color:${borderColor}">
<div class="msg-header">
<span class="msg-from">${m.from_agent}</span>
<span class="msg-arrow">→</span>
<span class="msg-to ${isBroadcast ? 'msg-broadcast' : ''}">${m.to_agent}</span>
<span class="msg-time">${fmtDate(m.created_at)}</span>
<div class="msg-actions">
${showMarkRead ? html`<button class="msg-btn msg-btn-read" onclick=${onMarkRead}>Mark read</button>` : ""}
<button class="msg-btn msg-btn-archive" onclick=${onArchive}>Archive</button>
</div>
</div>
<div class="msg-subject">${m.subject}</div>
<details class="msg-body-wrap">
<summary>body</summary>
<pre class="msg-body">${m.body}</pre>
</details>
${m.thread_id ? html`<div class="msg-thread">thread: ${m.thread_id}</div>` : ""}
</div>`;
}
if (unread.length === 0) {
display(html`<p class="dim">No unread messages.</p>`);
} else {
display(html`<div class="msg-list">${unread.map(m => renderMessage(m, true))}</div>`);
}
Read
if (read.length === 0) {
display(html`<p class="dim">No read messages.</p>`);
} else {
display(html`<div class="msg-list">${read.map(m => renderMessage(m, false))}</div>`);
}
Archived
if (archived.length === 0) {
display(html`<p class="dim">No archived messages.</p>`);
} else {
display(html`<div class="msg-list">${archived.map(m => renderMessage(m, false))}</div>`);
}