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={