agent page: inbox view of last 30 messages addressed to this agent

new wire request AgentRequest::Recent { limit } / ManagerRequest::Recent
(plus matching responses with Vec<InboxRow>). InboxRow moved to
hive-sh4re so it lives on both surfaces without an internal-to-wire
conversion. host-side dispatch in agent_server / manager_server
calls broker.recent_for(name, limit).

per-agent web_ui /api/state grew an inbox: Vec<InboxRow> populated
via the same per-agent socket (best-effort; transport failure
returns empty). frontend renders as a collapsible <details> section
between the state row and the terminal — fmt timestamp / from /
body in a tight grid, capped at 16em scrollable. only visible when
there are rows.
This commit is contained in:
müde 2026-05-15 20:32:19 +02:00
parent bd7d2d4860
commit 538e0446d7
13 changed files with 151 additions and 20 deletions

View file

@ -130,6 +130,42 @@ pre.diff {
align-items: center;
gap: 0.6em;
}
/* Per-agent inbox section collapsible, dim, lives between the
state row and the terminal so the operator can peek at what
landed without scrolling through the live tail. */
.agent-inbox {
margin: 0.4em 0;
font-size: 0.85em;
color: var(--muted);
}
.agent-inbox > summary {
cursor: pointer;
letter-spacing: 0.05em;
list-style: none;
}
.agent-inbox > summary::marker { content: ''; }
.agent-inbox[open] > summary > span::before { content: ''; }
.agent-inbox ul {
list-style: none;
padding: 0.4em 0.8em;
margin: 0.3em 0 0;
background: rgba(255, 255, 255, 0.02);
border-left: 2px solid var(--purple-dim);
max-height: 16em;
overflow-y: auto;
}
.agent-inbox li {
padding: 0.15em 0;
display: grid;
grid-template-columns: auto auto auto 1fr;
gap: 0.5em;
align-items: baseline;
}
.agent-inbox .inbox-ts { color: var(--muted); font-size: 0.9em; }
.agent-inbox .inbox-from { color: var(--amber); }
.agent-inbox .inbox-sep { color: var(--muted); }
.agent-inbox .inbox-body { color: var(--fg); white-space: pre-wrap; word-break: break-word; }
.last-turn {
color: var(--muted);
font-size: 0.8em;

View file

@ -337,6 +337,30 @@
}
renderStateBadge();
}
function renderInbox(rows) {
const root = $('inbox-section');
const list = $('inbox-list');
const summary = $('inbox-summary');
if (!root || !list || !summary) return;
if (!rows.length) {
root.hidden = true;
return;
}
root.hidden = false;
summary.textContent = 'inbox · ' + rows.length;
list.innerHTML = '';
const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(5, 19);
for (const m of rows) {
const li = el('li');
li.append(
el('span', { class: 'inbox-ts' }, fmt(m.at)), ' ',
el('span', { class: 'inbox-from' }, m.from), ' ',
el('span', { class: 'inbox-sep' }, '→'), ' ',
el('span', { class: 'inbox-body' }, m.body),
);
list.append(li);
}
}
function renderLastTurn(ms) {
const el_ = $('last-turn');
if (!el_) return;
@ -386,6 +410,7 @@
const s = await resp.json();
if (!headerSet) { setHeader(s.label, s.dashboard_port); headerSet = true; }
renderTermInput(s.label, s.status === 'online');
renderInbox(s.inbox || []);
// Drive the state badge from the harness status. Live SSE events
// override to 'thinking' / 'idle' as turns start/end; this only
// kicks in for the not-online (offline) case and the initial seed.

View file

@ -19,6 +19,11 @@
<button type="button" id="cancel-btn" class="btn-cancel-turn" hidden>■ cancel turn</button>
</div>
<details id="inbox-section" class="agent-inbox" hidden>
<summary><span id="inbox-summary">inbox</span></summary>
<ul id="inbox-list"></ul>
</details>
<div class="terminal-wrap">
<div id="live" class="live terminal"><div class="meta">connecting…</div></div>
<div id="term-input" class="term-input"></div>