diff --git a/frontend/packages/swarm-ui/src/pages/AgentsPage.css b/frontend/packages/swarm-ui/src/pages/AgentsPage.css new file mode 100644 index 00000000..38e7f78f --- /dev/null +++ b/frontend/packages/swarm-ui/src/pages/AgentsPage.css @@ -0,0 +1,74 @@ +/* list — a vertical stack of per-agent cards replacing the + old table body (mara: "more like card per agent"). Full-width rows, + not a grid: the second line (the agent's free-form status message) + reads better wrapping the card's own width than squeezed into a fixed + grid cell. */ +.ui-agent-card-list { + display: flex; + flex-direction: column; + gap: 0.5em; +} +.ui-agent-card-list-empty { + color: var(--muted); + text-align: center; + padding: 1.25em 0.75em; +} +.ui-agent-card { + display: flex; + flex-direction: column; + gap: 0.35em; + padding: 0.75em 1em; + border: 1px solid var(--border); + border-radius: 0.5em; + background: var(--bg-elev); + cursor: pointer; +} +.ui-agent-card:hover, +.ui-agent-card:focus-visible { + border-color: var(--purple); + outline: none; +} +.ui-agent-card-line1 { + display: flex; + align-items: center; + gap: 0.6em; +} +.ui-agent-card-name { + font-weight: 600; +} +/* Pushes the wanted-state control to the row's trailing edge regardless + of how wide the status badge next to it ends up. */ +.ui-agent-card-wanted { + display: inline-flex; + align-items: center; + gap: 0.4em; + margin-left: auto; +} +.ui-agent-card-message { + color: var(--muted); + white-space: normal; + word-break: break-word; +} + +/* Detail panel (`Dialog` body) — the fields `AgentCard`'s main view + doesn't show. */ +.ui-agent-detail-name { + margin: 0 0 0.75em; +} +.ui-agent-detail-fields { + display: grid; + grid-template-columns: auto 1fr; + gap: 0.4em 1em; + margin: 0 0 1em; +} +.ui-agent-detail-fields dt { + color: var(--muted); +} +.ui-agent-detail-fields dd { + margin: 0; +} +.ui-agent-detail-actions { + display: flex; + align-items: center; + gap: 0.75em; +} diff --git a/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx b/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx index 5d42ada8..24309aa3 100644 --- a/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx @@ -17,13 +17,14 @@ // `get_agents_status` handler for where `config_pr`/`wanted` get merged // in and why that's the handler's job rather than // `agent_status::AgentStatusReader`'s. No more per-page joining left to -// do here: the wire row *is* the table row. +// do here: the wire row *is* the row this page renders. // // Owns the "+ agent" trigger too — the roster this populates is the -// natural home for the action that populates it; a separate top-level -// nav entry would be one click of indirection for no benefit. The form -// itself (`CreateAgentForm`) mounts inside a `Dialog` here rather than -// its own route. +// natural home for the action that populates it. The form itself +// (`CreateAgentForm`) mounts inside a `Dialog` here, not its own route. +// +// Renders as one `AgentCard` per row, not a `Table` — see that +// component's own comment below for the card/detail-panel split. // // The "wanted" column is one `WantedMenu` badge+dropdown per row — see // that component's own comment above `AgentsPage` for why. @@ -44,9 +45,9 @@ import { useRefreshInterval, type RefreshIntervalMs, } from "../ui/refresh-interval/RefreshInterval.js"; -import { Table, type TableColumn } from "../ui/table/Table.js"; import { CreateAgentForm } from "./CreateAgentForm.js"; import { LinkMatrixAccountForm } from "./LinkMatrixAccountForm.js"; +import "./AgentsPage.css"; interface ConfigPrStatus { pr_number: number; @@ -69,8 +70,9 @@ interface AgentStatusSnapshot { type Wanted = string | null; // Mirrors `agent_status::AgentStatusRow` field-for-field — this *is* the -// table row now, not a shape assembled from it, so there's no separate -// join-result type to keep in sync with the wire contract by hand. +// row this page renders, not a shape assembled from it, so there's no +// separate join-result type to keep in sync with the wire contract by +// hand. interface AgentRow { name: string; hive: string | null; @@ -165,13 +167,24 @@ function WantedMenu({ onSelectOffline, onSelectPaused, onDestroy, + showDestroy = true, }: { row: AgentRow; pending: boolean; onSelectUp: (row: AgentRow) => void; onSelectOffline: (row: AgentRow) => void; onSelectPaused: (row: AgentRow) => void; - onDestroy: (row: AgentRow) => void; + onDestroy?: (row: AgentRow) => void; + /** + * The card view (see `AgentCard` below) keeps this menu to the + * three everyday states and gives "destroy" — the rare, one-way + * one — its own button in the detail panel instead, so it can't be + * reached by an extra click off a state that's already open (mara, + * scoping the card/detail split: destroy belongs with the "rarely + * used actions" bucket, not next to up/paused/offline). The + * `WantedMenu` inside that detail panel still gets the full set. + */ + showDestroy?: boolean; }) { const [open, setOpen] = useState(false); const anchorRef = useRef(null); @@ -188,7 +201,9 @@ function WantedMenu({ { value: "up", label: "up" }, { value: "paused", label: "paused" }, { value: "offline", label: "offline" }, - { value: "destroy", label: "destroy", danger: true }, + ...(showDestroy + ? [{ value: "destroy", label: "destroy", danger: true }] + : []), ]; return ( @@ -222,7 +237,7 @@ function WantedMenu({ if (value === "up") onSelectUp(row); else if (value === "offline") onSelectOffline(row); else if (value === "paused") onSelectPaused(row); - else onDestroy(row); + else onDestroy?.(row); }} onClose={() => setOpen(false)} anchorRef={anchorRef} @@ -231,6 +246,101 @@ function WantedMenu({ ); } +// One card per roster agent (mara: "main view: name, status, message, +// wanted" / "message as second line" / "more like card per agent", +// replacing the old table row). Everything else the old table's other +// columns carried (hive, matrix link-account, config-PR link, destroy) +// moved to the detail panel `AgentsPage` opens on card click — a plain +// `Dialog`, not the shared `hive-side-panel` slide-in drawer: that's a +// shadow-DOM custom element, and swarm-ui's esbuild config can't consume +// those at all yet (same already-tracked gap `Dialog.tsx`'s own comment +// notes for `hive-dialog`). Revisit once that gap closes. The old +// per-column sort/filter has no replacement yet in this view. +// +// A `role="button"` div, not a real `