fix(tui): tighten subagent background shortcut

This commit is contained in:
Kit Langton 2026-06-30 13:44:55 -04:00
commit 3d0ff1c705
5 changed files with 66 additions and 20 deletions

View file

@ -316,7 +316,7 @@ describe("SubagentTool", () => {
}).pipe(Effect.forkScoped)
const backgrounded = yield* Effect.gen(function* () {
for (const _ of Array.from({ length: 20 })) {
for (const _ of Array.from({ length: 100 })) {
const result = yield* jobs.backgroundAll({ sessionID: parent.id, type: SubagentTool.name })
if (result.length > 0) return result
yield* Effect.sleep("10 millis")

View file

@ -7,6 +7,7 @@ import { useTheme, selectedForeground } from "../../../context/theme"
import { Locale } from "../../../util/locale"
import { useBindings, useCommandShortcut } from "../../../keymap"
import { useComposerTab } from "./index"
import { useTuiConfig } from "../../../config"
interface SubagentEntry {
sessionID: string
@ -23,6 +24,7 @@ export function SubagentsTab(props: { sessionID: string }) {
const { theme } = useTheme()
const fg = selectedForeground(theme)
const composer = useComposerTab()
const tuiConfig = useTuiConfig()
const interruptHint = useCommandShortcut("composer.subagent.interrupt")
const session = createMemo(() => data.session.get(props.sessionID))
@ -175,6 +177,7 @@ export function SubagentsTab(props: { sessionID: string }) {
{ key: "down", desc: "Next subagent", group: "Subagents", cmd: "composer.subagent.down" },
{ key: "return", desc: "Navigate to subagent", group: "Subagents", cmd: "composer.subagent.select" },
{ key: "ctrl+d", desc: "Interrupt subagent", group: "Subagents", cmd: "composer.subagent.interrupt" },
...tuiConfig.keybinds.gather("session.background", ["session.background"] as const),
],
}))

View file

@ -253,6 +253,13 @@ export function Session() {
}),
)
createEffect(
on(backgroundSessionID, (sessionID) => {
if (sessionID === route.sessionID) return
void data.session.message.refresh(sessionID)
}),
)
createEffect(
on(descendantSessionIDs, (sessionIDs) => {
void Promise.all(sessionIDs.map((sessionID) => data.session.permission.refresh(sessionID)))
@ -907,12 +914,6 @@ export function Session() {
bindings: tuiConfig.keybinds.gather("session.background", sessionBackgroundBindingCommands),
}))
useBindings(() => ({
mode: "composer",
enabled: () => foregroundSubagents() > 0 && composer.open,
bindings: tuiConfig.keybinds.gather("session.background", sessionBackgroundBindingCommands),
}))
// snap to bottom when session changes
createEffect(on(() => route.sessionID, toBottom))
createEffect(

View file

@ -65,11 +65,12 @@ export function foregroundSubagentCount(input: {
.map((session) => session.id),
)
const runningSessionTitles = new Set(
input.sessions
.filter((session) => runningSessionIDs.has(session.id) && session.title)
.map((session) => session.title),
)
const runningSessionTitleCounts = input.sessions
.filter((session) => runningSessionIDs.has(session.id) && session.title)
.reduce((counts, session) => {
if (!session.title) return counts
return counts.set(session.title, (counts.get(session.title) ?? 0) + 1)
}, new Map<string, number>())
const runningRows = input.messages.flatMap((message) =>
message.type === "assistant"
@ -91,16 +92,20 @@ export function foregroundSubagentCount(input: {
}),
)
const anonymousRunningRows = runningRows.filter((part) => {
if (subagentSessionID(part.state?.structured ?? {})) return false
const anonymousRunningRows = runningRows.filter((part) => !subagentSessionID(part.state?.structured ?? {}))
const matchedAnonymousRows = anonymousRunningRows.filter((part) => {
const input = part.state?.input
if (typeof input !== "object" || input === null || Array.isArray(input) || !("description" in input)) return true
return typeof input.description === "string" ? !runningSessionTitles.has(input.description) : true
if (typeof input !== "object" || input === null || Array.isArray(input) || !("description" in input)) return false
if (typeof input.description !== "string") return false
const remaining = runningSessionTitleCounts.get(input.description) ?? 0
if (remaining <= 0) return false
runningSessionTitleCounts.set(input.description, remaining - 1)
return true
}).length
const runningSessions = [...runningSessionIDs].filter((session) => !runningRowSessionIDs.has(session)).length
return runningSessions + runningRowSessionIDs.size + anonymousRunningRows
return runningSessions + runningRowSessionIDs.size + anonymousRunningRows.length - matchedAnonymousRows
}
export function subagentDisplayState(input: SubagentDisplayStateInput) {
@ -111,7 +116,7 @@ export function subagentDisplayState(input: SubagentDisplayStateInput) {
return {
background,
running,
icon: running ? "│" : "✓",
icon: running ? "│" : input.toolStatus === "completed" ? "✓" : "│",
}
}

View file

@ -65,6 +65,33 @@ describe("foregroundSubagentCount", () => {
).toBe(2)
})
test("does not undercount duplicate descriptions when only one child session is synced", () => {
expect(
foregroundSubagentCount({
sessionID: "parent",
sessions: [{ id: "child-1", parentID: "parent", title: "Same task" }],
messages: [
{
type: "assistant",
content: [
{
type: "tool",
name: "subagent",
state: { status: "running", input: { description: "Same task" }, structured: {} },
},
{
type: "tool",
name: "subagent",
state: { status: "running", input: { description: "Same task" }, structured: {} },
},
],
},
],
status: () => "running",
}),
).toBe(2)
})
test("counts running child sessions", () => {
expect(
foregroundSubagentCount({
@ -138,13 +165,23 @@ describe("subagentDisplayState", () => {
).toEqual({ background: true, running: false, icon: "✓" })
})
test("does not spin forever from stale background metadata when the child session is unknown", () => {
test("does not spin forever from stale background metadata without a child session ID", () => {
expect(
subagentDisplayState({
toolStatus: "completed",
metadata: { sessionID: "child", status: "running", background: true },
metadata: { status: "running", background: true },
sessionStatus: () => "idle",
}),
).toEqual({ background: true, running: false, icon: "✓" })
})
test("does not show a checkmark for errored subagent rows", () => {
expect(
subagentDisplayState({
toolStatus: "error",
metadata: { background: true },
sessionStatus: () => "idle",
}),
).toEqual({ background: true, running: false, icon: "│" })
})
})