Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: a running-subagents menu in the full-screen terminal header

An N running subagents badge opens a list of the agent's subagents, each a
link to its terminal, so you can switch without going back to the agents page.
A subagent counts as running when it published a row in the last 15 minutes;
the quiet ones are listed below it, dimmed. The list under the preview and the
menu now share one fetch hook and one definition of running.
This commit is contained in:
iris 2026-10-18 01:33:06 +02:00 • committed by mara
commit 664409c87f
8 changed files with 360 additions and 46 deletions

View file

@ -3,57 +3,24 @@
// the subagents the swarm queue holds terminal rows for, each with the
// publish time of its newest row. Newest first, with a ticking age beside
// the name; one quiet for `STALE_S` is dimmed. Subagents are spawned on
// demand, so it is refetched every `REFRESH_MS`. Picking one shows its
// demand, so `useSubagentList` refetches it. Picking one shows its
// terminal in `AgentTermPreview`, expand-to-tab included. Read-only like the
// agent's own: subagents take no input.
//
// Mount it keyed by agent name: the refresh timer is armed once per mount.
import { useState } from "preact/hooks";
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
import { Badge } from "@hive/shared/badge.js";
import { AgentTermPreview } from "./AgentTermPreview.js";
import { useRefreshInterval } from "../../ui/refresh-interval/RefreshInterval.js";
import { STALE_S } from "./subagent-list.js";
import { useSubagentList } from "./useSubagentList.js";
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
import "./SubagentTerms.css";
const REFRESH_MS = 10_000;
const STALE_S = 15 * 60;
interface Subagent {
name: string;
last_row_unix: number | null;
}
// Newest row first; a subagent whose time could not be read sorts last.
function byRecency(a: Subagent, b: Subagent): number {
const ta = a.last_row_unix ?? -Infinity;
const tb = b.last_row_unix ?? -Infinity;
if (ta !== tb) return tb > ta ? 1 : -1;
return a.name.localeCompare(b.name);
}
export function SubagentTerms({ agentName }: { agentName: string }) {
const [subagents, setSubagents] = useState<Subagent[] | null>(null);
const [error, setError] = useState<ProblemDetails | null>(null);
const { subagents, error } = useSubagentList(agentName);
const [selected, setSelected] = useState<string | null>(null);
async function refresh() {
const res = await fetch(
`/api/agents/${encodeURIComponent(agentName)}/subagents`,
);
if (!res.ok) {
setError(await readApiError(res));
return;
}
setSubagents(((await res.json()) as Subagent[]).sort(byRecency));
setError(null);
}
useRefreshInterval(REFRESH_MS, () => {
refresh().catch((e: unknown) => setError({ detail: String(e) }));
});
// An agent with no subagents shows nothing here — no heading, no
// "none" line — and so does the first fetch while it is in flight. A
// failed fetch still shows, so the error is not swallowed.