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()