From 1ab46eacd7c3660a439073951b954e9027bcad46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20T=C3=B6rcsv=C3=A1ri?= Date: Fri, 31 Jul 2026 10:54:21 +0200 Subject: [PATCH] fix(editor): show a fatal overlay instead of a blank page, above the console MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The boot overlay only renders while `!ready`, so anything that killed the runtime AFTER the editor came up — a wasm trap, an abort, a staging fetch failing late — unmounted the last thing on screen and left a white page with no explanation. Adds a `fatal` state rendered INDEPENDENTLY of `ready`, set both from the boot catch and from window error / unhandledrejection listeners that promote only genuinely terminal signatures (RuntimeError, abort, table index out of bounds, indirect call signature, unreachable). Ordinary app errors must not hijack a working editor, so anything else is ignored. The console panel moves from z-20 to z-40, above both overlays, and is forced visible on a fatal even with chrome hidden: when a load fails, that log is the only account of what was loading when it happened, and it was being covered by the very overlay reporting the failure. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0137pGo8W7asomGUTRMB7RzM --- web/standalone/src/components/WasmTool.tsx | 78 +++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/web/standalone/src/components/WasmTool.tsx b/web/standalone/src/components/WasmTool.tsx index e2360ef..0139741 100644 --- a/web/standalone/src/components/WasmTool.tsx +++ b/web/standalone/src/components/WasmTool.tsx @@ -927,6 +927,11 @@ export function WasmTool({ const [logs, setLogs] = React.useState([]); const [showLog, setShowLog] = React.useState(false); const [oomExhausted, setOomExhausted] = React.useState(false); + // Terminal failure, rendered INDEPENDENTLY of `ready`. The boot overlay only + // exists while `!ready`, so anything that killed the runtime after the editor + // came up (a wasm abort/trap, a failed staging fetch surfacing late) used to + // leave a blank page with no explanation at all — the "white screen of death". + const [fatal, setFatal] = React.useState(null); // Editor lifecycle for the loading chrome: false until the tool has booted + // opened (covers the big WASM-compile freeze with a full-screen overlay). const [ready, setReady] = React.useState(false); @@ -1141,6 +1146,37 @@ export function WasmTool({ return () => clearTimeout(t); }, [ready, consent]); + // Runtime death AFTER the boot succeeded. A wasm trap ("indirect call + // signature mismatch", "table index is out of bounds") or an abort() arrives + // as a window error / unhandled rejection long after `ready` flipped, with the + // boot overlay already gone — so without this the page just goes blank. Only + // genuinely terminal signatures promote to the fatal overlay; ordinary app + // errors must not hijack a working editor. + React.useEffect(() => { + const terminal = (msg: string) => + /RuntimeError|\babort(ed)?\b|table index is out of bounds|indirect call signature|memory access out of bounds|unreachable executed/i.test( + msg, + ); + const onError = (e: ErrorEvent) => { + const msg = e.error instanceof Error ? `${e.error.message}` : String(e.message ?? ""); + if (!terminal(msg)) return; + append(`[fatal] window error: ${msg}`); + setFatal(msg); + }; + const onRejection = (e: PromiseRejectionEvent) => { + const msg = e.reason instanceof Error ? e.reason.message : String(e.reason ?? ""); + if (!terminal(msg)) return; + append(`[fatal] unhandled rejection: ${msg}`); + setFatal(msg); + }; + window.addEventListener("error", onError); + window.addEventListener("unhandledrejection", onRejection); + return () => { + window.removeEventListener("error", onError); + window.removeEventListener("unhandledrejection", onRejection); + }; + }, [append]); + React.useEffect(() => { const win = window as ToolWindow; const removeNavigationHook = installToolNavigationHook(win, { @@ -1724,6 +1760,7 @@ export function WasmTool({ } catch (err) { append(`[fatal] ${String(err)}`); setStatus(`Error: ${String(err)}`); + setFatal(String(err)); } })(); @@ -2150,8 +2187,45 @@ export function WasmTool({ )} - {!effectiveChromeHidden && ( -
+ {/* Terminal failure — z-35, ABOVE the boot overlay but below the console + panel, and independent of `ready` so a post-boot runtime death still + says something instead of blanking the page. */} + {fatal && ( +
+

Something went wrong

+

+ {fatal} +

+

+ The editor stopped. Open the console below for the full log — it + records what was loading when this happened. +

+
+ + +
+
+ )} + + {/* z-40 (above the z-30 boot overlay and the z-35 fatal overlay): when a + load fails, the log this panel holds is the only account of WHY, so it + must never end up underneath the thing reporting the failure. Forced + visible on a fatal even with chrome hidden, for the same reason. */} + {(!effectiveChromeHidden || fatal) && ( +