Skip to content

fix(desktop): reuse existing window for recipe deep links - #9289

Closed
SarthakB11 wants to merge 2 commits into
aaif-goose:mainfrom
SarthakB11:fix/issue-9143
Closed

SarthakB11 wants to merge 2 commits into
aaif-goose:mainfrom
SarthakB11:fix/issue-9143

Conversation

@SarthakB11

Copy link
Copy Markdown
Contributor

Summary

Clicking a goose://recipe/... or goose://bot/... deep link while Goose Desktop is already running was spawning a fresh BrowserWindow every time instead of routing the deeplink into the focused window. Three protocol entry points in ui/desktop/src/main.ts (app.on('second-instance'), processProtocolUrl, app.on('open-url')) each called createChat(app, { recipeDeeplink, ... }) unconditionally.

This adds an openRecipeDeeplink(url, openDir) helper next to processProtocolUrl in ui/desktop/src/main.ts. It picks the most-recently-focused BrowserWindow (falling back to the newest open window, then the oldest), restores and focuses it, and forwards the deeplink as an open-recipe-deeplink IPC payload (recipeDeeplink URL string, recipeParameters, scheduledJobId). When no window exists it falls back to createChat(...) and threads windowDeeplinkURL so the cold-start dir-picker path keeps its prior behavior. All three protocol call sites now go through the helper.

On the renderer (ui/desktop/src/App.tsx), a new open-recipe-deeplink IPC listener mirrors the existing recipeDeeplinkFromConfig branch in PairRouteWrapper: it calls createSession(getInitialWorkingDir(), { recipeDeeplink, allExtensions }), dispatches ADD_ACTIVE_SESSION, and navigates to /pair?resumeSessionId=.... The payload sends the raw deeplink URL string so the renderer can route through the same decodeRecipe path used at cold launch — the IPC contract stays compatible with createSession's existing recipeDeeplink: string signature.

Reuse is unconditional rather than opt-in; matches the behavior of the surrounding goose://extension / goose://sessions deeplinks.

Testing

Manual only. The change is pure routing in the Electron main process plus a new renderer-side IPC listener; there is no existing harness around main.ts deeplink dispatch. Reviewer test path: with Goose Desktop running, trigger goose://recipe/... from an external process and confirm the recipe loads as a new session in the focused window instead of opening a new one. Worth a quick check on both macOS (open-url) and Linux/Windows (second-instance).

Related Issues

Closes #9143

Screenshots/Demos (for UX changes)

N/A

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6e6743c2e0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ui/desktop/src/App.tsx
Comment on lines +438 to +440
const newSession = await createSession(getInitialWorkingDir(), {
recipeDeeplink,
allExtensions: extensionsList,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass the recipe config instead of the full URL

When a goose://recipe?... link is opened into an existing window, this passes the full URL to createSession, but createSession forwards recipeDeeplink directly to decodeRecipe, whose backend decoder expects only the base64 recipe config string (the cold-start path still passes deeplinkData?.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 the config value here, or update the decode path to accept full Goose URLs.

Useful? React with 👍 / 👎.

@DOsinga
DOsinga requested a review from lifeizhou-ap May 18, 2026 14:43
@DOsinga DOsinga added the needs_human label to set when a robot looks at a PR and can't handle it label May 18, 2026
@DOsinga

DOsinga commented May 18, 2026

Copy link
Copy Markdown
Collaborator

I merged origin/main into this branch and fixed the merge conflicts.

During conflict resolution, I also fixed a bug that Codex flagged: the IPC payload was sending the full goose://recipe?config=... URL as recipeDeeplink, but the renderer's createSession → decodeRecipe path expects just the base64 config string (matching the cold-start path which passes deeplinkData?.config). This would have caused recipe decoding to fail at runtime when reusing an existing window.

The fix: recipeDeeplink: url → recipeDeeplink: deeplinkData?.config in the IPC send.

Items for human reviewer to check:

  • recipeParameters and scheduledJobId are sent in the IPC payload but not consumed by the renderer handler. In the cold-start path, recipeParameters is read from appConfig by BaseChat.tsx for the ParameterInputModal. The IPC path doesn't thread these through, so parameterized recipes opened via deep link in an existing window won't get their URL query string parameter overrides. Worth deciding if this needs addressing.
  • The existingWindows[existingWindows.length - 1] ?? existingWindows[0] fallback is redundant (if the array is non-empty, the last element is always defined, and if it's length 1, last === first). Minor nit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 76ead475d9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ui/desktop/src/App.tsx
Comment on lines +438 to +441
const newSession = await createSession(getInitialWorkingDir(), {
recipeDeeplink,
allExtensions: extensionsList,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Forward deeplink recipe parameters into session creation

This handler receives recipeParameters/scheduledJobId in the IPC payload but only passes recipeDeeplink to createSession, so recipe links opened in an already-running window lose their query-parameter context. That regresses behavior compared with the new-window path (which injects recipeParameters via appConfig) and means parameterized recipe links open without prefilled values in the parameter modal.

Useful? React with 👍 / 👎.

Comment thread ui/desktop/src/main.ts Resolved
@DOsinga

DOsinga commented May 18, 2026

Copy link
Copy Markdown
Collaborator

can you look at the codex comments

@SarthakB11

SarthakB11 commented May 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks @DOsinga, your recipeDeeplink: deeplinkData?.config fix is correct.

On the two Codex items:

P2 — recipeParameters not threaded. Will plumb them through AppEvents.ADD_ACTIVE_SESSION so BaseChat can read them on the warm-launch path (cold path keeps using window.appConfig).

P1 — IPC race when handleProtocolUrl spawns a new window. Will cache the payload in a pendingRecipeDeeplink module slot and replay it on reactReady, matching the existing pendingDeepLink pattern.

Pushing both shortly.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de72a03d65

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ui/desktop/src/components/BaseChat.tsx Resolved
@SarthakB11

Copy link
Copy Markdown
Contributor Author

Sorry for going dark on this. Both fixes are in commit de72a03d, pushed 2026-05-19:

  • Codex P2 (recipeParameters not threaded): I used a per-session module store at ui/desktop/src/utils/recipeParametersStore.ts, populated on the warm-launch IPC path in App.tsx and read by ParameterInputModal via BaseChat.tsx. Avoids touching the ADD_ACTIVE_SESSION event type. scheduledJobId stays in the IPC payload alongside.
  • Codex P1 (IPC race): Reused the existing pendingDeepLinks map + react-ready handler in main.ts rather than adding a dedicated cache slot. Matches how extension and sessions deeplinks already replay.

Files touched: main.ts, App.tsx, BaseChat.tsx, plus the new recipeParametersStore.ts. No local Electron-loop test run (deps too heavy to bootstrap quickly), so worth a spot-check on the warm-window deep-link path before merging. Happy to follow up on anything that looks off.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale because it has not had recent activity for 23 days.

What happens next?

  • If no further activity occurs, this PR will be automatically closed in 7 days
  • To keep this PR active, simply add a comment, push new commits, or add the keep-open label
  • If you believe this PR was marked as stale in error, please comment and we'll review it

Thank you for your contribution! 🚀

@github-actions github-actions Bot added the stale label Jul 5, 2026
@DOsinga

DOsinga commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Merged latest main into this branch and resolved the conflicts. Heads up: this needed more than a mechanical merge, because main has moved on since May.

What changed while this PR sat: the recipe-parameter machinery this PR built on was refactored away on main. BaseChat.tsx no longer renders ParameterInputModal, and recipe parameters now flow through an ACP request (requestAcpRecipeParams) that previously only read the global window.appConfig.recipeParameters (fixed at preload).

How I resolved it:

  • Kept the core of the PR — the three protocol entry points (second-instance, processProtocolUrl, open-url) plus the react-ready pending-deeplink path now all route through the single openRecipeDeeplink helper and reuse the focused window. This is the Open new links in same window, not new window #9143 fix and still works.
  • Dropped the now-obsolete ParameterInputModal/recipeParametersStore plumbing in BaseChat.tsx (that code no longer exists on main).
  • To avoid a regression where a warm-launched parameterized recipe would lose its query-param values, I bridged your per-session recipeParametersStore into the new ACP flow: requestAcpRecipeParams now does takeRecipeParametersForSession(sessionId) ?? configuredParameterValues().

Verified locally: typecheck ✓, lint ✓, all UI tests pass (including the recipeParamRequests test).

Could you sanity-check a parameterized deep link on a warm window — i.e. with Goose already running, trigger goose://recipe/...?someParam=value and confirm the parameter comes through pre-filled? The unparameterized window-reuse path is the main fix; the parameterized path is the bit I adapted to the new architecture and would value a second pair of eyes on.

@DOsinga DOsinga added snoozed:20260707 and removed stale needs_human label to set when a robot looks at a PR and can't handle it labels Jul 5, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47c0aab03b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ui/desktop/src/App.tsx Resolved
@SarthakB11

Copy link
Copy Markdown
Contributor Author

On the warm-window path the recipe param field opens blank, because the param store is written after the read that consumes it.

Ordering:

  1. handleOpenRecipeDeeplink awaits createSession(...), then calls setRecipeParametersForSession(newSession.id, ...) only once that promise resolves.
  2. createSession drives session/new, and the server asks for params inside that call before it responds: handle_new_session -> configure_new_session -> render_recipe_for_session (crates/goose/src/acp/server/new_session.rs:110) -> request_recipe_params (crates/goose/src/acp/server/recipe/mod.rs:386).
  3. That request reaches the renderer at requestAcpRecipeParams, which reads takeRecipeParametersForSession(request.sessionId) ?? configuredParameterValues(). The store is still empty at this point: setRecipeParametersForSession runs after the await, and code after an await cannot execute until session/new has resolved, which only happens once this param request has already been answered.
  4. So it falls back to configuredParameterValues(), i.e. window.appConfig.recipeParameters. On a warm window that config was fixed at the window's original load and holds no recipe params, so the modal opens with empty fields.

Cold start works for the opposite reason: openRecipeDeeplink with no target window creates a fresh window via createChat with recipeParameters in appConfig, so configuredParameterValues() returns the values.

Fix: populate the store before createSession, not after. The sessionId is not known until createSession resolves, so stash the pending params in a module-level slot keyed independently (or let requestAcpRecipeParams fall back to that slot), so the request_recipe_params handler can read them synchronously when the request arrives mid-session/new.

Two smaller items in the same path:

  • scheduledJobId is parsed in openRecipeDeeplink but only threaded on the cold createChat branch. The warm webContents.send('open-recipe-deeplink', ...) payload, the react-ready pending-deeplink handler, and handleOpenRecipeDeeplink's createSession call all omit it, so a scheduled job opened into an existing window loses its id.
  • In the target-window selection, the ?? existingWindows[0] after existingWindows[existingWindows.length - 1] never adds a case. If the array is non-empty, [length - 1] is already defined; if it is empty, both [length - 1] and [0] are undefined and it falls through to null.

@lifeizhou-ap

Copy link
Copy Markdown
Collaborator

Hi @SarthakB11 Thanks for your work!

I did some testing on this branch and found that there are 2 issues with deeplink

  • The "Trust and Execute" windows blinks
  • For the recipe with params the parameter input form window does not disappear after submitting the form.

Would you like to fix these? Thank you!

Route the recipe/bot deep-link protocol handlers through a single
openRecipeDeeplink helper that reuses the focused window instead of
always spawning a new one, and defer the IPC send until the renderer
signals react-ready. Bridge URL-supplied recipe parameters into the
ACP recipe-parameter request so a warm-launched parameterized recipe
keeps its values.

Fixes aaif-goose#9143

Signed-off-by: SarthakB11 <[email protected]>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8edd19b9d6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ui/desktop/src/main.ts Resolved
Comment thread ui/desktop/src/App.tsx Resolved
@SarthakB11

Copy link
Copy Markdown
Contributor Author

Hi @lifeizhou-ap, thanks a lot for testing this and catching both. I pushed a fix for each and rebased onto latest main.

1. "Trust and Execute" window blinking
The recipe protocol URL was getting handled more than once per click. On macOS the open-url event can fire twice for a single deep link, and on Windows/Linux the URL arrives through both the second-instance event and the startup argv. Each delivery ran openRecipeDeeplink and created its own session, so the warning modal mounted twice and flashed. I added a short burst-dedup on the recipe path, the same guard the shared-session deep link already uses, so a repeated URL inside a 2s window is ignored.

2. Parameter form not closing after submit
On the warm path the form opened with empty fields, so pressing submit hit the required-field validation and the form stayed open. The URL values were stored under the new session id, but only after createSession resolved. The parameter request actually comes back from the server in the middle of session/new, before that id exists, so the store was still empty and the form fell back to blank values. I moved the stash into a session-less pending slot that is set before createSession, so requestAcpRecipeParams reads the values when the request arrives and the fields come through pre-filled. Submit then passes validation and the form closes.

I also dropped the dead existingWindows[0] fallback you and Douwe flagged.

Verified locally: the recipe-param unit tests pass (added two for the pre-fill and the one-shot consume), and the App and ParameterInputModal tests are green. I could not drive the full Electron window loop on my side, so a re-test of both flows would be great.

One item still open from the earlier notes: scheduledJobId is threaded only on the cold createChat path, not the warm one. Carrying it end to end needs a new field on the ACP recipe options, so I left it out here to keep the change focused. I can follow up on that separately if useful.

@DOsinga DOsinga left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for iterating on this. I think this is close, but there are a few remaining issues we should address before merging:

  1. Please make the warm recipe/bot deeplink reuse path target only normal chat windows. Right now openRecipeDeeplink uses BrowserWindow.getFocusedWindow() or the newest window, which can be the launcher or a standalone app window. Those windows still mount AppInner, so a recipe session can end up being created in the tiny launcher/standalone window instead of a chat window. We should filter to the tracked chat windows (or create a chat window if none exists).

  2. Please clear or scope the pending recipe parameter stash when no ACP parameter request consumes it. setPendingRecipeParameters() is read by takePendingRecipeParameters() only if requestAcpRecipeParams() runs. If the recipe has URL params but no declared recipe params, or session creation fails before the ACP request, those values can leak into the next unrelated recipe param request. A finally/success cleanup or more tightly scoped pending request would avoid that.

  3. scheduledJobId is still only preserved on the cold createChat branch. The warm IPC path drops it. If carrying this through needs an ACP option change, please either include that here or make a clear case for why scheduled recipe links into an existing window are intentionally out of scope and not regressing expected behavior.

  4. Please trim the longer explanatory comments added in App.tsx, main.ts, and recipeParametersStore.ts. Some explanation of the ACP timing is useful, but a lot of the current text documents the history of the fix rather than the resulting code.

The current checks look good, and the direction makes sense, but I would like these handled before approval.

Issues when opening a recipe deep link into an already running window:

- The 'Trust and Execute' warning flashed twice because the OS can
  deliver one protocol URL more than once (macOS re-fires 'open-url';
  Windows/Linux relay through both 'second-instance' and startup argv).
  Each delivery created its own session and modal. openRecipeDeeplink
  now drops burst duplicates, matching the existing shared-session
  deep-link guard.

- A parameterized recipe opened its parameter form with empty fields, so
  submitting failed required-field validation and the form never closed.
  The URL values were stashed per session id only after createSession
  resolved, but the parameter request arrives mid-session/new before that
  id exists. The values are now stashed in a session-less pending slot
  before createSession and cleared in a finally, so the request prefills
  them and unconsumed values cannot leak into a later recipe.

- The reuse path now targets only tracked chat windows. The launcher and
  standalone-app windows also mount AppInner but cannot host a chat
  session, so it falls through to createChat when no chat window exists.

Also drops the unreachable existingWindows[0] fallback.

Signed-off-by: SarthakB11 <[email protected]>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c344d36b9e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ui/desktop/src/App.tsx
});
toast.error(`Failed to open recipe: ${errorMessage(error, 'Unknown error')}`);
} finally {
setPendingRecipeParameters(undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope pending recipe params per deeplink

When two different recipe deeplinks are opened into the same warm renderer while the first createSession() is still in flight, this finally can clear parameters that were just stashed by the second handler. For example, if link A is slow/no-params and link B sets pendingRecipeParameters before A completes, A reaches this line and erases B's values before B's ACP parameter request consumes them, so B's modal opens without the URL-supplied prefills. The stash needs ownership/scoping per deeplink/session attempt, or the cleanup should only clear the values written by this handler.

Useful? React with 👍 / 👎.

@SarthakB11

Copy link
Copy Markdown
Contributor Author

Thanks @DOsinga, all four done and pushed:

  1. Window filter: now selects only from tracked chat windows, so the launcher and standalone windows can no longer get a recipe session.
  2. Pending stash: cleared in a finally, so an unconsumed stash never outlives its deep link.
  3. scheduledJobId: nothing reads it on either path today, so the warm path dropping it is not a regression. I can wire it as a follow-up if you want that supported.
  4. Comments: trimmed to the ACP-timing reason.

Tests pass; a re-test of the warm launcher case would be welcome.

@SarthakB11

Copy link
Copy Markdown
Contributor Author

All handled by the follow-up commit c344d36: config sent instead of the full URL, params stashed before session creation, IPC deferred until the renderer is ready, and reuse scoped to tracked chat windows.

@lifeizhou-ap

Copy link
Copy Markdown
Collaborator

Hi @SarthakB11,

Thanks for the update.

I tested this again, and the issue still seems to exist:

For a recipe with params, the parameter input form window does not disappear after submitting the form.

Could you please take another look? When you update the PR again, it would also be helpful to run through the recipe params flow end-to-end to make sure the submit behavior is working as expected.

Thanks again for working on this.

@alexhancock

Copy link
Copy Markdown
Collaborator

Closing due to that last followup not being addressed for two weeks #9289 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Open new links in same window, not new window

4 participants