diff --git a/web/standalone/src/components/WasmTool.tsx b/web/standalone/src/components/WasmTool.tsx index 95db000..4eb4cf9 100644 --- a/web/standalone/src/components/WasmTool.tsx +++ b/web/standalone/src/components/WasmTool.tsx @@ -13,7 +13,7 @@ import { memfsProjectDir } from "@/wasm/constants"; import { driveProjectIntoTool, type ToolFile } from "@/wasm/kicad-runner"; import type { CollabWindow } from "@/wasm/collab"; import { clog, cwarn } from "@/wasm/collab/debug"; -import { createOomWatch } from "@/recovery/oom-watch"; +import { createOomWatch, respawnInNewTab } from "@/recovery/oom-watch"; import { MemoryExhaustedDialog } from "@/recovery/MemoryExhaustedDialog"; // Tools with a working collab bridge (kicadCollabSnapshot/Apply embind exports). @@ -349,7 +349,10 @@ export function WasmTool({
{oomExhausted && ( - window.location.reload()} /> + respawnInNewTab()} + onReload={() => window.location.reload()} + /> )} {status && ( diff --git a/web/standalone/src/recovery/MemoryExhaustedDialog.tsx b/web/standalone/src/recovery/MemoryExhaustedDialog.tsx index 5fcab23..954d30b 100644 --- a/web/standalone/src/recovery/MemoryExhaustedDialog.tsx +++ b/web/standalone/src/recovery/MemoryExhaustedDialog.tsx @@ -4,20 +4,38 @@ import { BlockingDialog } from "@/preflight/BlockingDialog"; * Terminal "not enough memory" UI for feature 0002 — shown after the OOM retry * chain hits MAX_RETRIES. Reuses 0001's blocking dialog for visual consistency. * - * The copy is honest about the two limitations the spec calls out: a respawn - * re-opens the same file but loses in-tab edits not yet pushed to the backend / - * Y.Doc, and `window.close()` may not close the very first (user-opened) tab. + * Offers two manual actions: + * - "Open in a new tab" (primary): a user-gesture respawn in a fresh browsing + * context. This sometimes succeeds where the automatic in-place reload did + * not, because a brand-new tab is the most reliable way to actually release + * the OOM'd wasm heap (an in-place reload doesn't, reliably, in every + * browser — notably Firefox). + * - "Reload this tab" (secondary): the simple in-place retry. + * + * Copy is honest about lost unsaved edits (a respawn re-opens the same file but + * loses in-tab edits not yet synced to the backend / Y.Doc). */ -export function MemoryExhaustedDialog({ onReload }: { onReload?: () => void }) { +export function MemoryExhaustedDialog({ + onOpenNewTab, + onReload, +}: { + onOpenNewTab?: () => void; + onReload?: () => void; +}) { return ( void }) { }, ]} primary={ - onReload - ? { label: "Reload and try again", onClick: onReload } + onOpenNewTab + ? { label: "Open in a new tab", onClick: onOpenNewTab } : undefined } + secondary={ + onReload ? { label: "Reload this tab", onClick: onReload } : undefined + } /> ); } diff --git a/web/standalone/src/recovery/oom-watch.ts b/web/standalone/src/recovery/oom-watch.ts index 0a17195..1316a60 100644 --- a/web/standalone/src/recovery/oom-watch.ts +++ b/web/standalone/src/recovery/oom-watch.ts @@ -308,6 +308,33 @@ export function createOomWatch(opts: OomWatchOptions): OomWatch { return { start, stop, onAbort }; } +/** + * User-gesture escape hatch for the terminal dialog: open the editor in a brand + * new tab with a clean retry chain, then close this one. + * + * This is deliberately NOT the automatic recovery path: it runs from a real + * click, so `window.open` is not popup-blocked, and a fresh top-level browsing + * context is the most reliable way to actually drop the OOM'd wasm heap — an + * in-place `location.replace` reload does not reliably release it in every + * browser (notably Firefox). `window.close()` may still be refused on the + * original, user-opened tab; that's fine — the new tab is the working one and + * the user can close the old one. Returns whether a new tab was opened. + */ +export function respawnInNewTab(win: Window = window): boolean { + const url = new URL(win.location.href); + url.searchParams.delete(RETRY_PARAM); // fresh chain — this is a manual retry + const opened = win.open(url.toString(), "_blank"); + if (opened) { + try { + win.close(); + } catch { + /* original tab may refuse to close — new tab is the survivor anyway */ + } + return true; + } + return false; +} + function safeLocalStorage(win: Window): Storage | null { try { return win.localStorage;