From 675fc72e01ed88a0e2962b305ca02a0cef9de13d Mon Sep 17 00:00:00 2001 From: iris Date: Wed, 2 Sep 2026 20:53:11 +0200 Subject: [PATCH] swarm-ui: split agent status badge into technical status + free-text message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../swarm-ui/src/pages/AgentsPage.tsx | 22 ++++++++++++++----- .../packages/swarm-ui/src/ui/table/Table.css | 9 ++++++++ .../packages/swarm-ui/src/ui/table/Table.tsx | 8 ++++++- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx b/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx index 63128fad..454d569f 100644 --- a/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx @@ -212,13 +212,13 @@ export function AgentsPage() { { key: "status", header: "status", + // Technical freshness only — the agent's own free-text status + // string used to be concatenated into this same badge, which + // stuffed a full sentence into a pill meant for a short discrete + // label and blew the row out (mara: "looks messy"). That string + // now lives in its own "message" column below. render: (a) => { const { tone, label } = FRESHNESS[a.freshness]; - const text = a.snapshot?.status_text; - // A `running: false` snapshot always carries `status_text: null` - // (the wire contract's own rule, not something this page derives), - // so an agent that reported recently but isn't running still shows - // a bare freshness badge rather than a stale status string. return ( - {text ? `${text} — ` : ""} {label} {a.last_seen_unix !== null ? ( <> @@ -243,6 +242,17 @@ export function AgentsPage() { ); }, }, + { + key: "message", + header: "message", + cellClass: "ui-table-prose", + // Plain text, not a Badge — the agent's own free-form status + // sentence, as distinct from the technical freshness state in the + // "status" column. A `running: false` snapshot always carries + // `status_text: null` (the wire contract's own rule), so a stopped + // agent just shows an em dash here rather than a stale message. + render: (a) => a.snapshot?.status_text ?? "—", + }, { key: "wanted", header: "wanted", diff --git a/frontend/packages/swarm-ui/src/ui/table/Table.css b/frontend/packages/swarm-ui/src/ui/table/Table.css index cec0a17c..258cc238 100644 --- a/frontend/packages/swarm-ui/src/ui/table/Table.css +++ b/frontend/packages/swarm-ui/src/ui/table/Table.css @@ -26,3 +26,12 @@ text-align: center; padding: 1.25em 0.75em; } + +/* A cell holding free-form prose (an operator- or agent-provided string, + not a short discrete label) rather than open-ended growth that drags + the whole table wider — caps the column and wraps within it instead. */ +.ui-table-prose { + max-width: 32em; + white-space: normal; + word-break: break-word; +} diff --git a/frontend/packages/swarm-ui/src/ui/table/Table.tsx b/frontend/packages/swarm-ui/src/ui/table/Table.tsx index 2f6d0e44..3655c611 100644 --- a/frontend/packages/swarm-ui/src/ui/table/Table.tsx +++ b/frontend/packages/swarm-ui/src/ui/table/Table.tsx @@ -20,6 +20,10 @@ export interface TableColumn { // can pass real markup instead of forking a second table primitive. header: ComponentChildren; render: (row: T) => ComponentChildren; + /** Extra class on this column's `` — e.g. `ui-table-prose` for a + * free-text cell that should cap its width and wrap instead of + * dragging the whole table wider. Omit for the default cell styling. */ + cellClass?: string; /** * ARIA sort state for this column's `` — `'ascending'` / * `'descending'` while this is the active sort column, `'none'` while @@ -72,7 +76,9 @@ export function Table({ rows.map((row) => ( {columns.map((c) => ( - {c.render(row)} + + {c.render(row)} + ))} ))