run: replace subagent tabs with on-demand picker (#28508)

Move subagent navigation into the existing palette: a
"View subagents" command entry, a dedicated picker panel, and a
Down-arrow shortcut from the empty composer.
This commit is contained in:
Simon Klee
2026-05-20 21:07:35 +02:00
committed by GitHub
parent ba803dd89a
commit ed839846d1
7 changed files with 518 additions and 165 deletions

View File

@@ -4,13 +4,26 @@ import { testRender } from "@opentui/solid"
import { createSignal } from "solid-js"
import {
RUN_COMMAND_PANEL_ROWS,
RUN_SUBAGENT_PANEL_ROWS,
RunCommandMenuBody,
RunModelSelectBody,
RunSubagentSelectBody,
RunVariantSelectBody,
} from "@/cli/cmd/run/footer.command"
import { RunFooterView } from "@/cli/cmd/run/footer.view"
import { RunEntryContent } from "@/cli/cmd/run/scrollback.writer"
import { RUN_THEME_FALLBACK } from "@/cli/cmd/run/theme"
import type { FooterKeybinds, RunCommand, RunInput, RunProvider, StreamCommit } from "@/cli/cmd/run/types"
import type {
FooterKeybinds,
FooterState,
FooterSubagentState,
FooterSubagentTab,
FooterView,
RunCommand,
RunInput,
RunProvider,
StreamCommit,
} from "@/cli/cmd/run/types"
function bindings(...keys: string[]) {
return keys.map((key) => ({ key }))
@@ -111,6 +124,18 @@ function provider() {
} satisfies RunProvider
}
function subagent(input: { sessionID: string; label: string; description: string; status?: FooterSubagentTab["status"] }) {
return {
sessionID: input.sessionID,
partID: `part-${input.sessionID}`,
callID: `call-${input.sessionID}`,
label: input.label,
description: input.description,
status: input.status ?? "running",
lastUpdatedAt: 1,
} satisfies FooterSubagentTab
}
test("run entry content updates when live commit text changes", async () => {
const [commit, setCommit] = createSignal<StreamCommit>({
kind: "tool",
@@ -161,6 +186,7 @@ test("direct command panel renders grouped command palette", async () => {
command({ name: "deploy", description: "Deploy prompt", source: "mcp" }),
command({ name: "internal", description: "Skill command", source: "skill" }),
])
const [subagents] = createSignal([])
const [variants] = createSignal(["high", "minimal"])
const app = await testRender(
@@ -169,10 +195,12 @@ test("direct command panel renders grouped command palette", async () => {
<RunCommandMenuBody
theme={() => RUN_THEME_FALLBACK.footer}
commands={commands}
subagents={subagents}
variants={variants}
keybinds={keybinds}
onClose={() => {}}
onModel={() => {}}
onSubagent={() => {}}
onVariant={() => {}}
onVariantCycle={() => {}}
onCommand={() => {}}
@@ -214,6 +242,160 @@ test("direct command panel renders grouped command palette", async () => {
}
})
test("direct command panel shows subagent entry when available", async () => {
const [commands] = createSignal<RunCommand[] | undefined>([])
const [subagents] = createSignal([subagent({ sessionID: "s-1", label: "Explore", description: "Inspect auth flow" })])
const [variants] = createSignal<string[]>([])
const app = await testRender(
() => (
<box width={100} height={RUN_COMMAND_PANEL_ROWS}>
<RunCommandMenuBody
theme={() => RUN_THEME_FALLBACK.footer}
commands={commands}
subagents={subagents}
variants={variants}
keybinds={keybinds}
onClose={() => {}}
onModel={() => {}}
onSubagent={() => {}}
onVariant={() => {}}
onVariantCycle={() => {}}
onCommand={() => {}}
onNew={() => {}}
onExit={() => {}}
/>
</box>
),
{
width: 100,
height: RUN_COMMAND_PANEL_ROWS,
},
)
try {
await app.renderOnce()
const frame = app.captureCharFrame()
expect(frame).toContain("View subagents")
expect(frame).toContain("1 active")
} finally {
app.renderer.destroy()
}
})
test("direct subagent panel renders active 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" }),
])
const [current] = createSignal<string | undefined>("s-1")
let rows = 0
const app = await testRender(
() => (
<box width={100} height={RUN_SUBAGENT_PANEL_ROWS}>
<RunSubagentSelectBody
theme={() => RUN_THEME_FALLBACK.footer}
tabs={tabs}
current={current}
onClose={() => {}}
onSelect={() => {}}
onRows={(value) => {
rows = value
}}
/>
</box>
),
{
width: 100,
height: RUN_SUBAGENT_PANEL_ROWS,
},
)
try {
await app.renderOnce()
const frame = app.captureCharFrame()
expect(frame).toContain("Select subagent")
expect(frame).toContain("Inspect auth flow")
expect(frame).toContain("Write migration plan")
expect(frame).toContain("done")
expect(rows).toBe(8)
} finally {
app.renderer.destroy()
}
})
test("direct footer shows subagent indicator while prompt is running", async () => {
const [state] = createSignal<FooterState>({
phase: "running",
status: "",
queue: 0,
model: "gpt-5",
duration: "",
usage: "",
first: false,
interrupt: 0,
exit: 0,
})
const [view] = createSignal<FooterView>({ type: "prompt" })
const [subagents] = createSignal<FooterSubagentState>({
tabs: [subagent({ sessionID: "s-1", label: "Explore", description: "Inspect auth flow" })],
details: {},
permissions: [],
questions: [],
})
const app = await testRender(
() => (
<box width={100} height={8}>
<RunFooterView
directory="/tmp"
findFiles={async () => []}
agents={() => []}
resources={() => []}
commands={() => []}
providers={() => undefined}
currentModel={() => undefined}
variants={() => []}
currentVariant={() => undefined}
state={state}
view={view}
subagent={subagents}
theme={RUN_THEME_FALLBACK}
keybinds={keybinds}
agent="opencode"
onSubmit={() => true}
onPermissionReply={() => {}}
onQuestionReply={() => {}}
onQuestionReject={() => {}}
onCycle={() => {}}
onInterrupt={() => false}
onInputClear={() => {}}
onExit={() => {}}
onModelSelect={() => {}}
onVariantSelect={() => {}}
onRows={() => {}}
onLayout={() => {}}
onStatus={() => {}}
/>
</box>
),
{
width: 100,
height: 8,
},
)
try {
await app.renderOnce()
expect(app.captureCharFrame()).toContain("interrupt · 1 agent · ↓ to view")
} finally {
app.renderer.destroy()
}
})
test("direct model panel renders current model selector", async () => {
const [providers] = createSignal<RunProvider[] | undefined>([provider()])
const [current] = createSignal<RunInput["model"]>({ providerID: "opencode", modelID: "gpt-5" })