diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index cf57f4408d..3b4bda7f27 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -6,6 +6,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { Keybind } from "@opencode-ai/ui/keybind" import { Spinner } from "@opencode-ai/ui/spinner" import { showToast } from "@opencode-ai/ui/toast" +import { StatusPopover } from "../status-popover" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { getFilename } from "@opencode-ai/shared/util/path" import { createEffect, createMemo, For, onCleanup, onMount, Show } from "solid-js" @@ -24,7 +25,6 @@ import { useSessionLayout } from "@/pages/session/session-layout" import { messageAgentColor } from "@/utils/agent" import { decode64 } from "@/utils/base64" import { Persist, persisted } from "@/utils/persist" -import { StatusPopover } from "../status-popover" const OPEN_APPS = [ "vscode", diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx index f680315486..372f1dad29 100644 --- a/packages/app/src/context/server.tsx +++ b/packages/app/src/context/server.tsx @@ -200,7 +200,20 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( const isReady = createMemo(() => ready() && !!state.active) - const check = (conn: ServerConnection.Any) => checkServerHealth(conn.http).then((x) => x.healthy) + const check = (conn: ServerConnection.Any) => + checkServerHealth(conn.http).then((x) => { + if (!x.healthy) { + // Loud: makes it trivial to see why a server shows red in the + // status popover / switcher. The dot only goes red when this + // returns false; otherwise undefined (gray) is emitted first. + console.warn("[server health] unhealthy", { + key: ServerConnection.key(conn), + url: conn.http.url, + hasAuth: !!(conn.http.username || conn.http.password), + }) + } + return x.healthy + }) createEffect(() => { const current_ = current() @@ -211,6 +224,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( return } setState("healthy", undefined) + console.log("[server health] start polling", { + key: ServerConnection.key(current_), + url: current_.http.url, + }) onCleanup(startHealthPolling(current_)) }) diff --git a/packages/desktop-electron/src/renderer/index.tsx b/packages/desktop-electron/src/renderer/index.tsx index dee4990dcf..e9b105aa39 100644 --- a/packages/desktop-electron/src/renderer/index.tsx +++ b/packages/desktop-electron/src/renderer/index.tsx @@ -29,8 +29,36 @@ window.addEventListener("error", (event) => { window.addEventListener("unhandledrejection", (event) => { const reason = event.reason - const stack = reason instanceof Error ? reason.stack : null - console.error("[renderer unhandled rejection]", stack ?? reason) + // Log as much as possible: stack for Errors, JSON for plain objects with + // a fallback to a tagged shape so we never end up with just + // "[object Object]" in main.log. + if (reason instanceof Error) { + console.error("[renderer unhandled rejection]", reason.stack ?? reason.message ?? String(reason)) + return + } + let serialized: string + try { + serialized = JSON.stringify( + reason, + (_key, value) => { + if (value instanceof Error) { + return { __error: true, name: value.name, message: value.message, stack: value.stack } + } + return value + }, + 2, + ) + } catch { + serialized = String(reason) + } + console.error( + "[renderer unhandled rejection]", + `type=${typeof reason}`, + `ctor=${reason?.constructor?.name ?? "null"}`, + `keys=${reason && typeof reason === "object" ? Object.keys(reason).join(",") : "n/a"}`, + "value:", + serialized, + ) }) import {