-
Notifications
You must be signed in to change notification settings - Fork 4.7k
fix(desktop): reuse existing window for recipe deep links #9289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<string, string>; | ||
| scheduledJobId?: string; | ||
| } | ||
| | undefined; | ||
| const recipeDeeplink = payload?.recipeDeeplink; | ||
| if (!recipeDeeplink) { | ||
| return; | ||
| } | ||
| try { | ||
| const newSession = await createSession(getInitialWorkingDir(), { | ||
| recipeDeeplink, | ||
| allExtensions: extensionsList, | ||
| }); | ||
|
Comment on lines
+439
to
+442
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This handler receives Useful? React with 👍 / 👎. |
||
| 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| } | ||
|
|
@@ -486,34 +477,69 @@ async function handleProtocolUrl(url: string) { | |
| } | ||
| } | ||
|
|
||
| async function processProtocolUrl(url: string, parsedUrl: URL, window: BrowserWindow) { | ||
| const recentDirs = loadRecentDirs(); | ||
| const openDir = recentDirs.length > 0 ? recentDirs[0] : null; | ||
| let windowDeeplinkURL: string | null = null; | ||
|
|
||
| if (parsedUrl.hostname === 'extension') { | ||
| window.webContents.send('add-extension', url); | ||
| } else if (parsedUrl.hostname === 'sessions') { | ||
| window.webContents.send('open-shared-session', url); | ||
| } else if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') { | ||
| const deeplinkData = parseRecipeDeeplink(url); | ||
| const scheduledJobId = parsedUrl.searchParams.get('scheduledJob'); | ||
| async function openRecipeDeeplink(url: string, openDir: string | null) { | ||
| const deeplinkData = parseRecipeDeeplink(url); | ||
| const parsedUrl = new URL(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: deeplinkData?.config, | ||
| recipeParameters: deeplinkData?.parameters, | ||
| scheduledJobId: scheduledJobId || undefined, | ||
| }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The main process sends Useful? React with 👍 / 👎. |
||
| return; | ||
| } | ||
|
|
||
| if (deeplinkData) { | ||
| windowDeeplinkURL = url; | ||
| } | ||
| try { | ||
| await createChat(app, { | ||
| dir: openDir || undefined, | ||
| recipeDeeplink: deeplinkData?.config, | ||
| scheduledJobId: scheduledJobId || undefined, | ||
| recipeParameters: deeplinkData?.parameters, | ||
| }); | ||
| } finally { | ||
| windowDeeplinkURL = null; | ||
| } | ||
| } | ||
|
|
||
| let windowDeeplinkURL: string | null = null; | ||
| 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', url); | ||
| } else if (parsedUrl.hostname === 'sessions') { | ||
| window.webContents.send('open-shared-session', url); | ||
| } else if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') { | ||
| await openRecipeDeeplink(url, openDir); | ||
| } | ||
| } | ||
|
|
||
| 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(); | ||
|
|
||
|
|
@@ -528,23 +554,11 @@ 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; | ||
| } | ||
|
|
||
|
|
@@ -893,7 +907,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' | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a
goose://recipe?...link is opened into an existing window, this passes the full URL tocreateSession, butcreateSessionforwardsrecipeDeeplinkdirectly todecodeRecipe, whose backend decoder expects only the base64 recipe config string (the cold-start path still passesdeeplinkData?.config). As a result, any recipe/bot deep link handled through this new IPC path fails to decode instead of opening the recipe; extract and pass theconfigvalue here, or update the decode path to accept full Goose URLs.Useful? React with 👍 / 👎.