From 49e70746f07edb0b13a58879445627d472cc1cb9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 10 Dec 2025 01:27:47 +0000 Subject: [PATCH] webchat: show real ws errors --- .../Clawdis/Resources/WebChat/bootstrap.js | 4 ++- .../Clawdis/Resources/WebChat/format-error.js | 31 +++++++++++++++++ .../Resources/WebChat/webchat.bundle.js | 33 ++++++++++++++++++- test/format-error.test.ts | 30 +++++++++++++++++ vitest.config.ts | 2 +- 5 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 apps/macos/Sources/Clawdis/Resources/WebChat/format-error.js create mode 100644 test/format-error.test.ts diff --git a/apps/macos/Sources/Clawdis/Resources/WebChat/bootstrap.js b/apps/macos/Sources/Clawdis/Resources/WebChat/bootstrap.js index f4971f697..3daef912d 100644 --- a/apps/macos/Sources/Clawdis/Resources/WebChat/bootstrap.js +++ b/apps/macos/Sources/Clawdis/Resources/WebChat/bootstrap.js @@ -7,6 +7,8 @@ if (!globalThis.process) { globalThis.process = { env: {} }; } +import { formatError } from "./format-error.js"; + const logStatus = (msg) => { try { console.log(msg); @@ -331,7 +333,7 @@ const startChat = async () => { }; startChat().catch((err) => { - const msg = err?.stack || err?.message || String(err); + const msg = formatError(err); logStatus(`boot failed: ${msg}`); document.body.dataset.webchatError = "1"; ensureErrorStyles(); diff --git a/apps/macos/Sources/Clawdis/Resources/WebChat/format-error.js b/apps/macos/Sources/Clawdis/Resources/WebChat/format-error.js new file mode 100644 index 000000000..9c5447385 --- /dev/null +++ b/apps/macos/Sources/Clawdis/Resources/WebChat/format-error.js @@ -0,0 +1,31 @@ +// Shared formatter for WebChat bootstrap errors so UI shows actionable messages. +export const formatError = (err) => { + if (!err) return "Unknown error"; + if (err instanceof Error) return err.stack || err.message || String(err); + + const isCloseEvent = + (typeof CloseEvent !== "undefined" && err instanceof CloseEvent) || + (typeof err?.code === "number" && + (err?.reason !== undefined || err?.wasClean !== undefined)); + if (isCloseEvent) { + const reason = err.reason?.trim(); + const parts = [`WebSocket closed (${err.code})`]; + if (reason) parts.push(`reason: ${reason}`); + if (err.wasClean) parts.push("clean close"); + return parts.join("; "); + } + + const isWsErrorEvent = + err?.type === "error" && typeof err?.target?.readyState === "number"; + if (isWsErrorEvent) { + const states = ["connecting", "open", "closing", "closed"]; + const stateLabel = states[err.target.readyState] ?? err.target.readyState; + return `WebSocket error (state: ${stateLabel})`; + } + + try { + return JSON.stringify(err); + } catch { + return String(err); + } +}; diff --git a/apps/macos/Sources/Clawdis/Resources/WebChat/webchat.bundle.js b/apps/macos/Sources/Clawdis/Resources/WebChat/webchat.bundle.js index b82658058..9e736d118 100644 --- a/apps/macos/Sources/Clawdis/Resources/WebChat/webchat.bundle.js +++ b/apps/macos/Sources/Clawdis/Resources/WebChat/webchat.bundle.js @@ -43,6 +43,37 @@ var __require = /* @__PURE__ */ ((x$2) => typeof require !== "undefined" ? requi throw Error("Calling `require` for \"" + x$2 + "\" in an environment that doesn't expose the `require` function."); }); +//#endregion +//#region apps/macos/Sources/Clawdis/Resources/WebChat/format-error.js +const formatError = (err) => { + if (!err) return "Unknown error"; + if (err instanceof Error) return err.stack || err.message || String(err); + const isCloseEvent = typeof CloseEvent !== "undefined" && err instanceof CloseEvent || typeof err?.code === "number" && (err?.reason !== undefined || err?.wasClean !== undefined); + if (isCloseEvent) { + const reason = err.reason?.trim(); + const parts = [`WebSocket closed (${err.code})`]; + if (reason) parts.push(`reason: ${reason}`); + if (err.wasClean) parts.push("clean close"); + return parts.join("; "); + } + const isWsErrorEvent = err?.type === "error" && typeof err?.target?.readyState === "number"; + if (isWsErrorEvent) { + const states = [ + "connecting", + "open", + "closing", + "closed" + ]; + const stateLabel = states[err.target.readyState] ?? err.target.readyState; + return `WebSocket error (state: ${stateLabel})`; + } + try { + return JSON.stringify(err); + } catch { + return String(err); + } +}; + //#endregion //#region apps/macos/Sources/Clawdis/Resources/WebChat/pi-ai-stub.js var pi_ai_stub_exports = /* @__PURE__ */ __export({ @@ -196619,7 +196650,7 @@ const startChat = async () => { logStatus("boot: ready"); }; startChat().catch((err) => { - const msg = err?.stack || err?.message || String(err); + const msg = formatError(err); logStatus(`boot failed: ${msg}`); document.body.dataset.webchatError = "1"; ensureErrorStyles(); diff --git a/test/format-error.test.ts b/test/format-error.test.ts new file mode 100644 index 000000000..a2c8e1668 --- /dev/null +++ b/test/format-error.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { formatError } from "../apps/macos/Sources/Clawdis/Resources/WebChat/format-error.js"; + +describe("formatError", () => { + it("handles Error with stack", () => { + const err = new Error("boom"); + err.stack = "stack trace"; + expect(formatError(err)).toBe("stack trace"); + }); + + it("handles CloseEvent-like object", () => { + const err = { code: 1006, reason: "socket closed", wasClean: false }; + expect(formatError(err)).toBe("WebSocket closed (1006); reason: socket closed"); + }); + + it("handles WebSocket error event with state", () => { + const err = { type: "error", target: { readyState: 2 } }; + expect(formatError(err)).toBe("WebSocket error (state: closing)"); + }); + + it("stringifies plain objects", () => { + expect(formatError({ a: 1 })).toBe("{\"a\":1}"); + }); + + it("falls back to string", () => { + const circular = {} as any; + circular.self = circular; + expect(formatError(circular)).toBe("[object Object]"); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index ea693d5b7..c3b4a251d 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - include: ["src/**/*.test.ts"], + include: ["src/**/*.test.ts", "test/format-error.test.ts"], exclude: [ "dist/**", "apps/macos/**",