Skip to content

fix(desktop): handle opencode:// links reliably - #59058

Open
kitlangton wants to merge 3 commits into
devfrom
desktop-link-intake
Open

kitlangton wants to merge 3 commits into
devfrom
desktop-link-intake

Conversation

@kitlangton

@kitlangton kitlangton commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Desktop no longer drops or replays opencode:// links.
A link that launches the app on macOS could arrive before anything was listening, Windows and Linux never read the link from the first launch, and one global list replayed old links into every new window or reload. Links are now checked in the main process before any window sees them.

❌ Before

  1. ❌ macOS: a link that launches the app arrives before anything listens and is lost.
  2. ❌ Windows and Linux: the link on the first launch's command line is never read.
  3. ❌ Every link goes into one global list, so a new window or reload replays old links.
  4. Nothing checks a link before it reaches a window.

✅ After

  1. Links are caught before the app is ready (open-url, second instance, command line) and wait in a small capped buffer until windows are restored.
  2. Only opencode://session/<ses_…> (optional server) and the existing console sign-in link are accepted. Anything else, repeated or unknown parameters, control characters, and links over 2 KB are rejected, and the window gets a link rebuilt from the accepted fields only.
  3. A session link goes to the window whose saved tabs hold that session, else the last-focused window, else any window. With every window closed on macOS, windows are restored first, so no extra blank window appears. A rejected link still brings the app forward.
  4. Each page gets a link once, when it asks for them. A reload or new window never sees links an earlier page already received.
- link -> one global list -> replayed to whichever page loads next
+ link -> parseDeepLink (reject unknown) -> pick window -> queue per page -> sent once when that page asks

📊 Proof: by hand in a packaged build on a fresh macOS VM (before the latest rebase), together with #59061.

Case Before After
Only window closed, app running, subagent link ❌ nothing opens ✅ the window comes back with its tabs and opens the subagent in its root's tab, one window in the Window menu
New window or reload after a link ❌ old links replayed ✅ no replay
Malicious or oversized link ❌ not checked ✅ rejected

17 lifecycle tests pass, covering valid, malicious and oversized links, window choice, and no replay.

👀 Read: lifecycle/index.ts (which window gets the link, and restoring closed windows first) and link-queue.ts. The app side that opens a session is #59061; until it lands, the visible change is only which window comes forward. Two lint findings in touched files are suppressed with a reason. The rest is routine.

Found while wiring links for another app to open sessions · no user reports

Checks run
  • bun test src/main/lifecycle (packages/desktop, after the rebase): 17 pass
  • bun typecheck (packages/desktop, after the rebase): clean
  • pre-push bun run check (lint and typecheck): pass

Added by the opencode bot (AI-generated).

Related: #44167, #53529

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The change looks sound. I couldn't run it here: it needs the Electron app and OS-level opencode:// handling. In packages/desktop, the lifecycle tests (13) and all src/main tests (118) pass, and tsc -b is clean (the package's typecheck script calls bun check, which this Bun version doesn't have). I also ran sample links through parseDeepLink: uppercase schemes, encoded and plain http(s) origins and sidecar are accepted, while javascript: server values and trailing slashes are rejected. The main process reads the same window storage file and tabs fields the app writes, so tab matching should work.

The public API is unchanged: the only signature change is the internal consumeInitialDeepLinks.

One small regression, explained inline: an opencode:// link that is rejected no longer brings any window forward. Before, the app was focused.

Related: #44167, #53529

return win
}

const openLinks = (urls: string[]) => focusWindow(urls.map(openLink).findLast((win) => !!win) ?? null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When every link in a batch is rejected (for example an old v1 opencode://new-session link, see #55106, or anything malformed), openLinks calls focusWindow(null) and nothing comes forward. On Windows and Linux, secondInstance also skips its own focus because linkArguments(argv) isn't empty. On macOS, open-url used to focus the last-focused window too. So clicking a link the app doesn't recognise now does nothing visible, where before it at least brought OpenCode forward. Falling back to getLastFocusedWindow() when no link was accepted would keep the old behaviour.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f35e44b: when no link in a batch is accepted, the app now brings forward the last-focused window (restoring closed windows, or creating one if there is nothing to restore), covered by a new linkBatchWindow test.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new commit looks sound. When a session link arrives on macOS with every window closed, the windows from last time are restored first, so the link opens in the restored window that holds the session rather than a new blank one. windows.restore always returns at least one window, and because restore runs before activate, the windows aren't restored a second time. I couldn't run the desktop app here. In packages/desktop, the lifecycle tests (15) and all src/main tests (120) pass, and tsc -b is clean.

The public API is unchanged.

My earlier comment isn't addressed yet: when every link in a batch is rejected, no window is focused.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My earlier point is addressed: when every link in a batch is rejected, the app comes forward again. It focuses the last-focused or any open window, and on macOS with every window closed it restores the windows from last time. I couldn't run the desktop app here. In packages/desktop, the lifecycle tests (17) and all src/main tests (122) pass, and tsc -b is clean.

The public API is unchanged. Nothing new to raise.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This push only rebases the PR onto dev. The deep-link commits are unchanged apart from switching to dev's renamed mainWindows, which also removes the two lint suppressions the PR had added. I couldn't run the desktop app here. In packages/desktop, the lifecycle tests (17) and all src/main tests (163) pass, and tsc -b is clean.

The public API is unchanged. Nothing new to raise.

@kitlangton kitlangton changed the title fix(desktop): reliable deep link intake fix(desktop): handle opencode:// links reliably Oct 26, 2026

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant