# 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 · `) + `■ 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` 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 `
` `▸ ← 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) - `/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. 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.