summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-22 19:16:50 -0600
committerAdam <[email protected]>2025-12-22 19:16:54 -0600
commit680a63e3dee5bf2748eaf844b4e9b838d2859f16 (patch)
tree9b6c4de0b0f2c664a1075c58e5f1c39b125f4590 /packages
parent3a54ab68d1f9684d86434dd6e890d43c63adf461 (diff)
downloadopencode-680a63e3dee5bf2748eaf844b4e9b838d2859f16.tar.gz
opencode-680a63e3dee5bf2748eaf844b4e9b838d2859f16.zip
fix(desktop): better error messages on connection failure
Diffstat (limited to 'packages')
-rw-r--r--packages/desktop/src/app.tsx6
-rw-r--r--packages/desktop/src/context/global-sync.tsx9
-rw-r--r--packages/desktop/src/pages/error.tsx40
-rw-r--r--packages/opencode/src/server/server.ts22
-rw-r--r--packages/sdk/js/src/v2/gen/sdk.gen.ts13
-rw-r--r--packages/sdk/js/src/v2/gen/types.gen.ts19
6 files changed, 97 insertions, 12 deletions
diff --git a/packages/desktop/src/app.tsx b/packages/desktop/src/app.tsx
index 9d000b8ff..11216643e 100644
--- a/packages/desktop/src/app.tsx
+++ b/packages/desktop/src/app.tsx
@@ -35,10 +35,10 @@ const url = iife(() => {
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
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() {
diff --git a/packages/desktop/src/context/global-sync.tsx b/packages/desktop/src/context/global-sync.tsx
index 27a89e7bc..ae40555d6 100644
--- a/packages/desktop/src/context/global-sync.tsx
+++ b/packages/desktop/src/context/global-sync.tsx
@@ -295,6 +295,15 @@ function createGlobalSync() {
})
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([
retry(() =>
globalSDK.client.path.get().then((x) => {
diff --git a/packages/desktop/src/pages/error.tsx b/packages/desktop/src/pages/error.tsx
index c7330c298..9914279ad 100644
--- a/packages/desktop/src/pages/error.tsx
+++ b/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"
- const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
-
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) {
- 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) {
- 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")
}
- 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)
}
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index e92c46225..1da1d1ec6 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -47,6 +47,7 @@ import { SessionStatus } from "@/session/status"
import { upgradeWebSocket, websocket } from "hono/bun"
import { errors } from "./error"
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
globalThis.AI_SDK_LOG_WARNINGS = false
@@ -97,6 +98,27 @@ export namespace Server {
})
.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(
"/global/event",
describeRoute({
summary: "Get global events",
diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts
index fa7a86463..97bc92b86 100644
--- a/packages/sdk/js/src/v2/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts
@@ -30,6 +30,7 @@ import type {
FormatterStatusResponses,
GlobalDisposeResponses,
GlobalEventResponses,
+ GlobalHealthResponses,
InstanceDisposeResponses,
LspStatusResponses,
McpAddErrors,
@@ -189,6 +190,18 @@ class HeyApiRegistry<T> {
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
*
* Subscribe to global events from the OpenCode system using server-sent events.
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 403f1d6a0..c5f11c885 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -1897,6 +1897,25 @@ export type 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 = {
body?: never
path?: never