Watch
0
0
Fork
You've already forked hyperhive
0

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.
This commit is contained in:
iris 2026-11-15 15:38:28 +01:00 • committed by mara
commit c56faf9dff
5 changed files with 35 additions and 13 deletions

View file

@ -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 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 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 `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 subagent that runs past the 24-hour window still shows them. The status reads
concurrently, sixteen at a time, so the list waits at most two seconds for come first, then the row lookups for the subagents that have no status. Each
each sixteen subagents. 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 swarm-controller also relays one subagent's rows as SSE on
`GET /api/agents/<name>/subagents/<subagent>/term/stream`. Like the agent's `GET /api/agents/<name>/subagents/<subagent>/term/stream`. Like the agent's
terminal route below, it creates a short-lived consumer on that stream, here terminal route below, it creates a short-lived consumer on that stream, here

View file

@ -15,7 +15,7 @@ import { Badge } from "@hive/shared/badge.js";
import { AgentTermPreview } from "./AgentTermPreview.js"; import { AgentTermPreview } from "./AgentTermPreview.js";
import { import {
isDimmed, isDimmed,
lastActive, ageUnix,
runLabel, runLabel,
stateLabel, stateLabel,
stillListed, stillListed,
@ -51,7 +51,7 @@ export function SubagentTerms({ agentName }: { agentName: string }) {
{subagents?.map((sub) => { {subagents?.map((sub) => {
const { name } = sub; const { name } = sub;
const runs = runLabel(sub); const runs = runLabel(sub);
const active = lastActive(sub); const active = ageUnix(sub);
const status = stateLabel(sub); const status = stateLabel(sub);
const stale = isDimmed(sub, nowS); const stale = isDimmed(sub, nowS);
return ( return (

View file

@ -22,7 +22,7 @@ import { Popover } from "../../ui/popover/Popover.js";
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js"; import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
import { termPaths } from "./term-paths.js"; import { termPaths } from "./term-paths.js";
import { import {
lastActive, ageUnix,
runLabel, runLabel,
runningLabel, runningLabel,
splitRunning, splitRunning,
@ -56,9 +56,9 @@ function Entry({
{stateLabel(sub) && ( {stateLabel(sub) && (
<span class="ui-subagents-menu-age">{stateLabel(sub)}</span> <span class="ui-subagents-menu-age">{stateLabel(sub)}</span>
)} )}
{lastActive(sub) !== null && ( {ageUnix(sub) !== null && (
<span class="ui-subagents-menu-age"> <span class="ui-subagents-menu-age">
<RelativeTime epochMs={(lastActive(sub) ?? 0) * 1000} /> <RelativeTime epochMs={(ageUnix(sub) ?? 0) * 1000} />
</span> </span>
)} )}
</> </>

View file

@ -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 * The word shown before a subagent's age, which says what the age counts from:
* subagent: `finished`, or `stopped reporting` for a stale status. Empty * `running since` for a running status, `finished` for a finished one,
* when running or when the daemon published no status. * `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 { export function stateLabel(sub: Subagent): string {
switch (sub.state ?? null) { switch (sub.state ?? null) {
case "running":
return "running since";
case "finished": case "finished":
return "finished"; return "finished";
case "stale": 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. */ /** Newest first; a subagent whose time could not be read sorts last. */
export function byRecency(a: Subagent, b: Subagent): number { export function byRecency(a: Subagent, b: Subagent): number {
const ta = lastActive(a) ?? -Infinity; const ta = lastActive(a) ?? -Infinity;

View file

@ -5,6 +5,7 @@ import assert from "node:assert/strict";
import { test } from "node:test"; import { test } from "node:test";
import { import {
STALE_S, STALE_S,
ageUnix,
byRecency, byRecency,
isDimmed, isDimmed,
isRunning, 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); 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", "finished", 5)), "finished");
assert.equal(stateLabel(withState("a", "stale", 5)), "stopped reporting"); 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)), ""); 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", () => { test("a subagent with only a status sorts by when its state began", () => {
const list = [ const list = [
withState("old", "finished", 500), withState("old", "finished", 500),