فهرست منبع

fix(tui): fail fast on invalid session startup (#23837)

Shoubhit Dash 4 ماه پیش
والد
کامیت
6196b81e0a

+ 16 - 0
packages/opencode/src/cli/cmd/tui/attach.ts

@@ -3,6 +3,8 @@ import { UI } from "@/cli/ui"
 import { tui } from "./app"
 import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
 import { TuiConfig } from "@/cli/cmd/tui/config/tui"
+import { errorMessage } from "@/util/error"
+import { validateSession } from "./validate-session"
 
 export const AttachCommand = cmd({
   command: "attach <url>",
@@ -65,6 +67,20 @@ export const AttachCommand = cmd({
         return { Authorization: auth }
       })()
       const config = await TuiConfig.get()
+
+      try {
+        await validateSession({
+          url: args.url,
+          sessionID: args.session,
+          directory,
+          headers,
+        })
+      } catch (error) {
+        UI.error(errorMessage(error))
+        process.exitCode = 1
+        return
+      }
+
       await tui({
         url: args.url,
         config,

+ 34 - 21
packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

@@ -68,6 +68,7 @@ import { Flag } from "@/flag/flag"
 import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
 import parsers from "../../../../../../parsers-config.ts"
 import * as Clipboard from "../../util/clipboard"
+import { errorMessage } from "@/util/error"
 import { Toast, useToast } from "../../ui/toast"
 import { useKV } from "../../context/kv.tsx"
 import * as Editor from "../../util/editor"
@@ -180,31 +181,43 @@ export function Session() {
   const toast = useToast()
   const sdk = useSDK()
 
-  createEffect(async () => {
-    const previousWorkspace = project.workspace.current()
-    const result = await sdk.client.session.get({ sessionID: route.sessionID }, { throwOnError: true })
-    if (!result.data) {
+  createEffect(() => {
+    const sessionID = route.sessionID
+    void (async () => {
+      const previousWorkspace = project.workspace.current()
+      const result = await sdk.client.session.get({ sessionID }, { throwOnError: true })
+      if (!result.data) {
+        toast.show({
+          message: `Session not found: ${sessionID}`,
+          variant: "error",
+          duration: 5000,
+        })
+        navigate({ type: "home" })
+        return
+      }
+
+      if (result.data.workspaceID !== previousWorkspace) {
+        project.workspace.set(result.data.workspaceID)
+
+        // Sync all the data for this workspace. Note that this
+        // workspace may not exist anymore which is why this is not
+        // fatal. If it doesn't we still want to show the session
+        // (which will be non-interactive)
+        try {
+          await sync.bootstrap({ fatal: false })
+        } catch {}
+      }
+      await sync.session.sync(sessionID)
+      if (route.sessionID === sessionID && scroll) scroll.scrollBy(100_000)
+    })().catch((error) => {
+      if (route.sessionID !== sessionID) return
       toast.show({
-        message: `Session not found: ${route.sessionID}`,
+        message: errorMessage(error),
         variant: "error",
+        duration: 5000,
       })
       navigate({ type: "home" })
-      return
-    }
-
-    if (result.data.workspaceID !== previousWorkspace) {
-      project.workspace.set(result.data.workspaceID)
-
-      // Sync all the data for this workspace. Note that this
-      // workspace may not exist anymore which is why this is not
-      // fatal. If it doesn't we still want to show the session
-      // (which will be non-interactive)
-      try {
-        await sync.bootstrap({ fatal: false })
-      } catch (e) {}
-    }
-    await sync.session.sync(route.sessionID)
-    if (scroll) scroll.scrollBy(100_000)
+    })
   })
 
   let lastSwitch: string | undefined = undefined

+ 14 - 0
packages/opencode/src/cli/cmd/tui/thread.ts

@@ -16,6 +16,7 @@ import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
 import { writeHeapSnapshot } from "v8"
 import { TuiConfig } from "./config/tui"
 import { OPENCODE_PROCESS_ROLE, OPENCODE_RUN_ID, ensureRunID, sanitizedProcessEnv } from "@/util/opencode-process"
+import { validateSession } from "./validate-session"
 
 declare global {
   const OPENCODE_WORKER_PATH: string
@@ -202,6 +203,19 @@ export const TuiThreadCommand = cmd({
             events: createEventSource(client),
           }
 
+      try {
+        await validateSession({
+          url: transport.url,
+          sessionID: args.session,
+          directory: cwd,
+          fetch: transport.fetch,
+        })
+      } catch (error) {
+        UI.error(errorMessage(error))
+        process.exitCode = 1
+        return
+      }
+
       setTimeout(() => {
         client.call("checkUpgrade", { directory: cwd }).catch(() => {})
       }, 1000).unref?.()

+ 24 - 0
packages/opencode/src/cli/cmd/tui/validate-session.ts

@@ -0,0 +1,24 @@
+import { createOpencodeClient } from "@opencode-ai/sdk/v2"
+import { SessionID } from "@/session/schema"
+
+export async function validateSession(input: {
+  url: string
+  sessionID?: string
+  directory?: string
+  fetch?: typeof fetch
+  headers?: RequestInit["headers"]
+}) {
+  if (!input.sessionID) return
+
+  const result = SessionID.zod.safeParse(input.sessionID)
+  if (!result.success) {
+    throw new Error(`Invalid session ID: ${result.error.issues.at(0)?.message ?? "unknown error"}`)
+  }
+
+  await createOpencodeClient({
+    baseUrl: input.url,
+    directory: input.directory,
+    fetch: input.fetch,
+    headers: input.headers,
+  }).session.get({ sessionID: result.data }, { throwOnError: true })
+}

+ 9 - 0
packages/opencode/src/util/error.ts

@@ -26,6 +26,15 @@ export function errorMessage(error: unknown): string {
     return error.message
   }
 
+  if (
+    isRecord(error) &&
+    isRecord(error.data) &&
+    typeof error.data.message === "string" &&
+    error.data.message
+  ) {
+    return error.data.message
+  }
+
   const text = String(error)
   if (text && text !== "[object Object]") return text