From 12a931a220cbea59de656d67b50a47ed2dfa65bc Mon Sep 17 00:00:00 2001 From: Dax Date: Tue, 28 Jul 2026 22:13:46 -0400 Subject: [PATCH] feat(tui): filter subagents by activity --- packages/tui/src/mini/footer.command.tsx | 35 ++++++++++++------- .../routes/session/composer/subagents-tab.tsx | 32 +++++++++++++---- packages/tui/test/mini/footer.view.test.tsx | 18 +++++++--- 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/packages/tui/src/mini/footer.command.tsx b/packages/tui/src/mini/footer.command.tsx index e7c8da0d8b0..8564fbc0826 100644 --- a/packages/tui/src/mini/footer.command.tsx +++ b/packages/tui/src/mini/footer.command.tsx @@ -767,19 +767,23 @@ export function RunSubagentSelectBody(props: { onRows?: (rows: number) => void mono?: boolean }) { + const [active, setActive] = createSignal(true) const entries = createMemo(() => - props.tabs().map((item) => { - const title = item.description || item.title || item.label - return { - category: "", - display: title, - description: title === item.label ? undefined : item.label, - footer: subagentStatusLabel(item.status), - keywords: `${item.label} ${item.description} ${item.title ?? ""} ${item.status}`, - sessionID: item.sessionID, - current: props.current() === item.sessionID, - } - }), + props + .tabs() + .filter((item) => (active() ? item.status === "running" : item.status !== "running")) + .map((item) => { + const title = item.description || item.title || item.label + return { + category: "", + display: title, + description: title === item.label ? undefined : item.label, + footer: subagentStatusLabel(item.status), + keywords: `${item.label} ${item.description} ${item.title ?? ""} ${item.status}`, + sessionID: item.sessionID, + current: props.current() === item.sessionID, + } + }), ) const controller = createSearchablePanelController({ entries, @@ -788,6 +792,12 @@ export function RunSubagentSelectBody(props: { onSelect: (item) => props.onSelect(item.sessionID), isCurrent: (item) => item.current, closeOnFirstUp: true, + onKey(event) { + if (event.name.toLowerCase() !== "tab") return false + event.preventDefault() + setActive((value) => !value) + return true + }, onRows: props.onRows, }) @@ -801,6 +811,7 @@ export function RunSubagentSelectBody(props: { theme={props.theme} inputRef={controller.inputRef} onQuery={controller.setQuery} + hint={`tab show ${active() ? "inactive" : "active"}`} mono={props.mono} > data.session.get(props.sessionID)) + const [store, setStore] = createStore({ selected: 0, active: true }) const entries = createMemo(() => { const current = session() @@ -72,10 +73,9 @@ export function SubagentsTab(props: { sessionID: string }) { } } - return result + return result.filter((entry) => (store.active ? entry.status === "running" : entry.status !== "running")) }) - const [store, setStore] = createStore({ selected: 0 }) let selectedSessionID = "" let wasActive = false let scroll: ScrollBoxRenderable | undefined @@ -90,7 +90,7 @@ export function SubagentsTab(props: { sessionID: string }) { if (!active) { if (wasActive) { selectedSessionID = "" - setStore("selected", 0) + setStore({ selected: 0, active: true }) } wasActive = false return @@ -140,8 +140,15 @@ export function SubagentsTab(props: { sessionID: string }) { label: "Subagents", hints: () => { const entry = selectedEntry() - if (!entry || entry.status !== "running") return [] - return [{ label: "interrupt", shortcut: shortcuts.get("composer.subagent.interrupt") ?? "" }] + return [ + ...(entry?.status === "running" + ? [{ label: "interrupt", shortcut: shortcuts.get("composer.subagent.interrupt") ?? "" }] + : []), + { + label: `show ${store.active ? "inactive" : "active"}`, + shortcut: shortcuts.get("composer.subagent.toggle-activity") ?? "", + }, + ] }, onClose: () => { const parentID = session()?.parentID @@ -189,6 +196,16 @@ export function SubagentsTab(props: { sessionID: string }) { if (entry) navigate({ type: "session", sessionID: entry.sessionID }) }, }, + { + id: "composer.subagent.toggle-activity", + title: "Toggle active subagents", + group: "Composer", + bind: "ctrl+a", + run() { + setStore({ selected: 0, active: !store.active }) + scroll?.scrollTo(0) + }, + }, { id: "composer.subagent.interrupt", title: "Interrupt subagent", @@ -206,7 +223,10 @@ export function SubagentsTab(props: { sessionID: string }) { return ( (scroll = r)}> - 0} fallback={ No subagents}> + 0} + fallback={ No {store.active ? "active" : "inactive"} subagents} + > {(entry, index) => { const active = createMemo(() => index() === selected()) diff --git a/packages/tui/test/mini/footer.view.test.tsx b/packages/tui/test/mini/footer.view.test.tsx index 022e90623ba..8e44e01ebaf 100644 --- a/packages/tui/test/mini/footer.view.test.tsx +++ b/packages/tui/test/mini/footer.view.test.tsx @@ -820,7 +820,7 @@ test("direct command panel keeps completed subagents available", async () => { } }) -test("direct subagent panel renders active subagents", async () => { +test("direct subagent panel toggles between active and inactive subagents", async () => { const [tabs] = createSignal([ subagent({ sessionID: "s-1", label: "Explore", description: "Inspect auth flow" }), subagent({ sessionID: "s-2", label: "General", description: "Write migration plan", status: "completed" }), @@ -856,12 +856,22 @@ test("direct subagent panel renders active subagents", async () => { expect(frame).toContain("Select subagent") expect(frame).toContain("Inspect auth flow") - expect(frame).toContain("Write migration plan") - expect(frame).toContain("done") + expect(frame).not.toContain("Write migration plan") + expect(frame).not.toContain("done") + expect(frame).toContain("tab show inactive") expect(frame).not.toContain("┌") expect(frame).not.toContain("┃") expectPaletteList(list, 0) - expect(rows).toBe(8) + expect(rows).toBe(7) + + app.mockInput.pressKey("TAB") + await app.renderOnce() + const inactive = app.captureCharFrame() + + expect(inactive).not.toContain("Inspect auth flow") + expect(inactive).toContain("Write migration plan") + expect(inactive).toContain("done") + expect(inactive).toContain("tab show active") } finally { app.renderer.destroy() }