Commit graph

696 commits

Author SHA1 Message Date
damocles
76533b0c20 turn: simplify run_turn match arms 2026-05-25 21:50:21 +02:00
damocles
799804e3d1 harness: flip into needs_login on 401 mid-turn (closes #419) 2026-05-25 21:50:21 +02:00
lexis
599a71254a docs: add comments verb to CLAUDE.md, prompts, and gotchas (follow-up to #420) 2026-05-25 21:16:54 +02:00
damocles
9ab241dc24 hive-forge: add comments <number> [--json] [--limit] verb (closes #418) 2026-05-25 21:09:35 +02:00
iris
502e9e0e70 terminal: snappier snap-to-bottom — 140ms rAF animation (#400 follow-up)
Mara: *can we make this really fast to not be a problem? like its
okay if it is pretty fast*.

`scrollTo({ behavior: 'smooth' })` runs ~500ms in Chromium/Firefox
— "still smooth, but visibly slow." Swap it for a custom rAF loop
with an ease-out cubic over 140ms. Distances under 24px short-
circuit to instant — animating a 12px nudge is just jitter.

Each call cancels the previous rAF before starting a new one, so a
burst of mutations coalesces into one ride to the latest bottom
instead of two animations fighting over scrollTop.

Re-reads `scrollHeight - clientHeight` each frame so a renderer
mutation landing mid-animation (the common case — `api.row`
returned, then renderer appended badge + body) extends the
destination smoothly rather than landing short.

`smoothScrollingUntil` gate shrinks accordingly (140 + 80 = 220ms)
— still protects the scroll handler from flipping stickToBottom
on the intermediate scroll events the rAF fires.
2026-05-25 21:04:24 +02:00
iris
8305c0716a terminal: smooth-scroll to bottom instead of instant jump (#400)
Mara: "New scroll down behavior works, but jumps instead of scroll."

The autoscroll path in `afterAppend` + the MutationObserver re-snap
+ the tail-pill click handler all set `log.scrollTop = log.scrollHeight`
— instant jump. Reads as jerky on long mutations where the row
height grew a lot between the initial append and the body fill.

Switch to `log.scrollTo({ top: log.scrollHeight, behavior: 'smooth' })`
via a new `snapToBottom()` helper. All three call sites (afterAppend,
MO callback, pill click) route through it.

## Smooth-scroll vs `stickToBottom` flicker

`behavior: 'smooth'` fires a stream of scroll events as the position
eases toward the target. Without guarding, the scroll handler reads
the intermediate position, fails `isNearBottom()`, flips
`stickToBottom` to false — the next MO callback then skips the snap
and strands the operator mid-scroll.

Add a `smoothScrollingUntil` timestamp gate: every `snapToBottom()`
call (without `immediate`) re-arms it to `now + 800ms` (~Chromium /
Firefox smooth-scroll animation duration + headroom). The scroll
handler ignores events while the gate is active. Bursts of writes
coalesce into one smooth ride to the latest bottom rather than a
sequence of half-cancelled animations.

## Backfill replay

`currentNoAnim` is true during history backfill — the operator never
sees the intermediate positions there, so smooth scroll is just
wasted animation. `snapToBottom(immediate=true)` (and the noAnim
branch in afterAppend) falls back to instant scroll for that path.

Same shared `@hive/shared/terminal.js` is used by the dashboard +
per-agent terminal + flow page; all three inherit the change.
2026-05-25 21:04:24 +02:00
iris
c015f62764 dashboard: drop max-width cap, full-width layout (#416)
Mara: remove the 90em max-width so the dashboard fills wide
screens instead of boxing content into a centred column with
empty margins. `padding: 0 1.5em 1.5em` stays as the small
edge-of-viewport gutter; `.dashboard-chrome { margin: 0 -1.5em
... }` continues to pull the sticky chrome bar edge-to-edge
through that gutter.
2026-05-25 20:59:25 +02:00
iris
1819bee43c dashboard: subscribe to /dashboard/stream for live updates (#406 step 3)
Pre-step-2, the SSE subscription on /index.html was conditionally
created by the broker-terminal IIFE that only ran on /flow.html
(via the `if (!flow || !window.HiveTerminal) return;` guard) — so
the dashboard only updated on cold load + after async-form submits.
The split in step 2 made the gap more obvious; close it.

Bare `EventSource` (the dashboard doesn't render broker rows, so
none of the HiveTerminal infrastructure is wanted here). Each
event's `kind` looks up against a `MUTATION_HANDLERS` table that
fans out to the existing `applyXxx` handlers. Unknown kinds (broker
`sent` / `delivered`, anything the backend adds later) silently
no-op. On (re)connect we kick `refreshState()` to recover events
lost during the disconnect window — same pattern as flow.js's
`onStreamOpen`. EventSource handles auto-reconnect.

#408 will give /index.html its own stream that omits broker
traffic; for now both pages subscribe to `/dashboard/stream` and
filter client-side.

Closes the #406 architectural roadmap:
- step 1 (#410): common.js extraction ✓
- step 2 (#412): split into app.js + flow.js entry points ✓
- step 3 (this PR): /index.html re-acquires its SSE subscription
2026-05-25 20:55:53 +02:00
lexis
0e17459943 docs: correct agent icon size to 5em after #413 cap 2026-05-25 20:50:16 +02:00
iris
cf4fc5ce1c agent: cap icon at 5em, fix overflow menu hidden-state (#411)
Two regressions from #394 that landed once mara deployed:

1. **Icon way too big** — `.agent-icon { height: 100%; aspect-ratio: 1 }`
   sized off the parent's height. With `align-items: stretch` on the
   header and `min-height: 6em`, the `<img>`'s intrinsic dimensions
   fed back into the flex container's height calculation and pushed
   the header (and the icon stretched to fill it) far past 6em — the
   icon ended up dominating the page.

   Switch to explicit `width: 5em; height: 5em` (header content area
   = 6em min-h - 2 × 0.5em padding). `align-self: flex-start` so a
   state-row wrap doesn't drag the icon down with it.

2. **Overflow menu always visible** — `.overflow-menu { display: flex }`
   in author CSS overrode the `[hidden]` UA rule (`[hidden]
   { display: none }` has the same specificity). With nothing hiding
   it, the menu rendered at viewport 0,0 by default (no top/left set
   on the popover until JS opens it), so the operator saw `↑ dashboard
   / ↻ rebuild container / ↻ new claude session` stacked at the
   top-left of every page load.

   Split the rule: `.overflow-menu` keeps the chrome (frosted bg,
   position fixed, z-index, border, padding); `.overflow-menu:not([hidden])`
   carries `display: flex`. Now `[hidden]` wins when set, no menu
   shown until the trigger opens it.

agent.css 22.4kb → 22.5kb.
2026-05-25 02:19:47 +02:00
iris
918dccfedd flow.js: drop dead window.HiveTerminal (#406 step 2 follow-up)
Per @argus on PR #412: after the common.js step (#410)
`appendText` uses the direct `termLinkify` import and `termCreate`
is called directly in flow.js — nothing reads `window.HiveTerminal`
anywhere. Drop the line + the stale comment alongside.
2026-05-25 02:19:19 +02:00
iris
06c23e0bdc dashboard: extract flow.js as separate /flow.html entry (#406 step 2)
Splits the single bundle into two: `app.js` (entry for /index.html —
tab renderers + tab routing + refreshState) and a new `flow.js`
(entry for /flow.html — operator inbox derived store + inbox pill
flyout + broker terminal + @-mention composer). Both bundles inline
`./common.js` (DOM helpers, Panel, NOTIF, path linkification).

## What `flow.js` owns

- Operator inbox derived store (`operatorInbox`, `INBOX_LIMIT`,
  `inboxAppendFromEvent`, `buildInboxListNode`, `renderInbox`) +
  inbox-pill click wiring
- Broker terminal init (`HiveTerminal.create({ logEl: msgflow, ... })`)
  with `renderMsg`, `pulseBanner`, `msgRowMap` reply-thread indicator,
  and the renderers map for `sent` / `delivered` broker rows
- @-mention composer (`#op-compose-input` IIFE — sticky recipient,
  autocomplete, parseAddressed, /op-send POST)
- A small local `flowContainers` cache for the composer's
  autocomplete, refreshed on cold load + on every SSE reconnect via
  `onStreamOpen`, and live-updated by `container_state_changed` /
  `container_removed` SSE events (the dashboard's `containersState`
  lives in `app.js` and isn't available here)

## What `app.js` no longer does

- Drops the inbox derived store, the bindFlowInboxPill IIFE, the
  broker-terminal IIFE, and the composer IIFE — all moved
- Drops the `renderInbox()` call in `refreshState` (dashboard has
  no #inbox-section element)
- Drops `setTabCount('flow', operatorInbox.length)` — the FL0W tab
  count lives in flow.js now (cross-page count broadcasting is a
  future follow-up; the slot currently stays hidden on /index.html)
- Drops the `window.HiveTerminal` global — the bare-import pattern
  in common.js / flow.js made it unused on the dashboard

## What changes for /flow.html

- `<script src>` switches from `/static/app.js` → `/static/flow.js`
- Mutation events on the dashboard stream (`approval_added`,
  `container_state_changed`, etc.) are silently ignored on /flow.html
  via a `_default: () => {}` renderer (the dashboard tabs aren't on
  this page; firing the legacy applyXxx handlers from here just
  mutated dead stores). #408 follow-up filters this at the SSE level

## Validation

- `npm run build` clean.
- Bundle deltas:
    - `app.js`: 154kb → 135kb (dropped ~19kb of flow code)
    - `flow.js`: NEW 29kb (was bundled into the old 154kb app.js)
    - `flow.html` page total: 154kb → 29kb (flow.js + inlined common,
      no tab renderers shipped)
- Source: `app.js` 2287 → 1907 lines (-380); `flow.js` 423 lines (new)
- No HTML / CSS changes besides the `<script src>` swap on /flow.html.

## Known limitations (out of scope; tracked separately)

- /index.html still has no live SSE subscription — the dashboard
  updates only on cold load + after async-form submits. Pre-existing
  behaviour; the SSE wiring also lived in the flow IIFE before.
  Step 3 of #406 (or its own bug fix) re-wires it.
- /flow.html's `_default: noop` drops mutation events; #408 fixes
  the duplicate-traffic by splitting the SSE endpoint server-side.
- The FL0W tab-strip count pill on /index.html stays hidden — the
  count source is now in flow.js. Broadcast via localStorage /
  BroadcastChannel is a small follow-up if both pages are open.

Browser smoke test isn't possible from inside iris's container.
Worth eyeballing post-deploy:
  - /flow.html: terminal renders broker rows; inbox pill shows count
    + opens flyout; composer autocomplete suggests known agents
    + sends successfully
  - /index.html: tab renderers all work; notification toggle still
    binds; side panel still opens for diffs / file previews / logs
2026-05-25 02:19:19 +02:00
damocles
7e12da83e2 hive-forge: update agent/manager prompts + gotchas for rust rewrite (mara@#407) 2026-05-25 02:16:53 +02:00
damocles
15195b0c47 hive-forge: #[must_use] on Client::repo (mara@#407) 2026-05-25 02:16:53 +02:00
damocles
badf21714b hive-forge: global -r/--repo flag instead of per-verb [repo] positional (mara@#407) 2026-05-25 02:16:53 +02:00
damocles
76cf2ffd36 hive-forge: assign accepts [repo] override (argus@#407) 2026-05-25 02:16:53 +02:00
damocles
595e3c040c hive-forge: rewrite bash CLI helper as a rust binary (closes #280) 2026-05-25 02:16:53 +02:00
iris
560360d2e3 dashboard: extract shared helpers into common.js (#406 step 1)
First slice of the app.js split (#406). Pure utility / infrastructure
code that both /index.html and /flow.html use lifts out of the IIFE
into a sibling ES module:

- DOM helpers: `$`, `el`, `esc`, `form`, `fmtAgeSecs`
- Side-panel singleton (`Panel.open` / `openNamed` / `refresh` /
  `close` / `bind`). The `ensure()` lazy-init makes it tolerate
  being imported before the DOM element exists — `bind()` still
  needs to be called once the host page is ready.
- Path linkification + file-preview side panel:
  `appendLinkified`, `appendText`, `makePathLink`, plus the
  internal `openFilePanel` + `fetchStateFile` + `mdNode` /
  `svgImage` / `buildTabbedPreview` it depends on.
- Browser-notification module `NOTIF` (`bind`, `show`,
  `renderControls`).

`app.js` now imports these from `./common.js` and the duplicated
definitions are gone. Each removal is replaced by a one-line
breadcrumb comment so a reader chasing a name from the bundled
output can find where it landed.

`truncate`, `fmtAgo`, `fmtElapsed`, `fmtDuration` stay in app.js for
now — each has caller-specific phrasing ("X running", "X ago") that
doesn't generalise cleanly. Lift them when a second consumer needs
the same shape.

## Next steps (separate PRs)

- Step 2: split app.js into `tabs.js` (entry for /index.html — tab
  renderers + tab routing + refreshState) and `flow.js` (entry for
  /flow.html — broker terminal + inbox derived store + compose),
  both importing from common.js. Updates `build.mjs` for multiple
  entry points and switches each HTML file's `<script src>`.
- Step 3 (#408 follow-up): backend-side stream split so /flow.html
  doesn't have to subscribe to the dashboard's mutation events at all.

## Validation

- `npm run build` clean.
- Build deltas: `app.js` 154.3kb (was 153.6kb) — bundle size bumped
  slightly due to per-module overhead; same code under the hood.
  Source: app.js 2603 → 2287 lines (-316); common.js 367 lines (new).
- No HTML / CSS changes. Both pages still load `/static/app.js` as
  before.

Browser smoke test isn't possible from inside iris's container.
Worth eyeballing post-deploy:
  - Notification toggle + send still works (NOTIF.bind, NOTIF.show)
  - Side panel still opens for diff / file preview / logs (Panel)
  - Path tokens in messages still render as clickable anchors that
    open the file in the side panel (appendLinkified → makePathLink
    → openFilePanel)
2026-05-25 02:01:49 +02:00
lexis
38920d3af1 docs: update web-ui.md for agent header redesign and in-flight rebuild badge (#394 #398) 2026-05-25 02:00:47 +02:00
iris
c08218acdc dashboard: slug to page footer, not chrome (#389 follow-up)
PR #392 moved the slug below the tab strip but kept it INSIDE the
sticky chrome — Mara's report ("still at old position everywhere
except flow tab") makes the original intent clearer: the slug
should be at the page BOTTOM, with the chrome reduced to pure
navigation.

## index.html
- Remove the banner-thin from `.dashboard-chrome` entirely.
- Move it into the existing `<footer>` element (above the divider
  + project-link line). Sits at the bottom of every tab pane after
  the main content scrolls past.

## flow.html
- Remove the banner-thin from the chrome. The flow page is a
  full-viewport terminal with `body { overflow: hidden }` and
  no normal-flow footer position — the slug simply doesn't appear
  here. The frosted chrome is purely the tab strip now.

## dashboard.css
- `--flow-header-h: 4.7em → 3.6em` — chrome is shorter without
  the banner; terminal padding-top + tail-pill offset + inbox-pill
  top all derive from this variable, so they follow automatically.
- `footer .banner-thin { margin-bottom: 0.8em }` so the slug
  doesn't crash into the divider + link line below it.

No JS changes. Build clean.
2026-05-25 01:59:12 +02:00
iris
4559a56e2e dashboard: guard renderers against missing-root on /flow.html (#399)
Mara hit:

  TypeError: can't access property "innerHTML", root is null
      renderContainers app.js:666
      applyContainerStateChanged app.js:484
      container_state_changed app.js:2306

…on /flow.html. The same bundled `app.js` runs on both /index.html
(dashboard, has the tab panes) and /flow.html (flow page, has only
the broker terminal). SSE events arrive on every page —
`container_state_changed` / `tombstones_changed` / `approval_*` /
`question_*` route through their corresponding renderers, which
then `root.innerHTML = ''` on `$('section-id')` and crash when the
section isn't in the DOM.

The convention is already "no-op when the target DOM doesn't
exist" — `renderMetaInputs`, `renderRebuildQueue`, `renderReminders`
all guard with `if (!root) return;` at the top. Bring the rest in
line:

- `renderContainers`
- `renderTombstones`
- `renderQuestions`
- `renderApprovals`

`renderInbox` already handles the absent case via its
`if (root && !root.hidden)` branch — no change.

No behaviour change on /index.html. On /flow.html the failing
events silently no-op as intended (terminal renderers still
re-render the broker tail normally).
2026-05-25 01:30:11 +02:00
iris
92becdd951 agent: drop orphaned button rules in agent.css (#394 follow-up)
After moving `↑ DASHB04RD` / `↻ R3BU1LD` / `↻ new session` into
the overflow `⋯` menu in #394, the per-button rules in agent.css
have no live consumer:

- `.btn-dashlink` (cyan back-link chip)
- `.btn-rebuild` (amber rebuild chip — note: the dashboard has
  its own `.btn-rebuild` for the per-row R3BU1LD form on the
  SW4RM tab, that stays)
- `.btn-new-session` (amber round-pill button)
- `.btn-send` (green send-button variant — orphaned since an
  earlier compose-form retirement, swept here)

Plus a comment-only `#state-row` block (layout fully provided by
the renamed `.agent-state-row` / `.agent-header-row` classes).

Each removed block is replaced with a one-line "moved to overflow"
comment so future readers don't reintroduce the old chrome by
mistake.

agent.css 23.6kb → 22.4kb.
2026-05-25 01:29:39 +02:00
iris
69312b8553 agent: redesign terminal header — full-height icon, two-row main, overflow menu (#394)
Operator brief (#394): the header had thirteen distinct visual
elements in one flex row with three different border-radius
languages, four colour treatments, three label styles. Mara's
direction:

- agent icon bigger (full header height) as the identity anchor
- title glow stays
- nav links lose their default-anchor underline
- overflow `⋯` absorbs `↻ R3BU1LD` + `↻ new session` (rare,
  destructive — both worth one extra click; rebuild is normally
  done from the dashboard)
- accent stacking in the state strip stays — that's the vibe

## Layout shape

Three flex columns in `.agent-header`:

  [icon · full height] [main column · 2 rows] [pills + ⋯]

The main column carries row 1 (`◆ AGENT ◆` title + meta-nav) on
top and row 2 (alive · state · model · ctx · cost · last-turn ·
cancel-turn) below.

## Changes

### `index.html`
- Wrap title + nav in `.agent-header-row .agent-header-title-row`;
  wrap state-row siblings in `.agent-header-row .agent-state-row`;
  both go inside a new `.agent-header-main` column.
- Right cluster `.agent-header-pills` contains the inbox + loose
  pills + a new `<button id="overflow-btn">⋯</button>` trigger.
- Drop static `#new-session-btn` from `#state-row` — moved into the
  overflow menu, populated dynamically.
- Add `<div id="overflow-menu" role="menu" hidden>` as a sibling
  of `<header>` (lives outside the header so its `position: fixed`
  popover isn't trapped by any header stacking context).

### `agent.css`
- `--agent-header-h: 4.6em → 6em` so the icon can be square + full
  height without crowding the two-row main column. Terminal
  padding-top + status overlay top + tail-pill all derive from
  this variable, so they follow automatically.
- `.agent-header { align-items: stretch }` lets the icon stretch
  to full height; `.agent-icon { height: 100%; aspect-ratio: 1 }`
  sizes it as a square off the stretched height.
- `.agent-nav-link` rule added — `text-decoration: none`, cyan +
  soft glow, hover lights brighter (mara's spec).
- `.overflow-btn` (round trigger) + `.overflow-menu` (frosted
  popover, fixed-position) + `.overflow-item` (rows with an icon
  column + label, hover ink matches per-action accent — cyan for
  dashboard, amber for rebuild/new-session).
- Remove the old `#state-row` selector (layout now provided by
  `.agent-state-row` + `.agent-header-row`).

### `app.js`
- `setHeader` no longer appends DASHB04RD / R3BU1LD chips into the
  title — title is just the identity glyph now. Both actions get
  rendered into the overflow menu by `populateOverflowMenu()`.
- `populateOverflowMenu(label, dashUrl)` builds three rows:
  `↑ dashboard` (anchor), `↻ rebuild container` (button — same
  POST-form action as before), `↻ new claude session` (button —
  same `/api/new-session` call as the legacy header button).
- Overflow toggle / outside-click / Escape dismissal — same
  pattern as the side-panel flyout (`Panel`).
- Drop the static `new-session-btn` IIFE binder; the dynamically-
  rendered menu item owns its handler now.
- Drop the per-nav-link inline `marginLeft` (layout gap comes from
  the new `.agent-nav { gap }` rule).

## Validation

- `npm run build` clean.
- Build deltas: agent.css 21.0kb → 23.6kb (overflow + nav rules
  + comments), app.js 117.4kb → 118.9kb (menu builder + toggle).
- Browser smoke test isn't possible from inside iris's container.
  Worth eyeballing post-deploy:
    - Icon fills the full header height as a square
    - Title glow + uppercase styling preserved
    - Nav links render without underline; hover lights brighter
    - `⋯` opens a frosted popover with `↑ dashboard`, `↻ rebuild
      container`, `↻ new claude session`
    - Rebuild confirm + POST works the same as the legacy chip
    - New-session confirm + POST works the same as the legacy button
    - State strip still wraps when crowded (model/ctx/cost
      multi-line on narrow viewports)
    - Cancel-turn button still appears while thinking and clears
      on turn end
    - Terminal padding-top adjusts to the new 6em header height
      (no row hidden under the chrome)
2026-05-25 01:29:39 +02:00
iris
88bc07fbbe dashboard: surface in-flight rebuild on container card (#398)
The SW4RM tab's container card was reading container state
straight from the snapshot — when a rebuild was in flight and the
container was momentarily stopped between teardown and bring-up,
the card showed "stopped" while the SYST3M tab's rebuild queue
showed the operation running. The two surfaces disagreed.

Mara: *should show as building in container page as well*.

Cross-reference: build a `inFlightOpsByAgent()` map from
`rebuildQueueState` (kinds: rebuild / meta_update / destroy;
states: queued / running — skip `spawn` since transients already
drive that case). When rendering each container row, prefer the
operator-initiated transient if set; otherwise fall back to the
in-flight queue entry as a synthetic pending kind:
`rebuilding` / `meta-updating` / `destroying` (or `… queued`
when still waiting on the worker). The existing `pending-state`
spinner badge surfaces it visually — no new CSS rule needed.

Also wire `applyRebuildQueueChanged` to re-render containers so
the badge lights up the moment a rebuild lands in the queue and
clears the moment it finishes — no manual refresh.
2026-05-25 01:16:10 +02:00
iris
7743c07380 dashboard: connect tree-prefix vertical bars across taller rows (#388)
The container-row tree-prefix used text box-drawing glyphs (├ └ │)
positioned with `top: 0.6em` — a single text-line tall. Once rows
grew past one line (5em square icon + multi-line body), the `│`
columns of consecutive siblings no longer touched, leaving visible
breaks in the tree.

Replace the text-glyph string with structured DOM: one `.tree-lane`
per depth column. Continuation lanes (`.lane-line`) paint a 1px
border-left spanning the full row height + the `.containers` gap
below, so adjacent siblings' bars visually merge into one unbroken
vertical. The row's own joint lane is `├` (branch — bar continues
below) or `└` (last — bar stops at icon midline), with a horizontal
stub at 3.1em (row padding-top + icon half-height) reaching to the
icon edge.

Joint y / stub width are derived from the 5em icon + 0.6em row
padding-top + 0.8em row padding-left so they meet the icon cleanly.
2026-05-25 01:09:03 +02:00
iris
47403595f1 terminal: anchor tail pill outside .terminal-wrap stacking context (#375)
#375 set `z-index: 35` on the agent page's tail pill so it'd float
above the fixed composer (z-index 30). The fix only landed because
nothing else in `.agent-main` created a stacking context. But the
pill is anchored inside `.terminal-wrap`, which carries
`backdrop-filter: blur(...) saturate(...)` for the frost effect —
and `backdrop-filter` CREATES A STACKING CONTEXT. The pill's
z-index 35 was trapped inside that context and never got to compete
with the composer's z-index 30 in the root stacking context, so the
operator still saw the badge clipped under the input box.

Same root cause on the flow page — `.flow-main .terminal-wrap`
inherits the same backdrop-filter rule.

Fix: anchor the pill in `.agent-main` / `.flow-main` instead of
`log.parentElement` (= `.terminal-wrap`). Both ancestors are
`position: absolute` with `overflow: hidden` but NO backdrop-filter
or other stacking-context creators, so the pill's z-index reaches
the root and properly floats above the composer.

Geometry unchanged — `.agent-main` / `.flow-main` and the
`.terminal-wrap` they contain both `inset: 0` the same area, so the
pill's `bottom: calc(--composer-h + 0.6em)` lands at the same y.

Also added `.flow-main .tail-pill { z-index: 35 }` (the flow page
was missing the per-page z-index bump that the agent page already
had).

`pillAnchor` is an existing opt in @hive/shared/terminal.js (the
default is `log.parentElement`); both consumers now set it explicitly.
2026-05-25 00:52:51 +02:00
iris
9975be9c08 terminal: snap to bottom after post-row mutations (#393)
#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.
2026-05-25 00:47:31 +02:00
lexis
e9cce17828 docs: update web-ui.md for tabbed layout, topology tree, per-agent overhaul (#366 #373 #381) 2026-05-25 00:45:56 +02:00
iris
91a72b5cbb dashboard: tab strip above banner-thin in chrome (#389)
Swap the order of `.tabbar` and `.banner` inside `.dashboard-chrome`
so the operator's navigation surface sits at the very top of the
sticky/fixed header — the "WE ARE THE WIRED" slug becomes decoration
below the tabs rather than chrome above them.

Applied to both `index.html` (sticky chrome) and `flow.html`
(fixed-overlay chrome).

No CSS changes — `.tabbar { border-bottom }` still divides tabs from
the area below (now the banner), and active-tab `margin-bottom: -1px`
still merges into that boundary cleanly.
2026-05-25 00:38:12 +02:00
damocles
d79b8a0a38 hive-forge: --body-file + piped stdin for pr-create / issue-create / issue-edit (closes #382) 2026-05-25 00:12:43 +02:00
iris
ba669d2d6c flow: keep the dashboard tab strip on the flow page (#383)
Operator wanted the tab header visible on /flow.html so switching
tabs doesn't require navigating back to / first.

The flow page now reuses the same `<header class="dashboard-chrome">`
markup the dashboard renders, with a few tweaks:

- The SW4RM / Y3R C4LL / SYST3M tabs are cross-page links
  (`href="/#swarm"` etc.) — clicking lands on the dashboard with the
  destination tab pre-active via the hash router.
- The FL0W tab is rendered `.active.tab-link` + `aria-current="page"`
  so it reads as the current view (no clickable arrow / "go here"
  affordance — you're already here).
- Banner-thin echoes the dashboard for visual continuity.
- Notif controls cohabit with the tabs (same IDs the dashboard uses,
  so app.js's NOTIF binding picks them up unchanged).

Layout glue:
- `body.flow-shell .dashboard-chrome.flow-chrome` overrides the
  dashboard's `position: sticky` with `position: fixed` so the
  chrome stays put under flow-shell's `overflow: hidden` body
  layout, keeping the terminal full-viewport behind/beneath.
- New rule for the active FL0W tab — the `.tab-link` styling on the
  dashboard otherwise reads as a passive cross-page link; here we
  need it lit-up like a regular active tab.
- `--flow-header-h` bumped from 4.2em → 4.7em to match the natural
  height of the tab strip + banner combo. Terminal padding +
  inbox-pill top offset both derive from this variable, so they
  follow automatically.

Removed:
- Legacy `.flow-title`, `.flow-hint`, `.flow-back` CSS rules (their
  HTML counterparts are gone — the tab strip carries the
  identity now).
- The `<a class="flow-back">← d4shb04rd</a>` link and the
  `<h2 class="flow-title">` from flow.html.

## Validation

`npm run build` clean.
  dashboard.css: 38kb → 37kb (legacy rules removed, new shared-
                  chrome rules are smaller)
  flow.html:    4.4kb → 4.7kb (tab strip replaces title bar)
  app.js:       unchanged (no JS changes — the tab navigation is
                pure HTML href + cross-page hash)

Closes #383.
2026-05-25 00:12:28 +02:00
iris
3abd0ba711 dashboard: drop redundant C0NTAINERS heading from SW4RM tab (#385)
The SW4RM tab label already announces the section — an inline
`<h2>◆ C0NTAINERS ◆</h2>` + divider underneath was redundant
ink. The tab is single-section so there's no other content to
disambiguate from.

`#containers-section` div stays put (app.js targets it by id);
just the heading + divider go.

`npm run build` clean.

Closes #385.
2026-05-24 14:24:56 +02:00
damocles
1294ca2755 hive-forge: stdin beats bare positional for comment / comment-edit (closes #379) 2026-05-24 14:09:12 +02:00
iris
d4bb3e4aa0 agent: loose-ends flyout body on its own row (#376)
Operator: the per-entry header (from/sep/ts) + body were on the same
grid row, with the body squeezed into a 1fr column. In the side-
panel flyout's ~640px width, after the auto header columns ate
their share, the body wrapped over many narrow lines.

Fix: `.agent-inbox li` switches from `display: grid` to
`display: block`. The body element (currently a `<div>` for
loose-ends and a `<span>` for inbox messages) gets explicit
`display: block` so it always breaks to a new line under the
header. Light `padding-left + border-left` indent on the body
gives a visual relationship to the header row above without
needing a column structure.

`inbox-ts` + `inbox-sep` pick up small left margins to preserve
the inline spacing the grid's `gap: 0.5em` used to provide.

`.answer-form` drops its no-longer-applicable `grid-column: 1 / -1`
(replaced with a regular block layout + matching padding-left so
the form aligns with the question body it answers).

## Validation

`npm run build` clean. Bundle deltas: agent.css unchanged in
bundled size (rules swapped 1:1).

Browser smoke test isn't possible from inside iris's container.
Worth eyeballing post-deploy:
  - Loose-ends entries with long question/description bodies wrap
    to the panel width rather than getting clipped into a narrow
    fourth column.
  - The header line stays compact and readable.
  - Answer-form for a question loose-end still aligns visually
    under the question body it answers.

Closes #376.
2026-05-24 13:22:41 +02:00
iris
5615da9211 terminal: tail pill above floating chrome + autoscroll fix (#375)
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.
2026-05-24 13:22:18 +02:00
damocles
14b79f43cf lifecycle: stop before update for boot-style apply (mara@#372) 2026-05-24 13:16:31 +02:00
damocles
bc872fffb9 lifecycle: drop defensive is_running check per mara@#372 2026-05-24 13:16:31 +02:00
damocles
0540f213f1 lifecycle: use nixos-container restart for the running case (mara@#372) 2026-05-24 13:16:31 +02:00
damocles
48420890e0 lifecycle: preserve stopped state across rebuild (closes #371) 2026-05-24 13:16:31 +02:00
iris
9666cb8c3f dashboard: tab-bar restructure + extract FL0W to /flow.html (#369)
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.
2026-05-24 13:03:53 +02:00
iris
8c7bc850f3 dashboard: render agent topology as a tree in the container list
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.
2026-05-24 12:04:19 +02:00
damocles
3fa12bf363 rebuild_queue: dedup MetaUpdate on (kind, agent, inputs) (closes #365) 2026-05-24 11:43:47 +02:00
damocles
0b03d5bcfb topology: meta-repo agent hierarchy + ContainerView.parent (#361) 2026-05-24 04:47:55 +02:00
iris
e931c08739 frontend: vibec0re terminal overhaul (#360)
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.
2026-05-24 03:49:26 +02:00
damocles
4d7c767eb0 meta: emit nixpkgs follows for agents that declare it (closes #355) 2026-05-24 02:45:11 +02:00
damocles
8e5112aa27 hive-forge: fix assign endpoint + surface API errors via --fail-with-body (#353) 2026-05-23 15:53:06 +02:00
lexis
b3e94760e1 docs: add hyperhive.frontend.* options to README (follow-up to #350) 2026-05-23 15:03:51 +02:00
damocles
09c51c87aa meta: collapse hyperhive's nixpkgs into meta's via follows (#317) 2026-05-23 14:51:40 +02:00
iris
0e2319d206 frontend: populate real npmDepsHash from prefetch-npm-deps
Manager approval 1b1bcca added `pkgs.prefetch-npm-deps` to my
container. Ran `prefetch-npm-deps frontend/package-lock.json` →
`sha256-MHXxkZpe/5LAhpQ76ZK94znG2noTobthjUi6iNY8/K4=`. Replaced
the `lib.fakeHash` placeholder in `nix/frontend.nix` with the real
value; updated the comment to point at the recompute command instead
of the let-it-fail workflow.

This unblocks PR #350 for merge — `nix build .#frontend` will now
succeed without the operator having to compute and patch the hash.

Refs #273.
2026-05-23 14:51:01 +02:00
iris
65532e8387 frontend: tighten extraFiles target type to strMatching regex
damocles suggested using lib.types.strMatching for the target option
itself rather than relying solely on the post-hoc assertion. Pattern:
`^[A-Za-z0-9_][A-Za-z0-9_./-]*$` — first char alphanumeric/_, then
alphanumerics + _ + . + / + - allowed (so nested layouts like
"games/bitburner" still work).

This rejects at type-check time:
- leading `/` (absolute paths)
- leading `.` (so `..` as a full string blocked, also `./foo`)
- leading `-` (would parse as flag by some tools)
- spaces, control chars, weird unicode

The existing assertion stays — it catches mid-path `..` segments
(`foo/../bar`) that the regex can't reject without lookahead. POSIX
regex (which nix uses) doesn't support lookahead, so the
type-and-assertion split is the cleanest expression.

Refs #273.
2026-05-23 14:51:01 +02:00