Review feedback on this PR (mara): "i think the component should be
shared. motion setting is missing." Both addressed:
- `settings-storage.ts` (generic localStorage hook), `theme-apply.ts`,
`motion-apply.ts`, and `SettingsMenu.tsx`/`.css` all move from
swarm-ui's `lib/`/`shell/` into `@hive/shared/src/settings/` —
agent's previous local copies are deleted outright rather than kept
as a second implementation. One component, `Badge` trigger
everywhere (already used elsewhere in swarm-ui, so not a new visual
language there either) — storage keys stay caller-owned (`themeKey`/
`motionKey` props + matching `useApplyThemeOverride`/
`useApplyMotionOverride` calls at each package's single mount point)
so agent and swarm-ui keep fully independent, non-colliding
persisted settings.
- Agent's settings menu now includes the motion row, matching
swarm-ui's. No animation in the agent package is gated behind
`data-motion` yet — same as when swarm-ui first built this plumbing
ahead of having a consumer — so it's currently inert there, ready for
whenever agent grows a motion-guarded animation.
- swarm-ui's own theme default flips to `'dark'` as part of this move
(`theme-apply.ts`'s new default), superseding PR #3715 — that PR
becomes redundant once this lands and will be closed rather than
merged, to avoid the two colliding on the same file.
Verified end-to-end with real screenshots on both pages: shared
component renders identically (Badge trigger, theme+motion rows, dark
default) on agent's mock server and a static rebuild of swarm-ui's
dist.
Ports swarm-ui's `SettingsMenu` (mara: "agent terminal page should get
the settings panel from swarm ui as well") — same shape as `MetaNav`
already in this header: a `Badge` icon trigger ("⚙"), popover, close on
outside-click/Escape.
`theme-apply.ts` + `settings-storage.ts` are near-verbatim ports of
swarm-ui's own (duplicated rather than moved into `@hive/shared` for
this pass — lower risk than reworking swarm-ui's imports in the same
change). Defaults the stored override to 'dark', not 'system', for the
same reason as swarm-ui's own default flip: `prefers-color-scheme` has
no real "unset" value, so 'system' silently reads as light for anyone
who's never touched an OS dark-mode toggle.
Motion NOT ported — agent has no animation gated behind `data-motion`
yet, so that plumbing would have nothing to control.
Verified end-to-end: default dark on a fresh load, and an explicit
localStorage override to 'light' correctly re-themes the whole page via
the existing `colors.css` `:root[data-theme='light']` block (already
shipped, previously only reachable from swarm-ui).
--muted is color-mix()'d toward --base00/--bg — correct for dimmed text
on the page background, wrong for dimmed text sitting on the elevated
--purple-dim fill (badge/dropdown-item backgrounds). Badge's label/caret
used bare --muted there and mara measured it at ~1.3:1 contrast against
her theme's badge fill, essentially invisible.
--muted-on-dim mixes toward --purple-dim instead, same contrast-floor
technique as --muted itself, just anchored to the surface it's actually
used on. Applied to .ui-badge-label/.ui-badge-caret and to Dropdown's
active-item description (same bug: its row bg is --purple-dim too).
Ceiling note: even plain --fg only reaches ~3.4:1 against --purple-dim
in mara's theme, short of formal 4.5:1 AA — that's the theme's own
limit, not something the anchor choice can fix on its own. 90% gets
close to that ceiling (~3:1, more than double the old ~1.3:1) while
keeping a hint of the label/value visual distinction.
`OverflowMenu.tsx`/`.css` were deleted (rebuild button removed outright,
dashboard link moved into `MetaNav`), but `agent.css` still carried the
whole trigger+popover+item rule set — grepped the entire `agent`
package, none of these classnames appear in any current markup.
Also fixed three tombstone comments elsewhere in the file that pointed
at the now-also-deleted `.overflow-item-*` classes as "where it moved
to" — they'd have dangled once those rules were gone too. And two stale
doc claims caught in the same sweep: `docs/web-ui/agent.md` still listed
`OverflowMenu.tsx` in the header's component tree and described "the
overflow menu" as a live effort-picker location.
Badge already covers icon+label+value+onClick -- exactly HeaderPill's
shape. mara, reviewing the first cut (a bespoke .header-pill matched to
Badge's own CSS values): "cant we reuse the badge component". HeaderPill
now renders <Badge> directly and only owns hiding at count 0 + the
inbox/todos tone (amber/green on the count, same as before). Drops the
now-unused .header-pill* CSS from agent.css entirely.
HeaderPill used the bordered/transparent .hive-pill shape while every
other chip in .agent-header-pills (StatusChips, MetaNav's trigger) is
@hive/shared's filled Badge (.ui-badge) -- the one visibly hollow pill
in a row of filled ones. Restyled .header-pill with .ui-badge's own
values (filled --purple-dim background, no border, matching radius/
padding/hover) instead of a new shape, and dropped hive-pill from the
markup since it no longer describes what this component looks like.
Badge.css's colored-value classes (positive/warning/negative/accent)
used the raw semantic accent color directly on --purple-dim, same class
of bug already fixed by hand for the bundled Latte palette (see
colors.css's comment on --latte-base08..0F) but never applied to an
arbitrary stylix-fed scheme. Blend each accent 60/40 with --fg via
color-mix() instead of using it bare -- --fg is the one color the
base16 contract already guarantees legible on every surface, so mixing
toward it gives every accent a contrast floor without trying to
compute/fix an operator's own theme.
mara (#3704): 'remove rebuild button, move link to dashboards into
links menu.'
The overflow (⋯) menu existed for exactly two items: the dashboard
back-link and a rebuild-container action. Rebuild is gone outright —
the dashboard's own R3BU1LD button already covers it, this was just a
rarely-used shortcut not worth its own menu. The dashboard link moves
into MetaNav's links popover (now the first item, above stats/forge/
config/extras) instead. With both gone, OverflowMenu had nothing left
to justify existing as a separate component — deleted along with its
CSS and the now-unused rebuildAction.ts (only consumer).
MetaNav gained a dashboardBase prop (Root.tsx already computes this
via resolveDashboardBase for InboxPanel/pause — reused, not
duplicated) and renders the dashboard link as a real <a>, same
treatment as every other item in that popover — no dangling
window.open()-only affordance.
Updated docs/web-ui/agent.md's Header section and the couple of
now-stale OverflowMenu references in index.html's/MetaNav.css's own
comments.
Verified: header now shows a single trailing icon-badge (was two),
popover opens with dashboard first then the agent_links() set.
tsc --noEmit clean, build clean, both pre-push lints clean.
argus, non-blocking note on PR#3702: the relative-path-resolution
example still used static/app.js as its illustrative filename after
that file was deleted in the cutover. Mechanism described was still
correct, just a stale filename a future reader could get confused
chasing. Swapped to static/main.js (the agent page's own script) and
static/stats.js (the /stats subpage example, more accurate than
main.js there since that's specifically about a subpage's own
script).
mara: 'i merged this without realizing you still did not delete the
old code. pls follow up with ripping out the old code.'
- index.html: dropped the old static header/main/composer/overflow-
menu markup and app.js's <script> tag. Now just <div id=preact-root>
+ <script src=static/main.js>. main.css's <link> lands after
agent.css's — cascade order argus flagged matters for LoginFlow's
.login-card / MetaNav's popover to win against agent.css's legacy
rules, confirmed explicitly rather than assumed at this exact step.
- build.mjs: app.js dropped from the esbuild entryPoints (stats.js
keeps its own bundle, unaffected — separate page, separate script).
- frontend/packages/agent/src/app.js deleted (1717 lines). screen.html
has its own inline <script>, untouched — never depended on app.js.
- docs/web-ui/agent.md: rewrote the Header section to describe the
real Preact component tree and the badges+pills-together layout
(deferred from the earlier commits on this PR specifically so it
wouldn't describe a hybrid state — this is that promised follow-up).
Touched up the one other app.js-specific mention in the endpoints
section. Left the Main/composer/side-panel/live-view/slash-command
sections alone — behavior there is unchanged, verified faithfully
ported throughout this PR's earlier commits.
Verified against the REAL dist/index.html (not the dev-preview
harness) — a scratch mock server serving the actual built output
end-to-end, screenshotted clean. tsc --noEmit clean, build clean, both
pre-push lints clean.
mara, live, on the 500px screenshot: 'does not overflow properly.'
Real cause: .agent-header-pills had flex-shrink: 0 (fine for app.js's
2-3-small-pills case, which never needed to shrink) — once badges
joined that column last round, that told the flex layout 'give this
its full natural unwrapped width no matter what', so once that width
exceeded the viewport it just overflowed past the edge instead of
ever getting narrow enough to trigger its own flex-wrap. Dropping
flex-shrink: 0 (default 1) is the fix.
Tried pairing it with min-width: 0 first — made it worse. An unset
min-width still floors shrinking at the container's own min-content
(the widest single wrapped child, e.g. one badge — a reasonable
floor); min-width: 0 removes that floor entirely, so the container
shrinks past what its content needs and the content overflows its own
box sideways, landing on top of .agent-header-main instead of
wrapping cleanly. Confirmed reproducible at 320px, not a one-off
capture glitch, before settling on the flex-shrink-only fix.
Verified 500px (the width mara flagged) and 1400px both clean, no
overflow, no overlap. Below ~450px there's now a real mathematical
floor — icon + main's shortest-unbreakable-word + pills' widest-single-
badge together exceed the viewport — that's a genuine follow-up scope
(icon-only badges or similar at that point), not something this fix
claims to solve; flagging rather than silently leaving it implied.
tsc n/a (CSS-only change), build clean, both pre-push lints clean.
mara, live, correcting the prior round: 'oh i meant move badges to
where the dropdowns are not the other way around. this way the height
does not change even on widescreen.'
Prior commit nested pills INTO a new row inside .agent-header-main
(badges + pills sharing a row under the title). Backwards from what
she wanted: badges now join .agent-header-pills instead — the same
header-level column pills already lived in, which already handles a
variable child count via flex-wrap + justify-content: flex-end.
.agent-header-main goes back to just the title + hive-label rows,
which don't wrap in practice, so its height stays stable regardless of
how many badges/pills are showing.
The ResizeObserver dynamic-height fix from the prior commit is
untouched and still does the real work of keeping .agent-status-
overlay's offset correct if anything does wrap — this is a pure
layout-preference change on top of that, not a new bugfix.
Verified at 500/1024/1400px — badges+pills form one right-aligned
cluster next to the title, no overlap, no ResizeObserver console
error. tsc --noEmit clean, build clean, both pre-push lints clean.
mara, live: 'login panel still looks like it is behind', 'bottom input
row text not aligned / inconsistent sizes', 'can we put the badges on
the same row as the links button etc? then the header would be more
compact.'
- Real fix for the overlap this time, not another band-aid: the
header's downstream offset (.agent-status-overlay, .agent-main's
scroll padding) was reading a STATIC --agent-header-h guess (6em).
Every prior fix on this PR removed one specific contributor to the
header wrapping past that guess (meta-nav's inline list, then
disconnected badge/pills clusters) but the guess itself was always
the actual defect — any content that can wrap an extra line breaks
it at whatever width triggers it, and no width is safe to promise.
Header.tsx now measures its own rendered height via ResizeObserver
and writes it to a new --agent-header-real-h var; downstream
consumers prefer that over the static guess. Deliberately a SEPARATE
var from --agent-header-h, not an overwrite: .agent-header's own
min-height also reads that var, so overwriting it in place created
an actual ResizeObserver feedback loop (confirmed live on the first
pass of this fix, fixed by splitting the var).
- Badges (StatusChips) + pills (inbox/todos/links/overflow) now share
one row (.agent-state-row) instead of two visually disconnected
clusters (old app.js markup's 3-column layout, which the new page
had also carried over) — pills pushed to the row's right edge via a
margin-left: auto rule scoped to that nesting, so app.js's own
still-live separate-column usage of the same classes is untouched.
- Composer text alignment: real cause was .prompt (1em) and
.submit-hint (0.8em) each getting a different-sized default line-box
under align-items: flex-start, so the same nominal padding-top
landed at different absolute offsets. align-items: center sidesteps
the whole line-box-size mismatch. This one's shared with app.js too
(same markup/classes) — same bug there, now fixed for both.
Verified at 480/500/768/1024/1400px — no overlap, no ResizeObserver
loop, badges+pills share one row, composer aligned. tsc --noEmit
clean, build clean, both pre-push lints clean.
mara, live review: 'the login card looks like it is behind the header',
'badges and interactive elements on same row have inconsistent size',
'the links should have the same popout as the links in the nav bar of
swarm-ui.' All three, addressed:
- Root cause of the overlap: MetaNav rendered every link inline in the
header's title row, a variable-width list that can grow the row past
the fixed --agent-header-h the rest of the page (position: fixed
header, position: absolute content below it) is offset against, so
the header visually covers whatever's underneath it once it's
actually taller than assumed. Fix: MetaNav is now a single
fixed-size trigger (reuses @hive/shared's Badge, icon-only, same
shape as the header's existing overflow trigger) in the pills
cluster, not a variable-width list in the title row. A single badge
can't grow the row regardless of how many links the backend sends.
Verified at 1024px/1400px (no overlap, was previously untested below
1400px) — the deeper fragility (StatusChips' badge row itself can
still wrap on a genuinely narrow/mobile viewport and hit the same
class of bug) is real but predates this PR and is out of scope here.
- The popover mara asked for: same visual language as swarm-ui's own
LinksMenu (Shell/LinksMenu.tsx) and this page's Dropdown/OverflowMenu
— quiet icon trigger, popover on click. Not built from @hive/shared's
Dropdown itself (its items are always <button>s for command dispatch,
which would lose real <a> link semantics — ctrl/middle-click, copy
Ports the 3 remaining gaps argus's lost-functionality audit found beyond
login: the header meta-nav strip (stats/screen/forge/config +
hyperhive.dashboardLinks extras — the only on-page path to those, not
just polish), the header icon's /favicon.svg fallback on a broken
image, and the browser tab title update. All three are straight ports
of app.js's existing logic (refreshState's meta-links loop,
bindHeaderIconFallback, setHeader's document.title block), same
kind -> URL resolution rules, same dataset.fallback loop guard, same
qualified_label/hive_name fallback chain — no new backend fields
needed, hive_agent::web_ui::state::StateSnapshot already serves
links/forge_public_url and useAgentState's whole-payload cast already
threads them to Root.
New <MetaNav> component (reuses agent.css's existing .agent-nav/
.agent-nav-link rules verbatim, no new CSS) renders in Header's title
row via a new nav prop. Favicon fallback lives in Header itself
(onError handler on the .agent-icon <img>). document.title is a
useEffect in Root keyed on label/qualified_label/hive_name.
tsc --noEmit clean, build clean, both pre-push lints clean. Screenshot
verifies meta-nav renders 4 links (stats/forge/config/extra, forge
kind resolving against a mocked forge_public_url) and the favicon
fallback firing against a deliberately-404'd /icon.
mara: "reuse the popup component for the login, the floating separate
panels should be one card" — the old markup's .agent-status-overlay >
* rule boxed every direct-child <p>/<form> separately, a faithful port
of app.js's el().append()-per-line but not what a hand-authored
component should do. Wraps each state's content in one .login-card
div instead, styled to match Dropdown's .ui-dropdown (@hive/shared)
background/border/radius/shadow values exactly, so this reads as the
same popup family as every other floating surface in the app rather
than a one-off. Not the Dropdown component itself — its options.map
render is menu-item-shaped, not a fit for a login form + output pane.
agent.css's .agent-status-overlay > * rule is untouched (app.js's
still-live markup depends on it verbatim until the index.html
cutover) — .login-card coexists with it rather than editing it,
correctly overriding on equal specificity since main.css loads after
agent.css.
Ports app.js's renderNeedsLoginIdle/renderLoginInProgress as real
functional Preact UI, not just a StatusChip label: a start-login
button (POST login/start), the OAuth-URL link once claude emits it,
a code-paste form (POST login/code, masked + reveal toggle) with
cancel (POST login/cancel), and the streamed process output. Without
this, an agent needing re-login (post-/logout, credential rotation)
had no web-UI path back in through the new page — the gap I flagged
before the cutover; mara: "1 - do it now".
Reuses agent.css's existing .agent-status-overlay/.status-needs-login/
.btn-login/.loginform*/.diff rules verbatim (same class names as the
old markup), matching Header.tsx's precedent — no new CSS file needed.
Added SessionView to types.ts (mirrors hive-agent's SessionView struct
exactly: url/output/finished/exit_note) and lib/loginAction.ts (same
{ok,detail} fetch shape as modelEffort.ts — same-origin POSTs, no CORS
concerns like pauseAction.ts's dashboard-origin actions).
Screenshot-verified all 3 states (idle, in-progress-with-url, finished
with error) against a mock server.
Deliberately trimmed vs. app.js's `populateOverflowMenu`, which held
dashboard-link/rebuild/new-session/logout/model-picker/effort-picker
all in one flat list — the design guide's own named junk-drawer
anti-example. Model/effort already moved to StatusChips' own
Badge+Dropdown controls; new-session/logout now live as TermInput
slash commands (typed, with their own two-step confirm). What's left —
rebuild + a dashboard back-link — is genuinely menu-shaped (rare, not
tied to any other visible control), so it's still a `⋯` menu, just a
much shorter one.
Reuses `@hive/shared`'s Badge+Dropdown pair (same anchored-popover
shape as StatusChips' model/effort pickers), not a bespoke popover.
Rebuild's confirm is the same "select once to arm, select again to
fire" idea as the slash commands, adapted to a menu click. New
`lib/rebuildAction.ts` — same cross-origin-needs-a-real-form-submit
reasoning as pauseAction.ts (hive-c0re's `/api/rebuild/{name}` has no
CORS header either).
Found + fixed a real layout gap in the shared Dropdown while
screenshot-verifying: its CSS anchors left-edge-to-left-edge, correct
for a picker with room to its right, but the overflow trigger is the
header's right-most element, so the menu ran off-screen. Fixed with a
scoped override in this component's own CSS (`.header-overflow-anchor
.ui-dropdown { left: auto; right: 0; }`) rather than changing the
shared default, which is still correct for every other caller.
Ported from app.js's `renderTermInput`/`handleSlashCommand`/
`completeSlash`: prompt + auto-growing textarea, Enter sends (Shift+Enter
newline), Tab cycles slash-command completion. Same command set
(/help, /clear, /cancel, /compact, /model, /effort, /new-session,
/logout), same routes (`api/cancel`, `api/compact`, `api/new-session`,
`api/logout`, `send`) via new `lib/termActions.ts` (same `{ok, detail}`
shape as modelEffort.ts's POST helper).
One deliberate UX change: `/new-session` and `/logout` used to pop the
old shadow-DOM `themedConfirm` modal before firing — this rewrite's
destructive actions all avoid that widget family already (SidePanel,
useConfirmClick), and a modal doesn't fit a text-input flow anyway.
Typing the command once arms it (a local note explains what confirming
does); typing it again fires it — a keyboard-native two-step confirm.
`/help`/`/clear` need to reach into LiveStream's row list (local-only
echo rows, never sent anywhere) without lifting that state up to Root —
`useLiveStream` gained `pushLocalNote`/`clearLocal`, exposed off
`LiveStream` via `forwardRef`+`useImperativeHandle` (preact/compat),
same shape as app.js's old `termAPI` object but scoped as a ref handle
instead of a module-level variable.
Screenshot-verified end to end: typed "/help" + Enter into a real
mounted composer, confirmed the textarea clears and the local note rows
(command list) append to the live pane.
New Preact `SidePanel` drawer — deliberately not the shared
`<hive-side-panel>` shadow-DOM custom element, same rationale as
StatusChips' Badge/Dropdown. Drops that element's drag-to-resize +
localStorage width persistence for this first slice (fixed width via
CSS); reuses agent.css's existing `.agent-inbox`/`.inbox-*`/
`.side-panel-empty` content classes verbatim, only the drawer chrome
itself is new (SidePanel.css). Kept mounted regardless of open/closed
state (toggles the `open` prop) rather than conditionally rendered, so
the slide/fade CSS transitions actually fire on both open and close.
- InboxPanel: renders `state.inbox` (already polled by useAgentState)
+ a "mark all read" action — plain `fetch()` to the dashboard's
`/api/agent/{name}/mark-all-read` (JSON response, same as app.js;
unlike pause/resume this one doesn't need a form-submit workaround).
- TodosPanel: new `useTodos` hook (same 4s poll cadence as app.js's
`refreshTodos`) + bulk select/mark-done via `api/todos/mark-done`.
- Both destructive-but-recoverable actions use a new `useConfirmClick`
two-click-arm hook instead of the old shadow-DOM `themedConfirm`
modal — lighter weight, no backdrop/dialog machinery needed for a
"did you mean to click that" nudge.
- HeaderPill: the inbox/todos count triggers in the header's right
cluster; Header.tsx gained a `pills` slot for them (mirrors the old
markup's `.agent-header-pills` third column).
- types.ts: added `InboxRow`/`TodoRow`, mirroring
`hive_sh4re::inbox::{InboxRow, LooseEnd}` (todo variant only —
`/api/todos` never returns the others).
Screenshot-verified against a mock server exercising both panels.
- nix/checks.nix: 'hyperhive#3685' in a source comment trips the
tracker-tag lint (prose-only, per hive-rules.md) — argus caught it on
review. Reworded without the ref.
- frontend/npm-deps-hash: the earlier scaffold commit
(be18f460) added preact/typescript to agent/package.json, which
regenerated package-lock.json but never re-derived this hash in
lockstep — nix flake check's frontend derivation was failing on a
hash mismatch. Regenerated via
'nix run nixpkgs#prefetch-npm-deps -- frontend/package-lock.json'.
- StatusChips.tsx: drop the stale 'useAgentState lands in a later
commit' comment now that Root.tsx already wires it.
- nix/checks.nix: add agent-typecheck, same shape as swarm-ui-typecheck,
so a real TS error in the agent package fails CI instead of just
esbuild-stripping silently.
Ports the row taxonomy in docs/terminal-rendering.md (app.js's
renderStream/renderRichToolUse/renderToolResult/renderTaskEvent) to
real Preact components + hooks:
- lib/streamRow.ts: plain-data StreamRow model (one row = one line/
panel), lib/classifyEvent.ts: raw stream-json event -> StreamRow[],
almost entirely dispatching on the backend's precomputed _icon/
_summary/_category/_body/_body_type fields, same as the old client.
- lib/markdown.ts, lib/linkify.tsx: sanitized-markdown + auto-link
helpers, same behavior as app.js's mdNode/terminal.js's linkify.
- hooks/useLiveStream.ts: backfill + SSE + seq-dedupe + coalescing,
reduced to a plain StreamRow[] — deliberately has no opinion on
scroll position, only on what rows exist.
- components/Row.tsx: renders one StreamRow (flat or details).
- components/LiveStream.tsx: owns the scrollable DOM node + a from-
scratch sticky-bottom implementation — not the old MutationObserver
+ rAF snap-animation + smoothScrollingUntil gate. stickToBottom is
plain state driven by the scroll handler; snapping is an instant
scrollTop write in a useLayoutEffect that runs after Preact has
already committed the new rows, so it always sees the real
scrollHeight. A load-older prepend is told apart from a normal
append via an explicit ref set right before calling loadMore(),
rather than inferred from DOM mutation shape after the fact.
Wired into Root.tsx below Header/StatusChips; turn_start/turn_end
also nudge useAgentState's refresh() for a snappier badge update than
the plain poll interval.
Reuses @hive/shared/terminal.css's existing row-kind classes as-is —
the taxonomy's visual language isn't what changed, the component
model underneath it is.
- useAgentState hook: polls GET /api/state (4s interval for now — see
its file comment for why this isn't yet the SSE-triggered + login-
only-timer cadence the old page used; that lands with the live
stream + term-input commit, which is when clobbering the operator's
in-progress input actually becomes a risk).
- format.ts: fmtTokens/fmtAge, same output shapes as app.js's.
- modelEffort.ts: POST /api/model + /api/effort (same-origin, plain
fetch).
- dashboardBase.ts + pauseAction.ts: pause/resume POST to the
*dashboard's* origin via a real <form> submit, kept unchanged from
app.js — a cross-origin fetch needs CORS headers hive-c0re doesn't
send, a form submit sidesteps that same as it already did.
- Root.tsx: wires it all together, including app.js's "any non-online
status forces the turn-state badge to offline" behavior.
Screenshot-verified against a mock GET /api/state (real fetch, not
hardcoded props) + the real agent.css/theme.css/colors.css.
Builds + tsc --noEmit clean.
Adds a new Preact/TSX build alongside the existing app.js (esbuild
entry `main.tsx` → dist/static/main.{js,css}, same jsx/tsconfig shape
swarm-ui already uses) and the first real page slice: Header +
StatusChips, composing the Badge/Dropdown components from the prior
commit's PR. Not wired into index.html yet — app.js keeps rendering
the live page untouched while this fills in component by component
(state polling, the live SSE stream, login flow, inbox/todos, term
input) in follow-up commits on this branch.
StatusChips folds the model/effort pickers and pause into the badge
row itself (each badge IS its own control), replacing the old
overflow-menu-only pickers — the concrete fix the design guide already
names this page as needing. Presentational only for now (props, not
live data) so it's reviewable against sample data before being wired
to /api/state.
Screenshot-verified against the real agent.css/theme.css/colors.css
(headless chromium, sample data) — renders correctly.
Builds + tsc --noEmit clean.
Two findings on one panel, both from mara.
Form: 24 label series drawn as overlapping timeseries lines is
unreadable by construction. The panel is a current-count ranking, not a
time evolution, so it becomes a horizontal bar gauge on an instant
sort_desc query -- identity on the axis, magnitude in the bar, and a
fixed colour because colour was carrying no information that the axis
was not already carrying.
Title: the metric counts open AND closed issues. Measured, not assumed
-- the largest single label reads 1619 while gitea_issues_open reads
117, and no label can outnumber the set it is a subset of. The panel had
been asserting the opposite since it was written.
mara on the PR: "many hint texts are incorrect, most hives dont work on
hyperhive itself". Four panel descriptions asserted local practice as if
it were general:
Releases "expected to sit at zero on a hive that ships from
main" -- a claim about how THIS project releases
Automation surface "every hive registering the same webhook has bitten
this forge before" -- a war story from this
deployment, meaningless on any other
Organizations "near-constant by design" -- assumes our org layout
Issues open/closed "usually a bulk triage, not a productive afternoon"
-- a claim about our working rhythm
Each now describes what the metric IS and what a CHANGE means, and says
outright where the panel cannot know: whether zero releases is normal
depends on how a hive's projects ship, and a vertical step in closed is
some batch operation that this panel cannot distinguish from a burst of
real work.
The board title is NOT an instance of this and is unchanged -- all seven
dashboards use the 'hyperhive - <thing>' product namespace, checked
rather than assumed.
The forge restarted (new forgejo PID at 12:36), so it re-read [metrics]
and gitea_issues_by_label now exists -- 24 series carrying real label
names, read back out of the store rather than inferred from the metric
name appearing in a label-values list.
The panel shipped deliberately empty with EMPTY UNTIL THE FORGE RESTARTS
in its title and a matching noValue, because an empty graph and an
undeployed metric look identical. That warning is now false, and a stale
warning is worse than none: it tells a reader the panel cannot have data
at the moment it does.
Keeps the operational half rather than deleting it -- the restart
requirement is a fact about the NEXT metric someone enables, so it moves
into the description as a note and into noValue, where it fires only when
the series really is missing.
Adds the fourth per-service board. Unlike its three siblings it answers
"what does the forge CONTAIN", not "is the forge healthy" -- forgejo's
exporter publishes a census of stored objects and no request, error or
latency series at all.
That distinction is the board's own description rather than something a
reader has to infer, because a fourth per-service board that silently
answers a different question than the other three is worse than no board:
someone checks it during an incident and reads a flat line as calm.
Panels: repositories, users, orgs, issues open/closed, releases, the
open-vs-closed pair over time, repos-and-users together (they move in
lockstep when an agent is created, so divergence is the signal),
discussion volume, and the webhook/hooktask pair.
One panel ships deliberately EMPTY: issues-by-label. The metric is
enabled in the forge's config but forgejo reads its [metrics] section at
startup, so the series does not exist until that process restarts -- and
a config merge alone does not restart it. The panel title says so, the
description says how to tell "not deployed" from "no labelled issues",
and `noValue` says it again in the graph itself. An empty panel with no
explanation is the failure this board is trying not to be.
Gated: every panel's metric confirmed to exist in the store, with a
bogus name checked absent in the same run so the check can fail, plus a
known-positive control against the label-values query itself -- a
download that silently returned nothing would otherwise report every
metric missing. Gate kept as verify-3494-forge-board.sh.
mara: non-interactive Badge and StatusChip render identically (same
padding/radius/font, same tone-to-color mapping) and StatusChip's
4-tone/single-label shape is a strict subset of Badge's — no real
reason to keep both. Deletes StatusChip.tsx/.css, migrates AgentsPage's
config-PR chip, HivesPage's freshness chip, and ComponentsPage's own
table-status sample to Badge (tone/value, no onClick). Also drops the
now-redundant standalone StatusChip demo section on /components (the
Badge section already covers all 4 former chip tones plus accent).
Updated the two stale StatusChip references outside swarm-ui too:
design-guide.md's component-list example and colors.css's WCAG-
contrast-rationale comment.
New shared Preact primitives for the per-agent terminal redesign:
- Badge (@hive/shared/badge.js): a labelled status pill. Plain <span>
when static (e.g. "alive"), a real <button> with a disclosure caret
when given onClick (e.g. opens a Dropdown, or toggles itself in
place). Same component either way so a status row reads as one
consistent set of badges regardless of which are interactive.
- Dropdown (@hive/shared/dropdown.js): a small option list anchored
directly under whatever opened it (no portal, no native <dialog> —
see the file comment for why). Closes on outside click or Escape.
This is the fix for the design guide's own named anti-example: the
agent page's model/effort pickers live in the overflow menu while the
current model/effort only show as a disconnected chip. Badge+Dropdown
composed together is that control moved inline, next to what it shows.
Demoed on swarm-ui's /components page: all Badge tones, a label-prefixed
badge, an interactive badge that opens a Dropdown (model-picker shape),
and an interactive badge that toggles itself (pause/resume shape).
Both packages build + tsc --noEmit clean.
The comment-block lint (added in 79dc8ca6) now trips on this block —
genuinely pre-existing, unrelated to that change, just newly caught.
Per the lint's own suggested remedy: relocated the full per-directive
reasoning plus both footguns (session-cache keying, the Host-header
clobber that can recurse a subrequest into itself) to a new
"Dialing another vhost by name" section in docs/gateway.md, and left
a short why + pointer comment in the source. No behavior change.
verifiedProxyTo (43ae164d) verified TLS but left Host to nixpkgs'
recommendedProxySettings, which sets Host to the CALLING vhost, not
the target. Since every consumer resolves back to this same gateway,
nginx picks the vhost to answer by Host header (not by the SNI
proxy_ssl_name already sends) — so every auth subrequest looped back
into its own vhost's auth_request, recursing until nginx's subrequest
depth limit turned it into a 500, on every domain gated by SSO.
Pin Host (and the rest of the header set nixpkgs' recommended include
would otherwise still be the one to set) inside verifiedProxyTo, and
set recommendedProxySettings = false on each of the four call sites so
nixpkgs' own copy — appended after a location's extraConfig — can't
clobber it back.
One list, in nix/reserved-names.nix, handed to everything that needs it
as HIVE_RESERVED_NAMES. Keeping it current becomes a config change
rather than a rebuild, and hive names and agent names -- one namespace
going forward -- are checked against the same file: swarm-otel.nix's
hand-written reservedOwners is gone.
Whitespace-separated rather than JSON, deliberately, unlike the
structured env vars beside it. Every entry is an Ident ([a-z0-9-]), so
whitespace cannot occur inside a name and the encoding is provably
lossless; JSON would mean either a parser dependency in a crate whose
purpose is to have none, or a copy of the parse in every consumer.
An UNSET variable is not "nothing is reserved". Both creation sites log
an error and return a warning saying the check did not run, so a
misconfigured deployment says so instead of silently accepting every
name. A blank value folds into unset: nix always renders a non-empty
list, so present-but-empty is a rendering fault, not a declaration.
Two guards whose subject moved out of their own file now assert their
own case is still in it, because a guard that can be retired by an edit
elsewhere is not a guard:
- swarm-otel.nix asserts reserved-names.nix still contains its
swarmTierName.
- hive-sh4re's sentinel drift test PANICS when the variable is missing
rather than skipping -- a drift test that quietly does nothing still
reports green. checks.nix and devshell.nix both export it so CI and a
local cargo test agree. Verified as a pair: with the variable set, 8
tests pass; with it unset, exactly the 4 drift tests fail and the
unrelated ones still pass.