Per-agent page

Part of Web UI. See also: Shape (shared) · Dashboard layout

Three fixed-position layers frame a full-viewport terminal:

Fixed-overlay header (<header class="agent-header">): frosted glass — backdrop-filter: blur lets scrolled terminal rows show through. Three flex columns:

/api/state is fetched once on cold load (+ while status === 'needs_login_in_progress'); all other updates arrive via SSE. Snapshot includes context_window_tokens for the ctx badge tooltip, and qualified_label — the hive-qualified agent name (name@domain form when HYPERHIVE_HIVE_DOMAIN is set, otherwise just name). The frontend uses qualified_label to set the browser tab title so two tabs from different hives are distinguishable; the header <h2 id="title"> stays short.

Main content (<main class="agent-main">): fills the viewport and scrolls behind the fixed header + footer.

Fixed-overlay footer (<footer class="agent-composer">): frosted glass, symmetric with the header. Contains the operator-input textarea (#term-input) — multi-line, Enter sends, Shift+Enter newlines, Tab-completes slash commands (see "Terminal-embedded prompt" below).

Side panel (slide-in from right): singleton shared with the dashboard's side panel shape. Carries inbox and todos flyouts (opened via the header pills) as well as long content (file previews, diffs, journald logs). Inbox flyout: unread messages addressed to this agent (acked_at IS NULL, newest-first, up to 30); reply messages indented with ↳ reply · in amber. A ✓ mark all read button appears in the flyout header when the inbox is non-empty; clicking it confirms then POSTs cross-origin to the core dashboard's POST /api/agent/{name}/mark-all-read — all pending messages for this agent are acked, the harness won't receive wake-prompts for them. A { marked: N } pill surfaces the count. After the drain the inbox list empties on reload (the filter is acked_at IS NULL, so drained messages disappear). Todos flyout ("loose-ends v2"): the harness-local todos other subsystems push at this agent (GET /api/todos) — matrix, forge, and bash are the built-in producers, but any user-configured MCP server can push its own via the same in-agent socket. Each row shows the producing subsystem, an optional source label, a summary, and age. A checkbox per row plus a select-all / select-none / ✓ mark done bulk row above the list POSTs the checked ids to POST /api/todos/mark-done, which dismisses them from the harness-local store (same effect as cancel_loose_end(kind: "todo"), just from the web UI instead of the agent's own tool calls).

Older per-agent flyouts this doc used to describe (a "loose-ends" list of questions/approvals/reminders backed by a since-removed GET /api/loose-ends, and a read-only "tasks" list of in-flight bash tasks backed by a since-removed GET /api/bash-tasks) no longer exist — todos superseded both. A third casualty of that migration — an "ask → operator" inline-answer form that used to mount under an mcp__hyperhive__ask(to: "operator", ...) row in the terminal scrollback — depended on the same removed /api/loose-ends endpoint and was found dead (hyperhive#2922: the binding never fired, so the slot never mounted a form). Per mara's call on that issue, it was removed rather than rebuilt — the main dashboard's own question surfacing (dashboard/src/swarm.js + call.js) is the one supported path for answering a pending question as the operator; this agent's own terminal just shows the ask tool call like any other tool call, with no inline answer affordance.

Live view

Each agent runs an events::Bus: a tokio::sync::broadcast<LiveEvent> plus a sqlite-backed history at /state/hyperhive-events.sqlite. The harness emits TurnStart { from, body, unread }, Stream(value) (one per parsed stream-json line), Note, TurnEnd { ok, note }. Each event also carries a ts (unix seconds) — a flattened sibling of the event tag on both the live SSE frame and the replayed history rows. The web UI:

Per-stream rendering (see docs/terminal-rendering.md for the full row taxonomy and dispatch logic):

Terminal-embedded prompt

The operator input lives inside the terminal-wrap as a prompt-style textarea below the live tail: multi-line (Enter sends, Shift+Enter newlines), tab-completes slash commands.

Slash commands today:

Unknown /foo shows an error row instead of being silently sent.

Per-agent endpoints

Successful POSTs return 200 (no 303 redirects). Error responses use semantic status codes: 400 for missing/invalid input (body required, unknown model name, invalid effort level), 409 for retryable state conflicts (turn in flight when /compact is called), 500 only for genuine server/transport failures. The matching mutations fire LiveEvent variants on the per-agent bus, so the client doesn't refetch /api/state on submit — the SSE stream delivers the new state faster anyway. Only the login flow still polls (session output streams in updates that aren't event- shaped).

Bus events (new vocabulary on /events/stream):

Stats page

GET /stats is a separate per-agent page (served by the harness, linked from the per-agent page's 📊 stats → and from each dashboard container row). A ← live back-link in the page header returns to the main per-agent page. Turn analytics, read-only, from /state/hyperhive-turn-stats.sqlite. GET /api/stats?window= 24h|7d|30d|all returns a time-bucketed Snapshot; the page renders it with Chart.js (bundled into stats.js via esbuild — no CDN dependency). Charts: turns, duration (p50 · p95 · avg), context tokens, token cost per bucket, a turns-by-model stacked bar (model choice drives token cost, so it sits directly under the cost chart), doughnuts for tool / wake-source / result mix, and a result-trend stacked bar — per-bucket result_counts so error / rate-limit / compaction outcomes are visible over time (the doughnut shows only the window total). A favorite tools doughnut shows the most-run shell commands — normalised bash_commands heads written per bash task by the hive-bash-daemon capture: the basename of the first real command, looking past cd repo && prefixes, env-assignments, and prefix-runners like sudo / env (so cd /repo && cargo build records cargo, not cd). Read via bash_breakdown; the card stays hidden until the agent has run a bash command (a missing bash_commands table degrades to an empty list), so it never renders an empty chart. A summary chip row carries window totals, plus two token-efficiency chips derived from the bucket sums: cache hit-rate (cache_read over all input-side tokens) and tokens/turn. When reminder_stats is present (fetched via ReminderRollup RPC and merged into the snapshot in web_ui/stats.rs::api_stats) three more chips appear: reminders scheduled / delivered / pending for the window. When the per-session capture has data, a first-turn ctx chip shows the input tokens of the most recent fresh claude session's first turn — a proxy for system-prompt + CLAUDE.md sprawl (a fresh session's first turn pays the full static prefix uncached). It's derived in stats.rs (first_turn_ctx: the first turn, ORDER BY started_at LIMIT 1, of the latest sessions row in the window) and is omitted from the JSON until the sessions / turn_stats.session_id capture has rows — so the chip stays hidden on older dbs (inert-until-capture). stats.rs opens the sqlite db read-only and degrades to an empty snapshot on any error — the page is decorative, never authoritative.