feat(tui): filter subagents by activity

This commit is contained in:
Dax 2026-07-28 22:13:46 -04:00 committed by GitHub
commit 12a931a220
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 63 additions and 22 deletions

View file

@ -767,19 +767,23 @@ export function RunSubagentSelectBody(props: {
onRows?: (rows: number) => void onRows?: (rows: number) => void
mono?: boolean mono?: boolean
}) { }) {
const [active, setActive] = createSignal(true)
const entries = createMemo<SubagentEntry[]>(() => const entries = createMemo<SubagentEntry[]>(() =>
props.tabs().map((item) => { props
const title = item.description || item.title || item.label .tabs()
return { .filter((item) => (active() ? item.status === "running" : item.status !== "running"))
category: "", .map((item) => {
display: title, const title = item.description || item.title || item.label
description: title === item.label ? undefined : item.label, return {
footer: subagentStatusLabel(item.status), category: "",
keywords: `${item.label} ${item.description} ${item.title ?? ""} ${item.status}`, display: title,
sessionID: item.sessionID, description: title === item.label ? undefined : item.label,
current: props.current() === item.sessionID, footer: subagentStatusLabel(item.status),
} keywords: `${item.label} ${item.description} ${item.title ?? ""} ${item.status}`,
}), sessionID: item.sessionID,
current: props.current() === item.sessionID,
}
}),
) )
const controller = createSearchablePanelController({ const controller = createSearchablePanelController({
entries, entries,
@ -788,6 +792,12 @@ export function RunSubagentSelectBody(props: {
onSelect: (item) => props.onSelect(item.sessionID), onSelect: (item) => props.onSelect(item.sessionID),
isCurrent: (item) => item.current, isCurrent: (item) => item.current,
closeOnFirstUp: true, closeOnFirstUp: true,
onKey(event) {
if (event.name.toLowerCase() !== "tab") return false
event.preventDefault()
setActive((value) => !value)
return true
},
onRows: props.onRows, onRows: props.onRows,
}) })
@ -801,6 +811,7 @@ export function RunSubagentSelectBody(props: {
theme={props.theme} theme={props.theme}
inputRef={controller.inputRef} inputRef={controller.inputRef}
onQuery={controller.setQuery} onQuery={controller.setQuery}
hint={`tab show ${active() ? "inactive" : "active"}`}
mono={props.mono} mono={props.mono}
> >
<RunFooterMenu <RunFooterMenu

View file

@ -27,6 +27,7 @@ export function SubagentsTab(props: { sessionID: string }) {
const shortcuts = Keymap.useShortcuts() const shortcuts = Keymap.useShortcuts()
const session = createMemo(() => data.session.get(props.sessionID)) const session = createMemo(() => data.session.get(props.sessionID))
const [store, setStore] = createStore({ selected: 0, active: true })
const entries = createMemo<SubagentEntry[]>(() => { const entries = createMemo<SubagentEntry[]>(() => {
const current = session() 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 selectedSessionID = ""
let wasActive = false let wasActive = false
let scroll: ScrollBoxRenderable | undefined let scroll: ScrollBoxRenderable | undefined
@ -90,7 +90,7 @@ export function SubagentsTab(props: { sessionID: string }) {
if (!active) { if (!active) {
if (wasActive) { if (wasActive) {
selectedSessionID = "" selectedSessionID = ""
setStore("selected", 0) setStore({ selected: 0, active: true })
} }
wasActive = false wasActive = false
return return
@ -140,8 +140,15 @@ export function SubagentsTab(props: { sessionID: string }) {
label: "Subagents", label: "Subagents",
hints: () => { hints: () => {
const entry = selectedEntry() const entry = selectedEntry()
if (!entry || entry.status !== "running") return [] return [
return [{ label: "interrupt", shortcut: shortcuts.get("composer.subagent.interrupt") ?? "" }] ...(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: () => { onClose: () => {
const parentID = session()?.parentID const parentID = session()?.parentID
@ -189,6 +196,16 @@ export function SubagentsTab(props: { sessionID: string }) {
if (entry) navigate({ type: "session", sessionID: entry.sessionID }) 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", id: "composer.subagent.interrupt",
title: "Interrupt subagent", title: "Interrupt subagent",
@ -206,7 +223,10 @@ export function SubagentsTab(props: { sessionID: string }) {
return ( return (
<Show when={composer.active("subagents")}> <Show when={composer.active("subagents")}>
<scrollbox scrollbarOptions={{ visible: false }} maxHeight={5} ref={(r: ScrollBoxRenderable) => (scroll = r)}> <scrollbox scrollbarOptions={{ visible: false }} maxHeight={5} ref={(r: ScrollBoxRenderable) => (scroll = r)}>
<Show when={entries().length > 0} fallback={<text fg={theme.text.subdued}> No subagents</text>}> <Show
when={entries().length > 0}
fallback={<text fg={theme.text.subdued}> No {store.active ? "active" : "inactive"} subagents</text>}
>
<For each={entries()}> <For each={entries()}>
{(entry, index) => { {(entry, index) => {
const active = createMemo(() => index() === selected()) const active = createMemo(() => index() === selected())

View file

@ -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([ const [tabs] = createSignal([
subagent({ sessionID: "s-1", label: "Explore", description: "Inspect auth flow" }), subagent({ sessionID: "s-1", label: "Explore", description: "Inspect auth flow" }),
subagent({ sessionID: "s-2", label: "General", description: "Write migration plan", status: "completed" }), 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("Select subagent")
expect(frame).toContain("Inspect auth flow") expect(frame).toContain("Inspect auth flow")
expect(frame).toContain("Write migration plan") expect(frame).not.toContain("Write migration plan")
expect(frame).toContain("done") expect(frame).not.toContain("done")
expect(frame).toContain("tab show inactive")
expect(frame).not.toContain("┌") expect(frame).not.toContain("┌")
expect(frame).not.toContain("┃") expect(frame).not.toContain("┃")
expectPaletteList(list, 0) 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 { } finally {
app.renderer.destroy() app.renderer.destroy()
} }