From 86522f1b3e0a702aa4a90ca63cf3f48589c78171 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 1 Dec 2025 16:35:40 -0800 Subject: [PATCH] fix: tui crash when no authed providers and default provider disabled (#4964) --- packages/opencode/src/cli/cmd/tui/app.tsx | 18 +++++++-- .../cli/cmd/tui/component/prompt/index.tsx | 31 ++++++++++++--- .../src/cli/cmd/tui/context/local.tsx | 38 +++++++++++++------ .../src/cli/cmd/tui/routes/session/index.tsx | 13 ++++++- 4 files changed, 79 insertions(+), 21 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 7d892f0cc8..5b7f522534 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -2,7 +2,7 @@ import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentu import { Clipboard } from "@tui/util/clipboard" import { TextAttributes } from "@opentui/core" import { RouteProvider, useRoute } from "@tui/context/route" -import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show } from "solid-js" +import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js" import { Installation } from "@/installation" import { Global } from "@/global" import { DialogProvider, useDialog } from "@tui/ui/dialog" @@ -197,6 +197,17 @@ function App() { } }) + createEffect( + on( + () => sync.status === "complete" && sync.data.provider.length === 0, + (isEmpty, wasEmpty) => { + // only trigger when we transition into an empty-provider state + if (!isEmpty || wasEmpty) return + dialog.replace(() => ) + }, + ), + ) + command.register(() => [ { title: "Switch session", @@ -367,8 +378,9 @@ function App() { ]) createEffect(() => { - const providerID = local.model.current().providerID - if (providerID === "openrouter" && !kv.get("openrouter_warning", false)) { + const currentModel = local.model.current() + if (!currentModel) return + if (currentModel.providerID === "openrouter" && !kv.get("openrouter_warning", false)) { untrack(() => { DialogAlert.show( dialog, diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index b4dc26168a..7271e2fc69 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -22,6 +22,9 @@ import { TuiEvent } from "../../event" import { iife } from "@/util/iife" import { Locale } from "@/util/locale" import { createColors, createFrames } from "../../ui/spinner.ts" +import { useDialog } from "@tui/ui/dialog" +import { DialogProvider as DialogProviderConnect } from "../dialog-provider" +import { useToast } from "../../ui/toast" export type PromptProps = { sessionID?: string @@ -50,12 +53,25 @@ export function Prompt(props: PromptProps) { const sdk = useSDK() const route = useRoute() const sync = useSync() + const dialog = useDialog() + const toast = useToast() const status = createMemo(() => sync.data.session_status[props.sessionID ?? ""] ?? { type: "idle" }) const history = usePromptHistory() const command = useCommandDialog() const renderer = useRenderer() const { theme, syntax } = useTheme() + function promptModelWarning() { + toast.show({ + variant: "warning", + message: "Connect a provider to send prompts", + duration: 3000, + }) + if (sync.data.provider.length === 0) { + dialog.replace(() => ) + } + } + const textareaKeybindings = createMemo(() => { const newlineBindings = keybind.all.input_newline || [] const submitBindings = keybind.all.input_submit || [] @@ -388,6 +404,11 @@ export function Prompt(props: PromptProps) { if (props.disabled) return if (autocomplete.visible) return if (!store.prompt.input) return + const selectedModel = local.model.current() + if (!selectedModel) { + promptModelWarning() + return + } const sessionID = props.sessionID ? props.sessionID : await (async () => { @@ -424,8 +445,8 @@ export function Prompt(props: PromptProps) { body: { agent: local.agent.current().name, model: { - providerID: local.model.current().providerID, - modelID: local.model.current().modelID, + providerID: selectedModel.providerID, + modelID: selectedModel.modelID, }, command: inputText, }, @@ -448,7 +469,7 @@ export function Prompt(props: PromptProps) { command: command.slice(1), arguments: args.join(" "), agent: local.agent.current().name, - model: `${local.model.current().providerID}/${local.model.current().modelID}`, + model: `${selectedModel.providerID}/${selectedModel.modelID}`, messageID, }, }) @@ -458,10 +479,10 @@ export function Prompt(props: PromptProps) { id: sessionID, }, body: { - ...local.model.current(), + ...selectedModel, messageID, agent: local.agent.current().name, - model: local.model.current(), + model: selectedModel, parts: [ { id: Identifier.ascending("part"), diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index 1703b365d2..3bfedf34eb 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -175,8 +175,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ return item } } + const provider = sync.data.provider[0] - const model = sync.data.provider_default[provider.id] ?? Object.values(provider.models)[0].id + if (!provider) return undefined + const defaultModel = sync.data.provider_default[provider.id] + const firstModel = Object.values(provider.models)[0] + const model = defaultModel ?? firstModel?.id + if (!model) return undefined return { providerID: provider.id, modelID: model, @@ -185,11 +190,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const currentModel = createMemo(() => { const a = agent.current() - return getFirstValidModel( - () => modelStore.model[a.name], - () => a.model, - fallbackModel, - )! + return ( + getFirstValidModel( + () => modelStore.model[a.name], + () => a.model, + fallbackModel, + ) ?? undefined + ) }) return { @@ -205,11 +212,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }, parsed: createMemo(() => { const value = currentModel() - const provider = sync.data.provider.find((x) => x.id === value.providerID)! - const model = provider.models[value.modelID] + if (!value) { + return { + provider: "Connect a provider", + model: "No provider selected", + } + } + const provider = sync.data.provider.find((x) => x.id === value.providerID) + const info = provider?.models[value.modelID] return { - provider: provider.name ?? value.providerID, - model: model.name ?? value.modelID, + provider: provider?.name ?? value.providerID, + model: info?.name ?? value.modelID, } }), cycle(direction: 1 | -1) { @@ -236,7 +249,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ return } const current = currentModel() - let index = favorites.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID) + let index = -1 + if (current) { + index = favorites.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID) + } if (index === -1) { index = direction === 1 ? 0 : favorites.length - 1 } else { diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index baa6631fbe..2338604e3c 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -269,13 +269,22 @@ export function Session() { keybind: "session_compact", category: "Session", onSelect: (dialog) => { + const selectedModel = local.model.current() + if (!selectedModel) { + toast.show({ + variant: "warning", + message: "Connect a provider to summarize this session", + duration: 3000, + }) + return + } sdk.client.session.summarize({ path: { id: route.sessionID, }, body: { - modelID: local.model.current().modelID, - providerID: local.model.current().providerID, + modelID: selectedModel.modelID, + providerID: selectedModel.providerID, }, }) dialog.clear()