diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 5aad6263..d23fc9c7 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -1070,13 +1070,13 @@ all no-ops and the layout reads like a plain flat list. ## Selection bar -Bulk actions (`R3ST4RT` / `ST0P` / `ST4RT` / `R3BU1LD` / `DESTR0Y` / -`PURG3`) live here rather than as per-card buttons — see **Container -row** above. Clicking an agent's icon toggles its selection (an -in-memory `Set`); `Esc` or the bar's `✕ clear` button drops -everything. The selection persists across tab switches in-memory — -the bar just hides on non-SW4RM tabs since other tabs don't show the -agent cards needed to cross-reference. +Bulk actions (`R3ST4RT` / `ST0P` / `ST4RT` / `P4US3` / `R3SUM3` / +`R3BU1LD` / `DESTR0Y` / `PURG3`) live here rather than as per-card +buttons — see **Container row** above. Clicking an agent's icon +toggles its selection (an in-memory `Set`); `Esc` or the bar's +`✕ clear` button drops everything. The selection persists across tab +switches in-memory — the bar just hides on non-SW4RM tabs since other +tabs don't show the agent cards needed to cross-reference. When one or more agents are selected (via icon click), a sticky frosted-mauve bar slides up from the bottom of the viewport @@ -1089,6 +1089,9 @@ frosted-mauve bar slides up from the bottom of the viewport - `↺ R3ST4RT` — running agents only - `■ ST0P` — running agents only - `▶ ST4RT` — stopped agents only + - `⏸ P4US3` — unpaused agents only (orthogonal to running/stopped — + see **Container row**'s own pause/resume note) + - `▶ R3SUM3` — paused agents only - `↻ R3BU1LD` — always available - `DESTR0Y` / `PURG3` — always available - `⇡ M0V3 → ROOT` — promote selected agents to top-level diff --git a/frontend/packages/dashboard/src/common.css b/frontend/packages/dashboard/src/common.css index dffb99d1..f3c5df99 100644 --- a/frontend/packages/dashboard/src/common.css +++ b/frontend/packages/dashboard/src/common.css @@ -152,6 +152,12 @@ code { .btn-restart { color: var(--cyan); border-color: var(--cyan); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; } .btn-stop { color: var(--pink); border-color: var(--pink); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; } .btn-start { color: var(--green); border-color: var(--green); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; } +/* Same yellow as .badge-paused (the paused-state pill) — pause enters + that state, so the trigger and the resulting badge read as one + colour, not two unrelated ones. Resume reads as green like + .btn-start: both are "go" actions, back to the turn loop running. */ +.btn-pause { color: var(--yellow); border-color: var(--yellow); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; } +.btn-resume { color: var(--green); border-color: var(--green); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; } /* M0V3 affordance (selection bar) — mauve reads as "structural change" rather than the destructive red / amber chrome of destroy / rebuild. See docs/web-ui.md::Selection bar. */ diff --git a/frontend/packages/dashboard/src/swarm.js b/frontend/packages/dashboard/src/swarm.js index 6c4d2e3a..80937f63 100644 --- a/frontend/packages/dashboard/src/swarm.js +++ b/frontend/packages/dashboard/src/swarm.js @@ -783,9 +783,19 @@ export function renderSelectionBar(containers) { actions.replaceChildren(); const allRunning = selected.every((c) => c.running); const allStopped = selected.every((c) => !c.running); + // Pause/resume is orthogonal to running (see hive-agent-menu.js's own + // comment on the per-agent version of these two actions) — a paused + // stopped agent boots paused, a paused running agent keeps its + // container but drives no turns. Same all-or-nothing enablement rule + // as every other bulk action here: mixed pause state disables the + // button rather than silently no-op'ing on part of the selection. + const allUnpaused = selected.every((c) => !c.paused); + const allPaused = selected.every((c) => c.paused); const stoppedNames = selected.filter((c) => !c.running).map((c) => c.name); const runningNames = selected.filter((c) => c.running).map((c) => c.name); + const pausedNames = selected.filter((c) => c.paused).map((c) => c.name); + const unpausedNames = selected.filter((c) => !c.paused).map((c) => c.name); function why(label, blockers) { if (!blockers.length) return null; @@ -811,6 +821,16 @@ export function renderSelectionBar(containers) { confirm: (names) => `start ${names.length} agent${names.length === 1 ? '' : 's'} (${names.join(', ')})?`, disabledTitle: why('▶ ST4RT', runningNames.map((n) => `\`${n}\` is already running`)), }); + addBulkButton(actions, 'btn-pause', '⏸ P4US3', allUnpaused, selected, { + action: '/api/pause/', + confirm: (names) => `pause ${names.length} agent${names.length === 1 ? '' : 's'} (${names.join(', ')})? parks the turn loop — inbox messages queue unacked.`, + disabledTitle: why('⏸ P4US3', pausedNames.map((n) => `\`${n}\` is already paused`)), + }); + addBulkButton(actions, 'btn-resume', '▶ R3SUM3', allPaused, selected, { + action: '/api/resume/', + confirm: (names) => `resume ${names.length} agent${names.length === 1 ? '' : 's'} (${names.join(', ')})? the turn loop restarts and drains queued messages.`, + disabledTitle: why('▶ R3SUM3', unpausedNames.map((n) => `\`${n}\` is not paused`)), + }); addBulkButton(actions, 'btn-rebuild', '↻ R3BU1LD', true, selected, { action: '/api/rebuild/', confirm: (names) => `rebuild ${names.length} agent${names.length === 1 ? '' : 's'} (${names.join(', ')})? hot-reloads each container.`,