fix(desktop): eliminate cross-window deep link contamination (#9273)

Signed-off-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Lifei Zhou <lifei@squareup.com>
This commit is contained in:
Douwe Osinga
2026-05-17 23:41:40 -04:00
committed by GitHub
parent 2eb4685c2c
commit 06e6e2e850
+32 -54
View File
@@ -445,77 +445,65 @@ if (process.platform !== 'darwin') {
}
}
let firstOpenWindow: BrowserWindow;
let pendingDeepLink: string | null = null;
const pendingDeepLinks = new Map<number, string>(); // windowId -> deep link URL
let openUrlHandledLaunch = false;
async function handleProtocolUrl(url: string) {
if (!url) return;
pendingDeepLink = url;
const parsedUrl = new URL(url);
const recentDirs = loadRecentDirs();
const openDir = recentDirs.length > 0 ? recentDirs[0] : null;
if (parsedUrl.hostname === 'new-session') {
await createChat(app, { dir: openDir || undefined });
pendingDeepLink = null;
return;
} else if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') {
// For bot/recipe URLs, get existing window or create new one
const existingWindows = BrowserWindow.getAllWindows();
const targetWindow =
existingWindows.length > 0
? existingWindows[0]
: await createChat(app, { dir: openDir || undefined });
await processProtocolUrl(parsedUrl, targetWindow);
await processProtocolUrl(url, parsedUrl, targetWindow);
} else {
// For other URL types, reuse existing window if available
const existingWindows = BrowserWindow.getAllWindows();
let targetWindow: BrowserWindow;
if (existingWindows.length > 0) {
firstOpenWindow = existingWindows[0];
if (firstOpenWindow.isMinimized()) {
firstOpenWindow.restore();
targetWindow = existingWindows[0];
if (targetWindow.isMinimized()) {
targetWindow.restore();
}
firstOpenWindow.focus();
targetWindow.focus();
} else {
firstOpenWindow = await createChat(app, { dir: openDir || undefined });
targetWindow = await createChat(app, { dir: openDir || undefined });
}
if (firstOpenWindow) {
const webContents = firstOpenWindow.webContents;
if (webContents.isLoadingMainFrame()) {
webContents.once('did-finish-load', async () => {
await processProtocolUrl(parsedUrl, firstOpenWindow);
});
} else {
await processProtocolUrl(parsedUrl, firstOpenWindow);
}
if (targetWindow.webContents.isLoadingMainFrame()) {
pendingDeepLinks.set(targetWindow.id, url);
} else {
await processProtocolUrl(url, parsedUrl, targetWindow);
}
}
}
async function processProtocolUrl(parsedUrl: URL, window: BrowserWindow) {
async function processProtocolUrl(url: string, parsedUrl: URL, window: BrowserWindow) {
const recentDirs = loadRecentDirs();
const openDir = recentDirs.length > 0 ? recentDirs[0] : null;
if (parsedUrl.hostname === 'extension') {
window.webContents.send('add-extension', pendingDeepLink);
window.webContents.send('add-extension', url);
} else if (parsedUrl.hostname === 'sessions') {
window.webContents.send('open-shared-session', pendingDeepLink);
window.webContents.send('open-shared-session', url);
} else if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') {
const deeplinkData = parseRecipeDeeplink(pendingDeepLink ?? parsedUrl.toString());
const deeplinkData = parseRecipeDeeplink(url);
const scheduledJobId = parsedUrl.searchParams.get('scheduledJob');
// Create a new window and ignore the passed-in window
await createChat(app, {
dir: openDir || undefined,
recipeDeeplink: deeplinkData?.config,
scheduledJobId: scheduledJobId || undefined,
recipeParameters: deeplinkData?.parameters,
});
pendingDeepLink = null;
}
}
@@ -560,25 +548,21 @@ app.on('open-url', async (_event, url) => {
return;
}
// For extension/session URLs, store the deep link for processing after React is ready
pendingDeepLink = url;
log.info('[Main] Stored pending deep link for processing after React ready:', url.includes('key=') ? url.replace(/key=[^&]+/, 'key=REDACTED') : url);
// For extension/session URLs, send to existing window or store pending for new one
const existingWindows = BrowserWindow.getAllWindows();
if (existingWindows.length > 0) {
firstOpenWindow = existingWindows[0];
if (firstOpenWindow.isMinimized()) firstOpenWindow.restore();
firstOpenWindow.focus();
const targetWindow = existingWindows[0];
if (targetWindow.isMinimized()) targetWindow.restore();
targetWindow.focus();
if (parsedUrl.hostname === 'extension') {
firstOpenWindow.webContents.send('add-extension', pendingDeepLink);
pendingDeepLink = null;
targetWindow.webContents.send('add-extension', url);
} else if (parsedUrl.hostname === 'sessions') {
firstOpenWindow.webContents.send('open-shared-session', pendingDeepLink);
pendingDeepLink = null;
targetWindow.webContents.send('open-shared-session', url);
}
} else {
openUrlHandledLaunch = true;
firstOpenWindow = await createChat(app, { dir: openDir || undefined });
const newWindow = await createChat(app, { dir: openDir || undefined });
pendingDeepLinks.set(newWindow.id, url);
}
}
});
@@ -1150,8 +1134,8 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => {
mainWindow.on('closed', () => {
windowMap.delete(windowId);
// Clean up pending initial message
pendingInitialMessages.delete(windowId);
pendingDeepLinks.delete(windowId);
if (windowPowerSaveBlockers.has(windowId)) {
const blockerId = windowPowerSaveBlockers.get(windowId)!;
@@ -1518,27 +1502,21 @@ ipcMain.on('react-ready', (event) => {
pendingInitialMessages.delete(windowId);
}
if (pendingDeepLink && window) {
log.info('Processing pending deep link:', pendingDeepLink);
if (windowId && pendingDeepLinks.has(windowId) && window) {
const deepLinkUrl = pendingDeepLinks.get(windowId)!;
pendingDeepLinks.delete(windowId);
log.info('Processing pending deep link for window:', windowId);
try {
const parsedUrl = new URL(pendingDeepLink);
const parsedUrl = new URL(deepLinkUrl);
if (parsedUrl.hostname === 'extension') {
log.info('Sending add-extension IPC to ready window');
window.webContents.send('add-extension', pendingDeepLink);
window.webContents.send('add-extension', deepLinkUrl);
} else if (parsedUrl.hostname === 'sessions') {
log.info('Sending open-shared-session IPC to ready window');
window.webContents.send('open-shared-session', pendingDeepLink);
window.webContents.send('open-shared-session', deepLinkUrl);
}
pendingDeepLink = null;
} catch (error) {
log.error('Error processing pending deep link:', error);
pendingDeepLink = null;
}
} else {
log.info('No pending deep link to process');
}
log.info('React ready - window is prepared for deep links');
});
ipcMain.handle('open-external', async (_event, url: string) => {