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:
@@ -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" })
|
||||
|
||||
Reference in New Issue
Block a user