From 0773acefaae1a24cb2acf3b5180ea4cbf4827d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20T=C3=B6rcsv=C3=A1ri?= Date: Wed, 10 Jun 2026 15:41:18 +0200 Subject: [PATCH] feat(standalone): "open in a new tab" escape hatch on OOM dialog (0002) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The automatic recovery uses an in-place location.replace reload, which does NOT reliably release the OOM'd wasm heap in every browser (notably Firefox), so the respawn can land right back in an out-of-memory state. Add a user-gesture escape hatch to the terminal MemoryExhaustedDialog: an "Open in a new tab" button that opens the editor in a fresh top-level browsing context (clean retry chain) and closes the current tab. Because it runs from a real click, window.open is not popup-blocked; window.close() may still be refused on the original user-opened tab, which is fine — the new tab is the survivor. Copy tells the user this sometimes helps where the in-place reload didn't. - oom-watch.ts: export respawnInNewTab(win) — fresh-tab respawn with ?oomRetry cleared. - MemoryExhaustedDialog: "Open in a new tab" (primary) + "Reload this tab" (secondary), with honest copy about Firefox + unsaved edits. - WasmTool: wire onOpenNewTab to respawnInNewTab. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/standalone/src/components/WasmTool.tsx | 7 +++- .../src/recovery/MemoryExhaustedDialog.tsx | 37 +++++++++++++++---- web/standalone/src/recovery/oom-watch.ts | 27 ++++++++++++++ 3 files changed, 61 insertions(+), 10 deletions(-) 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;