cli: add variant selection panel to command menu

Previously model variants (reasoning effort) could only be cycled
with a keybind. Add a searchable selection panel accessible from the
command palette, matching the existing model selection flow. The
footer hint bar now advertises the command palette keybind instead
of the variant cycle shortcut.
This commit is contained in:
Simon Klee
2026-05-06 09:44:57 +02:00
parent 991ed7a7c4
commit 2c260c4b46
10 changed files with 379 additions and 54 deletions
@@ -12,7 +12,12 @@ type PanelEntry = RunFooterMenuItem & {
keywords?: string
}
type CommandEntry = PanelEntry & ({ action: "model" } | { action: "slash"; name: string } | { action: "exit" })
type CommandEntry =
| (PanelEntry & { action: "model" })
| (PanelEntry & { action: "variant.cycle" })
| (PanelEntry & { action: "variant.list" })
| (PanelEntry & { action: "slash"; name: string })
| (PanelEntry & { action: "exit" })
type ModelEntry = PanelEntry & {
providerID: string
@@ -21,6 +26,11 @@ type ModelEntry = PanelEntry & {
current: boolean
}
type VariantEntry = PanelEntry & {
variant: string | undefined
current: boolean
}
type MenuState = ReturnType<typeof createFooterMenuState>
const PANEL_PAD = 2
@@ -265,8 +275,11 @@ function PanelShell(props: {
export function RunCommandMenuBody(props: {
theme: Accessor<RunFooterTheme>
commands: Accessor<RunCommand[] | undefined>
variants: Accessor<string[]>
onClose: () => void
onModel: () => void
onVariant: () => void
onVariantCycle: () => void
onSlash: (name: string) => void
onExit: () => void
}) {
@@ -281,6 +294,24 @@ export function RunCommandMenuBody(props: {
display: "Switch model",
description: "Choose model for future turns",
},
{
action: "variant.cycle",
category: "Suggested",
display: "Variant cycle",
description: "Cycle reasoning effort for future turns",
keywords: "variant cycle",
},
...(props.variants().length > 0
? [
{
action: "variant.list" as const,
category: "Suggested",
display: "Switch model variant",
description: "Choose reasoning effort for future turns",
keywords: `variant variants ${props.variants().join(" ")}`,
},
]
: []),
{
action: "slash",
category: "Session",
@@ -314,6 +345,16 @@ export function RunCommandMenuBody(props: {
return
}
if (item.action === "variant.cycle") {
props.onVariantCycle()
return
}
if (item.action === "variant.list") {
props.onVariant()
return
}
if (item.action === "exit") {
props.onExit()
return
@@ -375,6 +416,103 @@ export function RunCommandMenuBody(props: {
)
}
export function RunVariantSelectBody(props: {
theme: Accessor<RunFooterTheme>
variants: Accessor<string[]>
current: Accessor<string | undefined>
onClose: () => void
onSelect: (variant: string | undefined) => void
}) {
let field: InputRenderable | undefined
const [query, setQuery] = createSignal("")
const entries = createMemo<VariantEntry[]>(() => [
{
category: "",
display: "Default",
description: props.current() === undefined ? "current" : undefined,
keywords: "default",
variant: undefined,
current: props.current() === undefined,
},
...props.variants().map((variant) => ({
category: "",
display: variant,
description: props.current() === variant ? "current" : undefined,
keywords: variant,
variant,
current: props.current() === variant,
})),
])
const items = createMemo<VariantEntry[]>(() => match(query(), entries()))
const menu = createFooterMenuState({ count: () => items().length, limit: PANEL_LIST_ROWS })
const pick = (item: VariantEntry) => {
props.onSelect(item.variant)
}
const select = () => {
const item = items()[menu.selected()]
if (!item) {
return
}
pick(item)
}
createEffect(() => {
query()
menu.reset()
})
createEffect(() => {
if (query().trim()) {
return
}
const index = items().findIndex((item) => item.current)
if (index !== -1) {
menu.reveal(index)
}
})
useKeyboard((event) => {
if (event.defaultPrevented) {
return
}
handleKey({ event, menu, field: () => field, setQuery, select, close: props.onClose })
})
return (
<PanelShell
id="run-direct-footer-variant-panel"
title="Select variant"
query={query()}
count={items().length}
total={entries().length}
placeholder="Search"
theme={props.theme}
inputRef={(input) => {
field = input
}}
onQuery={setQuery}
>
<RunFooterMenu
id="run-direct-footer-variant-list"
theme={props.theme}
items={items}
selected={menu.selected}
offset={menu.offset}
rows={() => PANEL_LIST_ROWS}
limit={PANEL_LIST_ROWS}
empty="No results found"
border={false}
paddingLeft={PANEL_PAD}
paddingRight={PANEL_PAD}
grouped={false}
/>
</PanelShell>
)
}
export function RunModelSelectBody(props: {
theme: Accessor<RunFooterTheme>
providers: Accessor<RunProvider[] | undefined>
@@ -39,7 +39,7 @@ export const HINT_BREAKPOINTS = {
send: 50,
newline: 66,
history: 80,
variant: 95,
command: 95,
}
type Mention = Extract<RunPromptPart, { type: "file" | "agent" }>
@@ -181,7 +181,7 @@ export function hintFlags(width: number) {
send: width >= HINT_BREAKPOINTS.send,
newline: width >= HINT_BREAKPOINTS.newline,
history: width >= HINT_BREAKPOINTS.history,
variant: width >= HINT_BREAKPOINTS.variant,
command: width >= HINT_BREAKPOINTS.command,
}
}
@@ -964,7 +964,7 @@ export function createPromptState(input: PromptInput): PromptState {
return
}
if (input.view() === "command" || input.view() === "model") {
if (input.view() === "command" || input.view() === "model" || input.view() === "variant") {
return
}
@@ -61,6 +61,8 @@ import type {
type CycleResult = {
modelLabel?: string
status?: string
variant?: string | undefined
variants?: string[]
}
type RunFooterOptions = {
@@ -74,6 +76,7 @@ type RunFooterOptions = {
agentLabel: string
modelLabel: string
model: RunInput["model"]
variant: string | undefined
first: boolean
history?: RunPrompt[]
theme: RunTheme
@@ -84,6 +87,7 @@ type RunFooterOptions = {
onQuestionReject: (input: QuestionReject) => void | Promise<void>
onCycleVariant?: () => CycleResult | void
onModelSelect?: (model: NonNullable<RunInput["model"]>) => CycleResult | void | Promise<CycleResult | void>
onVariantSelect?: (variant: string | undefined) => CycleResult | void | Promise<CycleResult | void>
onInterrupt?: () => void
onExit?: () => void
onSubagentSelect?: (sessionID: string | undefined) => void
@@ -94,6 +98,7 @@ const PERMISSION_ROWS = 12
const QUESTION_ROWS = 14
const COMMAND_ROWS = RUN_COMMAND_PANEL_ROWS
const MODEL_ROWS = RUN_COMMAND_PANEL_ROWS
const VARIANT_ROWS = RUN_COMMAND_PANEL_ROWS
const AUTOCOMPLETE_COMPACT_ROWS = 2
function createEmptySubagentState(): FooterSubagentState {
@@ -174,6 +179,10 @@ export class RunFooter implements FooterApi {
private setProviders: Setter<RunProvider[] | undefined>
private currentModel: Accessor<RunInput["model"]>
private setCurrentModel: Setter<RunInput["model"]>
private variants: Accessor<string[]>
private setVariants: Setter<string[]>
private currentVariant: Accessor<string | undefined>
private setCurrentVariant: Setter<string | undefined>
private state: Accessor<FooterState>
private setState: Setter<FooterState>
private view: Accessor<FooterView>
@@ -224,6 +233,12 @@ export class RunFooter implements FooterApi {
const [currentModel, setCurrentModel] = createSignal<RunInput["model"]>(options.model)
this.currentModel = currentModel
this.setCurrentModel = setCurrentModel
const [variants, setVariants] = createSignal<string[]>([])
this.variants = variants
this.setVariants = setVariants
const [currentVariant, setCurrentVariant] = createSignal(options.variant)
this.currentVariant = currentVariant
this.setCurrentVariant = setCurrentVariant
const [subagent, setSubagent] = createStore<FooterSubagentState>(createEmptySubagentState())
this.subagent = () => subagent
this.setSubagent = (next) => {
@@ -256,6 +271,8 @@ export class RunFooter implements FooterApi {
commands: this.commands,
providers: this.providers,
currentModel: this.currentModel,
variants: this.variants,
currentVariant: this.currentVariant,
theme: options.theme,
diffStyle: options.diffStyle,
keybinds: options.keybinds,
@@ -272,6 +289,7 @@ export class RunFooter implements FooterApi {
onRequestExit: this.setRequestExitHandler,
onExit: () => this.close(),
onModelSelect: this.handleModelSelect,
onVariantSelect: this.handleVariantSelect,
onRows: this.syncRows,
onLayout: this.syncLayout,
onStatus: this.setStatus,
@@ -335,6 +353,16 @@ export class RunFooter implements FooterApi {
return
}
if (next.type === "variants") {
if (this.isGone) {
return
}
this.setVariants(next.variants)
this.setCurrentVariant(next.current)
return
}
const patch = eventPatch(next)
if (patch) {
this.patch(patch)
@@ -537,6 +565,8 @@ export class RunFooter implements FooterApi {
? 1 + tabs + COMMAND_ROWS
: this.promptRoute.type === "model"
? 1 + tabs + MODEL_ROWS
: this.promptRoute.type === "variant"
? 1 + tabs + VARIANT_ROWS
: this.promptRoute.type === "subagent"
? this.base + tabs + SUBAGENT_INSPECTOR_ROWS
: Math.max(base + TEXTAREA_MIN_ROWS, Math.min(base + PROMPT_MAX_ROWS, base + this.rows))
@@ -627,6 +657,14 @@ export class RunFooter implements FooterApi {
status: result.status ?? "variant updated",
}
if ("variants" in result) {
this.setVariants(result.variants ?? [])
}
if ("variant" in result) {
this.setCurrentVariant(result.variant)
}
if (result.modelLabel) {
patch.model = result.modelLabel
}
@@ -654,6 +692,56 @@ export class RunFooter implements FooterApi {
return
}
if ("variants" in result) {
this.setVariants(result.variants ?? [])
}
if ("variant" in result) {
this.setCurrentVariant(result.variant)
}
const patch: FooterPatch = {}
if (result.modelLabel) {
patch.model = result.modelLabel
}
if (result.status) {
patch.status = result.status
}
if (patch.model || patch.status) {
this.patch(patch)
}
})
.catch(() => {})
}
private handleVariantSelect = (variant: string | undefined): void => {
if (this.isClosed) {
return
}
const model = this.currentModel()
void Promise.resolve()
.then(() => this.options.onVariantSelect?.(variant))
.then((result) => {
const current = this.currentModel()
if (
!result ||
this.isClosed ||
(model && (!current || current.providerID !== model.providerID || current.modelID !== model.modelID))
) {
return
}
if ("variants" in result) {
this.setVariants(result.variants ?? [])
}
if ("variant" in result) {
this.setCurrentVariant(result.variant)
}
const patch: FooterPatch = {}
if (result.modelLabel) {
patch.model = result.modelLabel
@@ -15,7 +15,7 @@ import { Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup
import "opentui-spinner/solid"
import { createColors, createFrames } from "../tui/ui/spinner"
import * as Keybind from "@/util/keybind"
import { RunCommandMenuBody, RunModelSelectBody } from "./footer.command"
import { RunCommandMenuBody, RunModelSelectBody, RunVariantSelectBody } from "./footer.command"
import { FOOTER_MENU_ROWS, RunFooterMenu } from "./footer.menu"
import { RunFooterSubagentBody, RunFooterSubagentTabs } from "./footer.subagent"
import { RunPromptBody, createPromptState, hintFlags } from "./footer.prompt"
@@ -63,6 +63,8 @@ type RunFooterViewProps = {
commands: () => RunCommand[] | undefined
providers: () => RunProvider[] | undefined
currentModel: () => RunInput["model"]
variants: () => string[]
currentVariant: () => string | undefined
state: () => FooterState
view?: () => FooterView
subagent?: () => FooterSubagentState
@@ -82,6 +84,7 @@ type RunFooterViewProps = {
onRequestExit?: (fn: (() => boolean) | undefined) => void
onExit: () => void
onModelSelect: (model: NonNullable<RunInput["model"]>) => void
onVariantSelect: (variant: string | undefined) => void
onRows: (rows: number) => void
onLayout: (input: { route: FooterPromptRoute; tabs: boolean; autocomplete: boolean }) => void
onStatus: (text: string) => void
@@ -127,7 +130,8 @@ export function RunFooterView(props: RunFooterViewProps) {
const inspecting = createMemo(() => active().type === "prompt" && route().type === "subagent")
const commanding = createMemo(() => active().type === "prompt" && route().type === "command")
const modeling = createMemo(() => active().type === "prompt" && route().type === "model")
const panel = createMemo(() => commanding() || modeling())
const varianting = createMemo(() => active().type === "prompt" && route().type === "variant")
const panel = createMemo(() => commanding() || modeling() || varianting())
const selected = createMemo(() => {
const current = route()
return current.type === "subagent" ? current.sessionID : undefined
@@ -138,7 +142,7 @@ export function RunFooterView(props: RunFooterViewProps) {
const current = route()
return current.type === "subagent" ? subagent().details[current.sessionID] : undefined
})
const variant = createMemo(() => printableBinding(props.keybinds.variantCycle, props.keybinds.leader))
const command = createMemo(() => printableBinding(props.keybinds.commandList, props.keybinds.leader))
const interrupt = createMemo(() => printableBinding(props.keybinds.interrupt, props.keybinds.leader))
const commandKeys = createMemo(() => Keybind.parse(props.keybinds.commandList))
const hints = createMemo(() => hintFlags(term().width))
@@ -195,6 +199,11 @@ export function RunFooterView(props: RunFooterViewProps) {
props.onSubagentSelect?.(undefined)
}
const openVariant = () => {
setRoute({ type: "variant" })
props.onSubagentSelect?.(undefined)
}
const closePanel = () => {
setRoute({ type: "composer" })
}
@@ -328,7 +337,7 @@ export function RunFooterView(props: RunFooterViewProps) {
}
const current = route()
if (current.type !== "command" && current.type !== "model") {
if (current.type !== "command" && current.type !== "model" && current.type !== "variant") {
return
}
@@ -404,8 +413,14 @@ export function RunFooterView(props: RunFooterViewProps) {
<RunCommandMenuBody
theme={theme}
commands={props.commands}
variants={props.variants}
onClose={closePanel}
onModel={openModel}
onVariant={openVariant}
onVariantCycle={() => {
props.onCycle()
closePanel()
}}
onSlash={(name) => {
composer.replaceDraft(`/${name} `)
closePanel()
@@ -425,6 +440,18 @@ export function RunFooterView(props: RunFooterViewProps) {
}}
/>
</Match>
<Match when={varianting()}>
<RunVariantSelectBody
theme={theme}
variants={props.variants}
current={props.currentVariant}
onClose={closePanel}
onSelect={(variant) => {
props.onVariantSelect(variant)
closePanel()
}}
/>
</Match>
<Match when={active().type === "permission"}>
<RunPermissionBody
request={permission()!.request}
@@ -608,9 +635,9 @@ export function RunFooterView(props: RunFooterViewProps) {
{usage()}
</text>
</Show>
<Show when={variant().length > 0 && hints().variant}>
<text id="run-direct-footer-hint-variant" fg={theme().muted} wrapMode="none" truncate>
{variant()} variant
<Show when={command().length > 0 && hints().command}>
<text id="run-direct-footer-hint-command" fg={theme().text} wrapMode="none" truncate>
{command()} <span style={{ fg: theme().muted }}>commands</span>
</text>
</Show>
</box>
@@ -113,32 +113,27 @@ function footerKeybinds(config: Config | undefined): FooterKeybinds {
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = Effect.fn("RunBoot.config")(() =>
Effect.tryPromise({
try: loadConfig,
catch: () => undefined,
}),
)
const config = Effect.fn("RunBoot.config")(() => Effect.promise(() => loadConfig().catch(() => undefined)))
const resolveModelInfo = Effect.fn("RunBoot.resolveModelInfo")(function* (
sdk: RunInput["sdk"],
directory: string,
model: RunInput["model"],
) {
const connected = yield* Effect.tryPromise({
try: () => sdk.config.providers({ directory }),
catch: () => undefined,
}).pipe(
Effect.map((item) => item?.data?.providers),
const connected = yield* Effect.promise(() =>
sdk.config
.providers({ directory })
.then((item) => item.data?.providers)
.catch(() => undefined),
)
const providers = yield* Effect.promise(() =>
connected
? Promise.resolve(connected)
: sdk.provider
.list()
.then((item) => item.data?.all ?? [])
.catch(() => []),
)
const providers = yield* (connected
? Effect.succeed(connected)
: Effect.tryPromise({
try: () => sdk.provider.list(),
catch: () => undefined,
}).pipe(
Effect.map((item) => item?.data?.all ?? []),
))
const limits = Object.fromEntries(
providers.flatMap((provider) =>
Object.entries(provider.models ?? {}).flatMap(([modelID, info]) => {
@@ -173,10 +168,7 @@ const layer = Layer.effect(
sessionID: string,
model: RunInput["model"],
) {
const session = yield* Effect.tryPromise({
try: () => resolveSession(sdk, sessionID),
catch: () => undefined,
})
const session = yield* Effect.promise(() => resolveSession(sdk, sessionID).catch(() => undefined))
if (!session) {
return emptySessionInfo()
}
@@ -39,6 +39,8 @@ type SplashState = {
type CycleResult = {
modelLabel?: string
status?: string
variant?: string | undefined
variants?: string[]
}
type FooterLabels = {
@@ -66,6 +68,7 @@ export type LifecycleInput = {
onQuestionReject: (input: QuestionReject) => void | Promise<void>
onCycleVariant?: () => CycleResult | void
onModelSelect?: (model: NonNullable<RunInput["model"]>) => CycleResult | void | Promise<CycleResult | void>
onVariantSelect?: (variant: string | undefined) => CycleResult | void | Promise<CycleResult | void>
onInterrupt?: () => void
onSubagentSelect?: (sessionID: string | undefined) => void
}
@@ -222,6 +225,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
sessionID: input.getSessionID ?? (() => input.sessionID),
...labels,
model: input.model,
variant: input.variant,
first: input.first,
history: input.history,
theme,
@@ -233,6 +237,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
onQuestionReject: input.onQuestionReject,
onCycleVariant: input.onCycleVariant,
onModelSelect: input.onModelSelect,
onVariantSelect: input.onVariantSelect,
onInterrupt: input.onInterrupt,
onSubagentSelect: input.onSubagentSelect,
})
+39 -9
View File
@@ -292,6 +292,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
return {
status: state.activeVariant ? `variant ${state.activeVariant}` : "variant default",
modelLabel: formatModelLabel(state.model, state.activeVariant, state.providers),
variant: state.activeVariant,
}
},
onModelSelect: async (model) => {
@@ -329,6 +330,33 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
return {
modelLabel: formatModelLabel(model, state.activeVariant, state.providers),
status: `model ${model.modelID}`,
variant: state.activeVariant,
variants: state.variants,
}
},
onVariantSelect: async (variant) => {
if (!state.model || state.variants.length === 0) {
return {
status: "no variants available",
}
}
if (variant && !state.variants.includes(variant)) {
return {
status: `variant ${variant} unavailable`,
}
}
state.activeVariant = variant
saveVariant(state.model, state.activeVariant)
setRunSpanAttributes(span, {
"opencode.model.variant": state.activeVariant,
})
return {
status: state.activeVariant ? `variant ${state.activeVariant}` : "variant default",
modelLabel: formatModelLabel(state.model, state.activeVariant, state.providers),
variant: state.activeVariant,
variants: state.variants,
}
},
onInterrupt: () => {
@@ -420,20 +448,22 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
state.providers = info.providers
state.variants = variantsFor(state.providers, state.model)
state.limits = info.limits
if (!footer.isClosed) {
footer.event({ type: "models", providers: info.providers })
}
const next = resolveVariant(ctx.variant, session.variant, savedVariant, state.variants)
if (next === state.activeVariant) {
if (next !== state.activeVariant) {
state.activeVariant = next
setRunSpanAttributes(span, {
"opencode.model.variant": state.activeVariant,
})
}
if (footer.isClosed) {
return
}
state.activeVariant = next
setRunSpanAttributes(span, {
"opencode.model.variant": state.activeVariant,
})
if (!state.model || footer.isClosed) {
footer.event({ type: "models", providers: info.providers })
footer.event({ type: "variants", variants: state.variants, current: state.activeVariant })
if (!state.model) {
return
}
@@ -163,6 +163,7 @@ export type FooterPromptRoute =
| { type: "subagent"; sessionID: string }
| { type: "command" }
| { type: "model" }
| { type: "variant" }
export type FooterSubagentTab = {
sessionID: string
@@ -209,6 +210,11 @@ export type FooterEvent =
type: "models"
providers: RunProvider[]
}
| {
type: "variants"
variants: string[]
current: string | undefined
}
| {
type: "queue"
queue: number
@@ -2,7 +2,7 @@
import { expect, test } from "bun:test"
import { testRender } from "@opentui/solid"
import { createSignal } from "solid-js"
import { RUN_COMMAND_PANEL_ROWS, RunCommandMenuBody, RunModelSelectBody } from "@/cli/cmd/run/footer.command"
import { RUN_COMMAND_PANEL_ROWS, RunCommandMenuBody, RunModelSelectBody, RunVariantSelectBody } from "@/cli/cmd/run/footer.command"
import { RunEntryContent } from "@/cli/cmd/run/scrollback.writer"
import { RUN_THEME_FALLBACK } from "@/cli/cmd/run/theme"
import type { RunCommand, RunInput, RunProvider, StreamCommit } from "@/cli/cmd/run/types"
@@ -17,7 +17,13 @@ function command(input: { name: string; description: string; source?: "command"
} satisfies RunCommand
}
function model(input: { id: string; name: string; status?: "active" | "deprecated"; cost?: number }) {
function model(input: {
id: string
name: string
status?: "active" | "deprecated"
cost?: number
variants?: Record<string, Record<string, never>>
}) {
return {
id: input.id,
providerID: "opencode",
@@ -64,6 +70,7 @@ function model(input: { id: string; name: string; status?: "active" | "deprecate
options: {},
headers: {},
release_date: "2026-01-01",
variants: input.variants,
} satisfies RunProvider["models"][string]
}
@@ -75,7 +82,7 @@ function provider() {
env: [],
options: {},
models: {
"gpt-5": model({ id: "gpt-5", name: "GPT-5" }),
"gpt-5": model({ id: "gpt-5", name: "GPT-5", variants: { high: {}, minimal: {} } }),
"gpt-free": model({ id: "gpt-free", name: "GPT Free", cost: 0 }),
old: model({ id: "old", name: "Old Model", status: "deprecated" }),
},
@@ -129,14 +136,18 @@ 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 [variants] = createSignal(["high", "minimal"])
const app = await testRender(() => (
<box width={100} height={RUN_COMMAND_PANEL_ROWS}>
<RunCommandMenuBody
theme={() => RUN_THEME_FALLBACK.footer}
commands={commands}
variants={variants}
onClose={() => {}}
onModel={() => {}}
onVariant={() => {}}
onVariantCycle={() => {}}
onSlash={() => {}}
onExit={() => {}}
/>
@@ -154,6 +165,8 @@ test("direct command panel renders grouped command palette", async () => {
expect(frame).toContain("Search")
expect(frame).toContain("Suggested")
expect(frame).toContain("Switch model")
expect(frame).toContain("Variant cycle")
expect(frame).toContain("Switch model variant")
expect(frame).toContain("Session")
expect(frame).toContain("/new")
expect(frame).toContain("Project Commands")
@@ -199,3 +212,36 @@ test("direct model panel renders current model selector", async () => {
app.renderer.destroy()
}
})
test("direct variant panel renders current variant selector", async () => {
const [variants] = createSignal(["high", "minimal"])
const [current] = createSignal<string | undefined>("high")
const app = await testRender(() => (
<box width={100} height={RUN_COMMAND_PANEL_ROWS}>
<RunVariantSelectBody
theme={() => RUN_THEME_FALLBACK.footer}
variants={variants}
current={current}
onClose={() => {}}
onSelect={() => {}}
/>
</box>
), {
width: 100,
height: RUN_COMMAND_PANEL_ROWS,
})
try {
await app.renderOnce()
const frame = app.captureCharFrame()
expect(frame).toContain("Select variant")
expect(frame).toContain("Default")
expect(frame).toContain("high")
expect(frame).toContain("minimal")
expect(frame).toContain("current")
} finally {
app.renderer.destroy()
}
})
@@ -208,14 +208,7 @@ describe("run runtime boot", () => {
default: {},
connected: [],
}
spyOn(sdk.config, "providers").mockImplementation(() =>
Promise.resolve({
data: undefined,
error: undefined,
request: new Request("https://opencode.test"),
response: new Response(),
}),
)
spyOn(sdk.config, "providers").mockRejectedValue(new Error("boom"))
spyOn(sdk.provider, "list").mockImplementation(() =>
Promise.resolve({
data,