diff --git a/CLAUDE.md b/CLAUDE.md index c7db0ed..0b2bf15 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,8 +19,19 @@ hive-c0re/ host daemon + CLI (one binary, subcommand-dispatched) src/client.rs admin-socket client src/manager_server.rs manager-privileged socket (ManagerRequest) src/agent_server.rs per-sub-agent socket listener (long-poll Recv) - src/broker.rs sqlite Message store + broadcast channel for SSE + - hourly vacuum of delivered>30d + src/broker.rs sqlite Message store + intra-process broadcast + channel (`MessageEvent`) for `recv_blocking` + + the dashboard forwarder; hourly vacuum of + delivered>30d + src/dashboard_events.rs unified wire-facing event channel feeding + `/dashboard/stream`. Carries broker `Sent` / + `Delivered` (mirrored by the forwarder task + in main.rs) + mutation events + (`ApprovalAdded` / `ApprovalResolved`, + `QuestionAdded` / `QuestionResolved`, + `TransientSet` / `TransientCleared`). Each + frame carries a monotonic per-process `seq` + clients use to dedupe against snapshot reads. src/approvals.rs sqlite Approval queue + kinds src/operator_questions.rs sqlite question queue backing `ask` / `answer` (both operator + agent-to-agent) @@ -49,9 +60,26 @@ hive-c0re/ host daemon + CLI (one binary, subcommand-dispatched) (idempotent, marker-guarded phase 4) src/dashboard.rs axum HTTP: static shell + /api/state JSON + actions + journald viewer + bind-with-retry (SO_REUSEADDR) - + deployed_sha chip per container + + deployed_sha chip per container + + /dashboard/{stream,history} subscribing to the + unified DashboardEvent channel assets/ index.html, dashboard.css, app.js (include_str!) +hive-fr0nt/ shared frontend-assets crate (browser only). + src/lib.rs pub const BASE_CSS / TERMINAL_CSS / TERMINAL_JS + re-exports; both binaries `include_str!` them + and prepend to their per-page serving routes. + assets/base.css Catppuccin palette + body typography (one source + of truth, no per-page redeclaration). + assets/terminal.css `.terminal-wrap` + `.live` + `.tail-pill` + + `.row` / `details.row` styling for both + pages' lit log panes. + assets/terminal.js `window.HiveTerminal.create(opts)`: scroll- + sticky log + "↓ N new" pill + history + backfill + SSE subscribe-buffer-snapshot- + dedupe dance. Pages register a kind→renderer + map; the terminal owns the lifecycle. + hive-ag3nt/ in-container harness crate; produces TWO binaries src/lib.rs re-exports + DEFAULT_SOCKET, DEFAULT_WEB_PORT src/client.rs generic JSON-line request/response over unix socket @@ -165,6 +193,37 @@ Prune freely. domain tooling — the agent flake's `inputs` block pulls the external flake, `agent.nix` references it via `flakeInputs..packages.${pkgs.system}.default`. +- **Just landed:** dashboard event refactor. New `hive-fr0nt` + workspace crate hosts shared frontend assets (palette + terminal + CSS + `window.HiveTerminal.create` JS) so both the dashboard and + the per-agent web UI render their live panes through the same + code; the dashboard's `#msgflow` now feels like the agent's + terminal (sticky-bottom + pill + lit chrome). New unified + `DashboardEvent` channel on `Coordinator` (replaces the + broker-only `/messages/stream`); a background forwarder mirrors + broker traffic onto it as `Sent` / `Delivered` variants, and + the mutation-event variants + (`ApprovalAdded` / `ApprovalResolved`, `QuestionAdded` / + `QuestionResolved`, `TransientSet` / `TransientCleared`) cover + every in-process state change the dashboard cares about. Each + frame carries a monotonic per-process `seq`; snapshot endpoints + return their seq alongside the state, and the terminal's + open-buffer-then-fetch-history dance drops any buffered frame + with `seq <= history_seq` so an event landing between subscribe + and history-fetch is neither shown twice nor lost. Operator + inbox + approvals + questions + transients are now derived + client-side from the event stream (cold-loaded from + `/api/state` for first paint, mutated live from SSE + thereafter); `/op-send` + per-agent `/send` return 200 instead + of 303-and-refetch. Container-list events still pending — + `ContainerView` is sourced from external `nixos-container list`, + so the 5s `/api/state` poll continues to drive the containers + section. Approval diffs are now raw unified-diff text on the + wire (per-line classification happens in JS) so they fit in + SSE payloads without HTML escaping. Bug fix: `LiveEvent::Note` + was a newtype variant that serde silently failed to serialize + — converted to `Note { text: String }` (wire shape matches what + the JS already read). - **Just landed:** `ask_operator` → `ask` rename + optional `to: ` param for agent-to-agent structured Q&A. Recipient defaults to the operator (dashboard); peer diff --git a/docs/conventions.md b/docs/conventions.md index 8a709ec..a72c6e3 100644 --- a/docs/conventions.md +++ b/docs/conventions.md @@ -24,9 +24,12 @@ admin socket. ## Wire protocol JSON line-delimited over unix sockets in both directions (host admin -/ manager / agent). SSE streams (`/messages/stream`, -`/events/stream`) are `text/event-stream`. Request/response types -live in `hive-sh4re` — change them in one place. +/ manager / agent). SSE streams (`/dashboard/stream` on hive-c0re, +`/events/stream` on the per-agent web UIs) are `text/event-stream`; +each frame carries a `seq` field for the snapshot-dedupe dance +(see `docs/web-ui.md`). Request/response types live in `hive-sh4re` +— change them in one place. The dashboard event vocabulary lives +in `hive-c0re::dashboard_events::DashboardEvent`. ## Async forms diff --git a/docs/web-ui.md b/docs/web-ui.md index c00f548..79c9ca2 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -10,10 +10,31 @@ and the per-agent UIs (manager on :8000, sub-agents on a hashed - `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. +- `GET /static/*.css` + `GET /static/*.js` → static assets. Both + pages prepend `hive_fr0nt::BASE_CSS` + `TERMINAL_CSS` to their + per-page stylesheet, and `GET /static/hive-fr0nt.js` serves the + shared `window.HiveTerminal.create` runtime. The dashboard's + `#msgflow` and the per-agent `#live` log are both backed by + this terminal — sticky-bottom auto-scroll, "↓ N new" pill, + history backfill, SSE plumbing all live there. Each page + registers a kind→renderer map; unknown kinds fall through to + a JSON-dump note row. +- `GET /api/state` → JSON snapshot the JS app renders into the + DOM. Includes a top-level `seq` (the dashboard event channel's + high-water mark at the moment the snapshot was assembled); + clients use it to dedupe their buffered SSE traffic against + the snapshot (drop frames with `seq <= snapshot.seq`). +- `GET /dashboard/stream` (dashboard) / `GET /events/stream` + (per-agent) → `text/event-stream` SSE for live updates. The + dashboard stream carries broker `Sent` / `Delivered` (mirrored + by a forwarder task from the broker's intra-process channel) + plus mutation events (`approval_added` / `approval_resolved`, + `question_added` / `question_resolved`, `transient_set` / + `transient_cleared`). Each frame carries a `seq`. The + matching backfill endpoint is `GET /dashboard/history` (last + ~200 broker messages wrapped in `{ seq, events }`) on the + dashboard and `GET /events/history` (last 2000 `LiveEvent`s + also wrapped in `{ seq, events }`) on the agent. The JS app handles all `form[data-async]` submissions via a delegated listener: read `data-confirm`, swap the button to a spinner, POST @@ -71,26 +92,37 @@ the previous process's socket release resolves itself. with `[cancelled]`. Questions with a `ttl_seconds` show a `⏳ MM:SS` chip; the host-side watchdog auto-cancels with `[expired]` when the deadline fires. -5. **0PER4T0R 1NB0X** — recent messages addressed to `operator` - (last 50, from the broker). +5. **0PER4T0R 1NB0X** — recent messages addressed to `operator`, + derived client-side from the dashboard event stream (no longer + a snapshot field). Cold load seeds from + `/dashboard/history`'s 200-message backfill; subsequent + `sent` events with `to == "operator"` are appended live. Cap + 50, newest-first. 6. **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. -7. **MESS4GE FL0W** — live broker SSE tail (newest-first). - Each row is one broker event — `sent` or `delivered` — with - `from → to: body`; per-agent thinking / tool calls / claude - chatter stay out of this view, only what passes through - hive-c0re's broker. Below the stream sits a terminal-style - compose box: `@name` picks the recipient (sticky across - sends via localStorage; auto-complete from the live - container list, Tab/Enter to confirm), starting a message - with `@ body` retargets in one stroke, plain text - sends to the sticky recipient. `POST /op-send` drops - `{from:"operator", to, body}` into the broker — same shape - any sub-agent sees as a regular inbox message. Manager is - addressed as `@manager` (the broker recipient string), not - `@hm1nd` (the container name); the auto-complete swaps - automatically. +7. **MESS4GE FL0W** — live broker tail wrapped in a + `.terminal-wrap` (same chrome as the per-agent terminal). + Cold load backfills the last ~200 messages from + `/dashboard/history`; live frames arrive on + `/dashboard/stream` and dispatch through + `HiveTerminal.create`. Each row is one broker event — + `sent` or `delivered` — with `from → to: body`; per-agent + thinking / tool calls / claude chatter stay out of this + view, only what passes through hive-c0re's broker. Sticky- + bottom auto-scroll + "↓ N new" pill match the per-agent + page. Below the stream sits a terminal-style compose box: + `@name` picks the recipient (sticky across sends via + localStorage; auto-complete from the live container list, + Tab/Enter to confirm; `@*` broadcasts to every registered + agent), starting a message with `@ body` retargets + in one stroke, plain text sends to the sticky recipient. + `POST /op-send` drops `{from:"operator", to, body}` into + the broker and returns 200; the resulting SSE frame + re-renders both the terminal row and the inbox section + (no `/api/state` refetch). Manager is addressed as + `@manager` (the broker recipient string), not `@hm1nd` + (the container name); the auto-complete swaps automatically. ### Container row @@ -143,10 +175,13 @@ not ours. ### Dashboard endpoints -- `POST /approve/{id}` — approve a pending approval. +- `POST /approve/{id}` — approve a pending approval. Fires + `ApprovalResolved` on the dashboard event channel; client + updates derived approvals state from the event. - `POST /deny/{id}` (`note=`, optional) — deny a pending approval with an optional operator-supplied reason. The reason - travels to the manager as `HelperEvent::ApprovalResolved.note`. + travels to the manager as `HelperEvent::ApprovalResolved.note` + and also rides on the dashboard's `ApprovalResolved` event. Dashboard prompts via `window.prompt()` on click. - `POST /{rebuild,kill,restart,start,destroy}/{name}` — lifecycle. `destroy` accepts `purge=on` to also wipe state dirs. @@ -157,12 +192,52 @@ not ours. - `POST /request-spawn` — queue a Spawn approval. - `POST /update-all` — rebuild every stale container. - `POST /op-send` (`to=`, `body=`) — drop an - operator-authored message into ``'s inbox. Used by the + operator-authored message into ``'s inbox. `to=*` fans + out to every registered agent. Returns 200; the broker + `Sent` event re-renders both the message-flow terminal and + the operator inbox without a snapshot refetch. Used by the compose textbox under MESS4GE FL0W. - `GET /api/journal/{name}?unit=&lines=` — journalctl viewer for a managed container. - `GET /api/agent-config/{name}` — read-only view of the applied `agent.nix`. +- `GET /dashboard/stream` — unified live event channel: + broker `sent` / `delivered`, plus the mutation events listed + below. Each frame carries `seq`. +- `GET /dashboard/history` — last ~200 broker messages + (wrapped as `{ seq, events }`) for the message-flow + terminal's backfill on page load. + +### Dashboard event channel + +Wire vocabulary on `/dashboard/stream` (kind tag is in the JSON +payload): + +- `sent` / `delivered` — broker traffic, mirrored from the + intra-process channel by a forwarder task. Used by the + message-flow terminal renderer and the operator-inbox + derived state. +- `approval_added` (id, agent, approval_kind, sha_short, diff, + description) / `approval_resolved` (id, agent, approval_kind, + sha_short, status, resolved_at, note, description) — pending + queue + history mutations. Client mutates a derived store and + re-renders only the approvals section. +- `question_added` (id, asker, question, options, multi, + asked_at, deadline_at) / `question_resolved` (id, answer, + answerer, answered_at, cancelled) — operator-targeted + questions only (peer-to-peer questions never fire these). The + ttl watchdog fires `question_resolved` with + `answerer = "ttl-watchdog"` on expiry. +- `transient_set` (name, transient_kind, since_unix) / + `transient_cleared` (name) — lifecycle action spinners. The + client ticks the elapsed-seconds badge off `since_unix` + client-side, no polling. + +`/api/state` still serves `approvals` / `approval_history` / +`questions` / `question_history` / `transients` for cold-start +on first page load and as a safety-net resync from the 5s poll; +the client maintains the same arrays in derived stores and +applies the events on top. Generalised form helpers: `form[data-confirm="…"]` pops `confirm()` before submit; `form[data-prompt="…"]` pops