Skip to content

feat(desktop): register t3:// OS protocol handler for thread deep links#2424

Open
davidmashburn wants to merge 2 commits intopingdotgg:mainfrom
davidmashburn:feat/t3-deep-link-protocol
Open

feat(desktop): register t3:// OS protocol handler for thread deep links#2424
davidmashburn wants to merge 2 commits intopingdotgg:mainfrom
davidmashburn:feat/t3-deep-link-protocol

Conversation

@davidmashburn
Copy link
Copy Markdown

@davidmashburn davidmashburn commented Apr 30, 2026

What Changed

Adds a t3://thread/<threadId> OS-level protocol handler to the Electron desktop app.

  • apps/desktop/src/main.ts: registers open-url (macOS) and second-instance (Win/Linux) listeners before whenReady; handleDeepLink() parses the URL and sends desktop:open-thread to the renderer via webContents.send; pendingDeepLinkThreadId queues deep links that arrive before the window finishes loading; setAsDefaultProtocolClient('t3') called in whenReady
  • apps/desktop/src/preload.ts: exposes onOpenThread over the context bridge (same pattern as onMenuAction)
  • packages/contracts/src/ipc.ts: adds onOpenThread to the DesktopBridge interface
  • apps/web/src/components/AppSidebarLayout.tsx: subscribes to onOpenThread and navigates to /$environmentId/$threadId using TanStack Router
  • scripts/build-desktop-artifact.ts: adds protocols: [{name:"T3 Code", schemes:["t3"]}] to the mac electron-builder config so CFBundleURLTypes is written into the packaged Info.plist
  • test mocks (localApi.test.ts, SettingsPanels.browser.tsx): stub onOpenThread: () => () => {} added

Why

Clicking a thread link (e.g. from Slack or the terminal) currently opens a browser tab that hits the pairing screen. With this change, t3://thread/<id> links route directly to the correct thread inside the already-running desktop app — or launch it if it's closed.

Verified end-to-end in dev: open "t3://thread/<id>" in the terminal fired the open-url event, handleDeepLink parsed it correctly, and webContents.send("desktop:open-thread", threadId) was confirmed in the Electron main-process logs.

UI Changes

N/A — no visual changes. Navigation behavior only.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

davidmashburn and others added 2 commits April 29, 2026 16:05
Clicking a t3://thread/<threadId> link from anywhere — browser, terminal,
another app — now focuses the running T3 window and navigates directly
to that thread without a pairing screen.

Changes:
- main.ts: setAsDefaultProtocolClient("t3"), open-url handler (macOS),
  requestSingleInstanceLock + second-instance handler (Windows/Linux),
  handleDeepLink() parses t3://thread/<threadId> and sends IPC to the
  renderer; queues in pendingDeepLinkThreadId if window not yet loaded
- preload.ts: expose onOpenThread via desktopBridge (desktop:open-thread)
- AppSidebarLayout: subscribe to onOpenThread, navigate to
  /$activeEnvironmentId/$threadId on receipt
- contracts/ipc.ts: add onOpenThread to DesktopBridge interface
- Update two test mocks for the new bridge method

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Include the `t3` protocol in the electron-builder mac config so the
packaged app registers the t3:// URL scheme at install time via
CFBundleURLTypes in Info.plist, rather than relying solely on the
runtime setAsDefaultProtocolClient call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 34994830-a516-4636-8a2a-888f8411af5a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Apr 30, 2026
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c1e8393. Configure here.

Comment thread apps/desktop/src/main.ts
if (pendingDeepLinkThreadId !== null) {
window.webContents.send(OPEN_THREAD_CHANNEL, pendingDeepLinkThreadId);
pendingDeepLinkThreadId = 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.

Cold-start deep link permanently lost due to timing

High Severity

The cold-start deep link path is broken due to two compounding timing issues. First, a comment in the same file notes that did-finish-load "typically fires before the first paint," but React's useEffect (which registers the onOpenThread IPC listener) runs after paint — so the IPC message is sent before any listener exists. Second, even if that race were avoided, activeEnvironmentId is initialized to null and only set once serverConfig loads from the backend; during cold start the callback will hit the if (!activeEnvironmentId) return guard and silently discard the thread ID. Since pendingDeepLinkThreadId is cleared after sending and never retried, the deep link is permanently lost.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c1e8393. Configure here.

@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp Bot commented Apr 30, 2026

Approvability

Verdict: Needs human review

This PR introduces a new OS protocol handler feature with deep link navigation capabilities, which constitutes new user-facing behavior requiring human review. Additionally, an unresolved review comment identifies a high-severity timing bug where cold-start deep links may be permanently lost.

You can customize Macroscope's approvability policy. Learn more.

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

Labels

size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant