feat(dashboard): add paused badge and pause/resume toggle to agent cards

When ContainerView.paused is true, show a clickable yellow `⏸ paused`
badge on the agent card that POSTs to the new /api/resume/{name} endpoint
to un-park the turn loop. The badge doubles as the resume button so the
state is self-documenting and one click to fix.

The agent action menu gains ⏸ P4US3 (when not paused) and ▶ R3SUM3
(when paused), orthogonal to the running/stopped start/stop actions.

On the backend, /api/pause/{name} and /api/resume/{name} POST routes
wire to Coordinator::set_paused and trigger an immediate rescan so the
badge flips via the existing SSE ContainerUpdate without polling.

Depends on the ContainerView.paused field and Coordinator::set_paused
added in the parent PR.
This commit is contained in:
iris 2026-07-26 02:52:45 +02:00 committed by mara
commit 59041d4f03
4 changed files with 80 additions and 0 deletions

View file

@ -81,6 +81,12 @@ code {
color: var(--purple); border-color: var(--purple);
text-shadow: 0 0 6px color-mix(in srgb, var(--purple) 40%, transparent);
}
/* Paused agent: turn loop parked, container still up. Clickable (btn-inline):
clicking sends POST /api/resume/{name} so the badge doubles as a resume button. */
.badge-paused {
color: var(--yellow); border-color: var(--yellow);
background: color-mix(in srgb, var(--yellow) 10%, transparent);
}
/* Active Claude model badge on dashboard container rows. */
.badge-model {
color: var(--blue); border-color: var(--blue);

View file

@ -307,7 +307,19 @@ function buildAgentMenu(c, forgeBase) {
menuItem('▶ ST4RT', { action: '/api/start/', confirm: `start ${c.name}?` }),
);
}
// Pause/resume is orthogonal to running: a paused stopped agent boots
// paused; a paused running agent keeps its container but drives no turns.
if (c.paused) {
dropdown.append(
menuItem('▶ R3SUM3', { action: '/api/resume/', confirm: `resume ${c.name}? the turn loop restarts and drains queued messages.` }),
);
} else {
dropdown.append(
menuItem('⏸ P4US3', { action: '/api/pause/', confirm: `pause ${c.name}? parks the turn loop — inbox messages queue unacked.` }),
);
}
dropdown.append(
menuSep(),
menuItem('↻ R3BU1LD', { action: '/api/rebuild/', confirm: `rebuild ${c.name}? hot-reloads the container.` }),
menuSep(),
// Deep-link to the AGENT log tab pre-filtered to this container.
@ -457,6 +469,7 @@ function containerRowFingerprint(c, node, pending, opRunning, selected,
askerCount, targetCount, gatewayLinks, hostname) {
return JSON.stringify({
running: c.running,
paused: c.paused,
needs_login: c.needs_login,
needs_update: c.needs_update,
active_model: c.active_model,
@ -657,6 +670,15 @@ function buildContainerLi(c, node, opts) {
{ class: 'badge badge-warn', href: url, target: '_blank', rel: 'noopener' },
'needs login →'));
}
if (c.paused) {
// Paused badge is also a resume button: clicking POSTs /api/resume/{name}
// which removes the marker and flips the badge off via the SSE rescan.
head.append(form(
'/api/resume/' + c.name, 'badge badge-paused btn-inline', '⏸ paused',
`resume ${c.name}? the turn loop restarts and drains queued messages.`,
{}, { noRefresh: true },
));
}
if (c.needs_update) {
head.append(form(
'/api/rebuild/' + c.name, 'badge badge-warn btn-inline', 'needs update ↻',