Verified against hive-agent/src/web_ui/state.rs: the struct is AgentLink (AgentLinkKind for kind), not NavLink -- NavLink doesn't exist anywhere in the Rust or frontend source.
476 lines
26 KiB
Markdown
476 lines
26 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: a header,
|
||
scrollable main content, and a footer composer — plus a slide-in side
|
||
panel for flyouts and long content.
|
||
|
||
## Header
|
||
|
||
**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 `AgentLink.kind` resolves
|
||
differently in the frontend: `Container` → same-origin path
|
||
(the agent page is itself container-local); `Forge` →
|
||
`state.forge_public_url + url` (sourced from
|
||
`services.hyperhive.forge.publicUrl`), and the link is omitted
|
||
entirely when that's unset — never guessed from `<host>:3000`;
|
||
`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.
|
||
- **Todos pill** (`📋 todos · N`): hidden when empty; click opens
|
||
the todos flyout — see the todos-flyout paragraph below
|
||
(`GET /api/todos`, refreshed on cold load + every `turn_end`).
|
||
- **Overflow button** (`⋯`): always visible. Opens a frosted popover
|
||
(`#overflow-menu`, positioned outside the header to escape any
|
||
stacking context) with five 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), 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.
|
||
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.
|
||
A second separator + **effort quick-picker** section labelled
|
||
`effort` appears when the backend declares effort levels in
|
||
`state.available_efforts`. One button per level (e.g. `low`,
|
||
`medium`, `high`, `xhigh`, `max`); clicking POSTs `/api/effort` (same endpoint as
|
||
the `/effort <level>` slash command). The active level's button is
|
||
highlighted; `renderEffortChip` keeps the picker in sync with
|
||
`StateSnapshot.effort` from the cold-load snapshot.
|
||
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 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.
|
||
|
||
## Footer / composer
|
||
|
||
**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](#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; 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).
|
||
|
||
The todos flyout is the only per-agent flyout — there is no separate
|
||
"loose-ends" or "tasks" list. There is also no inline answer form for
|
||
`ask` tool calls in this terminal: an `ask` renders like any other
|
||
tool call, and the operator answers a pending question from the
|
||
dashboard's Y3R C4LL tab instead (see
|
||
[`terminal-rendering.md`](../terminal-rendering.md#inline-ask-operator-answer)).
|
||
|
||
## 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);
|
||
- 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;
|
||
- terminal-themed: phosphor mauve glow, Crust bg,
|
||
backdrop-filter blur, row fade-in slide-up.
|
||
|
||
The backfill/live-tail dedupe, sticky-bottom auto-scroll, and "↓ N
|
||
new" pill are the shared terminal-pane mechanics described in
|
||
[Shape](shape.md#shared-terminal-pane) — this page's log is one
|
||
instance of that same factory.
|
||
|
||
Per-stream rendering (see [`docs/terminal-rendering.md`](../terminal-rendering.md) for
|
||
the full row taxonomy and dispatch logic):
|
||
|
||
- `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 with
|
||
per-tool salient-arg extraction (`fmtToolUse`).
|
||
- `Stream` `tool_result` short → flat `← ...`; long → collapsed
|
||
`<details>` `▸ ← Nl · headline` (click to expand full body).
|
||
- `Stream` `thinking` → `.thinking` row with a `💭 thinking …`
|
||
indicator.
|
||
- `Stream` `system` → handled by subtype: `plugin_install` and
|
||
`compact_boundary` emit muted notes; `commands_changed` emits an
|
||
expandable details row listing slash commands; `thinking_tokens`
|
||
updates a single in-place `🧠` counter; `init`, `result`, and
|
||
`rate_limit_event` are dropped (noise / used elsewhere); other
|
||
subtypes → muted `⚙ <subtype>` note.
|
||
- `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` → sets the deferred
|
||
`Bus::request_compact()` flag and returns immediately. The harness
|
||
runs the `/compact` at the next turn boundary (end of the in-flight
|
||
turn, or `turn::run_pending_compact` when idle); output streams into
|
||
the live panel. Works mid-turn, not only when idle.
|
||
- `/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.
|
||
- `/effort <level>` — `POST /api/effort` setting the claude effort
|
||
level (`low` / `medium` / `high` / `xhigh` / `max`). Takes effect on the next
|
||
turn. The overflow menu surfaces an effort picker that calls the
|
||
same endpoint; both stay in sync via `StateSnapshot.effort`.
|
||
- `/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
|
||
|
||
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).
|
||
|
||
- `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/effort` (`effort=<level>`) — switch the claude
|
||
effort level for future sessions. Validated server-side against
|
||
`EFFORT_LEVELS` (`low`/`medium`/`high`/`xhigh`/`max`) — unknown values are
|
||
rejected rather than forwarded to `claude --effort`. Persists via
|
||
`Bus::set_effort`, which emits `EffortChanged`. Applies on the next
|
||
session start (no mid-session swap).
|
||
- `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.
|
||
⚠️ Deleting the whole `~/.claude/` directory instead breaks
|
||
session continuity — logout must stay narrowed to just the
|
||
credential files.
|
||
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`, `effort`, `available_efforts`, `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/todos` — harness-local todos snapshot (loose-ends v2)
|
||
consumed by the todos flyout (`refreshTodos` / `buildTodosList`).
|
||
See the todos-flyout paragraph above for the producer/subsystem
|
||
model and the bulk mark-done row.
|
||
- `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 /icon` — agent's icon as `image/svg+xml`. Returns
|
||
`/etc/hyperhive/icon.svg` (set via `hyperhive.icon` in `agent.nix`)
|
||
when present, otherwise **404** — there is no server-side default.
|
||
Consumers (dashboard container row, this page's own header icon)
|
||
hit `/icon` optimistically and fall back client-side on load failure
|
||
to the frontend-bundled `/favicon.svg` rather than probing first.
|
||
- `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`).
|
||
- `GET|POST /extra/<name>/…` — **extra web proxies** declared via
|
||
`hyperhive.extraWebProxies` in `agent.nix` (serialised to the
|
||
`HIVE_EXTRA_WEB_PROXIES` env var as a JSON object
|
||
`{"<name>": "<upstream_url>"}`). Each entry mounts a transparent
|
||
reverse-proxy at `/extra/<name>/` that forwards every request (method,
|
||
headers, body) to the configured upstream, strips hop-by-hop headers
|
||
on both sides, and buffers the full response body (MVP — SSE
|
||
connections will appear as one large response rather than streaming).
|
||
The `/extra/` namespace ensures user-declared proxies can never
|
||
conflict with native agent endpoints. Upstream values are either an
|
||
`http(s)://` URL (forwarded via `reqwest`) or a Unix domain socket,
|
||
spelled `unix:<path>` (e.g. `unix:/run/myapp/http.sock`) — dialed
|
||
directly with a raw HTTP/1.1 client per request, since `reqwest` has
|
||
no UDS transport. Implemented in `web_ui/proxy.rs`.
|
||
|
||
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.
|
||
- `effort_changed { effort }` — drives the effort picker chip.
|
||
Emitted from `Bus::set_effort` on `POST /api/effort`; applies on
|
||
the next session start.
|
||
- `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-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.
|