mara, PR review: "make agentspage a subdir now that its split into
sub components". AgentsPage.tsx/.css, AgentCard.tsx/.css,
AgentTypes.ts, and WantedMenu.tsx move as a family into their own
pages/agents/ directory; CreateAgentForm and LinkMatrixAccountForm
stay in pages/ since they aren't part of this split (CreateAgentForm
is still rendered inside AgentsPage's own dialog but is a standalone,
independently-named form, not one of the pieces carved out of the
page itself).
Pure move: relative imports within the new pages/agents/ family are
unchanged (they were always siblings), only the ones reaching back
out to ui/ and the two forms above gained one more '../', plus
App.tsx's route import.
Three more asks from the same review thread:
- "agentspage is now giant and deserves a split" - AgentsPage.tsx was
1047 lines. Split into AgentTypes.ts (AgentRow and friends),
WantedMenu.tsx, AgentCard.tsx (+ its own CSS), leaving AgentsPage.tsx
as state/actions/columns/the render tree - 649 lines, and every piece
it composes is now independently readable.
- "what about the component that represents filtered data ... that the
card view and table can both use?" - extracted FilterableView
(ui/filterable-view/): takes columns/rows/rowKey/storageKey/view/
renderCard, builds its filter bar from *every* filterable column (not
a hand-picked subset - the old AgentFilterBar only showed 4 of the
agent columns' 6 filterable fields, an accidental gap the table's own
popovers didn't have), and renders either the card list or Table.
AgentsPage now just tells it which view to show; the view toggle
itself stays page-side since it's Panel-header chrome, not filtering.
Disclosed side effect: card view's filter bar now also covers
message/config-PR (text filters), matching table view exactly instead
of a narrower subset.
- CSS audit: AgentsPage.css now holds only what's genuinely page-specific
(the view toggle, the detail-panel field grid) - everything else moved
to its owning component's own colocated CSS.
FilterableView gets a /components demo (view toggle + filter bar + both
render modes, same day per the design guide). Verified: AgentsPage
still renders the same (real screenshot), and the demo's own table
toggle produces a real Table with the same rows.
Mara: "result looks like the shape i am looking for, but the code does
not. you did not follow component first principle" - the card's
clickable/selectable mechanics, the card-view filter trigger+popover,
and the list+detail split layout were all one-off page-local JSX in
AgentsPage.tsx instead of docs/web-ui/design-guide.md's "Component-first
design" primitives. Three new ui/ components, each with a same-day
/components demo section per that doc's own rule:
- ui/card/Card.tsx - the role=button/keyboard-activation/selected
mechanics AgentCard now wraps agent-specific content around, instead
of owning them itself.
- ui/multiselect-filter/MultiselectFilter.tsx - the checkbox-list
trigger+popover control. This was also a straight duplicate of
Table's own inline popover content once AgentsPage's filter toolbar
needed the identical thing; Table now renders the same
MultiselectFilterOptions piece too (keeping its own th-anchored
trigger and fixed+portal positioning, which are genuinely
table-specific), not a second copy.
- ui/split-view/SplitView.tsx - the list+detail flex-wrap layout, no
opinion on what's inside either pane.
AgentsPage.tsx's own CSS shrinks to just the agent-specific content
inside these primitives (card line/message layout, detail-panel field
grid, the name-search input) - the container/positioning rules moved
to each component's own colocated CSS.
No behavior change for any other Table caller (HivesPage,
IssueReportPage, the components demo's own Table samples) - the
popover's visual output is identical, just sourced from the shared
component instead of inline JSX.
Verified: typecheck/build clean, real screenshots of both AgentsPage
(pixel-identical to before) and the three new /components sections,
plus a live click confirming MultiselectFilter's popover opens
correctly on the demo page too.
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.
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.
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.
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.
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.
mara: the route should reflect creating an agent, and stay separate
from a future agent list page. Renamed AgentsPage -> CreateAgentPage
(file, component, css classes) and moved the route from /agents to
/create-agent - flat, not /agents/new, since index.html's relative
asset links only resolve correctly one path segment deep (filed
separately as a real bug, not fixed here). Leaves the bare /agents
path free for a future roster page.
Adds /agents: a name field that POSTs to swarm-controller's
POST /api/agents (from the CreateIdentity work), shows the queued
job node id, and links to /jobs to watch it settle. Scope matches
the issue exactly - no forge/deploy options, those aren't wired
server-side yet.