diff --git a/CLAUDE.md b/CLAUDE.md index db2098e5..438b7c41 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -118,9 +118,11 @@ frontend/ npm workspaces (esbuild → static dist). Built + base typography (was hive-fr0nt). ES module exporting { create, linkify }; pure JS, no IIFE globals; consumed by dashboard + agent. - packages/dashboard/ @hive/dashboard SPA: src/{index.html, app.js, - dashboard.css} + build.mjs → dist/{index.html, - static/{app.js, dashboard.css}}. + packages/dashboard/ @hive/dashboard SPA: src/{index.html, tabs.js, + flow.html, flow.js, common.js, dashboard.css} + + build.mjs → dist/{index.html, flow.html, + static/{tabs.js, flow.js, dashboard.css, + stream-worker.js}}. packages/agent/ @hive/agent default per-container UI: src/ {index, stats, screen}.html + {app, stats}.js + agent.css → dist/{*.html, static/*}. diff --git a/docs/conventions.md b/docs/conventions.md index a72c6e34..f61a56ae 100644 --- a/docs/conventions.md +++ b/docs/conventions.md @@ -34,7 +34,7 @@ in `hive-c0re::dashboard_events::DashboardEvent`. ## Async forms Dashboard + per-agent mutating forms carry `data-async`; a delegated -`submit` listener in `assets/app.js` intercepts, shows a spinner, +`submit` listener in `assets/tabs.js` intercepts, shows a spinner, POSTs `application/x-www-form-urlencoded` (axum's `Form` extractor rejects multipart), calls `refreshState()` on success. New mutating forms should add `data-async` and optionally `data-confirm` (for a diff --git a/docs/web-ui.md b/docs/web-ui.md index fec6d2ec..63db1d0d 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -67,7 +67,7 @@ listener: read `data-confirm`, swap the button to a spinner, POST 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. +relevant `assets/tabs.js` render function. **Focus preservation:** `refreshState` checks whether `document.activeElement` sits inside one of the managed sections @@ -298,7 +298,7 @@ the tooltip says "select … for bulk actions" or "deselect … (or press Esc to clear all)". The `` points at `/icon`; load failure falls back to the dimmed hyperhive mark (`/favicon.svg`). The card body sits to the right with three stacked lines -(`assets/app.js::renderContainers`). +(`assets/tabs.js::renderContainers`). - Line 1: agent name (link → new tab), m1nd/ag3nt chip, an **icon-only nav strip** populated async from the agent backend @@ -382,7 +382,7 @@ every render before the bar appears. ### Approval card -Each pending approval renders as a card (`assets/app.js:: +Each pending approval renders as a card (`assets/tabs.js:: renderApprovals`) with three stacked sections: - **identity header** — glyph, `#id`, agent, kind chip, (for diff --git a/frontend/packages/dashboard/build.mjs b/frontend/packages/dashboard/build.mjs index e8cf87b3..346f1125 100644 --- a/frontend/packages/dashboard/build.mjs +++ b/frontend/packages/dashboard/build.mjs @@ -2,11 +2,11 @@ // // dist/index.html served by the Rust router at GET / // dist/flow.html served at GET /flow.html -// dist/static/app.js /index.html entry — tab renderers + +// dist/static/tabs.js /index.html entry — tab renderers + // tab routing + refreshState // dist/static/flow.js /flow.html entry — broker terminal + // operator inbox + @-mention composer -// dist/static/{app,flow}.js.map source map siblings +// dist/static/{tabs,flow}.js.map source map siblings // dist/static/dashboard.css served at /static/dashboard.css // (@import resolved from @hive/shared) // @@ -35,7 +35,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('app.js'), src('flow.js')], + entryPoints: [src('tabs.js'), src('flow.js')], outdir: staticDir(''), bundle: true, format: 'esm', @@ -47,7 +47,7 @@ await build({ // Stream-worker entry (#448). 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 app.js / flow.js. Output is +// `window`) so they can't be inlined into tabs.js / flow.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 diff --git a/frontend/packages/dashboard/src/common.js b/frontend/packages/dashboard/src/common.js index 5b59da4f..2bc430d8 100644 --- a/frontend/packages/dashboard/src/common.js +++ b/frontend/packages/dashboard/src/common.js @@ -1,14 +1,13 @@ -// Shared dashboard helpers — extracted from app.js as step 1 of the -// #406 split. These bits are used by both the tab dashboard -// (index.html) and the flow page (flow.html): pure DOM helpers, the -// side-panel singleton, the OS-notification module, and the -// path-link / file-preview infrastructure for the side panel. +// Shared dashboard helpers — extracted from the original monolithic +// dashboard JS as step 1 of the #406 split. These bits are used by +// both the tab dashboard (index.html) and the flow page (flow.html): +// pure DOM helpers, the side-panel singleton, the OS-notification +// module, and the path-link / file-preview infrastructure for the +// side panel. // -// Both pages currently still load `app.js` as their single entry -// point; this module is bundled inline by esbuild via the import -// below. The follow-up step splits app.js into per-page entry -// points (tabs.js + flow.js), at which point both will import from -// here directly. +// Each page now has its own entry point — `./tabs.js` for index.html, +// `./flow.js` for flow.html — and both import from here directly +// (#406 steps 2 + 3 complete; #406 closed). import { linkify as termLinkify } from '@hive/shared/terminal.js'; @@ -53,7 +52,7 @@ export const form = (action, btnClass, btnLabel, confirmMsg, extra = {}, opts = return f; }; -// `truncate`, `fmtAgo`, `fmtElapsed`, `fmtDuration` stay in app.js +// `truncate`, `fmtAgo`, `fmtElapsed`, `fmtDuration` stay in tabs.js // for now — each has display-specific phrasing ("X running", "X ago") // tied to its caller, so they don't generalise cleanly. We can lift // them when a second consumer needs the same shape. @@ -62,7 +61,7 @@ export const form = (action, btnClass, btnLabel, confirmMsg, extra = {}, opts = // Returns an EventSource-shaped object backed by a SharedWorker that // holds ONE upstream `new EventSource(url)` and fans events out to // every connected tab. Replaces direct `new EventSource(url)` at the -// dashboard's two consumer sites (app.js inline + flow.js via +// dashboard's two consumer sites (tabs.js inline + flow.js via // terminal.js's `streamFactory` option) so N hyperhive tabs share // ONE backend connection — way under the browser's per-host // connection cap, immune to per-tab throttling that drops the SSE diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index c7b53f9d..de637f63 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -108,7 +108,7 @@ body.dashboard-shell { } /* Tab pane visibility — show only the active one. The .tab-pane-active - class is set by app.js based on the URL hash; default (no hash) + class is set by tabs.js based on the URL hash; default (no hash) resolves to SW4RM. */ .tab-pane { display: none; } .tab-pane.tab-pane-active { display: block; } @@ -190,7 +190,7 @@ a:hover { transition: opacity 200ms ease, border-color 200ms ease; } /* Topology indent (#363). Each depth level shifts the row right by one - step; the .tree-prefix span (drawn by app.js::treePrefix) carries + step; the .tree-prefix span (drawn by tabs.js::treePrefix) carries the ├─ / └─ glyph and any continuation lines that thread through ancestor columns. When every container has parent=null (pre-#361 state) `[data-depth]` is absent on every row and these rules are @@ -1592,7 +1592,7 @@ body.flow-shell .tabbar .tab.active.tab-link { } /* Tail pill (↓ N new): bottom offset clears the floating composer. z-index escapes the stacking context the .terminal-wrap's - backdrop-filter creates (issue #375) — app.js anchors the pill + backdrop-filter creates (issue #375) — tabs.js anchors the pill on .flow-main now (not .terminal-wrap), so this z-index reaches the root stacking context and properly floats above the composer at z-index 30. */ @@ -1861,6 +1861,6 @@ body.flow-shell .tabbar .tab.active.tab-link { /* Pad the dashboard body so the sticky bar doesn't cover the bottom of the agent list. ~3.4em covers the bar's vertical footprint with breathing room; only applies when the bar is - visible (`body.has-selection`, toggled by app.js when the + visible (`body.has-selection`, toggled by tabs.js when the selection set is non-empty). */ body.dashboard-shell.has-selection { padding-bottom: 4.5em; } diff --git a/frontend/packages/dashboard/src/flow.html b/frontend/packages/dashboard/src/flow.html index 407f3b85..2b48915e 100644 --- a/frontend/packages/dashboard/src/flow.html +++ b/frontend/packages/dashboard/src/flow.html @@ -41,7 +41,8 @@
@@ -84,7 +85,7 @@
- + live in `/static/tabs.js` which /flow.html doesn't load. --> diff --git a/frontend/packages/dashboard/src/flow.js b/frontend/packages/dashboard/src/flow.js index 41487071..eefe8710 100644 --- a/frontend/packages/dashboard/src/flow.js +++ b/frontend/packages/dashboard/src/flow.js @@ -1,4 +1,6 @@ -// /flow.html entry point (#406 step 2 — flow-specific split from app.js). +// /flow.html entry point (#406 step 2 — flow-specific split from the +// previous combined entry; #406 step 3 renamed that combined entry +// from `app.js` to `tabs.js`). // // Owns the full-page broker terminal, the operator-inbox derived store // (populated from the broker stream), the inbox pill flyout, and the @@ -6,10 +8,10 @@ // panel, OS notifications, path linkification) from `./common.js`. // // Does NOT contain the dashboard's tab renderers, mutation-event -// dispatchers, or refreshState — that's `./app.js` (the legacy entry -// kept until step 3 renames it to `tabs.js`). For now the flow page -// runs purely on the broker stream + an initial /api/state fetch -// (compose autocomplete needs the live container list). +// dispatchers, or refreshState — that's `./tabs.js`, loaded only by +// /index.html. 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 { diff --git a/frontend/packages/dashboard/src/index.html b/frontend/packages/dashboard/src/index.html index 834ca0b5..a14e24e4 100644 --- a/frontend/packages/dashboard/src/index.html +++ b/frontend/packages/dashboard/src/index.html @@ -173,7 +173,7 @@ @@ -216,9 +216,11 @@ title="clear selection (esc)">✕ clear - - + + diff --git a/frontend/packages/dashboard/src/app.js b/frontend/packages/dashboard/src/tabs.js similarity index 98% rename from frontend/packages/dashboard/src/app.js rename to frontend/packages/dashboard/src/tabs.js index 87fb01a9..11aa7be1 100644 --- a/frontend/packages/dashboard/src/app.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -1,16 +1,18 @@ // /index.html entry point: tab renderers + tab routing + refreshState // + notification deltas. Reads /api/state on cold load and after every // async-form submit; live updates run through `applyXxx` mutation -// handlers triggered by the dashboard event stream. (Live SSE -// subscription on /index.html is still missing — see #406 step 3 / #408.) +// handlers triggered by the dashboard event stream. // // #406 step 1: pure helpers + side panel + OS notifications + path // linkification moved to `./common.js`. // #406 step 2: the flow-only IIFEs (operator inbox, inbox-pill, broker -// terminal, @-mention composer) moved to `./flow.js`. /flow.html now -// loads `flow.js` as its own bundle entry; this file (still named -// `app.js` for the moment; rename to `tabs.js` is the last step -// of #406) is loaded only by /index.html. +// terminal, @-mention composer) moved to `./flow.js`. /flow.html loads +// `flow.js` as its own bundle entry; this file is loaded only by +// /index.html. +// #406 step 3: file renamed from `app.js` → `tabs.js` since it owns +// the dashboard *tabs* surface only (the FL0W page has its own bundle). +// Live SSE subscription is wired through `openStream` from common.js +// (#406 step 3 hookup; see also #408 for stream-side filtering). import { marked } from 'marked'; import { @@ -446,14 +448,17 @@ window.marked = marked; function renderContainers(s) { const root = $('containers-section'); - // #containers-section only exists on /index.html. The same - // bundled app.js runs on /flow.html (which has the broker - // terminal but no container list) and on any future pages — - // `container_state_changed` SSE events arrive on every page and - // route through `applyContainerStateChanged → renderContainersFromState`, - // so without this guard the renderer throws `root is null` on - // every event (#399). Matches the no-op-when-target-absent - // convention the other renderers (renderTombstones, etc.) follow. + // #containers-section only exists on /index.html. tabs.js is the + // bundle for that page only (#406 step 3 — /flow.html loads + // flow.js instead), but historical context: pre-split the + // `container_state_changed` SSE handler routed through + // `applyContainerStateChanged → renderContainersFromState` on + // every page that loaded the single combined bundle, and the + // guard prevented a `root is null` throw on /flow.html (#399). + // Today it's 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 + // (renderTombstones, etc.) follow. if (!root) return; root.innerHTML = ''; @@ -1102,9 +1107,10 @@ window.marked = marked; function renderQuestions() { const root = $('questions-section'); // #questions-section only lives on /index.html (Y3R C4LL tab); - // no-op on /flow.html etc. — the bundled app.js runs everywhere - // and `question_added` / `question_resolved` SSE events route - // through here (#399). + // no-op when missing (#406 step 3 — only /index.html loads + // tabs.js, but kept as belt-and-suspenders for any future page + // adding it without that section). `question_added` / + // `question_resolved` SSE events route through here (#399). if (!root) return; root.innerHTML = ''; const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(0, 19);