diff --git a/packages/opencode/src/cli/cmd/run/footer.command.tsx b/packages/opencode/src/cli/cmd/run/footer.command.tsx index 0f4debb702..bbb53e47e1 100644 --- a/packages/opencode/src/cli/cmd/run/footer.command.tsx +++ b/packages/opencode/src/cli/cmd/run/footer.command.tsx @@ -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 const PANEL_PAD = 2 @@ -265,8 +275,11 @@ function PanelShell(props: { export function RunCommandMenuBody(props: { theme: Accessor commands: Accessor + variants: Accessor 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 + variants: Accessor + current: Accessor + onClose: () => void + onSelect: (variant: string | undefined) => void +}) { + let field: InputRenderable | undefined + const [query, setQuery] = createSignal("") + const entries = createMemo(() => [ + { + 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(() => 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 ( + { + field = input + }} + onQuery={setQuery} + > + PANEL_LIST_ROWS} + limit={PANEL_LIST_ROWS} + empty="No results found" + border={false} + paddingLeft={PANEL_PAD} + paddingRight={PANEL_PAD} + grouped={false} + /> + + ) +} + export function RunModelSelectBody(props: { theme: Accessor providers: Accessor diff --git a/packages/opencode/src/cli/cmd/run/footer.prompt.tsx b/packages/opencode/src/cli/cmd/run/footer.prompt.tsx index ab3a64c9f1..c2ebebe8b3 100644 --- a/packages/opencode/src/cli/cmd/run/footer.prompt.tsx +++ b/packages/opencode/src/cli/cmd/run/footer.prompt.tsx @@ -39,7 +39,7 @@ export const HINT_BREAKPOINTS = { send: 50, newline: 66, history: 80, - variant: 95, + command: 95, } type Mention = Extract @@ -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 } diff --git a/packages/opencode/src/cli/cmd/run/footer.ts b/packages/opencode/src/cli/cmd/run/footer.ts index 70720a579d..b98323385b 100644 --- a/packages/opencode/src/cli/cmd/run/footer.ts +++ b/packages/opencode/src/cli/cmd/run/footer.ts @@ -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 onCycleVariant?: () => CycleResult | void onModelSelect?: (model: NonNullable) => CycleResult | void | Promise + onVariantSelect?: (variant: string | undefined) => CycleResult | void | Promise 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 private currentModel: Accessor private setCurrentModel: Setter + private variants: Accessor + private setVariants: Setter + private currentVariant: Accessor + private setCurrentVariant: Setter private state: Accessor private setState: Setter private view: Accessor @@ -224,6 +233,12 @@ export class RunFooter implements FooterApi { const [currentModel, setCurrentModel] = createSignal(options.model) this.currentModel = currentModel this.setCurrentModel = setCurrentModel + const [variants, setVariants] = createSignal([]) + this.variants = variants + this.setVariants = setVariants + const [currentVariant, setCurrentVariant] = createSignal(options.variant) + this.currentVariant = currentVariant + this.setCurrentVariant = setCurrentVariant const [subagent, setSubagent] = createStore(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 diff --git a/packages/opencode/src/cli/cmd/run/footer.view.tsx b/packages/opencode/src/cli/cmd/run/footer.view.tsx index 765ee90caa..1044e0916e 100644 --- a/packages/opencode/src/cli/cmd/run/footer.view.tsx +++ b/packages/opencode/src/cli/cmd/run/footer.view.tsx @@ -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) => 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) { { + props.onCycle() + closePanel() + }} onSlash={(name) => { composer.replaceDraft(`/${name} `) closePanel() @@ -425,6 +440,18 @@ export function RunFooterView(props: RunFooterViewProps) { }} /> + + { + props.onVariantSelect(variant) + closePanel() + }} + /> + - 0 && hints().variant}> - - {variant()} variant + 0 && hints().command}> + + {command()} commands diff --git a/packages/opencode/src/cli/cmd/run/runtime.boot.ts b/packages/opencode/src/cli/cmd/run/runtime.boot.ts index 6e9dc4618e..5109b8c0cd 100644 --- a/packages/opencode/src/cli/cmd/run/runtime.boot.ts +++ b/packages/opencode/src/cli/cmd/run/runtime.boot.ts @@ -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() } diff --git a/packages/opencode/src/cli/cmd/run/runtime.lifecycle.ts b/packages/opencode/src/cli/cmd/run/runtime.lifecycle.ts index 72b92908fe..c02f8fa5a5 100644 --- a/packages/opencode/src/cli/cmd/run/runtime.lifecycle.ts +++ b/packages/opencode/src/cli/cmd/run/runtime.lifecycle.ts @@ -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 onCycleVariant?: () => CycleResult | void onModelSelect?: (model: NonNullable) => CycleResult | void | Promise + onVariantSelect?: (variant: string | undefined) => CycleResult | void | Promise onInterrupt?: () => void onSubagentSelect?: (sessionID: string | undefined) => void } @@ -222,6 +225,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise 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 { 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 { 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 { 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 } diff --git a/packages/opencode/src/cli/cmd/run/types.ts b/packages/opencode/src/cli/cmd/run/types.ts index 3560e444b2..022e0db3d7 100644 --- a/packages/opencode/src/cli/cmd/run/types.ts +++ b/packages/opencode/src/cli/cmd/run/types.ts @@ -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 diff --git a/packages/opencode/test/cli/run/footer.view.test.tsx b/packages/opencode/test/cli/run/footer.view.test.tsx index 32f0ee5fe1..99602b71b5 100644 --- a/packages/opencode/test/cli/run/footer.view.test.tsx +++ b/packages/opencode/test/cli/run/footer.view.test.tsx @@ -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> +}) { 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(() => ( 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("high") + + const app = await testRender(() => ( + + RUN_THEME_FALLBACK.footer} + variants={variants} + current={current} + onClose={() => {}} + onSelect={() => {}} + /> + + ), { + 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() + } +}) diff --git a/packages/opencode/test/cli/run/runtime.boot.test.ts b/packages/opencode/test/cli/run/runtime.boot.test.ts index 025fc20aa6..5fa67ce716 100644 --- a/packages/opencode/test/cli/run/runtime.boot.test.ts +++ b/packages/opencode/test/cli/run/runtime.boot.test.ts @@ -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,