Watch
0
0
Fork
You've already forked hyperhive
0
Commit graph hyperhive/frontend/packages/swarm-ui/src/pages
Author SHA1 Message Date
iris
60d7e8ce84 swarm-ui/agents: shared filters across views, real second panel for detail
Two more asks from mara's live review:

- "i want the same filters for the cards tho, thats why i suggested
  separating data and filter from view" - extracted Table's filter
  *state* (not its popover UI, which stays table-shaped) into a new
  exported useTableFilters hook. Table calls it internally, unchanged
  behavior for every existing caller. AgentsPage now calls the same hook
  with the same storageKey, so card view and table view read/write one
  shared filter state instead of each having their own (or cards having
  none at all). Card view gets its own toolbar (AgentFilterBar) - a
  name search input plus one FilterMultiselect per multiselect column,
  same checkbox-list markup Table's own popover uses, driving the same
  state. Switching the view toggle no longer loses or hides whatever's
  filtered.

- "why no separate panel? i mean a second panel on agent page" - replaced
  the modal Dialog with a real second Panel, always mounted (empty state
  when nothing's selected, so selecting an agent never shifts the page's
  own layout). Panel gained an optional `class` prop so the two panels
  can flex-size themselves in a row. List/detail panels sit side by side
  in a flex-wrap row that stacks on a narrow viewport - content-driven,
  same approach the shell's own nav uses, not a second hardcoded
  breakpoint. Selected card gets a highlight so it's clear which one the
  detail panel is showing.

Verified with real CDP clicks: split layout with nothing selected,
selecting a card highlights it and populates the detail panel, opening
a card-view filter and checking a value narrows both the card list AND
(after switching the toggle) the table to the identical row set.
2026-09-11 22:30:41 +02:00
iris
8786f24111 swarm-ui/agents: detail panel shows full agent state, fix dropdown-behind-dialog bug
Three things from mara's PR review + argus's:

- "the info from main list should be included in the agent view" - detail
  panel now repeats status/message/wanted alongside the panel-only fields
  (hive, config PR, matrix link), not just the leftovers.
- "destroy is already available via wanted state" - dropped the standalone
  "destroy agent" button; the detail panel's wanted field is a real
  WantedMenu (default showDestroy) instead, same control as the card/table,
  just with the fourth option back. Also resolves argus's stale-doc-comment
  finding (the comment described a second WantedMenu call site that didn't
  exist yet - now it does).
- Rebased onto main to pick up the just-merged dropdown-portal-clip fix -
  this branch was cut before that merged, so it had silently regressed
  back to the pre-fix Dropdown the whole time.

That rebase surfaced a real bug of its own, likely "the third screenshot
shows a layout bug": WantedMenu always passed `portal` to Dropdown, and a
portaled (position: fixed, body-appended) dropdown renders *behind* an
open native <dialog> - the dialog is promoted to the browser's top layer,
which composites above ordinary body content regardless of z-index. Only
the sliver of the dropdown extending past the dialog's own edge was
visible. WantedMenu's `portal` is now its own prop, opt-in, true only at
the table's call site (the one with an actual clipping ancestor to escape)
- card and detail-panel call sites render it as a plain child instead,
which is both correct inside the dialog and one fewer moving part where
it isn't needed.

Verified with real CDP clicks: detail panel shows all fields, and its
wanted dropdown now renders in the right place with all four options
visible instead of mostly hidden behind the dialog.
2026-09-11 22:30:41 +02:00
iris
1e6156d116 swarm-ui/agents: bring back the table view (and its filters) as a toggle
Mara's follow-up on #4257, right after the card-only version went up:
"still want the filters tho, maybe split the data component from the
view" / "if we split data component and view, we can make it switchable
between table and specialized card view."

AgentsPage already had its data/actions (rows, declareState, the
dialog-target state) separated from rendering by this point - the only
missing piece was a second renderer. Restores the original Table +
columns array as the "table" viewMode alongside the new card view,
adds a small two-button toggle in the Panel's actions row, and persists
the choice to localStorage (same pattern as Table's own per-column
filter persistence) so a reload keeps the last choice. Cards stays the
default.

Verified with a real CDP click switching to table view: columns,
sort arrows, and filter icons are all back.
2026-09-11 22:30:41 +02:00
iris
26b0b0d173 swarm-ui/agents: per-agent card view + detail panel, split from the roster table
Replaces AgentsPage's Table-rendered roster with one AgentCard per
agent: name/status/wanted on the first line, the free-form status
message on the second (mara, scoping #4257: "main view: name, status,
message, wanted" / "message as second line" / "more like card per
agent").

Everything the old table's other columns carried (hive, matrix
link-account, config-PR link, destroy) moves into a detail panel that
opens on card click, reusing the existing Dialog modal rather than a
new docked/slideover primitive - the shared hive-side-panel drawer is a
shadow-DOM custom element swarm-ui's esbuild config can't consume yet
(same gap Dialog.tsx's own comment already flags for hive-dialog).

WantedMenu gains a showDestroy flag: the card's own menu keeps the
three everyday states, destroy gets its own button in the detail panel
instead of a fourth dropdown entry next to states someone reaches for
often.

Known regression, flagged for follow-up rather than silently dropped:
the old table's per-column sort/filter has no replacement in this view
yet.

Backend list/detail endpoint split (also requested in #4257) is
deliberately left for a follow-up PR - it's an orthogonal optimization,
not required for this interaction to work correctly against the
existing single /api/agents/status response.
2026-09-11 22:30:41 +02:00
iris
613ca541e1 shared/dropdown: optional portal mode escapes a scroll-container clip
A `WantedMenu` badge dropdown on the agents table clips against
`.ui-table-scroll`s overflow the moment its row is the last (or
near-last) one — the popover extends past the table content the
scroll container bounds itself to. `Table.tsx` already solved the
identical clip for its own column-filter popover with a
position:fixed + portal computed from the anchor rect; `Dropdown` now
takes an optional `portal` prop that opts a caller into that same
recipe instead of a second hand-rolled copy of it. Off by default —
every other current caller (StatusChips x2, the components-page demo)
keeps its existing non-portal behavior unchanged.
2026-09-11 21:20:26 +02:00
iris
513554fe9a swarm: add a declared "paused" agent wanted state
mara (#4170): swarm-ui's wanted-state dropdown could only ever declare
up/offline/destroy, with no way to swarm-declare the existing hive-local
turn-loop pause (`hivectl agent pause|resume`).

`AgentState::Paused` is not a fifth peer of Up/Offline/Destroyed on the
power axis this enum otherwise answers — it's Up plus an orthogonal
turn-loop pause. `hive-c0re`'s `workers::wanted` reconcile loop now
decides the two axes independently (`decide` for power, the new
`decide_pause` for the marker), so a stopped agent declared Paused
converges with both a Start and a Pause in the same pass.

Known, deliberate limitation: a Paused declaration on an agent this
hive has never deployed only reaches Deploy this pass — writing the
pause marker into a harness dir that may not exist yet was judged not
worth the risk, so it converges on the next pass once the agent is
present instead.

swarm-ui's WantedMenu gains a fourth "paused" option (warning-tone
badge). No separate "resume" entry — selecting "up" from a paused row
already clears the marker via the same decide_pause path.

Pause/resume marker writes go through one shared
Coordinator::set_paused_by_name helper, used by both the interactive
dashboard pause/resume handlers and this reconcile loop, instead of
each duplicating the parse-name/write-marker/track-rescan shape.
swarm-ui's "offline" and "paused" confirm dialogs share one
confirmTarget state and one ConfirmDialog instead of two near-identical
copies.

Closes #4170
2026-09-11 01:35:40 +02:00
iris
61f60d0039 swarm-ui: add Dialog/ConfirmDialog sections to the components page
mara (#4172): the components page was out of date against its own
stated rule ("a new primitive gets a section here the same day it's
added"). Dialog and ConfirmDialog are real standalone exported
primitives (AgentsPage uses both directly) with no demo section.

FormField deliberately stays excluded — its own comment already says
it's not a reachable primitive, just TextField/SelectField's shared
internal wrapper, so it was never meant to get one.

Also gave the Table demo's "detail" column a multiselect filter, so
the checkbox popover and reset-filters button — real, daily-used Table
behavior — are actually shown rather than an unfiltered grid.
2026-09-11 01:17:05 +02:00
iris
74cb0c2793 swarm-ui: use multiselect for every enum-shaped table filter 2026-09-10 21:09:09 +02:00
iris
915c6c6f92 swarm-ui: persist table filters, add reset button, multi-select, negate
Per mara's issue: tables should remember their filters (with a reset
button), the labels filter should be multi-select, and any filter should
support negation (search vs exclude).

All in the shared Table component (ui/table/Table.tsx), used by
AgentsPage/HivesPage/IssueReportPage:

- filters now persist via the same useLocalSetting hook IssueReportPage
  already used for its own state, keyed by a new required storageKey
  prop (required, not optional, so no caller can forget it and every
  table gets persistence for free)
- a small 'reset filters' button clears every column's filter at once,
  shown only when at least one is active
- new filterMode: "multiselect" (+ a filterValues extractor, alongside
  the existing single-value filterValue) renders a checkbox list and
  matches on any overlap - IssueReportPage's own bespoke label-checkbox
  sidebar is folded into this instead of staying a second, separate
  filter mechanism
- a negate toggle ('exclude') sits under every filter mode's control,
  applying uniformly to text/select/multiselect

Verified: tsc --noEmit and the esbuild bundle both clean.
2026-09-09 23:31:24 +02:00
iris
52446cb273 matrix accounts: server-side password login for swarm-controller
Extends PutMatrixAccountRequest with a mode field (token, the existing
behavior and default; or password). Password mode has swarm-controller
itself perform m.login.password against the caller-given homeserver
(mirrors hive-c0re's own /api/matrix-account-login for the hive-local
case) and stores the resulting token instead of a caller-supplied one
-- the password is used once, over this PUT, and never stored. Also
adds the 'main is reserved' guard hive-c0re's login form already has,
which swarm-controller had no equivalent of before this.

swarm-ui's link-matrix-account form gets a credential-mode toggle
wired to the same contract: token mode is unchanged, password mode
swaps the token field for user-id + password fields and makes
homeserver required (no hive-side fallback to resolve it against, per
PutMatrixAccountRequest::homeserver's own doc).

Per #4122.
2026-09-09 01:36:52 +02:00
iris
85d0b8de5d swarm-ui: stop suggesting the reserved default account as an example name
The link-matrix-account form's placeholder read 'e.g. primary', which
sounds like it names the hive-provided default account. That account is
actually named main (reserved in hyperhive.matrixAccounts's schema) and
this route can neither create nor touch it. Swapped to an example with
no relationship to the real default, per #4122.
2026-09-09 00:30:27 +02:00
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
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
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
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
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
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
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
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
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
iris
fc65811d9f swarm-ui: fill create-agent's empty column with an explanatory panel
mara's follow-up on the field-alignment fix: narrowing the form panel
left a lot of bare space next to it, and asked for something that
fills it while helping a new user understand what the page does.

Adds a second panel beside the form (stacks under it on a narrow
viewport) explaining the job chain '/api/agents' actually queues:
an authelia identity, then a forge config repo — no container exists
yet after this page, and deploying one onto the chosen hive is a
separate step that isn't wired up server-side.
2026-08-19 17:11:59 +02:00
iris
f14fc154cc swarm-ui: fix create-agent form field alignment and panel width
TextField and SelectField's shared FormField wrapper had no width of
its own, so inside the form's shrink-to-fit flex column each field's
input/select resolved its 'width: 100%' against its own shrunk
wrapper rather than a shared column width — two fields with
differently-long labels ended up with differently-wide controls.
FormField now caps its own width the same way the control already
does, so every field in a form lines up regardless of label length.

Also wrapped the page in a max-width container: Panel has no width
opinion of its own, so it filled the full page column, leaving a lot
of bare panel to the right of the ~16em-wide form.
2026-08-19 17:11:59 +02:00
iris
fc44891ab5 swarm-ui: compact refresh-interval picker, moved into the panel header
Per mara's review on this PR: the labelled SelectField-based picker
read as way too heavy for a passive-until-touched setting - a full
label+bordered-control form field row above the table. Replace it with
a compact inline "clock icon - value - chevron" control (a native
<select> still drives the interaction, just stripped of SelectField/
FormField's chrome), quiet until hovered/focused the same way
LinksMenu's header button is.

Also move it out of the panel body entirely: Panel gains an `actions`
slot in its title row (right-aligned via margin-left: auto), so the
picker sits next to the "hives" heading instead of taking its own row
and pushing the table down - per the design guide's own "a control
belongs next to the thing it affects" rule.

Verified with headless chromium screenshots (full page + a tight
close-up crop) against the built bundle: the picker now reads as
"(clock) 30s (chevron)" inline with the panel title, no extra vertical
space taken from the table. Added a Panel "with actions" demo to
ComponentsPage.
2026-08-18 23:28:47 +02:00
iris
e270ba309b swarm-ui: shared refresh-interval polling control
Closes #3446.

New ui/refresh-interval/RefreshInterval — a RefreshIntervalPicker
(off/10s/30s/1m preset select) plus a useRefreshInterval hook that
polls on that cadence, pausing while the document is hidden and
resyncing immediately on becoming visible again (same pattern
RelativeTime already uses). The hook keeps the caller's onTick fresh
via a ref rather than an effect dependency, so a new closure each
render doesn't re-arm the timer.

HivesPage wires it in: defaults to 30s (no inputs on this page to
interrupt, and the point of the feature is not needing a manual
reload), replacing the old fetch-once-at-mount effect. A successful
refresh also clears any previous fetch error instead of leaving a
stale failure on screen after the data's recovered.

Verified: tsc clean, build succeeds, screenshotted the hives page
(auto-loads on mount, picker defaults to 30s) and the components page
demo.
2026-08-18 23:28:47 +02:00
iris
a5ef31ed8b swarm-ui: self-updating RelativeTime component
Closes #3445.

New ui/relative-time/RelativeTime — takes a UTC epoch-ms instant, not
a precomputed age, and re-renders itself on a 1s interval so a tab
left open doesn't silently show a frozen 'fresh (5s ago)' hours
later. Pauses while the document is hidden, resyncs immediately on
becoming visible again.

StatusChip's label widened from string to ComponentChildren so a
chip can embed the live ticker (a plain string couldn't carry it).
HivesPage's freshness column now derives the age from last_seen_unix
client-side via RelativeTime instead of rendering the once-computed
age_seconds from the API response.

Verified: tsc clean, build succeeds, screenshotted the components
page (ticking observed advancing within one virtual-time-budget
window) and the hives page (fresh/stale/never-reported all render
correctly).
2026-08-18 22:12:52 +02:00
iris
537d6f9dac swarm-ui: rename OverviewPage/'overview' to HivesPage/'hives'
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).
2026-08-18 21:26:52 +02:00