From 6e6743c2e095d9935f8888f93fc6acc9e597e3ff Mon Sep 17 00:00:00 2001 From: SarthakB11 Date: Sun, 17 May 2026 18:41:15 +0000 Subject: [PATCH 1/2] fix(desktop): reuse existing window for recipe deep links Closes #9143 --- ui/desktop/src/App.tsx | 62 +++++++++++++++++++++++--- ui/desktop/src/main.ts | 98 +++++++++++++++++++++++++----------------- 2 files changed, 114 insertions(+), 46 deletions(-) diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index e53dcde476ac..4de54af457a2 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -410,7 +410,7 @@ export function AppInner() { }; }, []); - const { addExtension } = useConfig(); + const { addExtension, extensionsList } = useConfig(); useEffect(() => { try { @@ -421,6 +421,51 @@ export function AppInner() { } }, []); + useEffect(() => { + const handleOpenRecipeDeeplink = async (_event: IpcRendererEvent, ...args: unknown[]) => { + const payload = args[0] as + | { + recipeDeeplink?: string; + recipeParameters?: Record; + scheduledJobId?: string; + } + | undefined; + const recipeDeeplink = payload?.recipeDeeplink; + if (!recipeDeeplink) { + return; + } + try { + const newSession = await createSession(getInitialWorkingDir(), { + recipeDeeplink, + allExtensions: extensionsList, + }); + window.dispatchEvent( + new CustomEvent(AppEvents.ADD_ACTIVE_SESSION, { + detail: { + sessionId: newSession.id, + initialMessage: newSession.recipe?.prompt + ? { msg: newSession.recipe.prompt, images: [] } + : undefined, + }, + }) + ); + navigate(`/pair?resumeSessionId=${encodeURIComponent(newSession.id)}`); + } catch (error) { + console.error('Failed to open recipe deeplink in existing window:', error); + trackErrorWithContext(error, { + component: 'AppInner', + action: 'open_recipe_deeplink', + recoverable: true, + }); + toast.error(`Failed to open recipe: ${errorMessage(error, 'Unknown error')}`); + } + }; + window.electron.on('open-recipe-deeplink', handleOpenRecipeDeeplink); + return () => { + window.electron.off('open-recipe-deeplink', handleOpenRecipeDeeplink); + }; + }, [navigate, extensionsList]); + useEffect(() => { const handleOpenSharedSession = async (_event: IpcRendererEvent, ...args: unknown[]) => { const link = args[0] as string; @@ -486,13 +531,18 @@ export function AppInner() { // Show a toast if mesh is the configured provider but isn't running. useEffect(() => { const handler = () => { - toast.warn('Inference Mesh is set as your provider but isn\'t running. Open Settings → Mesh to start it. Keep goose running to stay connected.', { - autoClose: false, - toastId: 'mesh-not-running', - }); + toast.warn( + "Inference Mesh is set as your provider but isn't running. Open Settings → Mesh to start it. Keep goose running to stay connected.", + { + autoClose: false, + toastId: 'mesh-not-running', + } + ); }; window.electron.on('mesh-not-running', handler); - return () => { window.electron.off('mesh-not-running', handler); }; + return () => { + window.electron.off('mesh-not-running', handler); + }; }, []); // Prevent default drag and drop behavior globally to avoid opening files in new windows diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 343b5e1cda51..ad942a1c1332 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -396,16 +396,7 @@ if (process.platform !== 'darwin') { app.whenReady().then(async () => { const recentDirs = loadRecentDirs(); const openDir = recentDirs.length > 0 ? recentDirs[0] : null; - - const deeplinkData = parseRecipeDeeplink(protocolUrl); - const scheduledJobId = parsedUrl.searchParams.get('scheduledJob'); - - await createChat(app, { - dir: openDir || undefined, - recipeDeeplink: deeplinkData?.config, - scheduledJobId: scheduledJobId || undefined, - recipeParameters: deeplinkData?.parameters, - }); + await openRecipeDeeplink(protocolUrl, openDir); }); return; // Skip the rest of the handler } @@ -496,6 +487,48 @@ async function handleProtocolUrl(url: string) { } } +let windowDeeplinkURL: string | null = null; + +async function openRecipeDeeplink(url: string, openDir: string | null) { + const parsedUrl = new URL(url); + const deeplinkData = parseRecipeDeeplink(url); + const scheduledJobId = parsedUrl.searchParams.get('scheduledJob'); + + const existingWindows = BrowserWindow.getAllWindows(); + const targetWindow = + BrowserWindow.getFocusedWindow() ?? + existingWindows[existingWindows.length - 1] ?? + existingWindows[0] ?? + null; + + if (targetWindow && !targetWindow.isDestroyed() && targetWindow.webContents) { + if (targetWindow.isMinimized()) { + targetWindow.restore(); + } + targetWindow.focus(); + targetWindow.webContents.send('open-recipe-deeplink', { + recipeDeeplink: url, + recipeParameters: deeplinkData?.parameters, + scheduledJobId: scheduledJobId || undefined, + }); + return; + } + + if (deeplinkData) { + windowDeeplinkURL = url; + } + try { + await createChat(app, { + dir: openDir || undefined, + recipeDeeplink: deeplinkData?.config, + scheduledJobId: scheduledJobId || undefined, + recipeParameters: deeplinkData?.parameters, + }); + } finally { + windowDeeplinkURL = null; + } +} + async function processProtocolUrl(parsedUrl: URL, window: BrowserWindow) { const recentDirs = loadRecentDirs(); const openDir = recentDirs.length > 0 ? recentDirs[0] : null; @@ -505,27 +538,19 @@ async function processProtocolUrl(parsedUrl: URL, window: BrowserWindow) { } else if (parsedUrl.hostname === 'sessions') { window.webContents.send('open-shared-session', pendingDeepLink); } else if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') { - const deeplinkData = parseRecipeDeeplink(pendingDeepLink ?? parsedUrl.toString()); - 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, - }); + await openRecipeDeeplink(pendingDeepLink ?? parsedUrl.toString(), openDir); pendingDeepLink = null; } } -let windowDeeplinkURL: string | null = null; - app.on('open-url', async (_event, url) => { if (process.platform !== 'win32') { const parsedUrl = new URL(url); - log.info('[Main] Received open-url event:', url.includes('key=') ? url.replace(/key=[^&]+/, 'key=REDACTED') : url); + log.info( + '[Main] Received open-url event:', + url.includes('key=') ? url.replace(/key=[^&]+/, 'key=REDACTED') : url + ); await app.whenReady(); @@ -540,29 +565,20 @@ app.on('open-url', async (_event, url) => { return; } - // Handle bot/recipe URLs by directly creating a new window + // Handle bot/recipe URLs by reusing an existing window when available if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') { - log.info('[Main] Detected bot/recipe URL, creating new chat window'); + log.info('[Main] Detected bot/recipe URL'); openUrlHandledLaunch = true; - const deeplinkData = parseRecipeDeeplink(url); - if (deeplinkData) { - windowDeeplinkURL = url; - } - const scheduledJobId = parsedUrl.searchParams.get('scheduledJob'); - - await createChat(app, { - dir: openDir || undefined, - recipeDeeplink: deeplinkData?.config, - scheduledJobId: scheduledJobId || undefined, - recipeParameters: deeplinkData?.parameters, - }); - windowDeeplinkURL = null; + await openRecipeDeeplink(url, openDir); 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); + 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(); if (existingWindows.length > 0) { @@ -901,7 +917,9 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => { const stderrTail = diagnostics?.stderrTail ?? []; const failureDetailParts = [ diagnostics?.childExitCode !== null || diagnostics?.childExitSignal - ? `Child exit: code=${diagnostics?.childExitCode ?? 'null'} signal=${diagnostics?.childExitSignal ?? 'null'}` + ? `Child exit: code=${diagnostics?.childExitCode ?? 'null'} signal=${ + diagnostics?.childExitSignal ?? 'null' + }` : 'Child exit: unavailable', diagnostics?.certFingerprintSeen ? 'TLS fingerprint observed: yes' From de72a03d65cdec956b1597d4dc14e0f07a92cfe8 Mon Sep 17 00:00:00 2001 From: SarthakB11 Date: Tue, 19 May 2026 05:49:55 +0000 Subject: [PATCH 2/2] fix(desktop): thread recipe parameters and defer recipe IPC until renderer is ready Two follow-ups on #9143 per Codex review: - recipeParameters URL overrides were sent in the IPC payload but the renderer dropped them. Introduce a small per-session store in ui/desktop/src/utils/recipeParametersStore.ts that App.tsx writes from the warm-launch IPC handler and ParameterInputModal reads as initial values (falling back to window.appConfig.recipeParameters for the cold-launch path that seeds it at preload). - The bot/recipe IPC could race the renderer's listener when handleProtocolUrl created a fresh window on the same tick. Reuse the existing pendingDeepLinks/react-ready pattern: if the chosen window is still loading, queue the URL and dispatch the recipe IPC from the react-ready handler the same way extension/sessions deeplinks are already replayed. Signed-off-by: SarthakB11 --- ui/desktop/src/App.tsx | 2 ++ ui/desktop/src/components/BaseChat.tsx | 2 ++ ui/desktop/src/main.ts | 22 ++++++++++++---- ui/desktop/src/utils/recipeParametersStore.ts | 25 +++++++++++++++++++ 4 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 ui/desktop/src/utils/recipeParametersStore.ts diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 4de54af457a2..e814fb55bd4f 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -10,6 +10,7 @@ import { } from 'react-router-dom'; import { openSharedSessionFromDeepLink, importNostrSessionFromDeepLink } from './sessionLinks'; import { type SharedSessionDetails } from './sharedSessions'; +import { setRecipeParametersForSession } from './utils/recipeParametersStore'; import { ErrorUI } from './components/ErrorBoundary'; import { ExtensionInstallModal } from './components/ExtensionInstallModal'; import { toast, ToastContainer } from 'react-toastify'; @@ -439,6 +440,7 @@ export function AppInner() { recipeDeeplink, allExtensions: extensionsList, }); + setRecipeParametersForSession(newSession.id, payload?.recipeParameters); window.dispatchEvent( new CustomEvent(AppEvents.ADD_ACTIVE_SESSION, { detail: { diff --git a/ui/desktop/src/components/BaseChat.tsx b/ui/desktop/src/components/BaseChat.tsx index a72f5984dec3..def7385bec23 100644 --- a/ui/desktop/src/components/BaseChat.tsx +++ b/ui/desktop/src/components/BaseChat.tsx @@ -33,6 +33,7 @@ import { useToolCount } from './alerts/useToolCount'; import { getThinkingMessage, getTextAndImageContent } from '../types/message'; import ParameterInputModal from './ParameterInputModal'; import { substituteParameters } from '../utils/parameterSubstitution'; +import { takeRecipeParametersForSession } from '../utils/recipeParametersStore'; import CreateRecipeFromSessionModal from './recipes/CreateRecipeFromSessionModal'; import { toastSuccess } from '../toasts'; import { Recipe } from '../recipe'; @@ -542,6 +543,7 @@ export default function BaseChat({ onSubmit={setRecipeUserParams} onClose={() => setView('chat')} initialValues={ + takeRecipeParametersForSession(chat.sessionId) || (window.appConfig?.get('recipeParameters') as Record | undefined) || undefined } diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 8294ac0f712c..e47c361bad67 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -496,11 +496,15 @@ async function openRecipeDeeplink(url: string, openDir: string | null) { targetWindow.restore(); } targetWindow.focus(); - targetWindow.webContents.send('open-recipe-deeplink', { - recipeDeeplink: deeplinkData?.config, - recipeParameters: deeplinkData?.parameters, - scheduledJobId: scheduledJobId || undefined, - }); + if (targetWindow.webContents.isLoadingMainFrame()) { + pendingDeepLinks.set(targetWindow.id, url); + } else { + targetWindow.webContents.send('open-recipe-deeplink', { + recipeDeeplink: deeplinkData?.config, + recipeParameters: deeplinkData?.parameters, + scheduledJobId: scheduledJobId || undefined, + }); + } return; } @@ -1536,6 +1540,14 @@ ipcMain.on('react-ready', (event) => { window.webContents.send('add-extension', deepLinkUrl); } else if (parsedUrl.hostname === 'sessions') { window.webContents.send('open-shared-session', deepLinkUrl); + } else if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') { + const deeplinkData = parseRecipeDeeplink(deepLinkUrl); + const scheduledJobId = parsedUrl.searchParams.get('scheduledJob'); + window.webContents.send('open-recipe-deeplink', { + recipeDeeplink: deeplinkData?.config, + recipeParameters: deeplinkData?.parameters, + scheduledJobId: scheduledJobId || undefined, + }); } } catch (error) { log.error('Error processing pending deep link:', error); diff --git a/ui/desktop/src/utils/recipeParametersStore.ts b/ui/desktop/src/utils/recipeParametersStore.ts new file mode 100644 index 000000000000..4f0d2f1de78c --- /dev/null +++ b/ui/desktop/src/utils/recipeParametersStore.ts @@ -0,0 +1,25 @@ +// Bridges URL-supplied recipe parameters from the warm-launch IPC path into +// ParameterInputModal. The cold-launch path seeds the same data into +// window.appConfig.recipeParameters, which is fixed at preload and cannot be +// mutated after window load. + +const store = new Map>(); + +export function setRecipeParametersForSession( + sessionId: string, + parameters: Record | undefined +): void { + if (parameters && Object.keys(parameters).length > 0) { + store.set(sessionId, parameters); + } +} + +export function takeRecipeParametersForSession( + sessionId: string +): Record | undefined { + const value = store.get(sessionId); + if (value) { + store.delete(sessionId); + } + return value; +}