agent: "mark all read" button on the inbox flyout (#559 frontend half)
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.
This commit is contained in:
parent
11db710ddf
commit
fe2f9c2396
2 changed files with 100 additions and 0 deletions
|
|
@ -522,6 +522,42 @@ pre.diff {
|
||||||
.agent-inbox .answer-form button:disabled { opacity: 0.5; cursor: default; }
|
.agent-inbox .answer-form button:disabled { opacity: 0.5; cursor: default; }
|
||||||
.agent-inbox .answer-status { color: var(--muted); align-self: center; }
|
.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 {
|
.last-turn {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
|
|
|
||||||
|
|
@ -706,6 +706,58 @@ window.marked = marked;
|
||||||
return wrap;
|
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) {
|
function buildInboxList(rows) {
|
||||||
const wrap = el('div', { class: 'agent-inbox' });
|
const wrap = el('div', { class: 'agent-inbox' });
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
|
|
@ -713,6 +765,18 @@ window.marked = marked;
|
||||||
'inbox empty.'));
|
'inbox empty.'));
|
||||||
return wrap;
|
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 list = el('ul');
|
||||||
const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(5, 19);
|
const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(5, 19);
|
||||||
for (const m of rows) {
|
for (const m of rows) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue