Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: close a subagent terminal when its subagent leaves the list

This commit is contained in:
iris 2026-10-21 20:58:54 +02:00 • committed by mara
commit 393213054d
3 changed files with 38 additions and 4 deletions

View file

@ -8,18 +8,27 @@
// 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 { useEffect, useState } from "preact/hooks";
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { Badge } from "@hive/shared/badge.js";
import { AgentTermPreview } from "./AgentTermPreview.js";
import { STALE_S } from "./subagent-list.js";
import { STALE_S, stillListed } from "./subagent-list.js";
import { useSubagentList } from "./useSubagentList.js";
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
import "./SubagentTerms.css";
export function SubagentTerms({ agentName }: { agentName: string }) {
const { subagents, error } = useSubagentList(agentName);
const [selected, setSelected] = useState<string | null>(null);
const [picked, setPicked] = useState<string | null>(null);
// The refresh can drop the picked subagent (its stream is gone). Only a
// name still in the list counts as selected, so the terminal closes with
// its badge and a later subagent of the same name does not reopen it.
const selected = stillListed(picked, subagents);
useEffect(() => {
if (picked !== null && subagents !== null && selected === null) {
setPicked(null);
}
}, [picked, subagents, selected]);
// 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
@ -49,7 +58,7 @@ export function SubagentTerms({ agentName }: { agentName: string }) {
? `hide ${name}'s terminal`
: `show ${name}'s terminal`
}
onClick={() => setSelected(name === selected ? null : name)}
onClick={() => setPicked(name === selected ? null : name)}
/>
{last_row_unix !== null && (
<span class="ui-subagent-terms-age">

View file

@ -48,3 +48,18 @@ export function splitRunning(
export function runningLabel(count: number): string {
return count === 1 ? "1 running subagent" : `${count} running subagents`;
}
/**
* The picked subagent while the list still holds it, else `null`. The list
* refreshes on a timer and can drop a subagent whose stream is gone, so a
* pick outlives its badge unless it is checked against the list. `null`
* list (not fetched yet) holds no pick either.
*/
export function stillListed(
picked: string | null,
subs: Subagent[] | null,
): string | null {
return picked !== null && subs?.some((s) => s.name === picked)
? picked
: null;
}

View file

@ -9,6 +9,7 @@ import {
isRunning,
runningLabel,
splitRunning,
stillListed,
} from "../src/agent/terminal/subagent-list.ts";
const NOW = 1_000_000;
@ -59,3 +60,12 @@ test("the badge counts in words", () => {
assert.equal(runningLabel(1), "1 running subagent");
assert.equal(runningLabel(4), "4 running subagents");
});
test("a pick holds only while the refreshed list still has it", () => {
const subs = [sub("a", 10), sub("b", 20)];
assert.equal(stillListed("a", subs), "a");
assert.equal(stillListed("gone", subs), null);
assert.equal(stillListed("a", []), null);
assert.equal(stillListed("a", null), null);
assert.equal(stillListed(null, subs), null);
});