From c56faf9dffefb9dea9ef46cdbc38ffd7e82d06e2 Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 15 Nov 2026 15:38:28 +0100 Subject: [PATCH] swarm ui: say what a subagent's age counts from, and the list's two lookup passes A running status shows 'running since', a stale one shows no age, since its time is when the run began. The reference now says the status reads and the row lookups are two passes, so the list waits up to four seconds in all. --- docs/4-implementation/web-ui/swarm-ui.md | 8 +++++--- .../src/agent/terminal/SubagentTerms.tsx | 4 ++-- .../src/agent/terminal/SubagentsMenu.tsx | 6 +++--- .../src/agent/terminal/subagent-list.ts | 18 +++++++++++++++--- .../swarm-ui/test/subagent-list.test.mjs | 12 ++++++++++-- 5 files changed, 35 insertions(+), 13 deletions(-) diff --git a/docs/4-implementation/web-ui/swarm-ui.md b/docs/4-implementation/web-ui/swarm-ui.md index b7a240856..182e22bb4 100644 --- a/docs/4-implementation/web-ui/swarm-ui.md +++ b/docs/4-implementation/web-ui/swarm-ui.md @@ -579,9 +579,11 @@ also carries the `Swarm-Model` and `Swarm-Effort` message headers, the model and effort the subagent runs with, and the list returns them from the newest row as `model` and `effort`. A subagent on the default model has no `Swarm-Model` header and lists `model: null`; every row has the headers, so a -subagent that runs past the 24-hour window still shows them. The lookups run -concurrently, sixteen at a time, so the list waits at most two seconds for -each sixteen subagents. +subagent that runs past the 24-hour window still shows them. The status reads +come first, then the row lookups for the subagents that have no status. Each +pass runs its lookups concurrently, sixteen at a time, so the list waits at +most two seconds for each sixteen subagents in each pass, and at most four +seconds in all. swarm-controller also relays one subagent's rows as SSE on `GET /api/agents//subagents//term/stream`. Like the agent's terminal route below, it creates a short-lived consumer on that stream, here diff --git a/frontend/packages/swarm-ui/src/agent/terminal/SubagentTerms.tsx b/frontend/packages/swarm-ui/src/agent/terminal/SubagentTerms.tsx index 07d9bfdb1..8f1c8256d 100644 --- a/frontend/packages/swarm-ui/src/agent/terminal/SubagentTerms.tsx +++ b/frontend/packages/swarm-ui/src/agent/terminal/SubagentTerms.tsx @@ -15,7 +15,7 @@ import { Badge } from "@hive/shared/badge.js"; import { AgentTermPreview } from "./AgentTermPreview.js"; import { isDimmed, - lastActive, + ageUnix, runLabel, stateLabel, stillListed, @@ -51,7 +51,7 @@ export function SubagentTerms({ agentName }: { agentName: string }) { {subagents?.map((sub) => { const { name } = sub; const runs = runLabel(sub); - const active = lastActive(sub); + const active = ageUnix(sub); const status = stateLabel(sub); const stale = isDimmed(sub, nowS); return ( diff --git a/frontend/packages/swarm-ui/src/agent/terminal/SubagentsMenu.tsx b/frontend/packages/swarm-ui/src/agent/terminal/SubagentsMenu.tsx index d01e82a96..962b32ac3 100644 --- a/frontend/packages/swarm-ui/src/agent/terminal/SubagentsMenu.tsx +++ b/frontend/packages/swarm-ui/src/agent/terminal/SubagentsMenu.tsx @@ -22,7 +22,7 @@ import { Popover } from "../../ui/popover/Popover.js"; import { RelativeTime } from "../../ui/relative-time/RelativeTime.js"; import { termPaths } from "./term-paths.js"; import { - lastActive, + ageUnix, runLabel, runningLabel, splitRunning, @@ -56,9 +56,9 @@ function Entry({ {stateLabel(sub) && ( {stateLabel(sub)} )} - {lastActive(sub) !== null && ( + {ageUnix(sub) !== null && ( - + )} diff --git a/frontend/packages/swarm-ui/src/agent/terminal/subagent-list.ts b/frontend/packages/swarm-ui/src/agent/terminal/subagent-list.ts index 9f871e960..19041d18c 100644 --- a/frontend/packages/swarm-ui/src/agent/terminal/subagent-list.ts +++ b/frontend/packages/swarm-ui/src/agent/terminal/subagent-list.ts @@ -68,12 +68,15 @@ export function isDimmed(sub: Subagent, nowS: number): boolean { } /** - * A word for a state that is neither running nor unknown, to show beside the - * subagent: `finished`, or `stopped reporting` for a stale status. Empty - * when running or when the daemon published no status. + * The word shown before a subagent's age, which says what the age counts from: + * `running since` for a running status, `finished` for a finished one, + * `stopped reporting` for a stale one. Empty when the daemon published no + * status, where the age is the newest row's. */ export function stateLabel(sub: Subagent): string { switch (sub.state ?? null) { + case "running": + return "running since"; case "finished": return "finished"; case "stale": @@ -83,6 +86,15 @@ export function stateLabel(sub: Subagent): string { } } +/** + * The time to show as an age, unix seconds, or `null` for none. A stale + * status has none: its time is when the run began, which would read as when + * the daemon stopped reporting. + */ +export function ageUnix(sub: Subagent): number | null { + return (sub.state ?? null) === "stale" ? null : lastActive(sub); +} + /** Newest first; a subagent whose time could not be read sorts last. */ export function byRecency(a: Subagent, b: Subagent): number { const ta = lastActive(a) ?? -Infinity; diff --git a/frontend/packages/swarm-ui/test/subagent-list.test.mjs b/frontend/packages/swarm-ui/test/subagent-list.test.mjs index d43ece4dd..1c8b9e03f 100644 --- a/frontend/packages/swarm-ui/test/subagent-list.test.mjs +++ b/frontend/packages/swarm-ui/test/subagent-list.test.mjs @@ -5,6 +5,7 @@ import assert from "node:assert/strict"; import { test } from "node:test"; import { STALE_S, + ageUnix, byRecency, isDimmed, isRunning, @@ -99,13 +100,20 @@ test("only a subagent without a status falls back to the row's age", () => { assert.equal(isDimmed(withState("a", "stale", 5), NOW), true); }); -test("the state word names finished and stale subagents only", () => { +test("the state word says what the age counts from", () => { assert.equal(stateLabel(withState("a", "finished", 5)), "finished"); assert.equal(stateLabel(withState("a", "stale", 5)), "stopped reporting"); - assert.equal(stateLabel(withState("a", "running", 5)), ""); + assert.equal(stateLabel(withState("a", "running", 5)), "running since"); assert.equal(stateLabel(sub("a", 5)), ""); }); +test("a stale status shows no age, the others show theirs", () => { + assert.equal(ageUnix(withState("a", "stale", 500)), null); + assert.equal(ageUnix(withState("a", "running", 500)), NOW - 500); + assert.equal(ageUnix(withState("a", "finished", 500)), NOW - 500); + assert.equal(ageUnix(sub("a", 30)), NOW - 30); +}); + test("a subagent with only a status sorts by when its state began", () => { const list = [ withState("old", "finished", 500),