Просмотр исходного кода

fix: tui crash when no authed providers and default provider disabled (#4964)

Aiden Cline 2 месяцев назад
Родитель
Сommit
86522f1b3e

+ 15 - 3
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(() => <DialogProviderList />)
+      },
+    ),
+  )
+
   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,

+ 26 - 5
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(() => <DialogProviderConnect />)
+    }
+  }
+
   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"),

+ 27 - 11
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 {

+ 11 - 2
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()