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.
|
// agent's own: subagents take no input.
|
||||||
//
|
//
|
||||||
// Mount it keyed by agent name: the refresh timer is armed once per mount.
|
// 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 { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
||||||
import { Badge } from "@hive/shared/badge.js";
|
import { Badge } from "@hive/shared/badge.js";
|
||||||
import { AgentTermPreview } from "./AgentTermPreview.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 { useSubagentList } from "./useSubagentList.js";
|
||||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||||
import "./SubagentTerms.css";
|
import "./SubagentTerms.css";
|
||||||
|
|
||||||
export function SubagentTerms({ agentName }: { agentName: string }) {
|
export function SubagentTerms({ agentName }: { agentName: string }) {
|
||||||
const { subagents, error } = useSubagentList(agentName);
|
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
|
// 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
|
// "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`
|
? `hide ${name}'s terminal`
|
||||||
: `show ${name}'s terminal`
|
: `show ${name}'s terminal`
|
||||||
}
|
}
|
||||||
onClick={() => setSelected(name === selected ? null : name)}
|
onClick={() => setPicked(name === selected ? null : name)}
|
||||||
/>
|
/>
|
||||||
{last_row_unix !== null && (
|
{last_row_unix !== null && (
|
||||||
<span class="ui-subagent-terms-age">
|
<span class="ui-subagent-terms-age">
|
||||||
|
|
|
||||||
|
|
@ -48,3 +48,18 @@ export function splitRunning(
|
||||||
export function runningLabel(count: number): string {
|
export function runningLabel(count: number): string {
|
||||||
return count === 1 ? "1 running subagent" : `${count} running subagents`;
|
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,
|
isRunning,
|
||||||
runningLabel,
|
runningLabel,
|
||||||
splitRunning,
|
splitRunning,
|
||||||
|
stillListed,
|
||||||
} from "../src/agent/terminal/subagent-list.ts";
|
} from "../src/agent/terminal/subagent-list.ts";
|
||||||
|
|
||||||
const NOW = 1_000_000;
|
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(1), "1 running subagent");
|
||||||
assert.equal(runningLabel(4), "4 running subagents");
|
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