The broker's recent_for was fixed to filter acked_at IS NULL (commit
5264828091), so the agent inbox flyout
now shows only unread messages and empties after 'mark all read'.
The agent.md prose still described the old behaviour ('shows the
most-recent N regardless of ack state; the list stays put after drain').
Update it to match: unread-only, list empties on reload after drain.
420 lines
23 KiB
Markdown
420 lines
23 KiB
Markdown
# Per-agent page
|
||
|
||
> Part of [Web UI](../web-ui.md). See also:
|
||
> [Shape (shared)](shape.md) · [Dashboard layout](dashboard.md)
|
||
|
||
|
||
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:
|
||
|
||
- **Agent icon** (`<img class="agent-icon">`): fixed-size square
|
||
identity anchor — `width: 5em; height: 5em` with explicit pixel
|
||
sizing so the `<img>`'s intrinsic (large) dimensions don't push
|
||
the parent flex container open via `align-items: stretch`-driven
|
||
height feedback. 5em ≈ header content area (header `min-height: 6em`
|
||
minus `2 × 0.5em` padding). `align-self: flex-start` keeps the
|
||
icon stuck to the top so a state-row line-wrap doesn't drag it
|
||
down with it. Falls back to the dimmed hyperhive mark on load
|
||
error.
|
||
- **Main column** (`.agent-header-main`): two rows.
|
||
- Row 1 (`.agent-header-title-row`): title (`<h2 id="title">`) +
|
||
meta-nav (`<nav id="meta-links">`). Meta-nav renders
|
||
backend-supplied `StateSnapshot.links` as icon-only anchors — always
|
||
`📊 stats` (`kind = Container`); `🖥 screen` when VNC is enabled;
|
||
`⬡ forge` (profile) + `↳ config` (agent-configs mirror) when the
|
||
agent has a forge account; any `hyperhive.dashboardLinks` extras
|
||
(`kind = External`). A `↑ dashboard` link is prepended by the JS
|
||
so the host dashboard is one click away. Links come from
|
||
`StateSnapshot.links` (served by `GET /api/state`); the same set
|
||
also appears in `DashboardState.links` (`GET /api/dashboard-state`)
|
||
for the dashboard card's icon strip. Both are produced by
|
||
`agent_links()` in hive-ag3nt — the single source of truth.
|
||
Each `NavLink.kind` resolves
|
||
differently in the frontend: `Container` → same-origin path
|
||
(the agent page is itself container-local); `Forge` →
|
||
`http://<host>:3000<url>`; `External` → already absolute.
|
||
All anchors are built via `el()` — agent-declared icon /
|
||
label / url strings never reach `innerHTML` (XSS-safe by
|
||
construction).
|
||
- Row 2 (`.agent-state-row`): alive badge + state badge + model chip
|
||
+ ctx badge + cost badge + last-turn chip + cancel button.
|
||
- Alive badge: `● alive` (green) / `⊘ rate limited` (red) /
|
||
`◌ needs login` / `◌ logging in` / `○ offline` / `… connecting`.
|
||
Driven by `LiveEvent::StatusChanged`.
|
||
- State badge: `💤 idle` / `🧠 thinking` / `📦 compacting` /
|
||
`○ offline` / `… booting` + age suffix. Driven by
|
||
`LiveEvent::TurnStateChanged ({ state, since_unix })`.
|
||
- Model chip: `model · <name>`. Driven by `LiveEvent::ModelChanged`.
|
||
- Ctx badge: `ctx · 142k` — last inference's prompt size.
|
||
Tooltip shows % of window when `context_window_tokens` is known.
|
||
- Cost badge: `cost · 1.3M` — cumulative tokens billed across every
|
||
inference in the last turn (tool-heavy turns rebill the cached
|
||
prefix per call — cost signal, not size signal).
|
||
- Both driven by `LiveEvent::TokenUsageChanged { ctx, cost }` at
|
||
turn-end.
|
||
- `■ cancel turn` (visible while thinking) → `POST /api/cancel`.
|
||
- **Right cluster** (`.agent-header-pills`): flyout pills + overflow.
|
||
- **Inbox pill** (`📬 inbox · N`): hidden when empty; click opens
|
||
the inbox flyout in the side panel.
|
||
- **Loose-ends pill** (`🪢 loose ends · N`): hidden when empty;
|
||
click opens the loose-ends flyout.
|
||
- **Tasks pill** (`⚙ tasks · N`): hidden when empty; click opens the
|
||
running-bash-tasks flyout (`GET /api/bash-tasks` — the in-flight
|
||
Pending/Running tasks from the in-container `bash-tasks/` dir). Unlike
|
||
loose-ends (refreshed on turn_end), tasks start + finish asynchronously
|
||
between turns and `/api/state` isn't polled while online, so the pill
|
||
polls the endpoint on a light interval (≈4s). Snapshot only (no SSE push
|
||
yet).
|
||
- **Overflow button** (`⋯`): always visible. Opens a frosted popover
|
||
(`#overflow-menu`, positioned outside the header to escape any
|
||
stacking context) with four management rows followed by a model
|
||
quick-picker section: `↑ dashboard` (link), `↻ rebuild container`
|
||
(POST confirm, same action as the dashboard R3BU1LD button),
|
||
`↻ new claude session` (POST confirm → `POST /api/new-session`;
|
||
next turn drops `--continue`), `🔓 logout` (POST confirm → `POST
|
||
/api/logout`; SIGINTs any in-flight turn, wipes OAuth credential
|
||
files, flips the agent to `needs_login` — session history
|
||
preserved). All destructive actions require one extra click to
|
||
acknowledge — rare ops shouldn't live in the primary state strip.
|
||
Below a separator, a **model quick-picker** section labelled
|
||
`model` renders one button per model in the operator-configured
|
||
list. The list is driven by `state.available_models` (sourced from
|
||
the `HIVE_AVAILABLE_MODELS` env var, injected by the
|
||
`services.hyperhive.availableModels` NixOS option; defaults to
|
||
`["haiku", "sonnet", "opus"]` when unset). Well-known aliases get
|
||
a parenthetical description (`haiku (fast)`, `sonnet (balanced)`,
|
||
`opus (powerful)`); operator-declared custom names show as-is.
|
||
Clicking a button POSTs `/api/model` with the alias (same path as
|
||
the `/model <name>` slash command). The button for the
|
||
currently-active model is highlighted via the `active` class;
|
||
`renderModelChip` keeps the picker state in sync with live
|
||
`model_changed` events so it stays accurate when the model is
|
||
changed from another session. Clicking the already-active model
|
||
closes the menu without an extra POST.
|
||
The popover's display rules are scoped to `:not([hidden])` so the
|
||
`[hidden]` HTML attribute's UA `display: none` isn't overridden by
|
||
the author CSS's `display: flex` — the popover stays hidden until
|
||
JS removes the attribute.
|
||
|
||
`/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.
|
||
- `#status` overlay: empty when online; shows the login form / OAuth
|
||
URL when `status` is `needs_login_*`. The OAuth code input is
|
||
`type="password"` with a `👁 reveal` toggle that flips it back to
|
||
`text` on press so the operator can sanity-check the paste before
|
||
submit — avoids accidental on-screen token exposure to
|
||
shoulder-surfers or screenshots. `autocomplete="one-time-code"`
|
||
is the semantic value for OAuth codes (per WHATWG): browsers may
|
||
silently ignore `autocomplete="off"` on `type="password"`, but
|
||
`one-time-code` is honoured and suppresses the "save password
|
||
for this site?" prompt that would otherwise fire on submit.
|
||
- Terminal-wrap: live event tail (sticky-bottom auto-scroll +
|
||
`↓ N new` pill when not at bottom). The pill is **anchored in
|
||
`.agent-main`**, not in `log.parentElement = .terminal-wrap`:
|
||
`.terminal-wrap` applies `backdrop-filter: blur` for the frost
|
||
effect, which creates a CSS stacking context — anchoring the
|
||
pill inside that context would trap its `z-index` below the
|
||
fixed composer in the root stacking context, and it'd never
|
||
float. `.agent-main` has no backdrop-filter (no stacking-context
|
||
creators), so the pill's `z-index` reaches the root and properly
|
||
composites above the composer. Geometry is unchanged —
|
||
`.agent-main` and `.terminal-wrap` both `inset: 0` fill the same
|
||
area.
|
||
|
||
**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 loose-ends 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). Loose-ends flyout: questions,
|
||
approvals, and reminders pending against this agent (`GET /api/loose-ends`);
|
||
question rows carry an inline answer form that POSTs cross-origin to
|
||
the core dashboard's `/answer-question/{id}` so the operator answers
|
||
*as operator* (see `docs/boundary.md`). Tasks flyout: in-flight bash
|
||
tasks (`GET /api/bash-tasks`); each row shows status (`▶ running` /
|
||
`◷ queued`), the task id, elapsed time, and a truncated one-line
|
||
command preview. Read-only — kill/inspect lives in the harness, not
|
||
the page.
|
||
|
||
**Ask → operator inline-answer binding.** When the agent emits
|
||
`mcp__hyperhive__ask(to: "operator", ...)`, the tool_use renderer
|
||
mounts an empty slot (`<div class="ask-answer-inline-slot">`)
|
||
right under the `↳ ask → operator` row in the terminal scrollback
|
||
and pushes a reference into `pendingAskBinds`. The broker assigns
|
||
the question id asynchronously, so the slot waits — and the next
|
||
`/api/loose-ends` refresh runs `reconcileAskBinds()`, which walks
|
||
the slot list and pairs each unbound slot with the first unclaimed
|
||
pending operator-bound question whose `question` text matches the
|
||
slot's stashed `_askQuestion`. On match the slot mounts the
|
||
`buildAnswerForm` (same form shape as the loose-ends flyout —
|
||
POSTs to the core's `/answer-question/{id}` cross-origin). Slots
|
||
stay in the array after binding so the reconciler can flip them
|
||
to a neutral `[resolved]` tag when the question later disappears
|
||
from the pending list. Disappearance can mean answered, cancelled
|
||
by the asker, or TTL-expired — the neutral label avoids
|
||
mis-asserting "✓" on the cancel / expire paths; full resolution
|
||
state is visible via the side-panel history. A defensive prune
|
||
walks the slot list each tick and drops any whose DOM node has
|
||
been removed (e.g. via a future "clear single row" affordance),
|
||
so stale references don't accumulate. Slots whose question never
|
||
arrives (e.g. the agent cancelled the ask, or the question is
|
||
older than the loose-ends retention window) stay empty — the
|
||
operator can still answer via the side panel, no regression.
|
||
|
||
## 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:
|
||
|
||
- fetches `GET /events/history` on page load and replays the last
|
||
2000 events (oldest first, with `.no-anim` so they don't
|
||
stagger);
|
||
- then subscribes to `GET /events/stream` (SSE) for live tail;
|
||
- shows a granular state badge above the terminal, driven
|
||
authoritatively from `/api/state.turn_state`. SSE turn_start /
|
||
turn_end still flip the badge instantly between renders;
|
||
- sticky-bottom auto-scroll: scrolling up parks the view; new rows
|
||
surface a "↓ N new" pill instead of yanking;
|
||
- terminal-themed: phosphor mauve glow, Crust bg,
|
||
backdrop-filter blur, row fade-in slide-up.
|
||
|
||
Per-stream rendering:
|
||
|
||
- `Stream` `tool_use` →
|
||
- `Write` / `Edit`: collapsed `<details>` with a +/- diff body
|
||
(`-` lines from `input.old_string`, `+` lines from
|
||
`input.new_string` or every line of `input.content`).
|
||
Summary carries the path + line counts.
|
||
- others (`Read /path`, `Bash $ cmd`, `mcp__hyperhive__send →
|
||
operator: "..."`, etc.): flat one-line per-tool format.
|
||
- `Stream` `tool_result` short → flat `← ...`; long → collapsed
|
||
`<details>` `▸ ← Nl · headline` (click to expand full body).
|
||
- `Stream` `thinking` → text content if claude provided one,
|
||
otherwise the bare `· thinking …` indicator.
|
||
- `Stream` `system init`, `result`, `rate_limit_event` are
|
||
dropped — too noisy.
|
||
- `Note` → `· text`.
|
||
- `TurnStart` → `◆ TURN ← <from>` with the wake-prompt body; a
|
||
muted `· HH:MM:SS` time suffix from the event `ts`.
|
||
- `TurnEnd` → `✓ turn ok` / `✗ turn fail — note`, with a
|
||
`· HH:MM:SS · <duration>` suffix (duration = end − the paired
|
||
turn-start), triggers a `refreshState()`. The time suffix is
|
||
guarded on a numeric `ts`, so rows degrade cleanly when absent.
|
||
|
||
## 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:
|
||
|
||
- `/help` — list commands locally.
|
||
- `/clear` — wipe the local terminal view (server history kept).
|
||
- `/cancel` — `POST /api/cancel` → host shellouts `pkill -INT
|
||
claude`, emits a Note. Also surfaces as a `■ cancel turn`
|
||
button in the state row while state=thinking.
|
||
- `/compact` — `POST /api/compact` → host spawns
|
||
`turn::compact_session` in the background; output streams into
|
||
the live panel.
|
||
- `/model <name>` — `POST /api/model` flipping `Bus::set_model`.
|
||
Takes effect on the next turn; persisted to
|
||
`/state/hyperhive-model` so the override survives harness
|
||
restart / rebuild.
|
||
- `/new-session` — `POST /api/new-session` (confirms first).
|
||
Arms a one-shot on the Bus; next turn runs without
|
||
`--continue`, dropping the resume session entirely.
|
||
- `/logout` — `POST /api/logout` (confirms first). Wipes OAuth
|
||
credential files, parks the agent in `needs_login`. Session
|
||
history (`~/.claude/projects/`) is preserved.
|
||
|
||
Unknown `/foo` shows an error row instead of being silently sent.
|
||
|
||
## Per-agent endpoints
|
||
|
||
All POSTs return 200 (no 303 redirects). 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).
|
||
|
||
- `POST /send` — operator-injected message into this agent's inbox.
|
||
- `POST /login/{start,code,cancel}` — claude OAuth login flow.
|
||
Start/cancel emit `LiveEvent::StatusChanged` to flip the
|
||
badge to/from `needs_login_in_progress`.
|
||
- `POST /api/cancel` — SIGINT the in-flight claude turn. Emits a
|
||
`LiveEvent::Note`.
|
||
- `POST /api/compact` — run `/compact` on the persistent session
|
||
(same MCP config + system prompt + allowed tools as a normal
|
||
turn — only the stdin payload differs). Flips state to
|
||
`Compacting` via `Bus::set_state`, which emits
|
||
`TurnStateChanged`.
|
||
- `POST /api/model` (`model=<name>`) — switch the model for
|
||
future turns. `Bus::set_model` emits `ModelChanged`.
|
||
- `POST /api/new-session` — arm a one-shot for the next turn to
|
||
drop `--continue`. Emits a `LiveEvent::Note`.
|
||
- `POST /api/logout` — three-step teardown that re-uses the
|
||
existing `wait_for_login` resumption path:
|
||
1. SIGINT any running claude (matches `/api/cancel`'s pattern —
|
||
idempotent no-op when nothing is running) so the credential
|
||
wipe doesn't race a mid-API-call turn.
|
||
2. Delete only the OAuth credential files
|
||
(`.credentials.json` + `mcp-needs-auth-cache.json` under
|
||
`~/.claude/`). **Preserves** session history files
|
||
(`projects/<hash>/*.jsonl`), sessions, shell-snapshots,
|
||
plans, settings, telemetry, and the dir itself, so
|
||
`claude --continue` keeps working after a fresh login.
|
||
Wholesale `remove_dir_all` of `~/.claude/` was the previous
|
||
shape and broke session continuity; the narrowed allow-list
|
||
is the fix.
|
||
3. Flip `LoginState::NeedsLogin` + emit a `LiveEvent::Note`
|
||
describing exactly what was wiped, then emit
|
||
`needs_login_idle`. The turn-loop's next iteration parks
|
||
into `wait_for_login`, which snapshots the credential dir
|
||
(now missing the wiped files) and resumes when a fresh
|
||
credentials file appears via the dashboard's `/login/code`
|
||
flow (the same mtime-resumption path manual re-login uses).
|
||
|
||
Always returns 200 with a body describing what happened —
|
||
per-file errors are folded into the response + the Note so the
|
||
operator sees them in the live panel rather than as an HTTP
|
||
error. Missing files (already logged out) are treated as
|
||
idempotent.
|
||
- `GET /api/state` — cold-load snapshot (`StateSnapshot`) consumed by
|
||
`app.js` on page load and while `status === 'needs_login_in_progress'`.
|
||
Includes `turn_state`, `context_window_tokens`, `qualified_label`,
|
||
`available_models`, `links`, and other fields described inline
|
||
throughout this document. All subsequent state updates arrive via SSE.
|
||
- `GET /api/dashboard-state` — lean snapshot of agent-owned fields
|
||
fetched once per running agent by the dashboard's container row to
|
||
get fresh values without relying on hive-c0re's periodic file-reads.
|
||
Returns `{ status_text?, status_set_at?, ctx_tokens?, context_window_tokens,
|
||
rate_limited, links }`. Only called when the container is running;
|
||
skipped (muted badges) when stopped. Also accessible via the gateway
|
||
at `/agent/<name>/api/dashboard-state`.
|
||
- `GET /api/loose-ends` — loose-ends snapshot consumed by the inbox
|
||
flyout (`renderLooseEnds`). Returns pending questions the agent asked
|
||
or owes, plus pending reminders. Also calls `reconcileAskBinds()` to
|
||
wire inline answer forms to open `question_asked` events.
|
||
- `GET /api/stats?window=24h|7d|30d|all` — time-bucketed turn analytics
|
||
`Snapshot` consumed by the `/stats` page. `all` ranges from the
|
||
earliest recorded turn with an adaptive bucket width.
|
||
- `GET /events/history` — replay buffer for the terminal.
|
||
- `GET /screen` — VNC viewer page (minimal RFB-over-WebSocket
|
||
renderer — deliberately thin, just enough to display the
|
||
desktop + forward pointer + keyboard. A production-grade viewer
|
||
would vendor noVNC; this file ships the minimal in-tree variant).
|
||
Only accessible when `hyperhive.gui.enable = true` in the agent's
|
||
`agent.nix`; the harness shows a 🖥 screen link in the state row
|
||
when `gui_vnc_port` is present. A `← agent` back-link in the page
|
||
header returns to the main per-agent page. Controls: `⤢ fit` CSS-downscales
|
||
the canvas to the window via `relayoutCanvas()` setting explicit
|
||
pixel dimensions on the canvas — *not* CSS `max-width/max-height`,
|
||
because a flex item's automatic minimum size (`min-width: auto`
|
||
resolves to the canvas's intrinsic framebuffer resolution) silently
|
||
clamps `max-*` back up, making fit mode a no-op that just centred +
|
||
clipped the oversized canvas. The fit-mode rules pin the canvas
|
||
with `flex: none; min-width: 0; min-height: 0` so the JS-set size
|
||
sticks. `⤡ match size` sends an RFB `SetDesktopSize` request so the
|
||
server (weston) changes its real output resolution to the window
|
||
dimensions; enabled once the server advertises the
|
||
`ExtendedDesktopSize` pseudo-encoding (`-308` rect in the header).
|
||
Fit-mode state persists in `localStorage` (`screen-fit`); default
|
||
is on. Pointer coordinates are rescaled in `sendPointer` so clicks
|
||
land on the right pixel regardless of CSS scale.
|
||
- `GET /screen/ws` — raw RFB byte relay: proxies WebSocket
|
||
frames to the weston VNC server at `127.0.0.1:<vnc_port>`.
|
||
Transparent to any RFB variant. VNC port comes from the
|
||
`HIVE_GUI_VNC_PORT` env var (a fixed port set on the harness
|
||
service when `hyperhive.gui.enable`; see `weston-vnc.nix`).
|
||
|
||
Bus events (new vocabulary on `/events/stream`):
|
||
|
||
- `status_changed { status }` — `online` / `rate_limited` /
|
||
`needs_login_idle` / `needs_login_in_progress`. Drives the
|
||
alive-badge. `rate_limited` is set when the harness detects a
|
||
429 response and cleared when the retry sleep expires.
|
||
- `model_changed { model }` — drives the model chip.
|
||
- `token_usage_changed { ctx: TokenUsage, cost: TokenUsage }`
|
||
— drives the ctx + cost badges. Emitted from
|
||
`Bus::record_turn_usage` at turn-end; `ctx` is the last
|
||
inference's usage (current context size), `cost` is the
|
||
cumulative across every inference (the `result` event's
|
||
totals).
|
||
- `turn_state_changed { state, since_unix }` — drives the
|
||
state badge (`idle`/`thinking`/`compacting`).
|
||
|
||
## 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-mcp 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.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.
|