Per-agent page

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

Three fixed-position layers frame a full-viewport terminal: a header, scrollable main content, and a footer composer — plus a slide-in side panel for flyouts and long content.

Preact component tree (Header.tsx + StatusChips.tsx + MetaNav.tsx + HeaderPill.tsx, wired together in Root.tsx) — see frontend/packages/agent/src/components/. This section describes the rendered component tree: every element is Preact component output.

Fixed-overlay header (<header class="agent-header">): frosted glass — backdrop-filter: blur lets scrolled terminal rows show through. Measures its own rendered height via ResizeObserver (Header.tsx) and writes it to a CSS custom property the content below reads for its offset, so content never overlaps the header regardless of how many rows of badges/pills wrap onto an extra line at a given viewport width. Two columns:

Values throughout come from GET /api/state's cold-load snapshot, kept in sync afterwards by the SSE stream (see Live view below) — context_window_tokens for the ctx badge tooltip, qualified_label (the hive-qualified name@domain form, used for the browser tab title so two tabs from different hives are distinguishable; the header's own ◆ <label> ◆ stays short).

Main content

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

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; selecting it confirms then POSTs cross-origin to the core dashboard's POST /api/agent/{name}/mark-all-read, which acks all pending messages for this agent so 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). If the todo store errors or doesn't answer for any id, the endpoint returns 500 naming how many ids failed, and the panel shows failed: … instead of ✓ marked done.

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:

The backfill/live-tail dedupe, sticky-bottom autoscroll, and "↓ N new" pill are the shared terminal-pane mechanics described in Shape — this page's log is one instance of that same factory.

Per-stream rendering (see docs/web-ui/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 fires), 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 + AGENTS.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 stays out of 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.