From 0547081138e065c8faff5c096466adf25ff5eb59 Mon Sep 17 00:00:00 2001 From: iris Date: Tue, 10 Nov 2026 18:51:45 +0100 Subject: [PATCH] swarm ui: the agent's status message shows how long ago it was set The card and its detail showed the message alone, so a message from an hour ago read like the current one. Both now add the age, (5m ago), which ticks like the other relative times. An agent with no message still shows a dash. --- docs/2-handbook/swarm/ui.md | 2 +- .../swarm-ui/src/agent/detail/AgentDetail.tsx | 5 ++- .../src/agent/detail/StatusMessage.tsx | 26 +++++++++++++++ .../src/agent/detail/statusMessage.ts | 20 ++++++++++++ .../swarm-ui/src/agent/roster/AgentCard.tsx | 3 +- .../swarm-ui/test/status-message.test.mjs | 32 +++++++++++++++++++ 6 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 frontend/packages/swarm-ui/src/agent/detail/StatusMessage.tsx create mode 100644 frontend/packages/swarm-ui/src/agent/detail/statusMessage.ts create mode 100644 frontend/packages/swarm-ui/test/status-message.test.mjs diff --git a/docs/2-handbook/swarm/ui.md b/docs/2-handbook/swarm/ui.md index 421636976..69a29debb 100644 --- a/docs/2-handbook/swarm/ui.md +++ b/docs/2-handbook/swarm/ui.md @@ -78,7 +78,7 @@ action that asks first. `/agents` is a column of separate cards: a filter card, then one card per agent, none of them inside a bigger card. A card's -header shows its icon, name, the status message, a **terminal** +header shows its icon, name, the status message with how long ago the agent set it (`5m ago`), a **terminal** button that opens the agent's terminal tab, the **wanted** menu (up, offline, paused, destroy), a **paused by hive** badge when the agent's own harness paused its turn loop (hover for the reason; set the agent **up** to resume it), and a diff --git a/frontend/packages/swarm-ui/src/agent/detail/AgentDetail.tsx b/frontend/packages/swarm-ui/src/agent/detail/AgentDetail.tsx index 9990c1c31..fda669880 100644 --- a/frontend/packages/swarm-ui/src/agent/detail/AgentDetail.tsx +++ b/frontend/packages/swarm-ui/src/agent/detail/AgentDetail.tsx @@ -19,6 +19,7 @@ import { RelativeTime } from "../../ui/relative-time/RelativeTime.js"; import type { AgentActions } from "../roster/AgentActions.js"; import { AgentClaudeLogin } from "./AgentClaudeLogin.js"; import { AgentInference } from "./AgentInference.js"; +import { StatusMessage } from "./StatusMessage.js"; import { AgentInstructions } from "./AgentInstructions.js"; import { AgentLimits } from "./AgentLimits.js"; import { @@ -80,7 +81,9 @@ function HeaderRows({ />
message
-
{row.snapshot?.status_text ?? "—"}
+
+ +
wanted
{/* Full menu (destroy included) — mara: "destroy is already diff --git a/frontend/packages/swarm-ui/src/agent/detail/StatusMessage.tsx b/frontend/packages/swarm-ui/src/agent/detail/StatusMessage.tsx new file mode 100644 index 000000000..8b8f65f64 --- /dev/null +++ b/frontend/packages/swarm-ui/src/agent/detail/StatusMessage.tsx @@ -0,0 +1,26 @@ +// — an agent's status message with how long ago it set it, +// `working on #1 (5m ago)`. The age ticks, so a card left open stays true. An +// agent with no message shows a dash and no age. +import type { AgentStatusSnapshot } from "../AgentTypes.js"; +import { RelativeTime } from "../../ui/relative-time/RelativeTime.js"; +import { statusSetAtMs, statusText } from "./statusMessage.js"; + +export function StatusMessage({ + snapshot, +}: { + snapshot: AgentStatusSnapshot | null; +}) { + const text = statusText(snapshot); + const at = statusSetAtMs(snapshot); + return ( + <> + {text} + {text !== "—" && at !== null ? ( + <> + {" "} + () + + ) : null} + + ); +} diff --git a/frontend/packages/swarm-ui/src/agent/detail/statusMessage.ts b/frontend/packages/swarm-ui/src/agent/detail/statusMessage.ts new file mode 100644 index 000000000..c7f59b71e --- /dev/null +++ b/frontend/packages/swarm-ui/src/agent/detail/statusMessage.ts @@ -0,0 +1,20 @@ +// The agent's status message and when it set it, for the card and its detail. +// Kept out of the component so `npm test` runs it. +import type { AgentStatusSnapshot } from "../AgentTypes.js"; + +/** When the message was set, in ms since the epoch, or null when the agent has + * set none or the time is not known. The wire carries seconds. */ +export function statusSetAtMs( + snapshot: AgentStatusSnapshot | null, +): number | null { + const at = snapshot?.status_set_at; + return typeof at === "number" && Number.isFinite(at) && at > 0 + ? at * 1000 + : null; +} + +/** The message, or a dash when there is none. */ +export function statusText(snapshot: AgentStatusSnapshot | null): string { + const text = snapshot?.status_text; + return text === null || text === undefined || text === "" ? "—" : text; +} diff --git a/frontend/packages/swarm-ui/src/agent/roster/AgentCard.tsx b/frontend/packages/swarm-ui/src/agent/roster/AgentCard.tsx index 24d4ea5ca..ab08a64cf 100644 --- a/frontend/packages/swarm-ui/src/agent/roster/AgentCard.tsx +++ b/frontend/packages/swarm-ui/src/agent/roster/AgentCard.tsx @@ -21,6 +21,7 @@ import { useDynamicTabs } from "../../shell/useDynamicTabs.js"; import { AgentIcon } from "../AgentIcon.js"; import { ClaudeLoginDialog } from "../claude-login/ClaudeLoginDialog.js"; import { AgentDetail } from "../detail/AgentDetail.js"; +import { StatusMessage } from "../detail/StatusMessage.js"; import { termPaths } from "../terminal/term-paths.js"; import { hivePauseReason, @@ -126,7 +127,7 @@ export function AgentCard({ ) : null} } - message={row.snapshot?.status_text ?? "—"} + message={} chevron={