From fe2f9c2396859e65640aaf83e834c3012b5fda85 Mon Sep 17 00:00:00 2001 From: iris Date: Fri, 29 May 2026 13:11:19 +0200 Subject: [PATCH] agent: "mark all read" button on the inbox flyout (#559 frontend half) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pairs with damocles PR #566 (broker primitive + dashboard `POST /api/agent/{name}/mark-all-read` route). The agent's per-container inbox side-panel now gets a header row with a `✓ mark all read` button that: - confirms via a one-line dialog (the action is destructive: any pending broker message for this agent is acked, the harness won't receive a wake-prompt for them) - POSTs to the host dashboard (cross-origin, same pattern as the existing operator-answer flow on this page) - surfaces `{ marked: N }` in an inline status pill, then triggers a `refreshState` so any state-derived surfaces re-read fresh - stays out of the way when the inbox is empty (only renders above a non-empty rows list) Note: `recent_for` returns the most-recent-N messages regardless of ack state, so clicking does NOT visually empty the rows list. The status pill ("✓ marked N as read") is the operator-facing confirmation; the next `turn_start` will show `0 unread` in its badge. Tooltip on the button calls this out so the operator isn't surprised the row list stays put. CSS mirrors the existing answer-form button family (mauve hover on bg-elev background) so it reads as a peer affordance, with a border-bottom separating it from the message list. --- frontend/packages/agent/src/agent.css | 36 +++++++++++++++ frontend/packages/agent/src/app.js | 64 +++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/frontend/packages/agent/src/agent.css b/frontend/packages/agent/src/agent.css index cd91696d..abd1ba1c 100644 --- a/frontend/packages/agent/src/agent.css +++ b/frontend/packages/agent/src/agent.css @@ -522,6 +522,42 @@ pre.diff { .agent-inbox .answer-form button:disabled { opacity: 0.5; cursor: default; } .agent-inbox .answer-status { color: var(--muted); align-self: center; } +/* #559: "mark all read" header row sits above the recent-messages + list in the inbox side-panel flyout. Same look as the answer-form + button (mauve hover, bg-elev background) so they read as part of + the same affordance family. */ +.agent-inbox .inbox-mark-all-row { + display: flex; + gap: 0.6em; + align-items: center; + padding: 0.4em 0.2em 0.6em; + margin-bottom: 0.5em; + border-bottom: 1px solid var(--border); +} +.agent-inbox .inbox-mark-all-btn { + font-family: inherit; + font-size: inherit; + background: var(--bg-elev); + color: var(--fg); + border: 1px solid var(--border); + border-radius: 3px; + padding: 0.3em 0.7em; + cursor: pointer; + white-space: nowrap; +} +.agent-inbox .inbox-mark-all-btn:hover:not(:disabled) { + border-color: var(--purple); + color: var(--purple); +} +.agent-inbox .inbox-mark-all-btn:disabled { + opacity: 0.5; + cursor: default; +} +.agent-inbox .inbox-mark-status { + color: var(--muted); + font-size: 0.9em; +} + .last-turn { color: var(--muted); font-size: 0.8em; diff --git a/frontend/packages/agent/src/app.js b/frontend/packages/agent/src/app.js index d82ecce1..88e49215 100644 --- a/frontend/packages/agent/src/app.js +++ b/frontend/packages/agent/src/app.js @@ -706,6 +706,58 @@ window.marked = marked; return wrap; } + /** #559: "mark all read" affordance for the agent's inbox flyout. + * Returns a DOM row containing a button + an inline status pill. + * POSTs to the host dashboard's `/api/agent/{name}/mark-all-read` + * (damocles PR #566) and surfaces the `{ marked: N }` count back to + * the operator. Re-runs `onCleared` on success so the caller can + * refresh whatever state it owns. */ + function buildInboxMarkAllRow(label, onCleared) { + const status = el('span', { class: 'inbox-mark-status' }); + const btn = el('button', { + type: 'button', + class: 'inbox-mark-all-btn', + title: 'mark every queued message for this agent as read — ' + + 'drains the host broker\'s pending + delivered-unacked rows. ' + + 'history shown here is the most-recent-N regardless of state, ' + + 'so the list itself stays visible.', + }, '✓ mark all read'); + btn.addEventListener('click', async () => { + if (!dashboardBase) { + status.textContent = 'dashboard url unknown'; + return; + } + if (!label) { + status.textContent = 'agent label unknown'; + return; + } + if (!window.confirm( + `mark every queued message for ${label} as read? ` + + `the message history shown stays; only the unread queue is drained.` + )) return; + btn.disabled = true; + status.textContent = 'clearing…'; + try { + const resp = await fetch( + dashboardBase + 'api/agent/' + encodeURIComponent(label) + '/mark-all-read', + { method: 'POST' }); + if (resp.ok) { + const data = await resp.json().catch(() => ({})); + const n = Number(data.marked) || 0; + status.textContent = '✓ marked ' + n + ' as read'; + if (typeof onCleared === 'function') onCleared(); + } else { + status.textContent = 'failed: http ' + resp.status; + btn.disabled = false; + } + } catch (err) { + status.textContent = 'failed: ' + err; + btn.disabled = false; + } + }); + return el('div', { class: 'inbox-mark-all-row' }, btn, status); + } + function buildInboxList(rows) { const wrap = el('div', { class: 'agent-inbox' }); if (!rows.length) { @@ -713,6 +765,18 @@ window.marked = marked; 'inbox empty.')); return wrap; } + // #559: "mark all read" header row drains the host broker's + // pending + delivered-unacked rows for this agent (damocles PR + // #566). Visible rows here are the most-recent-N regardless of + // ack state, so the list itself doesn't visually empty on click — + // the status pill confirms the drain count, and the next + // turn_start's "unread" badge will read zero. + wrap.append(buildInboxMarkAllRow(currentLabel, () => { + // Refresh state so any UI surface that DOES depend on + // delivery state (eg future per-status filters) picks up + // the new shape. + refreshState(); + })); const list = el('ul'); const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(5, 19); for (const m of rows) {