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),