-
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 all 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 |
|---|---|---|
|
|
@@ -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'; | ||
|
|
@@ -410,7 +411,7 @@ export function AppInner() { | |
| }; | ||
| }, []); | ||
|
|
||
| const { addExtension } = useConfig(); | ||
| const { addExtension, extensionsList } = useConfig(); | ||
|
|
||
| useEffect(() => { | ||
| try { | ||
|
|
@@ -421,6 +422,52 @@ 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 👍 / 👎. |
||
| setRecipeParametersForSession(newSession.id, payload?.recipeParameters); | ||
| 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 +533,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 |
|---|---|---|
|
|
@@ -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) || | ||
|
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.
When a parameterized recipe link is opened into an existing window, this consumes and deletes the stored values during Useful? React with 👍 / 👎. |
||
| (window.appConfig?.get('recipeParameters') as Record<string, string> | undefined) || | ||
| undefined | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string, Record<string, string>>(); | ||
|
|
||
| export function setRecipeParametersForSession( | ||
| sessionId: string, | ||
| parameters: Record<string, string> | undefined | ||
| ): void { | ||
| if (parameters && Object.keys(parameters).length > 0) { | ||
| store.set(sessionId, parameters); | ||
| } | ||
| } | ||
|
|
||
| export function takeRecipeParametersForSession( | ||
| sessionId: string | ||
| ): Record<string, string> | undefined { | ||
| const value = store.get(sessionId); | ||
| if (value) { | ||
| store.delete(sessionId); | ||
| } | ||
| return value; | ||
| } |
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 👍 / 👎.