diff --git a/docs/web-ui/README.md b/docs/web-ui/README.md index 800d868f..d3c2a99d 100644 --- a/docs/web-ui/README.md +++ b/docs/web-ui/README.md @@ -12,7 +12,7 @@ what you actually do here. Everything starts at the **dashboard**, served at `/`. A pages menu (the `☰` button at the left of the tab strip) links to the other -surfaces — Flow, Stats, the API docs and the swarm UI — and each of +surfaces — Stats, the API docs and the swarm UI — and each of those pages links back to the dashboard. The dashboard is where you'll spend most of your time. It's a single @@ -26,9 +26,8 @@ page with five tabs: - **K3PT ST4T3** — kept state from agents with no container (tombstones), with a purge button per entry. -Two things live on their own page instead, reached from the pages -menu: **Flow** (`/flow.html`, the raw live message stream across the -whole swarm) and **Stats** (`/stats.html`, swarm-wide usage stats). +Usage stats live on their own page, reached from the pages menu: +**Stats** (`/stats.html`, swarm-wide usage stats). Each agent also has its own page — a full terminal view of that agent's session, reachable by selecting its name anywhere in the diff --git a/docs/web-ui/css-vars.md b/docs/web-ui/css-vars.md index 4bd90e65..90ba5f9c 100644 --- a/docs/web-ui/css-vars.md +++ b/docs/web-ui/css-vars.md @@ -12,7 +12,7 @@ swap touches only the first: what the app references and it **never changes on a swap**. Both files live in `frontend/packages/shared/src/`. Per-page stylesheets -(`common.css`, `dashboard.css`, `flow.css`, `agent.css`) +(`common.css`, `dashboard.css`, `agent.css`) reference the **semantic** names and must **not** redeclare them or reach for the raw `--baseNN` slots directly. (`base.css` holds only the shared `body` typography — it references the palette but no longer defines it.) diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 0a308514..f525f82b 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -12,21 +12,21 @@ hive-c0re serves the dashboard at `/`. It has a fixed chrome header at the top and a `
` that shows exactly one tab pane at a time. The URL hash (`#agents`, `#rebuild`, `#meta`, `#buildlogs`, `#kept`) drives which pane is -active; hash changes don't reload the page. FL0W and ST4TS -are standalone pages reached from the pages menu, not from the +active; hash changes don't reload the page. ST4TS +is a standalone page reached from the pages menu, not from the dashboard tab strip. **Chrome header** (fixed, overlays the active tab pane): - **Pages menu**: a `☰` button at the left of the chrome (`links-menu.js`) - opening a popover with links to **flow** (`/flow.html`), **stats** + opening a popover with links to **stats** (`/stats.html`), **api docs** (`/api/docs/`) and **swarm** (the swarm UI at the swarm apex domain, opened in a new tab). The swarm entry comes from `state.swarm_public_url`, which hive-c0re reads from `HIVE_SWARM_PUBLIC_URL` (set from `services.hyperhive.swarm.domain`); with no domain set there is no swarm entry. The standalone pages link back to `/`. - **Tab strip**: `◆ 4G3NTS ◆`, `◆ R3BU1LD QU3U3 ◆`, `◆ M3T4 1NPUTS ◆`, - `◆ BUILD L0GS ◆`, `◆ K3PT ST4T3 ◆`. In-page tabs only — FL0W / ST4TS - live on their own pages, reached from the pages menu. Count pills on + `◆ BUILD L0GS ◆`, `◆ K3PT ST4T3 ◆`. In-page tabs only — ST4TS + lives on its own page, reached from the pages menu. Count pills on 4G3NTS (stale containers), R3BU1LD QU3U3 (rebuilds in flight) and K3PT ST4T3 (kept-state entries). - **Banner-thin** (`░▒▓█▓▒░ HYPERHIVE / HIVE-C0RE / WE ARE THE WIRED ░▒▓█▓▒░`) @@ -46,10 +46,6 @@ dashboard tab strip. to ` / // h1ve-c0re` once `hive_name` / `swarm_name` arrive in the state snapshot. -The FL0W page uses a slim header (a `← dashboard` back-link + the page -title) rather than the dashboard tab strip — it's a standalone surface, -not a tab pane. - ## 4G3NTS tab **C0NTAINERS** — live containers as a flat list, one row per @@ -212,7 +208,7 @@ entry with that id pre-expanded. Hive-wide turn statistics, aggregated across every agent's `hyperhive-turn-stats.sqlite` for the selected window. Not a dashboard tab — a standalone page reached from the dashboard's pages menu, -same minimal chrome as `/flow.html`. Distinct from +minimal chrome (a back-link and a title). Distinct from each agent's own `/stats` page (which carries the per-agent trend charts): ST4TS is the swarm-level rollup. @@ -249,42 +245,6 @@ the model id, longest match wins) mapping to `{ input, output, cache_read, cache_write }` USD-per-million-token prices. Models not covered fall back to hive-c0re's built-in estimate. -## FL0W page (`/flow.html`) - -A dedicated full-page terminal (not a tab pane — a separate HTML -page). Slim chrome: a `← dashboard` back-link, the FL0W -title, and the agent-filter select (see below). No dashboard tab -strip — FL0W is a standalone surface reached from the pages menu. - -**MESS4GE FL0W** — live broker tail wrapped in a `.terminal-wrap`. -Cold load backfills the last ~200 messages from `/api/dashboard/history`; -live frames arrive on `/api/dashboard/stream`. Each row is one broker -event — `sent` or `delivered` — with `from → to: body`. When a `sent` -and `delivered` event for the same message arrive within 3 seconds -(immediate delivery to a live recipient), the page upgrades the row in -place (arrow becomes green ✓, title reads "sent + delivered") instead of -rendering two near-identical lines — genuine delivery latency (recipient -was busy) still appears as a second row. Each row carries `data-from` / -`data-to` attributes; an **agent filter select in the FL0W header** narrows -the timeline to messages involving the chosen agent (matched on `from` OR -`to`), with non-matching rows hidden (`.flow-hidden` class). The selection -persists in localStorage across reloads; new rows pick up the active filter -at render time. The dropdown populates from the live container list and -stays current on add/remove; a saved selection survives even if that agent -isn't currently listed. - -The row is a `flex-wrap: wrap` container holding ts / arrow / from / sep -/ to chips inline; the **body wraps to its own full-width line below** the -chips (`flex: 1 1 100%`) so long timestamps and agent names never push the -body into a narrow trailing column. `min-width: 0` keeps -`word-break: break-word` effective so the body doesn't force the row wider -than its container. Sticky-bottom autoscroll + "↓ N new" pill. Below the -stream sits a terminal-style compose box: `@name` picks the recipient -(sticky via localStorage; autocomplete from the live container list, -Tab/Enter to confirm; `@*` broadcasts). `POST /api/op-send` drops -`{from:"operator", to, body}` into the broker; the resulting SSE frame -re-renders the terminal row. The operator addresses the root agent as `@root`. - ## Container row A full-height **square agent icon** (5em, capped) on the left. The @@ -577,11 +537,6 @@ below — some endpoints aren't in it yet. not yet delivered and sets `acked_at = now`. Returns `{ "marked": N }`. Agent name validated against `[a-z0-9_-]`, 1-63 chars; 400 on bad input. -- `POST /api/op-send` (`to=`, `body=`) — drop an - operator-authored message into ``'s inbox. `to=*` fans - out to every registered agent. Returns 200; the broker - `Sent` event re-renders the message-flow terminal without a - snapshot refetch. Used by the compose textbox under MESS4GE FL0W. - `GET /api/build-logs/{agent}?limit=N` — most-recent build log headers for one agent, newest first. Returns `Vec` (JSON): `id`, `agent`, `kind`, `cmdline`, @@ -621,8 +576,7 @@ below — some endpoints aren't in it yet. The client trusts that list and linkifies only those tokens, so directories, missing files, and forbidden subtrees never become anchors. No probe endpoint, no client-side regex - heuristics. Historical messages get the same treatment on - `/api/dashboard/history` backfill. + heuristics. - `GET /api/reminders` — list pending reminders for the dashboard's queued-reminders panel. - `GET /api/stats-hive?window=<1h|4h|24h|3d|7d|30d|all>` — hive-wide @@ -639,9 +593,6 @@ below — some endpoints aren't in it yet. - `GET /api/dashboard/stream` — unified live event channel: broker `sent` / `delivered`, plus the mutation events listed below. Each frame carries `seq`. -- `GET /api/dashboard/history` — last ~200 broker messages - (wrapped as `{ seq, events }`) for the message-flow - terminal's backfill on page load. ## Dashboard event channel @@ -652,9 +603,7 @@ payload): - `sent` / `delivered` — broker traffic, mirrored from the intra-process channel by a forwarder task. Both carry `id: i64` (the broker row id) and `in_reply_to: Option` for thread - rendering. The dashboard message-flow terminal renders reply - rows with a `↳ reply` tag that scroll-highlights the parent - row on selection. Used by the message-flow terminal renderer. + rendering. - `transient_set` (name, transient_kind, since_unix) / `transient_cleared` (name) — lifecycle action spinners. The client ticks the elapsed-seconds badge off `since_unix` @@ -696,7 +645,7 @@ payload): forms that mutate non-event-derived state** (PURG3 + meta-update, since tombstones + meta_inputs aren't event- shaped yet). Every other section — transients, -containers, message flow — +containers — derives from `/api/dashboard/stream` after the initial snapshot, maintaining its own client-side store and applying events on top. diff --git a/docs/web-ui/shape.md b/docs/web-ui/shape.md index 45635d89..6e161de5 100644 --- a/docs/web-ui/shape.md +++ b/docs/web-ui/shape.md @@ -19,15 +19,15 @@ from `@hive/shared` (was `hive-fr0nt`); the dashboard ships one CSS bundle per page (`colors.css` + `theme.css` + `common.css`, loaded by every page, plus a page-specific bundle — - `dashboard.css` / `flow.css` / + `dashboard.css` / `stats.css`); `common.css` inlines `@hive/shared`'s `base.css` + `terminal.css` + `tabs.css` + `chrome.css` + `pill.css` via esbuild's `@import` resolution. `terminal.js` exports `{ create, linkify }` as ES module members; a back-compat shim in the IIFE bodies still exposes - a `window.HiveTerminal` global for callers that need it. The dashboard's - `#msgflow` and the per-agent `#live` log are both backed by + a `window.HiveTerminal` global for callers that need it. The + per-agent `#live` log is backed by this terminal — sticky-bottom autoscroll, "↓ N new" pill, history backfill, SSE plumbing all live there. Each page registers a kind→renderer map; unknown kinds fall through to @@ -51,10 +51,8 @@ dashboard stream carries broker `Sent` / `Delivered` (mirrored by a forwarder task from the broker's intra-process channel) plus mutation events (`transient_set` / `transient_cleared`). Each frame carries a `seq`. The - matching backfill endpoint is `GET /api/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. + agent's matching backfill endpoint is `GET /events/history` (last 2000 + `LiveEvent`s wrapped in `{ seq, events }`). **One unified channel**: browsers cap concurrent SSE connections per origin (~6 in Chrome). Using one channel per domain would exhaust this budget on a live hive; dispatching @@ -87,9 +85,8 @@ ## Shared terminal pane -The shared terminal factory in `@hive/shared/terminal.js` backs both -surfaces' scrollable log streams (`#msgflow` on the dashboard, `#live` -on the per-agent page). The factory wires up +The shared terminal factory in `@hive/shared/terminal.js` backs the +per-agent page's scrollable log stream (`#live`). The factory wires up sticky-bottom autoscroll, a "↓ N new" pill, history backfill, and SSE replay. Pages register a `kind → renderer` map; unknown kinds fall through to a JSON-dump note row. The factory ships three row diff --git a/docs/web-ui/terminal-rendering.md b/docs/web-ui/terminal-rendering.md index aae8b9af..85762198 100644 --- a/docs/web-ui/terminal-rendering.md +++ b/docs/web-ui/terminal-rendering.md @@ -69,7 +69,3 @@ read that when you need the specifics, this doc doesn't duplicate it. `marked.parse()` through DOMPurify into a `
`. Applied to any `TermMsg` with `body_format: "markdown"`. -## Dashboard side (not covered here) - -The main dashboard's message-flow pane is a different shape: broker -messages render as `.msgrow` grid lines, not agent-terminal rows. diff --git a/frontend/packages/dashboard/build.mjs b/frontend/packages/dashboard/build.mjs index 18d6e14c..92632559 100644 --- a/frontend/packages/dashboard/build.mjs +++ b/frontend/packages/dashboard/build.mjs @@ -1,5 +1,5 @@ // esbuild build for @hive/dashboard: one JS+CSS bundle per page (the -// dashboard at `/`, flow/stats at their own +// dashboard at `/`, stats at its own // `.html`), each named after its own entry point below — see the // `entryPoints`/`for` lists for the exact map, not repeated here to // avoid this comment drifting out of sync with the real build steps. @@ -29,7 +29,7 @@ mkdirSync(staticDir(""), { recursive: true }); // follow-up once asset sizes warrant it). esbuild writes each entry // to `static/.js` based on the entryPoint basename. await build({ - entryPoints: [src("tabs.js"), src("flow.js"), src("stats.js")], + entryPoints: [src("tabs.js"), src("stats.js")], outdir: staticDir(""), bundle: true, format: "esm", @@ -58,7 +58,7 @@ await build({ // Stream-worker entry. Lives in a separate bundle: SharedWorker // scripts run in a different global (`self` is the worker scope, no -// `window`) so they can't be inlined into tabs.js / flow.js. Output is +// `window`) so they can't be inlined into tabs.js / stats.js. Output is // at `static/stream-worker.js`; common.js's `openStream` references // `/static/stream-worker.js` as the SharedWorker URL. `format: 'iife'` // matches the classic-script load (`new SharedWorker(url, name)` with @@ -89,7 +89,6 @@ for (const entry of [ "theme.css", "common.css", "dashboard.css", - "flow.css", "stats.css", ]) { await build({ @@ -101,7 +100,7 @@ for (const entry of [ }); } -for (const html of ["index.html", "flow.html", "stats.html"]) { +for (const html of ["index.html", "stats.html"]) { copyFileSync(src(html), dist(html)); } diff --git a/frontend/packages/dashboard/src/agents.js b/frontend/packages/dashboard/src/agents.js index 62d4f72e..4e529a46 100644 --- a/frontend/packages/dashboard/src/agents.js +++ b/frontend/packages/dashboard/src/agents.js @@ -588,7 +588,7 @@ function buildContainerLi(c, opts) { export function renderContainers(s) { const root = $("containers-section"); // #containers-section only exists on the dashboard. tabs.js is the - // bundle for that page only (flow.html loads flow.js instead). + // bundle for that page only. // Belt-and-suspenders for any future page that adds tabs.js // without a #containers-section — matches the // no-op-when-target-absent convention the other renderers diff --git a/frontend/packages/dashboard/src/common.css b/frontend/packages/dashboard/src/common.css index be07df35..34e31ce3 100644 --- a/frontend/packages/dashboard/src/common.css +++ b/frontend/packages/dashboard/src/common.css @@ -337,148 +337,9 @@ code { pages' sticky back-link bar) moved to @hive/shared/chrome.css, imported at the top of this file. */ -/* `#msgflow` is a shared `.live` pane inside `.terminal-wrap`. The - msgrow / msg-* rules below power the /flow.html full-page terminal. */ -.live .msgrow { - display: flex; - flex-wrap: wrap; - align-items: baseline; - gap: 0.5em; - padding: 0.1em 0; - text-indent: 0; -} -.live .msgrow .msg-body { - flex: 1 1 100%; - min-width: 0; -} -.live .msgrow.sent .msg-arrow { - color: var(--cyan); -} -.live .msgrow.delivered .msg-arrow { - color: var(--green); -} -/* Reply-thread rendering: indented border-left + muted reply tag. */ -.live .msgrow.msg-reply { - padding-left: 1.2em; - border-left: 2px solid var(--border); - margin-left: 0.6em; -} -.msg-reply-tag { - color: var(--muted); - font-size: 0.8em; - white-space: nowrap; - order: -1; -} -.msg-reply-tag a { - color: var(--muted); - text-shadow: none; - font-weight: normal; -} -.msg-reply-tag a:hover { - color: var(--fg); -} -@keyframes msg-highlight-fade { - from { - background: color-mix(in srgb, var(--purple) 18%, transparent); - } - to { - background: transparent; - } -} -.msg-highlight { - animation: msg-highlight-fade 1.5s ease-out forwards; -} -.msg-ts { - color: var(--muted); - font-size: 0.85em; -} -.msg-arrow { - font-weight: bold; -} -.msg-from { - color: var(--amber); -} -.msg-sep { - color: var(--muted); -} -.msg-to { - color: var(--pink); -} -.msg-body { - color: var(--fg); - white-space: pre-wrap; - word-break: break-word; -} - -/* ─── operator compose box ────────────────────────────────────────── - Sits inside `.terminal-wrap` on both /flow.html and the dashboard - CALL tab. Dashed top separator mirrors the agent terminal prompt. */ -.op-compose { - position: relative; - display: flex; - align-items: flex-start; - gap: 0.6em; - padding: 0.55em 0.8em; - border-top: 1px dashed var(--purple-dim); -} -.op-compose-prompt { - color: var(--purple); - text-shadow: 0 0 4px currentColor; - font-weight: bold; - white-space: nowrap; - user-select: none; - padding-top: 0.15em; -} -.op-compose-input { - flex: 1; - background: transparent; - color: var(--fg); - border: none; - font-family: inherit; - font-size: 1em; - line-height: 1.4; - resize: none; - overflow: hidden; - max-height: 8em; - padding: 0; -} -.op-compose-input:focus { - outline: none; -} -.op-compose-input::placeholder { - color: var(--muted); -} -.op-compose-suggest { - position: absolute; - left: 3em; - bottom: 100%; - z-index: 50; - background: var(--bg-elev); - border: 1px solid var(--purple-dim); - border-radius: 4px; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45); - list-style: none; - margin: 0; - padding: 0.3em 0; - min-width: 12em; - max-height: 14em; - overflow-y: auto; -} -.op-compose-suggest .item { - padding: 0.35em 0.9em; - cursor: pointer; - font-size: 0.9em; - color: var(--muted); -} -.op-compose-suggest .item.active, -.op-compose-suggest .item:hover { - background: var(--border); - color: var(--purple); -} - /* ─── side panel ───────────────────────────────────────────────── Long content (file previews, diffs, applied config) opens in - a drawer that swipes in from the right — used by both /flow.html and + a drawer that swipes in from the right on the dashboard. The drawer chrome itself (backdrop, drawer, resize handle, header, title, close button) is now the shared `` element (@hive/shared/side-panel.js); what's left @@ -566,8 +427,8 @@ hive-side-panel .md td { carries the horizontal gutter. Full-width chrome (the tab strip, the footer) then spans edge-to-edge for free, with no negative-margin breakout hacks. Only matches on pages that opt - in by adding the wrapper (index.html); FL0W / ST4TS - are full-bleed with their own `.-main` padding. */ + in by adding the wrapper (index.html); ST4TS + is full-bleed with its own `.-main` padding. */ .page-content { padding: 0 1.5em; } diff --git a/frontend/packages/dashboard/src/common.js b/frontend/packages/dashboard/src/common.js index 3d1449fd..fc536038 100644 --- a/frontend/packages/dashboard/src/common.js +++ b/frontend/packages/dashboard/src/common.js @@ -1,6 +1,6 @@ -// Shared dashboard helpers used by both index.html (./tabs.js) and -// flow.html (./flow.js): pure DOM helpers, the side-panel singleton, -// and the path-link / file-preview infrastructure for the side panel. +// Shared dashboard helpers used by index.html (./tabs.js): pure DOM helpers, +// the side-panel singleton, and the path-link / file-preview infrastructure +// for the side panel. import { linkify as termLinkify } from "@hive/shared/terminal.js"; import { el } from "@hive/shared/dom.js"; diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index 9dae6251..25858573 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -180,8 +180,6 @@ body.dashboard-shell { display: none; } -/* FL0W is a separate page (`/flow.html`) — its full-viewport - styling lives in flow.css. */ .banner { text-align: center; margin: 0 0 1em 0; diff --git a/frontend/packages/dashboard/src/flow.css b/frontend/packages/dashboard/src/flow.css deleted file mode 100644 index e76d5094..00000000 --- a/frontend/packages/dashboard/src/flow.css +++ /dev/null @@ -1,111 +0,0 @@ -/* ─── /flow.html — full-page chat ───────────────────────────────── - See docs/web-ui/dashboard.md::FL0W page for the page-vs-pane rationale. - Shape mirrors the per-agent live page (frosted-glass header + - composer, full-viewport terminal). */ - -:root { - /* Approximate height of the flow chrome (the slim back-link header). - The slug banner lives at the page footer on the dashboard, omitted - on /flow.html since the full-viewport terminal has no normal-flow - footer position. */ - --flow-header-h: 3.6em; - --flow-composer-h: 3.6em; - --flow-frost-bg: color-mix(in srgb, var(--bg) 74%, transparent); - --flow-frost-blur: blur(12px) saturate(140%); -} - -body.flow-shell { - /* Override the dashboard's centred-max-width layout — flow owns - the whole viewport. */ - max-width: none; - margin: 0; - padding: 0; - height: 100vh; - overflow: hidden; - background: - radial-gradient( - ellipse 80% 60% at 50% 0%, - color-mix(in srgb, var(--purple) 6%, transparent) 0%, - transparent 60% - ), - var(--bg); -} - -.flow-main { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - overflow: hidden; -} -/* flow.html now has a slim header (back link only) instead of the full - tabbar — reduce the top padding to match the smaller header height. */ -body.flow-shell .flow-main-slim { - padding-top: calc(var(--flow-header-h) + 0.4em); -} -.flow-main .terminal-wrap { - position: absolute; - inset: 0; - border: 0; - background: transparent; - box-shadow: none; - border-radius: 0; - margin: 0; - padding: 0; -} -.flow-main .live { - position: absolute; - inset: 0; - height: auto; - max-height: none; - padding-top: calc(var(--flow-header-h) + 0.8em); - padding-bottom: calc(var(--flow-composer-h) + 0.8em); - scroll-padding-top: calc(var(--flow-header-h) + 0.8em); - scroll-padding-bottom: calc(var(--flow-composer-h) + 0.8em); - overflow: auto; -} -/* Tail pill (↓ N new): bottom offset clears the floating composer. - Pill is anchored on .flow-main (not .terminal-wrap) so the - backdrop-filter stacking context doesn't trap its z-index — see - docs/web-ui/agent.md::Per-agent page (Terminal-wrap) for the same - gotcha on the agent page. */ -.flow-main .tail-pill { - bottom: calc(var(--flow-composer-h) + 0.6em); - z-index: 35; -} - -.flow-composer { - position: fixed; - bottom: 0; - left: 0; - right: 0; - z-index: 30; - min-height: var(--flow-composer-h); - background: var(--flow-frost-bg); - -webkit-backdrop-filter: var(--flow-frost-blur); - backdrop-filter: var(--flow-frost-blur); - border-top: 1px solid var(--purple-dim); - box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.35); - padding: 0.4em 1em; -} - -/* Agent filter: a select in the FL0W header narrows the - timeline to messages involving one agent. Non-matching rows get - `.flow-hidden`. */ -.flow-filter { - font-family: inherit; - font-size: 0.85em; - background: var(--bg-elev); - color: var(--amber); - border: 1px solid var(--border); - border-radius: 4px; - padding: 0.25em 0.5em; - margin-left: auto; -} -.flow-filter:focus { - outline: 1px solid var(--purple); -} -.msgrow.flow-hidden { - display: none; -} diff --git a/frontend/packages/dashboard/src/flow.html b/frontend/packages/dashboard/src/flow.html deleted file mode 100644 index bb13ddba..00000000 --- a/frontend/packages/dashboard/src/flow.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - hyperhive // FL0W - - - - - - - - - - - -
-
-
-
connecting…
-
-
-
- - -
-
- @—> - - -
-
- - - - - diff --git a/frontend/packages/dashboard/src/flow.js b/frontend/packages/dashboard/src/flow.js deleted file mode 100644 index c721f695..00000000 --- a/frontend/packages/dashboard/src/flow.js +++ /dev/null @@ -1,479 +0,0 @@ -// /flow.html entry point. Owns the full-page broker terminal and the -// @-mention compose box. Pulls shared infrastructure (DOM helpers, -// side panel, path linkification) from `./common.js`. FL0W is the pure -// event firehose. -// -// Does NOT contain the dashboard's tab renderers, mutation-event -// dispatchers, or refreshState — that's `./tabs.js`, loaded only by -// the dashboard. The flow page runs purely on the broker stream + an -// initial /api/state fetch (compose autocomplete needs the live -// container list). - -import { create as termCreate } from "@hive/shared/terminal.js"; -import { $, appendLinkified, openStream, initStickyTop } from "./common.js"; -import { el } from "@hive/shared/dom.js"; -import { epochSec } from "./util.js"; - -(() => { - initStickyTop(); - - // ─── local containers cache (for compose autocomplete) ────────────────── - // The compose box's @-mention completion suggests known agent names. - // the dashboard (tabs.js) maintains the canonical `containersState` - // from /api/state + SSE; here we keep a small local mirror updated - // by the same `container_state_changed` / `container_removed` events - // the dashboard would handle. - const flowContainers = new Map(); - fetch("/api/state") - .then((r) => (r.ok ? r.json() : null)) - .then((s) => { - if (!s || !Array.isArray(s.containers)) return; - for (const c of s.containers) flowContainers.set(c.name, c); - populateAgentFilter(); - }) - .catch(() => { - /* graceful: compose just shows `*` and nothing else */ - }); - - // ─── agent filter ─────────────────────────────────────────────── - // A select in the FL0W header narrows the timeline to messages involving - // one agent (matched on `from` OR `to`). Each rendered row carries - // `data-from` / `data-to`; non-matching rows get `.flow-hidden`. New rows - // pick up the active filter at render time (see renderMsg); changing the - // filter re-scans existing rows. Selection persists in localStorage so a - // reload / tab-switch keeps the view. Uses `$('msgflow')` for row access - // so it works regardless of the message-flow IIFE's local scope. - let agentFilter = localStorage.getItem("flow-agent-filter") || ""; - function rowMatchesFilter(from, to) { - return !agentFilter || from === agentFilter || to === agentFilter; - } - function applyAgentFilter() { - const flow = $("msgflow"); - if (!flow) return; - for (const row of flow.children) { - const { from, to } = row.dataset; - if (from === undefined && to === undefined) continue; // non-message row - row.classList.toggle("flow-hidden", !rowMatchesFilter(from, to)); - } - } - function populateAgentFilter() { - const sel = $("flow-agent-filter"); - if (!sel) return; - const names = [...flowContainers.keys()].sort(); - sel.replaceChildren(); - sel.append(el("option", { value: "" }, "all agents")); - for (const n of names) sel.append(el("option", { value: n }, n)); - // Preserve a saved selection even if that agent isn't in the live - // container list (yet / anymore) so the filter doesn't silently reset. - if (agentFilter && !names.includes(agentFilter)) { - sel.append(el("option", { value: agentFilter }, agentFilter)); - } - sel.value = agentFilter; - } - { - const sel = $("flow-agent-filter"); - if (sel) { - sel.addEventListener("change", () => { - agentFilter = sel.value; - if (agentFilter) localStorage.setItem("flow-agent-filter", agentFilter); - else localStorage.removeItem("flow-agent-filter"); - applyAgentFilter(); - }); - } - } - - // ─── message flow: shared terminal pane ──────────────────────────────── - // Scroll, pill, backfill + SSE plumbing live in @hive/shared/terminal. - // What stays here is the broker-message renderer + the page-local - // side effect (banner pulse). - (() => { - const flow = $("msgflow"); - if (!flow) return; - flow.replaceChildren(); - const tsFmt = (ts) => new Date(ts).toISOString().slice(11, 19); - // Pulse the page banner whenever a broker event lands. The - // `.banner` element lives in the dashboard's
rather than - // in the flow chrome — `pulseBanner` no-ops on /flow.html since - // there's no element to find. Kept for parity if a future chrome - // change reintroduces a banner. - const banner = document.querySelector(".banner"); - let bannerOffTimer = null; - function pulseBanner() { - if (!banner) return; - banner.classList.add("active"); - if (bannerOffTimer) clearTimeout(bannerOffTimer); - bannerOffTimer = setTimeout( - () => banner.classList.remove("active"), - 4000, - ); - } - // Map of broker row id → rendered row element. Lets reply rows add - // a visual "↳ in reply to" indicator that links back to the parent. - // Bounded by the history window (~200 msgs from /dashboard/history), - // well within normal memory. - const msgRowMap = new Map(); - - function renderMsg(ev, api, glyph) { - const isReply = ev.in_reply_to != null; - const cls = "msgrow " + ev.kind + (isReply ? " msg-reply" : ""); - const row = api.row(cls, ""); - // Build via DOM so path anchors stay live + escape rules are - // automatic (text nodes don't need esc()). - const ts = document.createElement("span"); - ts.className = "msg-ts"; - ts.textContent = tsFmt(ev.at); - const arrow = document.createElement("span"); - arrow.className = "msg-arrow"; - arrow.textContent = glyph; - const from = document.createElement("span"); - from.className = "msg-from"; - from.textContent = ev.from; - const sep = document.createElement("span"); - sep.className = "msg-sep"; - sep.textContent = "→"; - const to = document.createElement("span"); - to.className = "msg-to"; - to.textContent = ev.to; - const body = document.createElement("span"); - body.className = "msg-body"; - appendLinkified(body, ev.body, ev.file_refs); - // Reply thread indicator: a small "↳ reply to " hint that - // shows which message this is responding to. If we have the parent - // in our row map, clicking scrolls it into view. - if (isReply) { - const replyTag = document.createElement("span"); - replyTag.className = "msg-reply-tag"; - const parentRow = msgRowMap.get(ev.in_reply_to); - if (parentRow) { - const link = document.createElement("a"); - link.href = "#"; - link.textContent = "↳ reply"; - link.title = "scroll to parent message"; - link.addEventListener("click", (e) => { - e.preventDefault(); - parentRow.scrollIntoView({ behavior: "smooth", block: "nearest" }); - parentRow.classList.add("msg-highlight"); - setTimeout(() => parentRow.classList.remove("msg-highlight"), 1500); - }); - replyTag.append(link); - } else { - replyTag.textContent = "↳ reply"; - } - row.prepend(replyTag); - row.append(ts, " ", arrow, " ", from, " ", sep, " ", to, " ", body); - } else { - row.append(ts, " ", arrow, " ", from, " ", sep, " ", to, " ", body); - } - // Tag with the participants so the agent filter can match - // on `from`/`to`, and hide the row up-front if a filter is active. - row.dataset.from = ev.from; - row.dataset.to = ev.to; - if (!rowMatchesFilter(ev.from, ev.to)) row.classList.add("flow-hidden"); - // Register this row so future replies can reference it. - if (ev.id != null && ev.id > 0) msgRowMap.set(ev.id, row); - return row; - } - - // Sent→Delivered collapse. When a message directly wakes its - // recipient the broker emits `sent` then `delivered` for the same row - // id back-to-back, rendering two near-identical lines. We track each - // recent `sent` row and, when its `delivered` lands within - // COLLAPSE_SECS, upgrade that row in place (✓) instead of adding a - // second line. A delivery that arrives *later* (recipient was busy) - // stays a separate row so the latency remains visible. - const COLLAPSE_SECS = 3; - const recentSent = new Map(); // broker row id → { row, at } - function rememberSent(ev, row) { - if (ev.id == null || ev.id <= 0) return; - recentSent.set(ev.id, { row, at: epochSec(ev.at) }); - // Bound the map — drop the oldest entries once it grows past a - // page of un-collapsed sends (insertion order = oldest first). - while (recentSent.size > 256) { - recentSent.delete(recentSent.keys().next().value); - } - } - // Returns true if the delivered event was folded into its sent row. - function collapseDelivered(ev) { - if (ev.id == null || ev.id <= 0) return false; - const s = recentSent.get(ev.id); - if (!s || epochSec(ev.at) - s.at > COLLAPSE_SECS) return false; - const arrow = s.row.querySelector(".msg-arrow"); - if (arrow) arrow.textContent = "✓"; - // Re-style the row as delivered (green ✓) — the collapsed line now - // represents the delivered state; it was sent + delivered as one. - s.row.classList.remove("sent"); - s.row.classList.add("delivered"); - s.row.title = "sent + delivered"; - recentSent.delete(ev.id); - return true; - } - // Anchor the `↓ N new` pill in `.flow-main` rather than the - // default `.terminal-wrap` parent — see docs/web-ui/agent.md::Per-agent - // page (Terminal-wrap) for the backdrop-filter stacking-context - // gotcha (same shape on the flow page). - const flowMain = document.querySelector(".flow-main"); - termCreate({ - logEl: flow, - pillAnchor: flowMain, - historyUrl: "/api/dashboard/history", - // Server-side filter — only the kinds this page actually renders - // or routes (sent/delivered → broker terminal, - // container_state_changed/_removed → local autocomplete cache). - // Backend pre-parses the allow-list at subscribe time so the - // per-frame hot path is one HashSet::contains and the - // JSON-serialise is skipped entirely on irrelevant kinds. The - // dashboard tabs page (tabs.js) keeps the unfiltered subscribe - // since it routes every mutation kind into its derived stores. - streamUrl: - "/api/dashboard/stream?kinds=sent,delivered,container_state_changed,container_removed", - // Route through the SharedWorker — see docs/web-ui/shape.md (SSE - // multiplexing paragraph). Worker keys on the full URL incl. - // query string, so this filtered subscribe is its own upstream - // and won't accidentally share with tabs.js's wider subscribe. - streamFactory: openStream, - renderers: { - sent: (ev, api) => rememberSent(ev, renderMsg(ev, api, "→")), - delivered: (ev, api) => { - // Fold into the matching sent row when it just happened; - // otherwise render the delivery as its own line. - if (!collapseDelivered(ev)) renderMsg(ev, api, "✓"); - }, - // Maintain the local containers cache from the same stream - // (compose autocomplete reads from `flowContainers`). The - // dashboard's tab renderers aren't on this page, so we don't - // need to dispatch to applyContainerStateChanged etc. — just - // keep the autocomplete list current. - container_state_changed: (ev) => { - if (ev.container && ev.container.name) { - flowContainers.set(ev.container.name, ev.container); - populateAgentFilter(); // keep the filter dropdown current - } - }, - container_removed: (ev) => { - flowContainers.delete(ev.name); - populateAgentFilter(); - }, - // Drop every other mutation kind silently — without this - // they'd fall through to the terminal module's default - // renderer and clutter the log with JSON dumps. The dashboard - // tabs handle these on the dashboard via tabs.js. - _default: () => {}, - }, - // Re-sync the local containers cache on every SSE (re)connect. - // Live mutation events that fired during a disconnect window - // are never replayed, so without this the compose autocomplete - // could drift stale. - onStreamOpen: () => { - fetch("/api/state") - .then((r) => (r.ok ? r.json() : null)) - .then((s) => { - if (!s || !Array.isArray(s.containers)) return; - flowContainers.clear(); - for (const c of s.containers) flowContainers.set(c.name, c); - }) - .catch(() => {}); - }, - onLiveEvent: () => pulseBanner(), - }); - })(); - - // ─── compose: @-mention with sticky recipient ─────────────────────────── - (() => { - const input = $("op-compose-input"); - const prompt = $("op-compose-prompt"); - const suggest = $("op-compose-suggest"); - if (!input || !prompt || !suggest) return; - const STORAGE_KEY = "hyperhive:op-compose:to"; - let stickyTo = localStorage.getItem(STORAGE_KEY) || ""; - let suggestActive = -1; - function renderPrompt() { - prompt.textContent = stickyTo ? `@${stickyTo}>` : "@—>"; - } - function knownAgents() { - // Read live from the flow-local containers cache so newly-spawned - // agents become addressable without a manual reload. - const names = Array.from(flowContainers.values()).map((c) => c.name); - // `*` fans out to every registered agent (server-side - // broadcast_send). - names.unshift("*"); - return names; - } - function autosize() { - input.style.height = "auto"; - input.style.height = `${input.scrollHeight}px`; - } - /// Parse "@name body…" — return {to, body} when the input opens - /// with a known @-mention, otherwise null. - function parseAddressed(raw) { - const m = raw.match(/^@([\w*-]+)\s+([\s\S]+)$/); - if (!m) return null; - return { to: m[1], body: m[2] }; - } - function hideSuggest() { - suggest.hidden = true; - suggest.replaceChildren(); - suggestActive = -1; - } - function renderSuggest(matches) { - suggest.replaceChildren(); - if (!matches.length) { - hideSuggest(); - return; - } - for (let i = 0; i < matches.length; i += 1) { - const item = document.createElement("div"); - item.className = "item" + (i === suggestActive ? " active" : ""); - item.textContent = "@" + matches[i]; - item.addEventListener("mousedown", (e) => { - e.preventDefault(); - applySuggestion(matches[i]); - }); - suggest.append(item); - } - suggest.hidden = false; - } - function applySuggestion(name) { - // Replace the partial @-token at the start with the full name. - const v = input.value; - const m = v.match(/^@(\S*)/); - if (m) { - input.value = `@${name} ` + v.slice(m[0].length).replace(/^\s+/, ""); - } else { - input.value = `@${name} ` + v; - } - hideSuggest(); - input.focus(); - input.setSelectionRange(input.value.length, input.value.length); - autosize(); - } - function updateSuggest() { - const v = input.value; - // Only suggest while the cursor is still inside the @-token at the - // very start of the input — switching recipient is always "redirect - // this whole line." Once whitespace follows the token the recipient - // is chosen: the list must close, or it swallows the Enter that - // should send. Mid-message @-mentions stay literal. - const m = v.match(/^@(\S*)$/); - if (!m) { - hideSuggest(); - return; - } - const partial = m[1].toLowerCase(); - const matches = knownAgents().filter((n) => - n.toLowerCase().startsWith(partial), - ); - if (!matches.length) { - hideSuggest(); - return; - } - if (suggestActive < 0 || suggestActive >= matches.length) - suggestActive = 0; - renderSuggest(matches); - } - async function submit() { - const raw = input.value.trim(); - if (!raw) return; - let to; - let body; - const addressed = parseAddressed(raw); - if (addressed) { - to = addressed.to; - body = addressed.body.trim(); - } else if (stickyTo) { - to = stickyTo; - body = raw; - } else { - flashError("no recipient — start with @name to address a message"); - return; - } - if (!body) return; - const fd = new FormData(); - fd.append("to", to); - fd.append("body", body); - input.disabled = true; - try { - // /op-send returns 200. The SSE channel carries the resulting - // MessageEvent → the terminal renders the sent row on its own; - // no /api/state refetch needed. - const resp = await fetch("/api/op-send", { - method: "POST", - body: new URLSearchParams(fd), - }); - if (!resp.ok) { - flashError(`send failed: http ${resp.status}`); - return; - } - } catch (err) { - flashError(`send failed: ${err}`); - return; - } finally { - input.disabled = false; - } - stickyTo = to; - localStorage.setItem(STORAGE_KEY, to); - input.value = ""; - autosize(); - renderPrompt(); - input.focus(); - } - function flashError(msg) { - const flow = $("msgflow"); - if (!flow) return; - const row = document.createElement("div"); - row.className = "msgrow meta"; - row.textContent = "✗ " + msg; - // Append at the bottom so the error is visible — the terminal - // renders newest-last, so inserting before firstChild would place - // the error at the top (oldest end) and hide it from view. - flow.append(row); - // Scroll the terminal wrap so the error is in view. - const wrap = flow.parentElement; - if (wrap) wrap.scrollTop = wrap.scrollHeight; - } - input.addEventListener("input", () => { - autosize(); - updateSuggest(); - }); - input.addEventListener("keydown", (e) => { - if (!suggest.hidden) { - if (e.key === "ArrowDown") { - const items = suggest.querySelectorAll(".item"); - suggestActive = (suggestActive + 1) % items.length; - renderSuggest(Array.from(items).map((i) => i.textContent.slice(1))); - e.preventDefault(); - return; - } - if (e.key === "ArrowUp") { - const items = suggest.querySelectorAll(".item"); - suggestActive = (suggestActive - 1 + items.length) % items.length; - renderSuggest(Array.from(items).map((i) => i.textContent.slice(1))); - e.preventDefault(); - return; - } - if (e.key === "Tab" || (e.key === "Enter" && !e.shiftKey)) { - const active = suggest.querySelector(".item.active"); - if (active) { - applySuggestion(active.textContent.slice(1)); - e.preventDefault(); - return; - } - } - if (e.key === "Escape") { - hideSuggest(); - e.preventDefault(); - return; - } - } - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - submit(); - } - }); - input.addEventListener("blur", () => { - // Defer so a click on a suggestion item (mousedown) lands first. - setTimeout(hideSuggest, 100); - }); - renderPrompt(); - autosize(); - })(); -})(); diff --git a/frontend/packages/dashboard/src/index.html b/frontend/packages/dashboard/src/index.html index 117f9734..81a65f07 100644 --- a/frontend/packages/dashboard/src/index.html +++ b/frontend/packages/dashboard/src/index.html @@ -17,7 +17,7 @@ button / shared links keep you on the same view. JS owns the show/hide. -->
- @@ -82,7 +82,7 @@ - @@ -216,8 +216,8 @@
-
@@ -271,7 +271,7 @@ diff --git a/frontend/packages/dashboard/src/links-menu.js b/frontend/packages/dashboard/src/links-menu.js index 6707ca85..b92f1239 100644 --- a/frontend/packages/dashboard/src/links-menu.js +++ b/frontend/packages/dashboard/src/links-menu.js @@ -1,5 +1,5 @@ // Pages menu in the dashboard chrome: one button that opens a popover -// linking the standalone pages (flow, stats, API docs) and the swarm UI. +// linking the standalone pages (stats, API docs) and the swarm UI. // Modelled on the swarm UI's `LinksMenu`. The swarm entry only exists // when `/api/state` carries `swarm_public_url`, so a hive without a // configured swarm domain shows no dead link. @@ -10,7 +10,6 @@ import { el } from "@hive/shared/dom.js"; /** Menu entries for a state snapshot. `external` opens in a new tab. */ export function menuLinks(state) { const links = [ - { icon: "📡", label: "flow", url: "/flow.html" }, { icon: "📊", label: "stats", url: "/stats.html" }, { icon: "🧬", label: "api docs", url: "/api/docs/" }, ]; diff --git a/frontend/packages/dashboard/src/stats.css b/frontend/packages/dashboard/src/stats.css index e8bd8000..3b4a3ccc 100644 --- a/frontend/packages/dashboard/src/stats.css +++ b/frontend/packages/dashboard/src/stats.css @@ -1,6 +1,6 @@ /* ─── /stats.html — hive-wide turn-stats rollup ──────────────────── - Extracted from the dashboard ST4TS tab. Same minimal- - chrome pattern as /flow.html: the back-link header + + Extracted from the dashboard ST4TS tab. Minimal chrome: + the back-link header + title (.page-header / .page-back / .page-title) live in @hive/shared/chrome.css. `.hive-stats-table` lives in common.css. Only the ST4TS-specific diff --git a/frontend/packages/dashboard/src/stats.html b/frontend/packages/dashboard/src/stats.html index 45c03776..5230fb5a 100644 --- a/frontend/packages/dashboard/src/stats.html +++ b/frontend/packages/dashboard/src/stats.html @@ -11,7 +11,7 @@ -