swarm-ui: close a subagent terminal when its subagent leaves the list
This commit is contained in:
parent
3fe53fe323
commit
393213054d
3 changed files with 38 additions and 4 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue