Commit graph hyperhive/frontend/packages/swarm-ui/src
Author SHA1 Message Date
iris
767f87610a swarm-ui: link a matrix account to an agent
New per-row action on AgentsPage: a quiet-variant icon badge (LinkIcon,
matching the LinksMenu/SettingsMenu chrome-not-chip convention) opens a
dialog with an account/token/homeserver form, PUTting
/api/hives/{hive}/agents/{agent}/matrix-accounts/{account} per the
contract atlas posted on hyperhive#3726 (issuecomment-72355).

Built against the contract before the backend endpoint exists per
atlas's explicit note that it doesn't change when the implementation
lands -- this 404s until that item merges. No linked-accounts list:
no route exposes one, and a credential store shouldn't hand a secret
back out anyway, so this is a blind set/update action, matching
mara's 1:1-for-now ruling on the issue.

Verified: tsc --noEmit and nix fmt clean, esbuild build clean. Real
DOM-interaction screenshots against a throwaway mock server (deleted
before this commit, never tracked) -- table column render + disabled
state on a hiveless row, dialog open, form filled with the token
masked, and the success path end to end (token field clears, success
message shows) against a mocked 200 response.
2026-09-08 15:54:01 +02:00
iris
63f3afae3e swarm-ui: portal the filter popover past the table's own clip
argus's review (reproduced, not speculative): .ui-table-scroll sets
overflow-x: auto with overflow-y left unset. Per the CSS overflow
spec, an axis left visible computes to auto too once the other axis
isn't visible -- so this box silently clips vertically as well as
horizontally. The popover was position: absolute; top: 100% under its
header <th>, itself inside .ui-table-scroll -- on a table shorter than
header-plus-popover (a small hive's roster, or any table narrowed by
an existing filter), the popover got cut off at the scroll box's own
bottom edge, with a stray vertical scrollbar as the visible symptom.

Computes the popover's viewport position from the anchor <th>'s own
getBoundingClientRect() and renders it via a portal onto
document.body, position: fixed -- escapes .ui-table-scroll's clip the
same way any position: fixed element escapes an ancestor's overflow
(neither .ui-table-scroll nor .ui-table establishes a new containing
block). Recomputes on scroll (capture-phase window listener, the
standard technique for detecting scroll on a nested scroll container
without binding to every ancestor by hand) and resize while open, so
the popover stays anchored rather than only positioning once at click
time. createPortal comes from preact/compat, already resolvable
through the existing preact dependency -- no new package.json entry.

Verified against argus's own repro shape: a 1-row table, scripted
click on the filter icon (same real-DOM-event technique as this PR's
first round), screenshot shows the popover rendering fully rather than
clipped, no stray scrollbar.
2026-09-08 12:14:56 +02:00
iris
e234afa26a swarm-ui: table filters move to a per-header icon + popover
Replaces the permanent filter-row under Table's headers with a small
filter-icon button in each filterable column's own header cell. The
icon fades in on header hover/focus via a CSS opacity transition, or
stays visible outright once that column actually has a filter set
(mara: "instead of a filter row, add little filter icons on header
hover with fade in out animation ... when a filter is set, the filter
icon does not disappear"). Clicking it opens a small anchored popover
directly under the header holding the exact same filter control
filterMode already provides (text input or select) -- the underlying
filter mechanism from hyperhive#4088 is unchanged, only where the
control lives moved. Close-on-outside-click/Escape mirrors the
contract Dropdown already gives its own popover, adapted to a shared
listener across every column instead of a ref per column since only
one popover is ever open at a time.

New FilterIcon in @hive/shared's icons.tsx (a plain inline SVG funnel,
same Feather/lucide-style shape as the existing GearIcon/LinkIcon) --
found and reused that pattern rather than reaching for an emoji glyph,
matching the documented reason those two exist as SVG in the first
place (mara, on the old emoji icons: inconsistent size/weight across
platforms).
2026-09-08 12:14:56 +02:00
iris
17d22f9e16 swarm-ui: filter the issue report by title, assignees, blocked
Three columns gain Table's filterValue/filterMode (the mechanism
hyperhive#4088 added): title (text, substring search -- there was no
way to search by title text at all), assignees (text, not select --
a row can carry more than one assignee and Table's select mode
matches one whole string per row exactly, so substring search over
the joined string is the shape that actually fits multi-value data),
and blocked (select, synthesized "blocked"/"not blocked" strings --
distinct from the existing "hide blocked" toggle, which only hides
blocked issues and has no way to show only them).

Deliberately not touched: repo (redundant with the existing repo
SelectField), labels (redundant with the existing label chip
multi-select -- chips are the better UI for a bounded label set
anyway), the three numeric columns (no clean filter shape, already
sortable). The existing hand-rolled sort (SortHeader, useLocalSetting-
persisted) is untouched too -- migrating it onto Table's own sortBy
would drop the localStorage persistence this page specifically wants,
and Table doesn't expose controlled sort state to a caller today. The
two layers compose without conflict: Table's own filter/sort runs
over whatever rows it's handed, which is already this page's own
filtered+sorted array.

Scoped on the issue first, including this exact reasoning, before
writing any code.
2026-09-08 12:14:42 +02:00
iris
8d8ac72d98 swarm-ui: size dialogs to their own content width
Dialog previously forced every caller to a fixed width: 90vw; max-width: 44em
shell regardless of content — fine for the create-agent form's wide
two-panel layout (the only caller until now), but a ConfirmDialog's short
paragraph then wrapped at its own narrower max-width while the shell stayed
the wide default, leaving a dead gutter before the close button (mara filed
a screenshot showing exactly this).

First pass added a narrow prop/second CSS class for ConfirmDialog to opt
into a smaller fixed width. Review pointed at the actual root cause one
level up: width: 90vw is a forced width, not a cap — a native dialog's own
UA default is width: fit-content. Switching .ui-dialog to
width: fit-content; max-width: min(90vw, 44em) lets each caller size to its
own content naturally: the create-agent form still hits the 44em cap (same
rendered width as before, confirmed via screenshot), ConfirmDialog's
paragraph settles at its own intrinsic width with no extra prop, no second
CSS class, and no second hardcoded number to keep in sync with the first.
2026-09-08 11:08:01 +02:00
iris
4544c458eb swarm-ui: bounded-domain table filters become dropdowns
Table columns whose value only ever comes from a small closed set
(freshness, wanted, hive) get a <select> in their filter-row cell
instead of a free-text input, populated from the distinct values
present in the currently-loaded rows plus an "any" option, matched by
exact equality instead of substring. Free-text columns (name, the
agent's own status message, config PR, hive domain) are unchanged.
2026-09-08 00:49:36 +02:00
iris
6f57b1f57c swarm-ui: one badge+dropdown for wanted state, not multiple buttons
mara, hyperhive#4079: "agent wanted state is multiple buttons insteaf
of a badge with dropdown ... same pattern as agent term badges with
dropdowns". The wanted column used to be a toggle badge plus a
separate quiet destroy badge, stacking under each other in the narrow
column. Replaced both with one WantedMenu badge that opens a Dropdown
with the three explicit states (up/offline/destroy) -- the exact
badge-triggers-a-dropdown shape the per-agent terminals StatusChips
already uses (and swarm-uis own ComponentsPage already demos with
sample data), built from the same shared Badge/Dropdown components.

"up" still declares straight away with no confirmation; "offline" and
"destroy" still go through the existing ConfirmDialog modals unchanged
-- only the trigger moved, the confirm behavior for the two directions
that already had one is untouched.

Explicit dropdown options also fix a real bug the old toggle had:
mara also asked "when no state is declared, i want to set it to
online" -- the old toggle inferred a target as the opposite of
snapshot.running for an undeclared row, so a click on an
undeclared-but-running agent silently declared it offline rather than
making its actual state explicit. The dropdown just lets "up" be
picked directly regardless of any inference, which is what she is
asking for -- flagging this reading explicitly in case an actual
one-time migration (auto-declaring every currently-undeclared agent
up) was intended instead, which this does not do.

Added a shared .ui-dropdown-anchor utility class to Dropdown.css --
this is the third near-identical "position: relative wrapper for a
badge that opens a Dropdown" (after agents own StatusChips.css and
swarm-uis ComponentsPage.css), so a new caller should not reinvent a
fourth copy. Left the two existing ones alone rather than migrating
them as a drive-by.

Verified with a local esbuild build + a throwaway mock /api/agents/status
server, screenshotted headlessly: the wanted column now shows exactly
one badge per row instead of stacked badges.
2026-09-07 22:31:44 +02:00
iris
29c94ea9e6 swarm-ui: disable the wanted-toggle badge on a destroyed row
argus, review: the primary wanted-toggle badge still had no
wanted === "destroyed" guard, so it stayed clickable on a destroyed
row and would PUT {state: "up"} on click -- directly contradicting
the destroy confirm dialogs own "not reversible from here" copy.
Missed this in the previous fix-up (which only addressed the
ConfirmDialog-for-stop ask). Added the guard to both the click handler
and disabled, plus a title explaining why on a destroyed row.
2026-09-07 19:11:50 +02:00
iris
2874badd4c swarm-ui: use ConfirmDialog for the stop confirmation too
mara: "use the new component where we already have confirm dialogs."
The stop/offline direction of the wanted toggle used a native
window.confirm -- the reasoning at the time was that a hand-rolled
Dialog felt heavy for a reversible action, with the real Dialog confirm
reserved for the irreversible destroy direction. Now that
ConfirmDialog is a one-line-per-caller shared component, that argument
no longer buys consistency anything, so both directions confirm the
same way.
2026-09-07 19:07:21 +02:00
iris
82a4324b17 swarm-ui: extract ConfirmDialog, use it for the destroy confirm
mara, reviewing the swarm-ui destroy-trigger PR: "why the extra
styling? shouldnt there be a component that does this already?" There
wasnt one -- Dialog is deliberately content-agnostic (see its own
file-top comment), so the destroy confirm had grown its own
page-scoped .agents-destroy-confirm/-actions CSS for what is really a
generic "message + cancel/confirm button row" shape.

Extracted ui/confirm-dialog/ConfirmDialog.tsx: wraps Dialog, owns the
button row, leaves the message body to the caller via children.
AgentsPage now uses it instead of a bare Dialog + bespoke CSS; deleted
the now-unused AgentsPage.css.
2026-09-07 19:02:52 +02:00
iris
becc025f13 swarm-ui: add a destroy trigger to the agents page
Closes #4067. Backend half (Wanted::Destroyed + reconcile) shipped in
#4065 with no swarm-controller API changes needed -- SetAgentStateRequest
already accepted {"state": "destroyed"}, it just had nothing in swarm-ui
sending it.

Destroy is a separate `quiet`-variant badge next to the existing
start/stop toggle (#3988's "wanted" column), not a third state folded
into that same click target -- one wrong click on a shared toggle
would be irreversible, where a dedicated badge only fires from its own
confirm dialog. That confirm is a real `Dialog`, not the native
`window.confirm` the reversible stop direction uses -- the wanted
column's own comment called this out as the case that would justify
one when it was first written.

Shared the PUT-declare/pending/error/patch-rows logic between the
existing toggleWanted and the new destroyAgent (declareState) rather
than duplicating it -- confirmation and target-state selection are the
only parts that differ between a toggle and a one-way declaration.

Verified: `tsc --noEmit` clean, `nix fmt` reports the expected
formatting-only diff, wire shapes (state string "destroyed",
AgentDeclaration response) checked against swarm-queue-client's
AgentState::as_str and swarm-controller's actual handler rather than
assumed from the issue description.
2026-09-07 18:53:55 +02:00
iris
247ff4498d swarm-ui: sortable + filterable table columns
hyperhive#4020, mara. TableColumn gets two new optional extractors,
each one the whole signal for its capability (mara: 'why separate
selector and flag?' — dropped the sortable/filterable booleans that
would've said the same thing twice and could disagree with the
extractor's presence):

- sortBy: (row) => string | number — column is click-to-sort iff
  present. Table owns the sort state (one active column, header click
  cycles none -> ascending -> descending), since a rendered cell often
  isn't the sortable value itself (e.g. AgentsPage's status column
  renders a Badge, not a plain string).
- filterValue: (row) => string — column is filterable iff present. A
  text input row under the headers, substring match case-insensitive.

Client-side only, no backend change - every consuming page already
fetches its full row set. Wired into AgentsPage and HivesPage, the two
pages with a real per-row Table. Left IssueReportPage alone (already
has its own purpose-built sort + label/hide-blocked filters, predates
this and covers its own domain better than a generic per-column text
filter would) and JobsPage alone (renders an indented state tree via
JobqGraph, no column table at all despite what my original scoping
comment assumed).

Verified: typecheck + build clean, nix fmt clean, static render of the
actual built CSS confirms the new sort-header + filter-row markup
doesn't break table layout.
2026-09-03 01:36:36 +02:00
iris
675fc72e01 swarm-ui: split agent status badge into technical status + free-text message
mara: 'separate technical status from agent provided status string' —
the agents page's status column concatenated the freshness label with
the agent's own free-text status_text into one Badge pill, meant for a
short discrete label, not a full sentence. Long status strings blew
the row out and made the table look messy.

Now two columns: 'status' is just the freshness badge (fresh/stale/
never reported/not in swarm identity) plus relative time, unchanged in
meaning; 'message' is the agent's own status string as plain prose in
its own capped-width, wrapping cell (Table's new cellClass, ui-table-
prose), not a badge. A stopped agent (status_text always null per the
wire contract) shows an em dash there instead of a stale message.

Verified against a static render of the built CSS (real dark theme,
real Badge/table classes, the exact long strings from the screenshot
mara attached) — wraps cleanly within the column instead of
overflowing. Typecheck + build clean.
2026-09-02 20:53:29 +02:00
iris
8d29ebf53c swarm-ui: give hive-reported-but-unknown agents their own status label
An agent whose freshness is "unknown" is a distinct case, not a
fallback: it reported into the KV bucket but isn't in the swarm-identity
roster. The label used to just reuse the enum name ("unknown"), which
doesn't tell an operator what to do about it. Give it a label that
names the actual situation ("not in swarm identity") plus a tooltip
spelling out the fix (register/migrate it), so an operator scanning the
roster during a migration can immediately tell which agents still need
work.

No backend change needed — GET /api/agents/status already emits a row
for every hive-reported agent outside the roster (AgentStatusReader,
unit-tested as an_agent_outside_the_roster_is_surfaced_as_unknown), and
the page already renders every row it gets back.
2026-09-02 20:30:57 +02:00
iris
c9d8628794 swarm-ui: add agent start/stop, backed by the wanted-state route
hyperhive#3896. The backend for start/stop (Up/Offline wanted-state
declarations) already existed and was merged (#3905's writer, the
PUT /api/hives/{hive}/agents/{agent}/state route) — nothing here was
waiting on Paused/Destroyed, which I'd mistakenly conflated with this
issue in an earlier comment (that's #3803, a different feature).

swarm-controller: merges each row's declared wanted state into
GET /api/agents/status, same shape as the config_pr merge (one read
per distinct hive, not per agent, since a declaration is a hive's
whole agent map).

swarm-ui: AgentsPage gets a "wanted" column — clicking the current-
state badge toggles it (Badge's own chip-plus-control shape, same as
its own header comment's pause/resume example), backed by the PUT
route above. A row with no declaration yet reads its implied current
state off the agent's own last-reported running flag. Stop asks for
confirmation (native window.confirm — no confirm-dialog component
exists in swarm-ui yet); start doesn't.
2026-09-02 19:57:04 +02:00
atlas
39b95c2ede treefmt: apply prettier
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.
2026-09-02 15:25:07 +02:00
iris
50650476ad swarm-controller+swarm-ui: merge config-PR into GET /api/agents/status
Per mara's review call on this PR: "the view should be filled by a single
backend call." AgentsPage.tsx was doing three fetches (/api/agents,
/api/config-prs, /api/agents/status) and joining them client-side by name.

Moves the config-PR join server-side instead: AgentStatusRow gains a
config_pr field, populated by get_agents_status's handler from
AppState::config_prs after agent_status::AgentStatusReader::view() returns
- not inside that module, which has no forge client and stays that way (see
the field's doc comment for why the handler is the right layer for this
merge, not the reader).

AgentsPage.tsx now does exactly one fetch and no client-side joining at all
- the wire row is the table row. Dropped the separate AgentStatusRow TS
interface (folded into AgentRow, which now mirrors the backend type
field-for-field) and the /api/agents + /api/config-prs fetches entirely;
neither is needed once /api/agents/status already returns every roster
agent with its config PR attached.

ConfigPrStatus gained Deserialize (previously Serialize-only) since
AgentStatusRow derives both and a struct's derive requires every field to
support it.
2026-09-02 14:18:02 +02:00
iris
00cca0c903 swarm-ui: merge per-agent status into AgentsPage
Continues #3341 item 3, unblocked now that #3568/#3569 (items 1/2)
are merged and GET /api/agents/status is live.

Third fetch alongside the existing roster + config-PR ones, joined
client-side by name same as the config-PR merge. Adds a hive column
and a status column (freshness badge + status_text + relative-time,
same rendering AgentsPage's sibling HivesPage already uses for the
hive-level status endpoint).
2026-09-02 14:18:02 +02:00
iris
c2733d2edf frontend: trim narrated-history comments in packages/agent, packages/swarm-ui
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.).
2026-09-02 08:38:46 +02:00
iris
e395737a25 swarm-ui: widen the shell body for the issue report route
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.
2026-08-31 22:51:07 +02:00
damocles
8a3766c60b issue-report: persist sort/filter state to localStorage 2026-08-31 22:40:13 +02:00
damocles
361121c7f6 issue-report: add transitively-blocks count alongside direct depended-on-by count 2026-08-31 21:51:42 +02:00
iris
6e6bf62437 IssueReportPage: unmount guard on the repos fetch, aria-sort on sortable headers
Two non-blocking notes from review:
- the /api/repos effect now uses the same cancelled guard the
  repo-filter effect already has, so an unmount mid-flight doesn't call
  setRepos/setError on a gone component.
- Table's TableColumn gains an optional ariaSort field, consumed as the
  <th>'s aria-sort attribute; the issue-report page's sortable columns
  now report ascending/descending/none so a screen reader can announce
  which column and direction is active, not just the sighted ▲/▼ glyph.
2026-08-31 19:02:14 +02:00
iris
f4b61db753 IssueReportPage: match the corrected row shape, assignees is a list
damocles caught it on the swarm-controller PR: forge's assignee field
is legacy single-value, assignees is the real multi-assignee list, and
this repo actually uses multiple. Widen the frontend's row type +
column to match -- rendered comma-joined the same way labels already
are.
2026-08-31 19:02:14 +02:00
iris
e815c7cb5c swarm-ui: add the custom issue-report page
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.
2026-08-31 19:02:14 +02:00
iris
b1254f89cd shared dropdown: don't reopen when the trigger is clicked while open
mara, #3775: clicking a badge with its own dropdown open reopens it
instead of closing it. Root cause: the outside-click listener only
excludes the dropdown's own ref, not the sibling trigger that opened
it — a click on the trigger closes via that listener (pointerdown
fires first), then the trigger's own onClick toggle fires straight
after and reopens it, since its closure reads the pre-close state.

Dropdown now takes an optional anchorRef (the trigger's wrapper,
which callers already have for CSS positioning) and excludes it from
the outside-click check too — the same shape MetaNav's own hand-rolled
popover already uses correctly. Wired into StatusChips's Picker +
StatusMenu and swarm-ui's ComponentsPage demo (the only three Dropdown
consumers).

Verified two ways: reverted the fix, rebuilt, confirmed the bug
reproduces via a raw-CDP interaction test (two real clicks dispatched
through headless chromium, not just a static screenshot); restored
the fix, rebuilt, confirmed it passes.
2026-08-30 15:29:59 +02:00
iris
e1aae29068 badge: add a quiet variant for icon-only header triggers
Badge's default look is a filled pill, right for status/picker chips
but wrong for an icon-only header button like the settings gear or
agent links trigger, which should read as chrome. Add variant='quiet'
(same naming/shape as Button's ButtonVariant) that drops the idle
background, keeping the existing hover/expanded feedback.

Apply it to the two icon-only Badge triggers: shared SettingsMenu
(used by both swarm-ui and the agent page) and the agent page's own
MetaNav links trigger. Add a components-page showcase sample so the
variant has a visible regression check going forward.
2026-08-29 20:28:19 +02:00
iris
4175ae86e3 icons: restore inline-SVG link/gear icons, share them across all three pages
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.
2026-08-29 11:54:21 +02:00
iris
9720bdfad0 settings: move the theme/motion panel into @hive/shared, add motion to agent
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.
2026-08-29 11:11:39 +02:00
iris
638e1ac2c6 swarm-ui: delete StatusChip, migrate its 3 callers to shared Badge
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.
2026-08-28 13:21:49 +02:00
iris
62cc0620c8 Add Badge + Dropdown Preact components, demo on swarm-ui /components
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.
2026-08-28 13:21:49 +02:00
iris
2e43205fc3 swarm-ui: add 5s refresh interval option
mara: add a 5s option to the refresh cadence picker.
2026-08-24 18:39:33 +02:00
iris
6a01850096 swarm-ui: fix inconsistent header icons + body edge gap
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.
2026-08-24 18:38:12 +02:00
iris
2c8d37d5b8 swarm-ui: animate the three header popovers on open
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.
2026-08-24 15:50:48 +02:00
iris
7aec2e339f swarm-ui: authelia settings link should go to /settings, not the domain root
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).
2026-08-24 12:31:07 +02:00
iris
46d1271603 swarm-ui: agents page — explainer message when the roster is empty
Table gains an optional emptyMessage prop, rendered as a single
full-width row in place of a bare empty tbody; AgentsPage is the first
consumer. Screenshot-verified against a mock server returning an empty
roster.
2026-08-24 11:26:43 +02:00
iris
f1ab7cd43d fixup: Dialog overflows a phone-width viewport (mara)
No sitewide box-sizing: border-box reset exists, so .ui-dialog's
padding + border were adding on top of width: 90vw instead of being
carved out of it -- 401px rendered against a 390px viewport, real
horizontal overflow. Caught via a real 390x844 screenshot per mara's
"phone size?" ask on the PR, not assumed.
2026-08-24 00:44:16 +02:00
iris
4fd6480c9c fixup: update stale CreateAgentPage cross-references (argus review)
Three doc comments elsewhere in the tree still named the old
CreateAgentPage identifier/page framing after the rename in this
branch -- TextField.tsx, Panel.tsx, FormField.css (x3). None of these
files are touched by the rest of the diff, which is exactly how the
staleness happened.
2026-08-24 00:34:34 +02:00
iris
13d7c73c12 swarm-ui: move agent creation into the agents page as a dialog
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.
2026-08-24 00:28:34 +02:00
iris
dabd0fc823 swarm-ui: header profile menu — initials avatar, authelia settings + logout (#3570)
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.
2026-08-24 00:01:30 +02:00
iris
0f52cbe40a swarm-ui: agent roster page with per-agent config-PR status
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.
2026-08-23 22:53:32 +02:00
iris
6d9a2fa9f6 swarm-ui: fix underline not showing on the initial page load
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.
2026-08-19 19:54:52 +02:00
iris
daabb4fe6a swarm-ui: brand text follows the nav accent too
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.
2026-08-19 19:54:52 +02:00
iris
a5584d0fcd swarm-ui: plain fade + multi-hop underline sweep, per review
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.
2026-08-19 19:54:52 +02:00
iris
6be1bbd4f7 swarm-ui: page-switch animation (pop entrance + nav underline travel)
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.
2026-08-19 19:54:52 +02:00
iris
213e0a1b89 swarm-ui: jobs page auto-refresh
Reuses HivesPage's exact RefreshIntervalPicker/useRefreshInterval
pattern (30s default, off/10s/30s/1m presets, pauses while the tab is
backgrounded). Ticks a refreshToken bump rather than doing its own
fetch — JobqGraph/JobqRollup already accept that prop as their
documented refetch lever, from the dashboard's rebuild-queue view.
2026-08-19 19:33:20 +02:00
iris
b25aa157df swarm-ui: give Panel an optional header icon slot
Adds an `icon` prop to Panel (small emoji glyph left of the title,
aria-hidden, chosen per panel with no default) and wires it into every
current Panel caller: hives (bee), create-agent's form (robot) and info
panel (identity card, moved off the info panel's body copy where it
started as a one-off), jobs (puzzle piece), the components gallery
itself (toolbox), and the 404 page (compass). Adds a components-page
demo section and a whimsy-section pointer in the design guide.

Closes: #3508
2026-08-19 17:22:48 +02:00
iris
1207a0e282 swarm-ui: create-agent layout fills the page width, centered
mara: the two cards were neither centered nor filling the space in
two-column mode. Dropped the page's own max-width entirely — Panel
already has no width opinion, and once both cards are equal-width
flex children there's no reason for an extra cap between them and
.shell-body's own 60em/centered column. Removed the now-pointless
wrapper div along with it.

Checked centered + filling behaviour at both a normal (1200px) and an
ultra-wide (1600px) viewport, and narrow-viewport stacking still
works.
2026-08-19 17:11:59 +02:00
iris
3b339b2232 swarm-ui: equal-width create-agent cards, full-width inputs
mara's follow-up: both cards should be the same size (flex: 1 1 0
instead of the form keeping its own narrower basis), and the form's
inputs should fill the card rather than stopping at their old 16em
cap. Dropped that cap from the shared form kit (.ui-form-control /
.ui-form-field) entirely rather than overriding it per-page — the kit
itself has no width opinion now, same as Panel already has none; a
page that wants a narrower field caps its layout, not the kit.

Checked ComponentsPage's standalone samples (no regression, just
wider) and narrow-viewport stacking (still moves the explanation
under the form).
2026-08-19 17:11:59 +02:00
iris
f30897b44e swarm-ui: create-agent info panel reads as the finished flow
mara's review: the info-panel copy should describe the intended end
state (a running agent on the chosen hive), not narrate today's
partial implementation (deploy isn't wired up server-side yet — noted
in this file's existing top comment for maintainers, kept separate
from the user-facing copy).
2026-08-19 17:11:59 +02:00