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 result, not the DOM ids the pre-Preact page used (there are none any more — every element is component output, not something a selector reaches by id).

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 — a fixed 6em guess used to be baked into agent.css, which silently broke (content overlapping the header) the moment any row of badges/pills wrapped onto an extra line at some viewport width; measuring instead of guessing closes that bug class structurally rather than for one specific trigger. 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 — 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).

The todos flyout is the only per-agent flyout — there is no separate "loose-ends" or "tasks" list. There used to be a note here about the ask/answer MCP tools having no inline answer form in this terminal — that whole mechanism (the tools, the dashboard's questions pane, the wire protocol) has since been removed entirely, so there's nothing left to render a form for.

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 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.