/messages reads GET /api/messages/stream. A history select picks the window
(live only up to 24 hours) and a filter keeps the messages whose sender or
recipient contains the text. Each message is a row of the terminal list an
agent's terminal uses: one summary line with the time, the two parties and
the start of the body, and the whole body under it. The list follows the
newest message and holds the last 2000.
The feed has no event id, so a reconnect sends the history again. The page
empties its list each time the connection opens and fills it from the
replay, where the terminal stream hook, which keeps its rows across a
reconnect, would list every message twice.
The stream URL, filter, cap and row logic is in its own module with tests,
and docs/swarm/ui.md describes the page.
Pick agents from a grid of their icons (running or all), type the text, send.
The page posts the names and the text to the controller and shows what
happened to each agent, with a retry that selects just the ones that failed.
The selection rules and the controller's answer are read by plain functions
with tests.
/builds lists the builder's recent builds newest first, one card each with how
it ended, and opens a failed build's nix tail. /builds?run=<id> opens that
build's card and scrolls to it. The swarm-builder commit status now links
there instead of to the raw JSON record.
Permission::default_for and hive-sh4re's MANAGER_AGENT are gone: every
agent with no permissions key holds Permission::DEFAULT, ruth included.
The agent-name parameters that only fed default_for go with it
(permissions::effective, HiveCopy::before_read/after_read and their
callers).
swarm-controller seeds the grant ruth held by name: at start, a declared
ruth whose permissions are unset gets DEFAULT plus manage_root_agent
written into her key, once, logged. A set key is never touched. It is a
one-shot to delete once every swarm has passed it.
The swarm UI setup assistant opens the create form with
manage_root_agent checked, so a new ruth is created with the grant.
permissions_import no longer adds ruth to the import or splits her
default. Docs and help text no longer say ruth holds the grant by
default; swarmctl-cli.md regenerated.
Drop the H0M3 tile hub and the core page. The dashboard is now the index;
kept state is its fifth tab. A pages menu in the chrome links flow, stats,
API docs and the swarm UI. The swarm apex URL comes from
services.hyperhive.swarm.domain via HIVE_SWARM_PUBLIC_URL and the new
swarm_public_url field of the state snapshot.
The shell had two body widths, a 60em readable column and a 90em wide body
that a short list of table routes opted into. Every route now gets the
90em body, and the opt-in route list, its modifier class and its test go
away.
swarm-bao-grafana-controller-token, on the host that runs Grafana, logs in
to Grafana's API over its socket as the built-in admin (the password
swarm-bao-grafana-admin minted, #5138) and keeps a `swarm-controller`
service account that is an enabled Viewer. It mints a token for that
account and writes it to bao at
swarm/controller/swarm-controller/grafana/token under Grafana's leaf. A
ten-minute timer runs it again. The unit keeps a stored token Grafana
accepts. If Grafana refuses the stored token with 401, or bao holds
none, the unit deletes every token of the account and mints a new one.
Grafana's grant gains create/read/update on that leaf, and the
controller's gains read.
swarm-controller serves GET /api/alerts from Grafana's
/api/prometheus/grafana/api/v1/alerts. It reads the token from bao,
keeps it in memory, and re-reads bao once on a 401. The HTTP client has
5s connect and 15s request timeouts. Any failure is a 503 that names its
cause. swarm-ui gets an /alerts page listing them, built the same way as
the hives page.
This is option (c) on #5129, the operator's choice.
The two review tabs, runs and try-out, are one: /review. It is a column of
three sections: try a classification and the classifier settings, each a
folded bar (the settings' bar says what they are now), then the recent runs,
always open. The settings had been pinned under the try-out form; they are now
a section of their own. A new Disclosure primitive is the folding bar, a
native details wearing the Panel's classes, so the folded and open sections
read as the same surface.
/review-runs and /review-tryout are gone, with no redirect.
The rain read only the OS reduced-motion query, once, at mount. The settings
menu's motion override (data-motion on the document) wins over the OS in
either direction everywhere else, and the shell applies it from an effect
that runs after this one. Whether the rain may move is now rainAllowed(), and
the component re-evaluates it on mount, on every document attribute change
and on the media query's change, starting or stopping the interval and
clearing the canvas.
The rain's column maths and fade colour move from MatrixRain into
shell/rain.ts, and the request-once swarm name loader from useSwarmName
into shell/swarm-name.ts. Both are plain functions with their input
passed in, so tests cover them; the components keep the DOM and canvas
work.
isFullBleedRoute and isWideBodyRoute are plain functions of a path, so
they move from Shell.tsx to shell/routes.ts, where the new tests cover
the start page, the hive directory's move to /hives, the terminal
paths and the wide-body routes.
/ is now the swarm's name over the digital-rain backdrop the hive
dashboard's home has, and the swarm name in the header links to it. The
hive directory moves to /hives, where the nav's hives tab points. The
swarm name is read once per page load by a shared hook, the header and
the start page both use it. Closes#5252
One card per run, newest first, from GET /api/review-classifier/runs: repo,
PR, head commit, trigger, when, how long, how many classifications and the
outcome as a badge. A card opens to the try-out's result table, the overall
verdict, the reason and the posted review's link. TryOutResult takes the
answer it shows rather than a whole try-out result, and its styles move to
their own file.
The row moves from the per-agent page into @hive/shared so both pages mount
the same one. The terminal's debug rows now follow the preference as it
changes, so rows already on screen hide or show at once instead of only the
rows that arrive after the toggle.
A page to try a review classification against a real PR: repo, PR number and one or more classifications (name, description, polarity, threshold). Run posts to the controller's try-out endpoint and polls until the run is done or failed, saying it is running meanwhile, then shows the review's table, the overall verdict and the head commit. A row that could not run shows its error in place of a reason, and a failed run shows its error. The inputs stay so the run can be edited and repeated. Nothing is stored and nothing is posted to the forge. Route, nav entry and a section in docs/swarm/ui.md.
pages/agents held the roster page, the terminal page and every component under them. It keeps the two pages; the components move to src/agent, by concern: roster (card, wanted menu, actions), detail (panel body, dialog, inference, linked accounts, link menu) and terminal (preview, input, subagents, stream hooks, favicon), with the shared row type and icon at the top. Moves and import rewrites only.
The agent terminal page swaps the page's favicon for the agent's icon while it is mounted, so tabs of different agents tell apart. A subagent terminal shows its agent's icon. An agent with no icon, or an unreachable controller, keeps the hyperhive mark; leaving the page restores the page's own icon.
/schedules lists the controller's scheduled prompts as cards: name as the
title, the agents it fires for, the message, its cadence and next fire.
Edit turns a card into its own form; pause, resume and remove act in place,
and + schedule opens an empty form. The form sends only what changed, so a
rename never moves the fire times, and warns when the start or the repeat
does. Filters reuse the shared card filter bar.
Adds a TextAreaField to the ui kit, and an upcoming mode on RelativeTime so
a next-fire time reads "in 5m". Needs the controller's /api/schedules
routes.
The agent terminal tab now takes the whole window except the header bar,
full width and height, instead of sitting in the padded content column.
The connection badge floats inside the terminal's top-left corner and the
toolbar row above it is gone on this route.
An agent's subagent daemon publishes each subagent's output as terminal
rows on `$SWARM.term.<agent>.sub.<subagent>`, as the agent, into a
per-agent stream it creates itself; swarm-controller lists an agent's
subagents from that stream's subjects and relays one subagent's rows as
SSE; the swarm UI lists them under the agent's terminal preview and
reuses AgentTermPreview, full-screen tab included, with no input.
- swarm-nats.nix: the agent token may also publish
`$SWARM.term.{agent}.sub.>` and `$JS.API.STREAM.CREATE|INFO` on
`term-sub-{agent}`, and nothing else of JetStream. A module-eval arm
pins the agent-token grant as an exact list.
- mcp.nix: hive-subagent-daemon loads the agent's store identity
(`hive-agent-bao-cert/-key/-server-ca`, the ones hive-agent loads)
whenever the agent has a store, not only on the opencode preset. The
agent's own queue secret lives in the store, so this is the credential
the harness connects with.
- hive-subagent-mcp: `swarm_term` reads the agent's queue secret under
that identity, connects with the agent token, opens or creates
`term-sub-<agent>` (max_age 24h), and publishes classified rows from
the sink every subagent line already passes through. The sink only
queues (bounded, drop-and-count); a missing store, refused credential,
failed stream create or failed publish is a log line.
- The stream-json classifier (`stream_enrich`) and the `TermMsg` row
types plus `fit` move from the hive-agent binary into hive-sh4re, so
the subagent daemon publishes the rows AgentTermPreview already
renders. hive-agent keeps its LiveEvent classifier on top.
- swarm-controller: `GET /api/agents/{name}/subagents` and
`GET /api/agents/{name}/subagents/{subagent}/term/stream`.
- docs/swarm: what the UI shows and what the queue carries.
Closes#4827
mara, PR review: "the dynamic tab should be in the top bar, not a new
one below". Moves the .shell-tabs group from its own sticky row under
the header into .shell-nav itself, right after the nav indicator.
Also adds a third re-measure effect for the sliding nav indicator,
keyed on tabs.length: with tabs inline in the same flex row the
indicator measures, closing a background tab (no navigation) can
shrink the row without the hop effect's own re-measure ever firing.
Same reflow-not-navigation reasoning as the existing resize-listener
effect.
Adds a full, non-capped agent terminal reachable from a new expand
trigger on the embedded AgentTermPreview (the detail-panel preview on
AgentsPage stays as-is, just gains the trigger). Opens
/agents/:name/terminal in a new dynamic tab in Shell's header, next to
the static nav row — tabs persist across a reload via useDynamicTabs,
a small localStorage-backed hook built on @hive/shared's existing
settings-storage primitive.
AgentTermPreview gains two new props to support both mounts from one
component: fullHeight (drops the 12em preview cap, fills its page)
and showHeaderBadges (default true — lets a future caller that
already shows turn_state/model/ctx/cost elsewhere suppress this
cluster; AgentsPage doesn't use it, see below).
Deviation from the originally posted plan (issue comment 80596): that
plan proposed AgentsPage's embedded preview pass showHeaderBadges as
false, reasoning the detail panel already duplicates that info.
Checked the actual code before implementing — it doesn't; AgentRow/
AgentTypes.ts carry none of turn_state/model/ctx/cost, and
AgentTermPreview's own floating badges are the only place swarm-ui
shows them. Left the badges visible there instead of shipping a
regression the plan's own stated justification didn't hold up to.
Also fixed a same-tab pub/sub race found by actually rendering a cold
load of /agents/:name/terminal (headless chromium, not just reasoning
about the code): useLocalSetting subscribes inside a useEffect, and
mount effects fire children-before-parents, so a descendant's
mount-time write (AgentTerminalPage registering its own tab) can beat
an ancestor's (Shell's) subscription into existence, leaving Shell's
tab row silently empty on a direct/reload load. Fixed by having
useDynamicTabs re-sync from storage on every location change, not
just on notify() — the fix lives in the new hook itself, not in the
shared settings-storage primitive theme/motion overrides also use.
Ports the subtle text-shadow glow @hive/agent's and the dashboard's own
headline rule already use (both independently converged on
`text-shadow: 0 0 Npx color-mix(color, transparent)`), not their whole
uppercase+letter-spacing headline typography -- that's those UIs' own
look, swarm-ui never adopted it and this issue only asked for the glow.
- Panel titles: `currentColor`-based, since a panel title has no one
fixed accent (some are dynamic, e.g. an agent's own name) -- a fixed
--purple glow on plain --fg text would read as a mismatched halo.
- Active nav tab: each item's own `accent` (NAV_ITEMS), the same value
its underline already uses, so the glow and the underline read as
one accent rather than two that happen to agree.
AgentsPage's table view (and its list+detail split) hits the same
60em readable-line-length cap the issue report's wide table already
opts out of via shell-body-wide. Adds /agents to WIDE_BODY_ROUTES --
the allowlist's own comment already anticipated this, a future wide
page reaches for the same class rather than inventing its own cap.
Pure `nix fmt` output from the commit before this one — no hand edits.
203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs.
Reproduce with `nix develop -c nix fmt` on the parent commit; the result
should be byte-identical to this tree.
None of the 13 `.prettierignore` entries appears here — verified by
intersecting the changed-file list against the ignore file, with a
control proving the intersection finds a match when one exists.
Continues #3901. Same pattern as tabs.js/swarm.js: cut 'used to be
X, now Y'/'moved to'/'no longer' change-history narration down to
the current design fact, keep every load-bearing rationale intact
(the ResizeObserver feedback-loop note in Header.tsx, the Dropdown-
vs-real-<a> semantics in MetaNav.tsx, etc.).
mara (#3877): issue report too narrow — the 8-column table was
clipped by .shell-body's 60em readable-line-length cap, which is right
for the rest of the UI's cards/forms but too tight for a wide table.
damocles diagnosed the root cause and scoped the shape (route
allowlist + an additive .shell-body-wide modifier, no prop plumbing
through App.tsx) before asking for a nod; mara then routed the actual
build to me. Implemented that shape as scoped: /issues opts into a
90em cap via WIDE_BODY_ROUTES, every other route keeps 60em untouched.
Verified with real headless-chromium screenshots at 1280px: the issue
report's full 8-column row (through 'transitively blocks') now fits
with no horizontal scroll, and a second screenshot of /agents confirms
every other route is unaffected.
New /issues route: a sortable, filterable table over open issues across
every repo that has one -- repo picker (default: no filter, every repo
combined), hide-blocked toggle, and a label multi-select, consuming
swarm-controller's GET /api/repos + GET /api/issue-report / GET
/api/repos/{org}/{repo}/issue-report (see hyperhive#3831 for the row
shape). blocked and depended_on_by_count arrive pre-resolved per row --
this page does no dependency-graph walking of its own, just sort/filter
over what it's given. Default sort is depended_on_by_count descending,
matching mara's framing of the report's headline ordering.
Widened Table's TableColumn.header from string to ComponentChildren so
a column can carry a real clickable sort-toggle button instead of
forking a second table primitive for this one page.
Fixes a regression + a longer-standing inconsistency (mara: "agent
page link icons is different from swarm ui, settings icon looks weird
since component extract").
- The `SettingsMenu` shared-component extraction swapped swarm-ui's
original inline-SVG gear trigger for a plain `⚙` text glyph (matching
agent's `MetaNav`, which was itself still on the `🔗` emoji at the
time). An emoji/text glyph is rendered by the OS/browser's own font
at that font's fixed metrics — it can never match a neighbouring
icon in size or weight, and some codepoints (the gear included)
aren't even reliably covered by every font.
- New `@hive/shared/icons.js` (`GearIcon`, `LinkIcon`) — the exact SVG
markup that used to live only in swarm-ui's `SettingsMenu`/
`LinksMenu` as two separate inline copies, now the one shared source.
- `SettingsMenu` (shared) uses `GearIcon`; agent's `MetaNav` and
swarm-ui's `LinksMenu` both use `LinkIcon` — three consumers, one
rendering path, matching stroke/viewBox/size everywhere.
Verified with real screenshots on both agent and swarm-ui — both
trigger icons render as the same crisp line-icon style now.
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.
LinksMenu and SettingsMenu triggers mixed a full-colour emoji (link)
with a plain text glyph (gear) - different rendering paths mean
different, unfixable sizes/styles. Replace both with matching inline
SVG icons (feather/lucide gear + link glyphs), same viewBox/stroke/
size, so the two buttons finally share one rendering path.
Also: shared base.css never zeroed the default UA body margin, which
showed as a bg-coloured strip around the whole viewport edge on any
full-bleed header (swarm-uis .shell-header among them). Zeroed it in
the shared file so every consumer (dashboard, agent UI, swarm-ui) gets
the fix, not just swarm-ui.
Screenshot-verified at both desktop and phone widths.
Part of #3591 (mara: "pop ups and menus appearing should animate").
Each popover mounts fresh on open ({open ? <div> : null}, not a state
transition), so a keyframe animation on the popover element itself is
the right tool -- same shape as Shell.css's own shell-page-enter
(fade + a slight translate/scale settle), including the identical
three-rule motion-guard (base rule, prefers-reduced-motion media
query, data-motion=reduce/allow explicit overrides).
Scope: LinksMenu, SettingsMenu, UserMenu -- the three header popovers.
Not included here (posted findings on the issue instead of guessing):
the refresh-interval picker's dropdown (native <select>, whose open
popup is OS/browser chrome outside CSS reach in current browsers --
"not themed" is a platform limitation, not a bug in this component's
own styling) and the jobs graph's node animations (JobqGraph is a
@hive/shared component consumed by both swarm-ui and the per-hive
dashboard, real design/implementation work on shared infra, not a
same-shape mechanical extension of an existing pattern).
Screenshot-verified the settled (post-animation) state renders
correctly; a static screenshot cannot show an in-flight CSS animation,
so this leans on exact structural parity with the already-shipped
Shell.css pattern for the animation's own correctness.
mara: "should be https://auth.constellation.darkest.space/settings in
profile pic menu" -- the UserMenu link was pointing at the plain
authelia domain root, which lands on the portal rather than the
account settings page. Appends /settings client-side, same base-URL
source as before (GET /api/links Authelia entry).
Adds a generic ui/dialog/Dialog primitive (native <dialog>, no
third-party modal lib and no shadow-DOM custom element per the esbuild
gap on those) and wires a "+ agent" button into AgentsPage that opens
the existing create-agent form inside it, content unchanged from its
former life as a standalone /create-agent route/nav item.
Removes the "new agent" top-level nav entry and the /create-agent
route entirely -- creation now only reachable from the roster that
gets populated by it. CreateAgentPage.tsx/css renamed to
CreateAgentForm.tsx/css to match its new role as a mounted component
rather than a page.
Screenshot-verified the dialog open/closed states against a mock
server.
Adds an "/api/whoami" same-origin nginx proxy to authelia's own
GET /api/user/info (session-cookie authenticated, no swarm-controller
code needed) and a new UserMenu header component: a generated initials
avatar (first letter of display name, coloured from the same seven
base16 chromatic slots the nav accent already cycles through) opening a
popover with the signed-in name, a link to authelia settings, and log
out — both reusing the existing "Authelia" entry from GET /api/links
rather than a second source of the domain.
Per mara's call on the open avatar-mechanism question: initials now,
a real uploaded photo (authelia's settings UI implies pics are
settable) is an explicit future item, not blocking this.
New AgentsPage at /agents: fetches GET /api/agents (roster names) and
GET /api/config-prs (bulk config-PR status) and merges them into one
table, one row per agent. Reuses the existing Panel/Table/StatusChip/
RefreshIntervalPicker components exactly as HivesPage does — the roster
page and the config-PR panel turned out to be the same page rather than
two separate pieces of UI.
Adds a nav entry (green accent, the next unused base16 chromatic slot)
between hives and new agent.
Root cause (found via a zoomed pixel-level screenshot check, not
guessed): the indicator span always exists in the DOM starting from
left:0/width:0/transparent, and the very first real position landed
via a CSS-transitioned change from that fallback rather than a snap —
so the underline visibly grew in from nothing over 140ms instead of
being there immediately, reading as entirely absent on a fast/slow
first paint alike depending on timing. Every subsequent navigation was
unaffected (always transitioning between two already-visible states).
Fixed with a one-render-only transition suppression
(indicatorSettledOnce, gated one tick behind the indicator's first
non-null commit) so the first placement snaps instead of animating in,
while every later hop still animates normally. Verified against the
exact repro: a fresh page load at the same short virtual-time-budget
that previously showed no underline now shows it immediately.
The brand/logo text now rides the same accent value the nav underline
uses, including mid-sweep, so the header reads as one accent changing
rather than the underline alone — per mara's review comment.
Content entrance was a scale+overshoot pop; mara didn't like it on
review, swapped for a plain fade. The nav underline now hops through
every nav item it passes over on its way to the new active one
(position and colour together) instead of tweening directly between
the two endpoints, matching what she actually meant by "move through
color wheel" — a->c visibly touches b's colour along the way, not a
smooth continuous hue rotation.
Shell's page content now remounts on every navigation and plays a
scale+fade pop-in entrance. The active nav link's underline is now a
single shared element that slides to its new position instead of
snapping, re-colouring to a per-tab accent — a discrete cycle through
the existing base16 chromatic slots, not a continuous hue rotation.
Both are gated on the data-motion/prefers-reduced-motion plumbing
lib/motion-apply.ts already had wired and waiting for a first real
consumer.
Adds the settings surface + storage plumbing swarm-ui has been missing:
nowhere to put a client-local preference and no shared code for one to
build on. Scoped small per explicit direction ("small thing somewhere",
localStorage, theme and motion in scope for now) rather than a full
/settings route + nav entry for two toggles.
- frontend/packages/swarm-ui/src/lib/settings-storage.ts: generic
useLocalSetting<T>(key, fallback) hook - read once, write through,
stay in sync with other same-tab consumers of the same key via a
small module-level pub/sub (localStorage's own storage event only
fires cross-tab).
- frontend/packages/swarm-ui/src/lib/theme-apply.ts: tri-state
system/light/dark override, applied by setting the 16 base16 custom
properties inline on <html> (an inline style always outranks a
stylesheet rule, including a media-query-gated one) - colors.css's
own comment on its light-mode block already named this as the
intended mechanism for a future override.
- frontend/packages/swarm-ui/src/lib/motion-apply.ts: tri-state
system/reduce/allow override, applied as a data-motion attribute.
Currently inert - swarm-ui has zero CSS animations yet - included
because the marginal cost riding alongside the theme override is
near zero and it was named in the same scoping answer; the first
swarm-ui animation's own CSS is what makes this do anything.
- frontend/packages/swarm-ui/src/shell/SettingsMenu.{tsx,css}: a
header icon-button + popover holding both selects, same shape as
LinksMenu (manages its own state, not a ui/ primitive, hence no
ComponentsPage demo - same exception LinksMenu already established).
- Shell.tsx/.css: mounts the two override-application hooks once
(every route renders through one Shell), and wraps SettingsMenu +
LinksMenu in a single .shell-header-actions flex wrapper so one
margin-left: auto pushes both to the right edge together - two
adjacent auto-margins on separate elements split the space between
them instead of sitting flush.
Verified the override actually outranks the media query, not just
"looks right": seeded localStorage with each override value while
forcing the opposite OS-level prefers-color-scheme via headless
chromium, both directions render the stored override, not the forced
OS preference. Typecheck and build clean.
The previous fix put both the touch-target min-height and the
active-state border-bottom on the same <a>: centering the text
within a 2.75em box pushed the border ~0.7em away from it, a real
visible regression argus caught by actually rendering the CSS rather
than reasoning about the box model abstractly.
Split the two concerns onto two elements: the <a> (.shell-nav-link)
owns the full-height tappable box, an inner <span>
(.shell-nav-link-text) wrapping just the label owns the underline,
so the indicator stays directly under the text regardless of the
tappable box's height.
Verified: tsc clean, build succeeds, screenshotted close-up (600x100)
and at 320px — underline sits flush under the text in both, no gap.
The proposed scope on this issue named three things to give the
2.75em touch-target floor: nav links, buttons, and the links-menu
trigger. Only the links-menu trigger actually got it in the first
pass — the primary nav (hives/new agent/jobs/components), the
highest-traffic touch target in the whole shell, was still sitting at
padding: 0.25em 0 (~27px tall).
display: flex + min-height rather than more padding: padding alone
would have pushed the active-state border-bottom away from the text
to reach 2.75em; flex centres the text within the full-height box so
the indicator still sits directly under it.
Verified: build succeeds, screenshotted at desktop width and 320px —
active underline still sits correctly under the text at the taller
height, wrap behavior from the earlier commit unaffected.
Closes#3447.
- Shell.css: .shell-header and .shell-nav wrap onto a second line
below a breakpoint instead of forcing horizontal overflow — no
hamburger/collapse menu, per mara's own 'don't break, don't
over-invest' scoping.
- LinksMenu.css: bumped .links-menu-button and .links-menu-item to the
same 2.75em (~44px, WCAG 2.5.5) touch-target floor the shared ui/
kit's form controls and buttons already carry — this trigger is
bespoke, not built from that kit, but the floor applies regardless.
- Table.tsx/.css: wrapped the table in a .ui-table-scroll container
(overflow-x: auto) so a wide table scrolls within its own box on a
narrow viewport instead of forcing the whole page to overflow
horizontally — not named in the issue's two concrete spots, but the
same 'should not break' failure mode, and fixing it at the shared
Table primitive covers every future caller.
.create-agent-input's fixed width -> max-width was already fixed as
part of #3448's form kit.
Verified: tsc clean, build succeeds, screenshotted at 320px (hives
nav wraps, table scrolls internally, page itself doesn't overflow)
and 480px (create-agent form fields fit).
Closes#3460.
Renamed for accuracy — the page/nav item is the hive roster, not a
general dashboard overview. Nav label, page title, component name,
and file all renamed together so the internal name doesn't drift from
what's displayed (a component still called OverviewPage under a
'hives' nav label would be exactly the kind of stale prior-art that
makes the next contributor search harder, not less).
New GET /api/swarm on swarm-controller, backed by
services.hyperhive.swarm.name (SWARM_CONTROLLER_NAME env var, same
optionalAttrs-gated-on-option-resolving shape queueEnv/forgeEnv/etc.
already use). swarm-ui's <Shell> fetches it once and sets both
document.title and the header's brand text; falls back to the
existing static "hyperhive swarm" label when the operator never set
a name or the fetch fails.
Extracted the swarm-queue connect block out of main() into its own
connect_status_reader() fn to keep main() under clippy's line-count
lint after adding the new field wiring — no behavior change, same
comments moved as-is.