Przeglądaj źródła

fix(desktop): better error messages on connection failure

Adam 2 miesięcy temu
rodzic
commit
680a63e3de

+ 3 - 3
packages/desktop/src/app.tsx

@@ -35,10 +35,10 @@ const url = iife(() => {
 
 
   if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
   if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
   if (window.__OPENCODE__) return `http://127.0.0.1:${window.__OPENCODE__.port}`
   if (window.__OPENCODE__) return `http://127.0.0.1:${window.__OPENCODE__.port}`
-  if (import.meta.env.VITE_OPENCODE_SERVER)
-    return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
+  if (import.meta.env.DEV)
+    return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
 
 
-  return "/"
+  return "http://localhost:4096"
 })
 })
 
 
 export function App() {
 export function App() {

+ 9 - 0
packages/desktop/src/context/global-sync.tsx

@@ -295,6 +295,15 @@ function createGlobalSync() {
   })
   })
 
 
   async function bootstrap() {
   async function bootstrap() {
+    const health = await globalSDK.client.global.health().then((x) => x.data)
+    if (!health?.healthy) {
+      setGlobalStore(
+        "error",
+        new Error(`Could not connect to server. Is there a server running at \`${globalSDK.url}\`?`),
+      )
+      return
+    }
+
     return Promise.all([
     return Promise.all([
       retry(() =>
       retry(() =>
         globalSDK.client.path.get().then((x) => {
         globalSDK.client.path.get().then((x) => {

+ 31 - 9
packages/desktop/src/pages/error.tsx

@@ -62,27 +62,49 @@ function formatInitError(error: InitError): string {
   }
   }
 }
 }
 
 
-function formatErrorChain(error: unknown, depth = 0): string {
+function formatErrorChain(error: unknown, depth = 0, parentMessage?: string): string {
   if (!error) return "Unknown error"
   if (!error) return "Unknown error"
 
 
-  const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
-
   if (isInitError(error)) {
   if (isInitError(error)) {
-    return indent + formatInitError(error)
+    const message = formatInitError(error)
+    if (depth > 0 && parentMessage === message) return ""
+    const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
+    return indent + message
   }
   }
 
 
   if (error instanceof Error) {
   if (error instanceof Error) {
-    const parts = [indent + `${error.name}: ${error.message}`]
-    if (error.stack) {
-      parts.push(error.stack)
+    const isDuplicate = depth > 0 && parentMessage === error.message
+    const parts: string[] = []
+    const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
+
+    if (!isDuplicate) {
+      // Stack already includes error name and message, so prefer it
+      parts.push(indent + (error.stack ?? `${error.name}: ${error.message}`))
+    } else if (error.stack) {
+      // Duplicate message - only show the stack trace lines (skip message)
+      const trace = error.stack.split("\n").slice(1).join("\n").trim()
+      if (trace) {
+        parts.push(trace)
+      }
     }
     }
+
     if (error.cause) {
     if (error.cause) {
-      parts.push(formatErrorChain(error.cause, depth + 1))
+      const causeResult = formatErrorChain(error.cause, depth + 1, error.message)
+      if (causeResult) {
+        parts.push(causeResult)
+      }
     }
     }
+
     return parts.join("\n\n")
     return parts.join("\n\n")
   }
   }
 
 
-  if (typeof error === "string") return indent + error
+  if (typeof error === "string") {
+    if (depth > 0 && parentMessage === error) return ""
+    const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
+    return indent + error
+  }
+
+  const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
   return indent + JSON.stringify(error, null, 2)
   return indent + JSON.stringify(error, null, 2)
 }
 }
 
 

+ 22 - 0
packages/opencode/src/server/server.ts

@@ -47,6 +47,7 @@ import { SessionStatus } from "@/session/status"
 import { upgradeWebSocket, websocket } from "hono/bun"
 import { upgradeWebSocket, websocket } from "hono/bun"
 import { errors } from "./error"
 import { errors } from "./error"
 import { Pty } from "@/pty"
 import { Pty } from "@/pty"
+import { Installation } from "@/installation"
 
 
 // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
 // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
 globalThis.AI_SDK_LOG_WARNINGS = false
 globalThis.AI_SDK_LOG_WARNINGS = false
@@ -96,6 +97,27 @@ export namespace Server {
         }
         }
       })
       })
       .use(cors())
       .use(cors())
+      .get(
+        "/global/health",
+        describeRoute({
+          summary: "Get health",
+          description: "Get health information about the OpenCode server.",
+          operationId: "global.health",
+          responses: {
+            200: {
+              description: "Health information",
+              content: {
+                "application/json": {
+                  schema: resolver(z.object({ healthy: z.literal(true), version: z.string() })),
+                },
+              },
+            },
+          },
+        }),
+        async (c) => {
+          return c.json({ healthy: true, version: Installation.VERSION })
+        },
+      )
       .get(
       .get(
         "/global/event",
         "/global/event",
         describeRoute({
         describeRoute({

+ 13 - 0
packages/sdk/js/src/v2/gen/sdk.gen.ts

@@ -30,6 +30,7 @@ import type {
   FormatterStatusResponses,
   FormatterStatusResponses,
   GlobalDisposeResponses,
   GlobalDisposeResponses,
   GlobalEventResponses,
   GlobalEventResponses,
+  GlobalHealthResponses,
   InstanceDisposeResponses,
   InstanceDisposeResponses,
   LspStatusResponses,
   LspStatusResponses,
   McpAddErrors,
   McpAddErrors,
@@ -188,6 +189,18 @@ class HeyApiRegistry<T> {
 }
 }
 
 
 export class Global extends HeyApiClient {
 export class Global extends HeyApiClient {
+  /**
+   * Get health
+   *
+   * Get health information about the OpenCode server.
+   */
+  public health<ThrowOnError extends boolean = false>(options?: Options<never, ThrowOnError>) {
+    return (options?.client ?? this.client).get<GlobalHealthResponses, unknown, ThrowOnError>({
+      url: "/global/health",
+      ...options,
+    })
+  }
+
   /**
   /**
    * Get global events
    * Get global events
    *
    *

+ 19 - 0
packages/sdk/js/src/v2/gen/types.gen.ts

@@ -1897,6 +1897,25 @@ export type WellKnownAuth = {
 
 
 export type Auth = OAuth | ApiAuth | WellKnownAuth
 export type Auth = OAuth | ApiAuth | WellKnownAuth
 
 
+export type GlobalHealthData = {
+  body?: never
+  path?: never
+  query?: never
+  url: "/global/health"
+}
+
+export type GlobalHealthResponses = {
+  /**
+   * Health information
+   */
+  200: {
+    healthy: true
+    version: string
+  }
+}
+
+export type GlobalHealthResponse = GlobalHealthResponses[keyof GlobalHealthResponses]
+
 export type GlobalEventData = {
 export type GlobalEventData = {
   body?: never
   body?: never
   path?: never
   path?: never