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 <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-07-31 11:23:28 +08:00
parent 7effa7b2b3
commit 87018721a3

View file

@ -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));
}