#375 captured `wasNearBottom` BEFORE the row's initial append so
the autoscroll wouldn't be tricked by the new row's own height.
That's still right, but it leaves a second gap: renderers commonly
call `api.row(cls, text)` to build the shell, then mutate the
returned element by appending more children (badges, multi-line
turn bodies, tool-result panes). The initial scrollTop assignment
in afterAppend only sees the row's INITIAL height — once the
renderer adds the body, the row's grown past the visible bottom
and the operator's stranded scrolled to the row's TOP, breaking
stick-to-bottom for every subsequent event.
Mara's symptom: two lines of text appear, the view scrolls only
one row, the terminal isn't at the bottom anymore, and the next
event doesn't auto-scroll because `isNearBottom()` now returns
false.
Fix: add a `stickToBottom` boolean (updated synchronously from the
scroll event handler) + a MutationObserver on the log subtree
(`childList`, `subtree`, `characterData`). On any mutation, if
stickToBottom is true, re-snap to bottom. MutationObserver
batches mutations into one microtask callback per synchronous
block, so it runs once per renderer call regardless of how many
children the renderer appends after `api.row` returned.
Programmatic `scrollTop = scrollHeight` doesn't trigger MO (the
scroll isn't a DOM mutation), so no feedback loop. Operator
scrolling up still flips stickToBottom to false via the existing
scroll handler — MO becomes a no-op until they scroll back.
Same shared `@hive/shared/terminal.js` powers the dashboard's
flow page + per-agent terminal, so both pages inherit the fix.
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.
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.