From 87018721a34d8227f2e29b364f668b7aeca4f033 Mon Sep 17 00:00:00 2001 From: oliver Date: Fri, 31 Jul 2026 11:23:28 +0800 Subject: [PATCH] Preserve existing module tabs when focusing from the workbench. Avoid window.open(url, namedWindow) reloads that wiped WebCRT sessions; reclaim named windows without navigating when already open. Co-authored-by: Cursor --- web/src/utils/moduleWindows.ts | 61 +++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/web/src/utils/moduleWindows.ts b/web/src/utils/moduleWindows.ts index bddd155..304f29a 100644 --- a/web/src/utils/moduleWindows.ts +++ b/web/src/utils/moduleWindows.ts @@ -29,6 +29,16 @@ function moduleAckKey(moduleId: string): string { return `netx-mod-focus-ack-${moduleId}`; } +function resolveModulePath(moduleId: string, path: string): string { + const mod = getModuleById(moduleId); + const base = mod?.path ?? "/"; + // Prefer caller path so query strings like /webcrt?ne_id=... are preserved. + if (path && (path === base || path.startsWith(`${base}?`) || path.startsWith(`${base}/`))) { + return path; + } + return base; +} + export function registerModuleWindow(moduleId: string): () => void { const sharedName = moduleWindowName(moduleId); try { @@ -50,15 +60,17 @@ export function registerModuleWindow(moduleId: string): () => void { } const targetPath = String(data.path || "").trim(); if (targetPath) { - // Compare pathname only for hard navigation. Same route + new query - // must soft-navigate so React state (open tabs) survives. try { const url = new URL(targetPath, window.location.origin); const next = `${url.pathname}${url.search}${url.hash}`; const cur = `${window.location.pathname}${window.location.search}${window.location.hash}`; if (window.location.pathname !== url.pathname) { + // Different module route — hard navigate. window.location.assign(next); - } else if (cur !== next) { + } else if (cur !== next && (url.search || url.hash)) { + // Same pathname: soft-nav only when caller supplies query/hash + // (e.g. /webcrt?ne_id=...). Bare /webcrt means focus-only — keep + // current query and in-memory session state (no reload). window.history.pushState({}, "", next); window.dispatchEvent(new PopStateEvent("popstate")); } @@ -75,12 +87,7 @@ export function registerModuleWindow(moduleId: string): () => void { } export function openOrFocusModule({ moduleId, path }: ModuleWindowSpec): void { - const mod = getModuleById(moduleId); - const base = mod?.path ?? "/"; - // Prefer caller path so query strings like /webcrt?ne_id=... are preserved. - const targetPath = path && (path === base || path.startsWith(`${base}?`) || path.startsWith(`${base}/`)) - ? path - : base; + const targetPath = resolveModulePath(moduleId, path); const requestId = createFocusRequestId(`mod-${moduleId}`); const name = moduleWindowName(moduleId); @@ -91,20 +98,44 @@ export function openOrFocusModule({ moduleId, path }: ModuleWindowSpec): void { requestId, }); - focusWindowSafe(window.open(targetPath, name)); + // Under the user-gesture: reclaim the named window WITHOUT passing a URL. + // window.open(url, existingName) reloads that tab and kills WebCRT sessions. + let handledByGesture = false; + let win: Window | null = null; + try { + win = window.open("", name); + } catch { + win = null; + } + if (win && !win.closed) { + handledByGesture = true; + let needsNavigate = false; + try { + const href = String(win.location?.href || ""); + needsNavigate = !href || href === "about:blank"; + } catch { + // Existing cross-context window — only focus; broadcast soft-nav handles path. + needsNavigate = false; + } + if (needsNavigate) { + try { + win.location.href = targetPath; + } catch { + handledByGesture = false; + } + } + focusWindowSafe(win); + } waitForFocusAck(moduleAckKey(moduleId), requestId, () => { + if (handledByGesture) return; focusWindowSafe(window.open(targetPath, name)); }); } /** Always open a fresh browser tab/window; never focus or merge with an existing module tab. */ export function openNewModuleWindow({ moduleId, path }: ModuleWindowSpec): void { - const mod = getModuleById(moduleId); - const base = mod?.path ?? "/"; - const targetPath = path && (path === base || path.startsWith(`${base}?`) || path.startsWith(`${base}/`)) - ? path - : base; + const targetPath = resolveModulePath(moduleId, path); const uniqueName = `${moduleWindowName(moduleId)}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; focusWindowSafe(window.open(targetPath, uniqueName)); }