Two bugs on the agent terminal page after the #362 overhaul:
## 1. `↓ N new` pill clipped by the composer
The fixed-overlay composer (z-index 30, agent.css) sits in the
root stacking context. The pill — `position: absolute` inside
`.live.terminal` with no z-index — defaults to its document-order
position in the body's stacking order, which the composer covers.
Fix: bump `.agent-main .tail-pill { z-index: 35 }` so the pill
participates in the root stacking context above the composer.
Scoped to the agent-page overlay layout — the shared `.tail-pill`
rule stays untouched (the dashboard's in-page layout doesn't need
the bump).
## 2. Autoscroll-on-new-message not firing when the operator was
already at the bottom
`afterAppend()` in terminal.js was calling `isNearBottom()` AFTER
appending the new row. The new row's own height is already in
`scrollHeight` at that point, so for any row taller than the
NEAR_BOTTOM_PX threshold (48px — easily passed by a multi-line
message body, a tool-result summary, a markdown block), the check
returns false and the pill shows + scroll stays put.
Fix: capture `wasNearBottom = isNearBottom()` BEFORE the
`log.appendChild(...)` in each of `row` / `details` /
`detailsDiff`, pass it into `afterAppend(wasNearBottom)`. Now the
auto-scroll triggers whenever the operator was visually at the
bottom an instant before the row landed, regardless of the new
row's height.
Same shared `@hive/shared/terminal.js` is used by the dashboard
+ per-agent UI + the upcoming /flow.html page, so both pages
inherit the fix.
## Validation
`npm run build` clean.
Bundle deltas: shared terminal bundle re-inlined into both consumers
unchanged in size (the wasNearBottom variable is a single bool, no
measurable delta). Agent CSS +0.1kb (z-index property).
Browser smoke test isn't possible from inside iris's container —
worth eyeballing post-deploy:
- With the operator scrolled to bottom, a tall message lands and
the view scrolls to keep it visible (instead of pinning the
pill).
- The pill appears above the composer when the operator is
scrolled up and new messages land.
Closes#375.
Operator: 'option A (tabs)' (#369#issuecomment-3434) +
'yes terminal can be a separate page' (#369#issuecomment-3437).
## Tab framework
`index.html` becomes a 3-tab dashboard with a sticky chrome header:
- `◆ SW4RM ◆` — containers list (the central thing)
- `◆ Y3R C4LL ◆` — pending approvals + operator-targeted questions
- `◆ SYST3M ◆` — meta inputs + rebuild queue + reminders + tombstones
Hash routing: `#swarm` / `#call` / `#system` (empty → SW4RM).
F5-reloadable + back-button-aware without a router framework.
SSE stays alive across tab switches — count pills on inactive tabs
update live so the operator never loses pulse on what's happening
elsewhere:
- SW4RM: containers with needs_update
- Y3R C4LL: approvals.pending + questions.pending (attn-coloured pill)
- SYST3M: rebuild_queue entries in Queued|Running
Pills hidden when count is zero. setInterval(1s) polls the existing
state stores (cheap, no per-renderer hookup needed).
## FL0W as its own page
The all-agents chat moves to /flow.html — full-viewport vibec0re
layout mirroring the per-agent live page (#362):
- Fixed-overlay frosted-glass header at top (back link + title +
notif controls), backdrop-filter blur shows the scrolled chat
text behind.
- Full-viewport terminal, scroll-padded for the floating chrome so
first/last rows stay reachable.
- Fixed-overlay frosted composer at the bottom.
- Operator inbox surfaces via a pill (📬 inbox · N) in the upper
right — click opens the side-panel flyout with the message list.
In the dashboard tab strip, FL0W is the right-most entry but
renders as a `<a class="tab tab-link" href="/flow.html">` — clicking
navigates to the page rather than swapping a pane. Same pattern
back from flow.html via the `← d4shb04rd` link.
## Implementation notes
- New `/flow.html` page rendered by the same bundled `app.js` — the
flow page just doesn't have the dashboard-chrome DOM, so the
matching renderers no-op silently (each `if (!el) return`).
Avoids splitting the bundle for v1; can extract later if size
becomes a concern.
- `Panel` module gains `openNamed(name, …)` + `refresh(name, …)` —
the legacy untyped `open(title, content)` calls clear the owner,
so file-preview / diff / log drill-ins behave unchanged. `refresh`
is no-op when a different view owns the panel, so live message
events re-render the inbox flyout only when it's actually open.
- `renderInbox` updates BOTH the dashboard's inline `#inbox-section`
(now living on the flow page) AND the flow page's pill count +
side-panel refresh. The dashboard's empty FL0W tab is removed —
inbox + message flow + compose box only exist in flow.html.
- Banner shrinks to a thin Catppuccin gradient strip at the top of
the dashboard chrome (dropped the multi-line ASCII art —
affectionate but pure chrome budget in a tabbed layout).
- `build.mjs` copies both `index.html` + `flow.html` into dist.
## Validation
`npm run build` clean. Dashboard bundle deltas:
app.js 150kb → 152kb (tab routing + count pills + named-Panel)
dashboard.css 33kb → 38kb (tab chrome + flow page layout)
+ dist/flow.html 4.4kb
Browser smoke test isn't possible from inside iris's container
(no JS engine) — drafting as a PR for operator visual review on
next deploy. Worth eyeballing:
- Tab switching feels right; counts update live across SSE events
- FL0W page reads like the agent live page (frosted header + composer)
- Inbox pill opens flyout; live message arrivals refresh it
- Back link from flow → dashboard returns to last tab via the
URL hash (browser remembers the hash across page nav)
Closes#369.
Closes#363 (frontend half of milestone #361). Consumes the
`ContainerView.parent: Option<String>` field landing in damocles'
backend slice — when present, the container list renders depth-first
with sibling-position tree glyphs (├─ / └─ joints + │ continuation
columns). When absent (pre-#361 state — every container's `parent` is
None) the tree collapses to a flat list with no glyphs and no indent
— bit-identical to the legacy render.
## Tree shape
- Roots: containers whose `parent` is None OR whose parent name isn't
in the current container set (orphan tolerance).
- Sibling ordering: alphabetical by name within each level (matches
damocles' wire spec at #363#issuecomment-3356).
- Cycle safety: any container not reached via the root-walk gets
emitted as a root at the end — no agent ever silently disappears
from the list when the topology is malformed.
- Tree glyphs: ancestor at depth d contributes a │ continuation column
when that ancestor has more siblings below; otherwise a 3-space gap.
The joint is ├─ for non-last siblings, └─ for the last child.
- Depth-0 ancestor column is suppressed: roots already separate
visually as top-level rows, no need for a column 0 vertical line.
## DOM / CSS
- New `buildAgentTree(containers)` + `treePrefix(node)` helpers in
app.js. The render loop walks the tree-ordered list instead of the
legacy alphabetical containers array.
- Each container row gets `data-depth=N` (only when N > 0) and a
`<span class="tree-prefix">…</span>` prepended (absolute-positioned
into the row's left margin so the existing flex icon/body layout
isn't disrupted).
- CSS: per-depth `margin-left` step rules for depths 1-6 (hardcoded
rather than typed-attr() because CSS Values 5 is Chromium-only as
of 2026). 6 depths cover any reasonable hive topology with
headroom; deeper agents render at depth 6 indent without further
step — visually clamps gracefully.
- `.tree-prefix` rendered with `var(--purple-dim)` so the structural
lines read as supporting chrome, not as content.
## Validation
- `npm run build` clean. Bundle deltas: dashboard app.js
+1.8kb (tree builder + treePrefix + render-loop tweak),
dashboard.css +0.4kb (tree-prefix + per-depth indent rules).
- The render is a no-op until `ContainerView.parent` is populated —
validation in production deferred to once damocles' meta-topology
field lands. The pre-#361 path (every parent=None) is exercised
by every existing dashboard load.
- Forward-compatible with damocles' design pivot at #364 (topology
source moved from agent.nix to meta/topology.json). The wire shape
on ContainerView is unchanged from the frontend's perspective — the
field is just sourced from a different backend store.
Per operator spec at #360#issuecomment-3333:
- full-screen terminal
- frosted-glass header overlaid on top
- inbox + loose-ends → flyout
- no a/b flag, just ship it
## Layout
`frontend/packages/agent/src/index.html` restructured to a three-zone
fixed-overlay shape:
- `<header.agent-header>` — fixed top, frosted glass via
`backdrop-filter: blur(12px) saturate(140%)`. Holds icon + title +
nav links + state-row (badges/buttons) + two new pill buttons that
surface inbox / loose-ends counts (and open the side panel on click).
- `<main.agent-main>` — fills the viewport. Terminal positioned absolute
inset:0 with padding-top/-bottom + scroll-padding equal to the
floating header/composer heights so first/last rows stay reachable
and `↓ N new` pill anchors land in the visible scroll zone.
- `<footer.agent-composer>` — fixed bottom, mirror-frosted. Owns
`#term-input`; dropped the in-frame dashed separator (border-top
+ box-shadow on the bar already separate it from the terminal).
- `<div.side-panel>` — singleton drawer (copy of the dashboard
pattern, candidate for extraction into @hive/shared). Inbox +
loose-ends details render here instead of expanding inline.
Dropped from the page: the pre-banner ASCII shimmer (`<pre.banner>`)
and the in-page `<details>` collapsibles for inbox + loose-ends. The
banner JS path (`setBannerActive`) is now a no-op (early-returns on
missing element); kept as dead code rather than ripped out to keep
the diff focused.
## JS
`frontend/packages/agent/src/app.js`:
- New `Panel` singleton with `open(name, title, content)` +
`close()` + `refresh(name, title, content)` (no-op if a different
view owns the panel — lets live updates re-render an open view
without grabbing focus from a closed one). Mirror of the
dashboard's Panel module; the duplication is intentional for now.
- `renderInbox` + `renderLooseEnds` refactored: update the header
pill counts, hide/show the pills, and `Panel.refresh` if the
matching view is open. The list-building DOM logic moved into
`buildInboxList` + `buildLooseEndsList` so the pill click handler
can call them on the latest snapshot kept in `lastInbox` /
`lastLooseEnds` module state.
- Pill click handlers `Panel.open(...)` with the freshly built list.
- Auto-expand behavior on first appearance dropped (the pill +
count badge is the discoverable signal; auto-popping the flyout
would interrupt whatever the operator is doing).
- `setHeader` no longer touches `#banner` (element removed); title +
dashboard back-link + rebuild button still get appended to `#title`.
## CSS
`frontend/packages/agent/src/agent.css` major additions, scoped
`body.agent-shell` so the sibling `stats.html` (which doesn't apply
the shell class) keeps its normal-document scroll + `.banner` ASCII
header via a `body:not(.agent-shell)` block.
New CSS custom properties on :root: `--agent-header-h`,
`--agent-composer-h`, `--agent-frost-bg`, `--agent-frost-blur`. The
terminal's padding + scroll-padding derive from these so a single
height tweak ripples consistently.
Added `.header-pill` (inbox/loose-ends triggers) +
`.agent-status-overlay` (centred login card when status != online).
Side-panel rules copied from `dashboard.css` with one delta: width
caps at 640px (vs dashboard's 760px) since per-agent inbox / loose-
ends rows are narrower than approval diffs / file previews.
## Validation
- `npm run build` — succeeds both workspaces.
- agent: `dist/static/{app.js (115kb), stats.js (435kb), agent.css (21kb)}`
- dashboard unchanged (no shared sources touched).
- Browser smoke test isn't possible from inside iris's container
(no JS engine) — op-side check on next deploy.
Closes#360.
The src/index.html / src/stats.html files reference assets at URLs
like /static/app.js, /static/dashboard.css. The initial Phase 1 build
flattened everything to dist/{app.js, dashboard.css, ...} which would
have forced the Phase 4 Rust ServeDir mount to do URL rewriting just
to make the existing HTML references resolve.
Rework: bundles now write to dist/static/, HTML stays at dist/ top
level. Layout matches the URLs the HTML uses, so the Phase 4 mount
is the simplest possible `fallback_service(ServeDir::new(dist))`.
No source-file changes — just the esbuild outfile/outdir paths.
Rebuilt; verified asset filenames + sizes unchanged.
Refs #273.
Phase 1 of the backend/frontend code split (#273). Additive — no
existing code is touched; the legacy hive-c0re/assets, hive-ag3nt/
assets and hive-fr0nt/assets trees stay in place until the Rust
cutover later in this branch.
Layout:
frontend/package.json npm workspaces root
frontend/packages/shared/ @hive/shared
src/{base,terminal}.css + terminal.js (ES module)
src/index.js re-exports terminal.js
frontend/packages/dashboard/ @hive/dashboard
src/{index.html, app.js, dashboard.css} ported from hive-c0re/assets
build.mjs esbuild config → dist/
frontend/packages/agent/ @hive/agent
src/{index,stats,screen}.html + agent.css
+ {app,stats}.js ported from hive-ag3nt/assets
build.mjs esbuild config → dist/
Changes vs the existing assets:
- terminal.js is an ES module exporting { create, linkify } instead
of assigning to window.HiveTerminal. The dashboard / agent app.js
files re-expose them on window so the IIFE bodies keep working
unchanged through Phase 1; the global aliases can be dropped in a
follow-up once the IIFEs are unwrapped.
- marked is imported from the marked@4.3.0 npm package (replacing
the vendored hive-fr0nt/assets/marked.umd.js bundle).
- chart.js is imported from chart.js@4.4.4 (replacing the jsDelivr
CDN script tag on the per-agent stats page — page now works
offline / on operator machines without internet egress).
- dashboard.css and agent.css both gain @import lines at the top
that pull base.css + terminal.css from @hive/shared, replacing
the runtime string concatenation in serve_css.
- index.html / stats.html collapse from three / two script tags to
one type="module" tag pointing at the bundled output.
package-lock.json is intentionally omitted from this commit — npm
isn't available in the iris container yet (approval pending) and the
lockfile will land in the next commit on this branch once the
toolchain is in place. The PR will not be opened until it's there.
Phase 2 (nix derivations), Phase 3 (container plumbing + the
hyperhive.frontend.extraFiles option for per-agent layering), and
Phase 4 (Rust cutover to tower_http::ServeDir, delete hive-fr0nt
+ legacy assets dirs) land as follow-up commits on this same
branch.
Refs #273.