diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 9ed1f91c9d86..d76945471d1e 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -202,7 +202,7 @@ if (process.platform === 'win32') { const recentDirs = loadRecentDirs(); const openDir = recentDirs.length > 0 ? recentDirs[0] : null; - const recipeDeeplink = parsedUrl.searchParams.get('config'); + const recipeDeeplink = parseRecipeDeeplink(protocolUrl); const scheduledJobId = parsedUrl.searchParams.get('scheduledJob'); createChat( @@ -319,6 +319,8 @@ async function processProtocolUrl(parsedUrl: URL, window: BrowserWindow) { pendingDeepLink = null; } +let windowDeeplinkURL: string | null = null; + app.on('open-url', async (_event, url) => { if (process.platform !== 'win32') { const parsedUrl = new URL(url); @@ -329,7 +331,10 @@ app.on('open-url', async (_event, url) => { console.log('[Main] Received open-url event:', url); if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') { console.log('[Main] Detected bot/recipe URL, creating new chat window'); - const recipeDeeplink = parsedUrl.searchParams.get('config'); + let recipeDeeplink = parseRecipeDeeplink(url); + if (recipeDeeplink) { + windowDeeplinkURL = url; + } const scheduledJobId = parsedUrl.searchParams.get('scheduledJob'); // Create a new window directly @@ -1037,12 +1042,52 @@ const openDirectoryDialog = async (): Promise => { } addRecentDir(dirToAdd); + + let recipeDeeplink: string | undefined = undefined; + if (windowDeeplinkURL) { + recipeDeeplink = parseRecipeDeeplink(windowDeeplinkURL); + } // Create a new window with the selected directory - await createChat(app, undefined, dirToAdd); + await createChat( + app, + undefined, + dirToAdd, + undefined, + undefined, + undefined, + undefined, + recipeDeeplink + ); } return result; }; +function parseRecipeDeeplink(url: string): string | undefined { + const parsedUrl = new URL(url); + let recipeDeeplink = parsedUrl.searchParams.get('config'); + if (recipeDeeplink && !url.includes(recipeDeeplink)) { + // URLSearchParams decodes + as space, which can break encoded configs + // Parse raw query to preserve "+" characters in values like config + const search = parsedUrl.search || ''; + // parse recipe deeplink from search params + const configMatch = search.match(/(?:[?&])config=([^&]*)/); + // get recipe deeplink from config match + let recipeDeeplinkTmp = configMatch ? configMatch[1] : null; + if (recipeDeeplinkTmp) { + try { + recipeDeeplink = decodeURIComponent(recipeDeeplinkTmp); + } catch { + // Leave as-is if decoding fails + return undefined; + } + } + } + if (recipeDeeplink) { + return recipeDeeplink; + } + return undefined; +} + // Global error handler const handleFatalError = (error: Error) => { const windows = BrowserWindow.getAllWindows();