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. -->