hyperhive/docs/web-ui.md
müde 8b10731aa4 split claude.md into docs/ — per-topic, human-readable
claude.md was eating 400 lines of subsystem detail that's useful
when you're working on that subsystem and noise the rest of the
time. split into:

- docs/conventions.md   naming, identity, async forms, commit style
- docs/gotchas.md       nspawn / nixos-container quirks
- docs/web-ui.md        dashboard + per-agent layouts and endpoints
- docs/turn-loop.md     claude invocation, wake prompt, mcp surface
- docs/approvals.md     approval flow, manager policy, helper events
- docs/persistence.md   sqlite dbs, retention, state dir layout

claude.md is now the entry point — file map, reading paths
("pick the doc that matches your task"), quick reminders that
fit on one screen, and a small scratchpad section for in-flight
context. references the docs; the docs don't reference claude.md.

no content was lost — the docs/ files cover everything the old
claude.md did, plus things i wrote up better while extracting.
2026-05-15 20:17:11 +02:00

6.1 KiB

Web UI

Two web surfaces share the same skeleton: the dashboard (port 7000) and the per-agent UIs (manager on :8000, sub-agents on a hashed :8100-8999). Both are SPAs — GET / returns a static shell, /api/state returns JSON, JS renders. No full-page reloads.

Shape (shared by both)

  • GET /assets/index.html (placeholders for state-driven sections, shipped via include_str! so the binary has no runtime file dependency).
  • GET /static/*.css + GET /static/*.js → static assets.
  • GET /api/state → JSON snapshot the JS app renders into the DOM.
  • GET /events/stream (per-agent) / GET /messages/stream (dashboard) → text/event-stream SSE for live updates.

The JS app handles all form[data-async] submissions via a delegated listener: read data-confirm, swap the button to a spinner, POST application/x-www-form-urlencoded, re-enable the button on success (refreshState may keep the form mounted, so we don't rely on a re-render), call refreshState(). State shapes live in dashboard.rs::StateSnapshot and web_ui.rs::StateSnapshot — when adding state fields, plumb through the snapshot struct and the relevant assets/app.js render function.

Dashboard sections (top to bottom)

  1. C0NTAINERS — live containers with their action surface.
  2. K3PT ST4T3 — destroyed-but-state-kept tombstones (size + age + claude-creds badge). Two actions: ⊕ R3V1V3 (queues a Spawn approval; existing state is reused), PURG3 (wipes state + applied dirs; POST /purge-tombstone/{name}).
  3. M1ND H4S QU3STI0NS — pending ask_operator questions (amber pulsing border). Always renders a free-text fallback alongside any option list; multi=true renders options as checkboxes; submit merges selections + free text comma-joined.
  4. 0PER4T0R 1NB0X — recent messages addressed to operator (last 50, from the broker).
  5. P3NDING APPR0VALS — the queue. The R3QU3ST SP4WN form lives at the top of this section since submitting it immediately queues an approval that lands directly below.
  6. MESS4GE FL0W — live broker SSE tail.

Container row

Two-line layout (assets/app.js::renderContainers):

  • Line 1: agent name (link → new tab), m1nd/ag3nt chip, needs login / needs update warning badges, in-flight ◐ pending-state… pill (replaces buttons during start/stop/restart/rebuild/destroy), container name + port.
  • Line 2: action buttons — ↻ R3BU1LD always, DESTR0Y + PURG3 on sub-agents, ↺ R3ST4RT + (sub-agents) ■ ST0P when running, ▶ ST4RT when stopped. Buttons dim + disable while a transient lifecycle action is in flight.

↻ UPD4TE 4LL button appears above the containers list when any agent is stale.

Banner pulses on each broker SSE event (pulseBanner with a 4 s grace timer).

Dashboard endpoints

  • POST /{approve,deny}/{id} — approve/deny a pending approval.
  • POST /{rebuild,kill,restart,start,destroy}/{name} — lifecycle.
  • POST /purge-tombstone/{name} — wipe a tombstone's state dirs.
  • POST /answer-question/{id} — answer a pending operator question.
  • POST /request-spawn — queue a Spawn approval.
  • POST /update-all — rebuild every stale container.

Per-agent page

Layout, top to bottom:

  • Banner (gradient shimmer while state=thinking).
  • Title with ↻ R3BU1LD button.
  • Status section (online / needs login / login-in-progress).
  • State badge row (💤 idle / 🧠 thinking / ○ offline · <age>) + ■ cancel turn button visible while state=thinking.
  • Terminal-wrap: live event tail (with sticky-bottom auto-scroll and a ↓ N new pill when not at bottom) followed by an operator-input textarea acting as a prompt.

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 }. 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 from turn_start/turn_end, with a flash animation on transition;
  • 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→ Read /path / → Bash $ cmd / → send → operator: "..." etc., per-tool pretty rather than raw JSON.
  • 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.
  • TurnEnd✓ turn ok / ✗ turn fail — note, triggers a refreshState().

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)
  • /cancelPOST /api/cancel → host shellouts pkill -INT claude, emits a Note. Also surfaces as a ■ cancel turn button in the state row while state=thinking.
  • /compactPOST /api/compact → host spawns turn::compact_session in the background; output streams into the live panel.

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

Per-agent endpoints

  • POST /send — operator-injected message into this agent's inbox.
  • POST /login/{start,code,cancel} — claude OAuth login flow.
  • POST /api/cancel — SIGINT the in-flight claude turn.
  • POST /api/compact — run /compact on the persistent session.
  • GET /events/history — replay buffer for the terminal.