web-ui: expose per-agent paused status, add pause/resume to the agent page's own overflow menu
hive-agent's own web_ui module never exposed the agent's own paused status to its own /api/state — the dashboard's cross-container view knew it, but a per-agent page had no way to know it's paused. Added StateSnapshot.paused (a direct stat of the same harness-local pause marker hive-c0re's Coordinator::is_paused checks). The per-agent page's ⋯ overflow menu now has a pause/resume item that POSTs to hive-c0re's existing /api/pause/<name> / /api/resume/<name> — the same endpoints the dashboard's <hive-agent-menu> already uses, same cross-origin form-submit pattern the existing rebuild-container item uses. The item's label tracks state.paused on every /api/state refresh so a pause/resume triggered from the dashboard while this page is open doesn't leave a stale action showing.
This commit is contained in:
parent
765bea2022
commit
5bd085fbac
3 changed files with 85 additions and 2 deletions
|
|
@ -67,14 +67,23 @@ through. Three flex columns:
|
||||||
(`GET /api/todos`, refreshed on cold load + every `turn_end`).
|
(`GET /api/todos`, refreshed on cold load + every `turn_end`).
|
||||||
- **Overflow button** (`⋯`): always visible. Opens a frosted popover
|
- **Overflow button** (`⋯`): always visible. Opens a frosted popover
|
||||||
(`#overflow-menu`, positioned outside the header to escape any
|
(`#overflow-menu`, positioned outside the header to escape any
|
||||||
stacking context) with four management rows followed by a model
|
stacking context) with five management rows followed by a model
|
||||||
quick-picker section: `↑ dashboard` (link), `↻ rebuild container`
|
quick-picker section: `↑ dashboard` (link), `↻ rebuild container`
|
||||||
(POST confirm, same action as the dashboard R3BU1LD button),
|
(POST confirm, same action as the dashboard R3BU1LD button),
|
||||||
`↻ new claude session` (POST confirm → `POST /api/new-session`;
|
`↻ new claude session` (POST confirm → `POST /api/new-session`;
|
||||||
next turn drops `--continue`), `🔓 logout` (POST confirm → `POST
|
next turn drops `--continue`), `🔓 logout` (POST confirm → `POST
|
||||||
/api/logout`; SIGINTs any in-flight turn, wipes OAuth credential
|
/api/logout`; SIGINTs any in-flight turn, wipes OAuth credential
|
||||||
files, flips the agent to `needs_login` — session history
|
files, flips the agent to `needs_login` — session history
|
||||||
preserved). All destructive actions require one extra click to
|
preserved), and `⏸ pause agent` / `▶ resume agent` (POST confirm →
|
||||||
|
hive-c0re's `/api/pause/<name>` / `/api/resume/<name>` — the same
|
||||||
|
endpoints the dashboard's own `<hive-agent-menu>` uses, since
|
||||||
|
pausing is a hive-c0re-owned write this unprivileged process can't
|
||||||
|
make directly). The label + target endpoint track `state.paused`
|
||||||
|
(this agent's own `/api/state`, a direct stat of the harness's
|
||||||
|
local pause marker — see `docs/persistence.md`), refreshed on every
|
||||||
|
snapshot so a pause/resume triggered from the *dashboard* while
|
||||||
|
this page is open doesn't leave the menu item showing the wrong
|
||||||
|
action. All destructive actions require one extra click to
|
||||||
acknowledge — rare ops shouldn't live in the primary state strip.
|
acknowledge — rare ops shouldn't live in the primary state strip.
|
||||||
Below a separator, a **model quick-picker** section labelled
|
Below a separator, a **model quick-picker** section labelled
|
||||||
`model` renders one button per model in the operator-configured
|
`model` renders one button per model in the operator-configured
|
||||||
|
|
|
||||||
|
|
@ -231,6 +231,41 @@ window.marked = marked;
|
||||||
});
|
});
|
||||||
menu.append(logoutBtn);
|
menu.append(logoutBtn);
|
||||||
|
|
||||||
|
// ⏸ pause / ▶ resume — this page has no pause state of its own to
|
||||||
|
// hold or mutate; it POSTs to the same hive-c0re endpoints
|
||||||
|
// (`/api/pause/<name>` / `/api/resume/<name>`) the dashboard's own
|
||||||
|
// `<hive-agent-menu>` already uses, and only needs `state.paused`
|
||||||
|
// (this agent's own `/api/state`, added alongside this menu item)
|
||||||
|
// to know which of the two to show. See `renderPausedChip`, called
|
||||||
|
// from `refreshState` on every snapshot so the label tracks reality
|
||||||
|
// even when the pause/resume actually happened from the dashboard.
|
||||||
|
const pauseBtn = el('button', {
|
||||||
|
type: 'button',
|
||||||
|
class: 'overflow-item overflow-item-pause',
|
||||||
|
role: 'menuitem',
|
||||||
|
id: 'pause-btn',
|
||||||
|
},
|
||||||
|
el('span', { class: 'overflow-item-icon', 'aria-hidden': 'true' }, '⏸'),
|
||||||
|
el('span', { id: 'pause-btn-label' }, 'pause agent'),
|
||||||
|
);
|
||||||
|
pauseBtn.addEventListener('click', async () => {
|
||||||
|
const paused = pauseBtn.dataset.paused === 'true';
|
||||||
|
const verb = paused ? 'resume' : 'pause';
|
||||||
|
const message = paused
|
||||||
|
? `resume ${label}? the turn loop restarts and drains queued messages.`
|
||||||
|
: `pause ${label}? parks the turn loop — inbox messages queue unacked.`;
|
||||||
|
if (!(await themedConfirm({
|
||||||
|
message, danger: true, confirmLabel: paused ? '▶ resume' : '⏸ pause',
|
||||||
|
}))) return;
|
||||||
|
closeOverflowMenu();
|
||||||
|
const f = document.createElement('form');
|
||||||
|
f.method = 'POST';
|
||||||
|
f.action = `${dashUrl}api/${verb}/${label}`;
|
||||||
|
document.body.appendChild(f);
|
||||||
|
f.submit();
|
||||||
|
});
|
||||||
|
menu.append(pauseBtn);
|
||||||
|
|
||||||
// ─── model quick-picker ────────────────────────────────────────
|
// ─── model quick-picker ────────────────────────────────────────
|
||||||
// One-click shortcuts for each model in `availableModels` (seeded
|
// One-click shortcuts for each model in `availableModels` (seeded
|
||||||
// from `state.available_models` / `HIVE_AVAILABLE_MODELS` nix option).
|
// from `state.available_models` / `HIVE_AVAILABLE_MODELS` nix option).
|
||||||
|
|
@ -493,6 +528,12 @@ window.marked = marked;
|
||||||
let effortPickerBtns = [];
|
let effortPickerBtns = [];
|
||||||
let availableEfforts = [];
|
let availableEfforts = [];
|
||||||
|
|
||||||
|
// Pause/resume toggle in the overflow menu — see `renderPausedChip`.
|
||||||
|
// Tracked so subsequent /api/state refreshes can flip the existing
|
||||||
|
// button's label without rebuilding the whole overflow menu (same
|
||||||
|
// reason `currentModel`/`currentEffort` are tracked above).
|
||||||
|
let currentPaused = false;
|
||||||
|
|
||||||
const SLASH_COMMANDS = [
|
const SLASH_COMMANDS = [
|
||||||
{ name: '/help', desc: 'list slash commands' },
|
{ name: '/help', desc: 'list slash commands' },
|
||||||
{ name: '/clear', desc: 'wipe the terminal panel (local-only)' },
|
{ name: '/clear', desc: 'wipe the terminal panel (local-only)' },
|
||||||
|
|
@ -1056,6 +1097,25 @@ window.marked = marked;
|
||||||
btn.setAttribute('aria-pressed', String(isActive));
|
btn.setAttribute('aria-pressed', String(isActive));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flips the overflow menu's pause/resume item to match the backend's
|
||||||
|
// reported `state.paused` (harness-local marker stat, cheap to refresh
|
||||||
|
// on every /api/state poll) — without this, an operator who pauses from
|
||||||
|
// the *dashboard* while this page is open would still see a stale
|
||||||
|
// "pause agent" item here, offering the wrong action.
|
||||||
|
function renderPausedChip(paused) {
|
||||||
|
currentPaused = !!paused;
|
||||||
|
const btn = $('pause-btn');
|
||||||
|
if (!btn) return;
|
||||||
|
btn.dataset.paused = String(currentPaused);
|
||||||
|
const icon = btn.querySelector('.overflow-item-icon');
|
||||||
|
const label_ = $('pause-btn-label');
|
||||||
|
if (icon) icon.textContent = currentPaused ? '▶' : '⏸';
|
||||||
|
if (label_) label_.textContent = currentPaused ? 'resume agent' : 'pause agent';
|
||||||
|
btn.title = currentPaused
|
||||||
|
? 'resume this agent — the turn loop restarts and drains queued messages'
|
||||||
|
: 'pause this agent — parks the turn loop, inbox messages queue unacked';
|
||||||
|
}
|
||||||
// Token badges — two separate chips:
|
// Token badges — two separate chips:
|
||||||
// ctx · N last inference's prompt size = current context window
|
// ctx · N last inference's prompt size = current context window
|
||||||
// utilisation (what to watch for compaction decisions)
|
// utilisation (what to watch for compaction decisions)
|
||||||
|
|
@ -1196,6 +1256,7 @@ window.marked = marked;
|
||||||
renderAliveBadge(s.status);
|
renderAliveBadge(s.status);
|
||||||
renderModelChip(s.model);
|
renderModelChip(s.model);
|
||||||
renderEffortChip(s.effort);
|
renderEffortChip(s.effort);
|
||||||
|
renderPausedChip(s.paused);
|
||||||
renderTokenUsage({ ctx: s.ctx_usage, cost: s.cost_usage });
|
renderTokenUsage({ ctx: s.ctx_usage, cost: s.cost_usage });
|
||||||
// Todos pill: cold-load populate; turn_end refreshes via renderTodos.
|
// Todos pill: cold-load populate; turn_end refreshes via renderTodos.
|
||||||
refreshTodos();
|
refreshTodos();
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ pub(super) async fn api_state(State(state): State<AppState>) -> axum::Json<State
|
||||||
.iter()
|
.iter()
|
||||||
.map(ToString::to_string)
|
.map(ToString::to_string)
|
||||||
.collect(),
|
.collect(),
|
||||||
|
paused: crate::paths::paused_marker().exists(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -185,6 +186,18 @@ pub(super) struct StateSnapshot {
|
||||||
/// [`crate::harness_state::EFFORT_LEVELS`], not operator-configurable like
|
/// [`crate::harness_state::EFFORT_LEVELS`], not operator-configurable like
|
||||||
/// `available_models`. The frontend renders one button per entry.
|
/// `available_models`. The frontend renders one button per entry.
|
||||||
available_efforts: Vec<String>,
|
available_efforts: Vec<String>,
|
||||||
|
/// Whether this agent's turn loop is currently parked (the harness
|
||||||
|
/// keeps serving this page + its MCP daemons but drives no turns).
|
||||||
|
/// Same on-disk marker hive-c0re's `Coordinator::is_paused` checks
|
||||||
|
/// (`crate::paths::paused_marker`) — read directly here rather than
|
||||||
|
/// asking hive-c0re over the socket, since the harness already has
|
||||||
|
/// the file locally. hive-c0re owns the actual pause/resume *writes*
|
||||||
|
/// (via hive-priv, this process runs unprivileged) — the per-agent
|
||||||
|
/// page's own `⋯` menu POSTs to hive-c0re's existing
|
||||||
|
/// `/api/pause/<name>` / `/api/resume/<name>` (same endpoints
|
||||||
|
/// `<hive-agent-menu>` on the dashboard already uses), this field
|
||||||
|
/// only tells the frontend which of the two to show.
|
||||||
|
paused: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue