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; const pendingDeepLinks = new Map<number, string>(); // windowId -> deep link URL
let pendingDeepLink: string | null = null;
let openUrlHandledLaunch = false; let openUrlHandledLaunch = false;
async function handleProtocolUrl(url: string) { async function handleProtocolUrl(url: string) {
if (!url) return; if (!url) return;
pendingDeepLink = url;
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
const recentDirs = loadRecentDirs(); const recentDirs = loadRecentDirs();
const openDir = recentDirs.length > 0 ? recentDirs[0] : null; const openDir = recentDirs.length > 0 ? recentDirs[0] : null;
if (parsedUrl.hostname === 'new-session') { if (parsedUrl.hostname === 'new-session') {
await createChat(app, { dir: openDir || undefined }); await createChat(app, { dir: openDir || undefined });
pendingDeepLink = null;
return; return;
} else if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') { } else if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') {
// For bot/recipe URLs, get existing window or create new one
const existingWindows = BrowserWindow.getAllWindows(); const existingWindows = BrowserWindow.getAllWindows();
const targetWindow = const targetWindow =
existingWindows.length > 0 existingWindows.length > 0
? existingWindows[0] ? existingWindows[0]
: await createChat(app, { dir: openDir || undefined }); : await createChat(app, { dir: openDir || undefined });
await processProtocolUrl(parsedUrl, targetWindow); await processProtocolUrl(url, parsedUrl, targetWindow);
} else { } else {
// For other URL types, reuse existing window if available
const existingWindows = BrowserWindow.getAllWindows(); const existingWindows = BrowserWindow.getAllWindows();
let targetWindow: BrowserWindow;
if (existingWindows.length > 0) { if (existingWindows.length > 0) {
firstOpenWindow = existingWindows[0]; targetWindow = existingWindows[0];
if (firstOpenWindow.isMinimized()) { if (targetWindow.isMinimized()) {
firstOpenWindow.restore(); targetWindow.restore();
} }
firstOpenWindow.focus(); targetWindow.focus();
} else { } else {
firstOpenWindow = await createChat(app, { dir: openDir || undefined }); targetWindow = await createChat(app, { dir: openDir || undefined });
} }
if (firstOpenWindow) { if (targetWindow.webContents.isLoadingMainFrame()) {
const webContents = firstOpenWindow.webContents; pendingDeepLinks.set(targetWindow.id, url);
if (webContents.isLoadingMainFrame()) { } else {
webContents.once('did-finish-load', async () => { await processProtocolUrl(url, parsedUrl, targetWindow);
await processProtocolUrl(parsedUrl, firstOpenWindow);
});
} else {
await processProtocolUrl(parsedUrl, firstOpenWindow);
}
} }
} }
} }
async function processProtocolUrl(parsedUrl: URL, window: BrowserWindow) { async function processProtocolUrl(url: string, parsedUrl: URL, window: BrowserWindow) {
const recentDirs = loadRecentDirs(); const recentDirs = loadRecentDirs();
const openDir = recentDirs.length > 0 ? recentDirs[0] : null; const openDir = recentDirs.length > 0 ? recentDirs[0] : null;
if (parsedUrl.hostname === 'extension') { if (parsedUrl.hostname === 'extension') {
window.webContents.send('add-extension', pendingDeepLink); window.webContents.send('add-extension', url);
} else if (parsedUrl.hostname === 'sessions') { } 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') { } else if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') {
const deeplinkData = parseRecipeDeeplink(pendingDeepLink ?? parsedUrl.toString()); const deeplinkData = parseRecipeDeeplink(url);
const scheduledJobId = parsedUrl.searchParams.get('scheduledJob'); const scheduledJobId = parsedUrl.searchParams.get('scheduledJob');
// Create a new window and ignore the passed-in window
await createChat(app, { await createChat(app, {
dir: openDir || undefined, dir: openDir || undefined,
recipeDeeplink: deeplinkData?.config, recipeDeeplink: deeplinkData?.config,
scheduledJobId: scheduledJobId || undefined, scheduledJobId: scheduledJobId || undefined,
recipeParameters: deeplinkData?.parameters, recipeParameters: deeplinkData?.parameters,
}); });
pendingDeepLink = null;
} }
} }
@@ -560,25 +548,21 @@ app.on('open-url', async (_event, url) => {
return; return;
} }
// For extension/session URLs, store the deep link for processing after React is ready // For extension/session URLs, send to existing window or store pending for new one
pendingDeepLink = url;
log.info('[Main] Stored pending deep link for processing after React ready:', url.includes('key=') ? url.replace(/key=[^&]+/, 'key=REDACTED') : url);
const existingWindows = BrowserWindow.getAllWindows(); const existingWindows = BrowserWindow.getAllWindows();
if (existingWindows.length > 0) { if (existingWindows.length > 0) {
firstOpenWindow = existingWindows[0]; const targetWindow = existingWindows[0];
if (firstOpenWindow.isMinimized()) firstOpenWindow.restore(); if (targetWindow.isMinimized()) targetWindow.restore();
firstOpenWindow.focus(); targetWindow.focus();
if (parsedUrl.hostname === 'extension') { if (parsedUrl.hostname === 'extension') {
firstOpenWindow.webContents.send('add-extension', pendingDeepLink); targetWindow.webContents.send('add-extension', url);
pendingDeepLink = null;
} else if (parsedUrl.hostname === 'sessions') { } else if (parsedUrl.hostname === 'sessions') {
firstOpenWindow.webContents.send('open-shared-session', pendingDeepLink); targetWindow.webContents.send('open-shared-session', url);
pendingDeepLink = null;
} }
} else { } else {
openUrlHandledLaunch = true; 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', () => { mainWindow.on('closed', () => {
windowMap.delete(windowId); windowMap.delete(windowId);
// Clean up pending initial message
pendingInitialMessages.delete(windowId); pendingInitialMessages.delete(windowId);
pendingDeepLinks.delete(windowId);
if (windowPowerSaveBlockers.has(windowId)) { if (windowPowerSaveBlockers.has(windowId)) {
const blockerId = windowPowerSaveBlockers.get(windowId)!; const blockerId = windowPowerSaveBlockers.get(windowId)!;
@@ -1518,27 +1502,21 @@ ipcMain.on('react-ready', (event) => {
pendingInitialMessages.delete(windowId); pendingInitialMessages.delete(windowId);
} }
if (pendingDeepLink && window) { if (windowId && pendingDeepLinks.has(windowId) && window) {
log.info('Processing pending deep link:', pendingDeepLink); const deepLinkUrl = pendingDeepLinks.get(windowId)!;
pendingDeepLinks.delete(windowId);
log.info('Processing pending deep link for window:', windowId);
try { try {
const parsedUrl = new URL(pendingDeepLink); const parsedUrl = new URL(deepLinkUrl);
if (parsedUrl.hostname === 'extension') { if (parsedUrl.hostname === 'extension') {
log.info('Sending add-extension IPC to ready window'); window.webContents.send('add-extension', deepLinkUrl);
window.webContents.send('add-extension', pendingDeepLink);
} else if (parsedUrl.hostname === 'sessions') { } else if (parsedUrl.hostname === 'sessions') {
log.info('Sending open-shared-session IPC to ready window'); window.webContents.send('open-shared-session', deepLinkUrl);
window.webContents.send('open-shared-session', pendingDeepLink);
} }
pendingDeepLink = null;
} catch (error) { } catch (error) {
log.error('Error processing pending deep link:', 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) => { ipcMain.handle('open-external', async (_event, url: string) => {