Skip to content

Fix browser deep-link popups (slack://, discord://, etc.) - #4226

Merged
austinywang merged 8 commits into
mainfrom
issue-4224-browser-deep-link-popups
May 17, 2026
Merged

austinywang merged 8 commits into
mainfrom
issue-4224-browser-deep-link-popups

Conversation

@austinywang

@austinywang austinywang commented May 16, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #4224.

Summary

  • Route browser external-scheme navigations regardless of main-frame vs subframe target so hidden-iframe app deep links reach LaunchServices.
  • Share external deep-link handling across the main browser pane and popup delegates, including debug browser.navigation.external ... opened= logs.
  • Parse safe HTTPS intent:// browser fallback URLs and load them in the web view.
  • Show a localized sheet with a copy-link action when macOS reports that an external URL could not be opened.

Reproduction

  • Used the running local cmux production app in workspace cmux78: #4224 browser deep-link popups.
  • Created a throwaway browser pane with a data URL containing a button that inserts a hidden iframe with vscode://file/Users/austinwang/manaflow/term/cmux78/README.md.
  • Clicking the button changed the page status to iframe fired and inserted the iframe, but Visual Studio Code did not launch.
  • Running /usr/bin/open with the same vscode:// URL launched Visual Studio Code, confirming the scheme handler worked and the failure was in cmux/WebKit routing.

Testing

  • Added regression coverage first in commit test: cover browser external deep links; it fails before the fix because subframe external routing stays false and intent fallback extraction returns nil.
  • Did not run local tests per repo instructions; CI should exercise the unit tests.

Note

Medium Risk
Changes browser navigation routing for external schemes and intent:// URLs across main and popup WebViews, which can affect link-opening behavior and user flows. Adds new prompts/sheets and clipboard copy behavior that should be validated across navigation paths.

Overview
Fixes external deep-link handling so non-HTTP(S) URLs (e.g., slack://, discord://) are routed out to macOS even when triggered from subframes and popups, using a shared browserHandleExternalNavigation path.

Adds user-facing UX around external navigation: a localized “Open external app?” prompt before handing off to NSWorkspace, plus a localized failure sheet with Copy Link (copies to pasteboard) when macOS can’t open the URL.

Introduces parsing of intent:// URLs to extract a safe HTTPS S.browser_fallback_url and load it in the WebView instead of launching an app, and adds regression tests covering subframe routing and intent fallback extraction.

Reviewed by Cursor Bugbot for commit f87b55b. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Fixes deep-link handling so app URLs like slack://, discord://, vscode://, and zoommtg:// open from pages, subframes, and popups (#4224). Adds an “Open App?” prompt, safe intent:// HTTPS fallbacks, a localized failure dialog with Copy Link, and masks URLs in debug logs.

  • Bug Fixes
    • Route external schemes from any frame in both main and popup views; cancel WebKit and open via macOS.
    • Ask before opening external app links (Open App / Stay in Browser), with localized text.
    • Extract HTTPS-only S.browser_fallback_url from intent:// and load it via browser navigation.
    • On open failure, show a localized warning with Copy Link; mask external deep-link URLs in debug logs.

Written for commit f87b55b. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

Release Notes

  • New Features

    • Improved external link handling with intelligent fallback support to enhance app compatibility
    • New error notifications with user-friendly messaging when external applications fail to open, including a quick link copy option
    • Enhanced multilingual support for external navigation error messages
  • Tests

    • Added comprehensive test coverage for external navigation routing and error handling scenarios

Review Change Stack

@vercel

vercel Bot commented May 16, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment May 16, 2026 1:37am
cmux-staging Building Building Preview, Comment May 16, 2026 1:37am

@coderabbitai

coderabbitai Bot commented May 16, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds centralized external-navigation routing for native app deep-links (slack://, discord://, etc.) with intent:// URL fallback extraction, NSWorkspace failure handling, and user-visible alert UI. Applied consistently across main browser pane and popup windows. Includes localization strings and test coverage.

Changes

External Navigation Routing and Intent URL Fallback

Layer / File(s) Summary
Core external navigation routing, intent URL parsing, and error UI
Resources/Localizable.xcstrings, Sources/Panels/BrowserPanel.swift
Adds browserShouldRouteExternalNavigation to classify URLs as external-routable, browserIntentFallbackURL to extract HTTPS fallbacks from intent:// URLs (only allowing http/https schemes), and browserHandleExternalNavigation to attempt NSWorkspace launch with fallback extraction, derive app name from URL scheme, support pasteboard copy, and present a localized failure alert when launch fails.
BrowserPanel WebKit delegate integration
Sources/Panels/BrowserPanel.swift
Updates WKNavigationDelegate.decidePolicyFor navigationAction and WKUIDelegate.createWebViewWith to route external navigations through the new centralized handlers instead of inline NSWorkspace calls, canceling WebKit navigation when routed externally.
BrowserPopupWindowController WebKit delegate integration
Sources/Panels/BrowserPopupWindowController.swift
Updates PopupUIDelegate.createWebViewWith and PopupNavigationDelegate.decidePolicyFor navigationAction to use the new external-routing handlers, with proper URL guarding and frame-context awareness for popup window decisions.
External navigation routing test coverage
cmuxTests/BrowserConfigTests.swift
Adds tests for external routing of custom app-schemes in subframes, HTTPS subframe containment, intent:// fallback extraction with allowed schemes, and rejection of external-scheme fallbacks.

Sequence Diagram

sequenceDiagram
  participant WKNav as WKNavigationDelegate
  participant BrowserPanel
  participant URLCheck as browserShouldRouteExternalNavigation
  participant IntentParser as browserIntentFallbackURL
  participant Handler as browserHandleExternalNavigation
  participant NSWrksp as NSWorkspace.shared
  participant Alert as NSAlert

  WKNav->>URLCheck: Check if URL is external-routable
  URLCheck-->>WKNav: true for scheme like slack://
  WKNav->>Handler: Route external navigation
  Handler->>IntentParser: Extract fallback from intent://
  IntentParser-->>Handler: HTTPS URL or nil
  alt Fallback extracted
    Handler->>BrowserPanel: Load fallback URL
  else No fallback
    Handler->>NSWrksp: Attempt open URL
  end
  alt NSWorkspace succeeds
    NSWrksp-->>Handler: true
    Handler-->>WKNav: true (handled)
  else NSWorkspace fails
    NSWrksp-->>Handler: false
    Handler->>Alert: Present failure alert with app name
    Alert-->>Handler: User dismisses or copies URL
    Handler-->>WKNav: false (failed)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • manaflow-ai/cmux#1150: Updates the popup controller delegates in BrowserPopupWindowController.swift that were originally added in this PR to use the new external-navigation routing flow.
  • manaflow-ai/cmux#2660: Modifies the same WebKit navigation-policy decision logic in BrowserPanel.swift and BrowserPopupWindowController.swift for passkey authorization, creating overlap at the shared navigation delegate control flow.

Poem

🐰 Deep links now launch with grace,
Intent URLs find their place,
When Slack won't open, users see
A kindly message, not silence free—
And macOS apps from subframes jump,
No more dropped requests by the pump! 🎉


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift File And Package Boundaries ❌ Error PR adds 131 lines to oversized BrowserPanel.swift (11k+ lines, 600+ over budget), mixing UI/parsing/pasteboard logic without extracting independently testable intent:// URL parsing. Extract browserIntentFallbackURL and browserExternalApplicationName to a SwiftPM package. Keep NSAlert/NSWorkspace integration in app target. Reduces oversized file pressure and satisfies boundary rules.
Cmux Swift Logging ❌ Error NSLog at line 1356 logs raw url.absoluteString containing sensitive data without sanitization in production code, violating swift-logging.md rules on personal data exposure. Replace NSLog with sanitized logging using browserNavigationDebugURL(url) or guard with #if DEBUG like adjacent debug logs in same function.
Cmux User-Facing Error Privacy ❌ Error User-facing alert embeds vendor names (Discord, Slack, Zoom, Teams) via browserExternalApplicationName(). Raw URLs logged in production at line 1356 expose sensitive identifiers. Use generic "This app" instead of vendor names in alerts. Remove NSLog exposing raw URLs. Use only generic terms in localized strings per user-facing-errors.md rule.
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (12 passed)
Check name Status Explanation
Linked Issues check ✅ Passed All key objectives from #4224 are implemented: subframe external routing is fixed, NSWorkspace failure feedback is added via localized alert, intent:// fallback extraction is implemented, and unit tests cover subframe routing and intent fallbacks.
Out of Scope Changes check ✅ Passed All changes directly address issue #4224: external navigation routing (BrowserPanel.swift, BrowserPopupWindowController.swift), localization strings for alert UI, and test coverage for subframe routing and intent fallback parsing.
Cmux Swift Actor Isolation ✅ Passed New pure-logic functions follow existing pattern of unmarked utilities like browserShouldOpenURLExternally—pre-existing debt not worsened by the PR.
Cmux Swift Blocking Runtime ✅ Passed No blocking primitives found. New functions use NSWorkspace.shared.open (non-blocking), NSAlert async handlers, and string parsing. No semaphores, locks, or sleeps.
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift and localization files. Custom check applies to non-Swift runtime code (TS, JS, shell, build). Swift sleeps covered by separate ruleset.
Cmux Swift Concurrency ✅ Passed No legacy async patterns introduced. Optional completion-handler parameter is valid AppKit boundary case for NSAlert.beginSheetModal, never used non-nil in practice.
Cmux Swift @Concurrent ✅ Passed All new Swift functions added are synchronous. The check targets async functions requiring @concurrent annotations. No violations found.
Cmux Swiftui State Layout ✅ Passed No SwiftUI state violations. PR adds only module-level functions and modifies AppKit/WebKit delegates with no new @Published, @Observable, @StateObject, or GeometryReader constructs.
Cmux Architecture Rethink ✅ Passed Correctness fix for external URL routing. Adds pure helpers and delegate routing. No timing, new state, observers, split UI ownership, or duplicate wiring.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR does not add or materially change standalone windows. NSAlert presentations via sheet/modal are explicitly allowed by the rule. Lint script passes. No cmux.* identifier registration needed.
Title check ✅ Passed The title accurately summarizes the main change: fixing browser deep-link popups for app schemes like slack:// and discord://.
Description check ✅ Passed PR description is comprehensive and well-structured, covering all template sections with detailed summaries, testing approach, and reproduction steps.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-4224-browser-deep-link-popups

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.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/Panels/BrowserPanel.swift">

<violation number="1" location="Sources/Panels/BrowserPanel.swift:1084">
P2: The localized failure message expects a `%@` argument, but `appName` is never formatted into the string.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Re-trigger cubic

Comment thread Sources/Panels/BrowserPanel.swift
@greptile-apps

greptile-apps Bot commented May 16, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes external deep-link (e.g. slack://, discord://, vscode://) routing from subframe navigations and popup WebViews by consolidating external-URL handling into a single shared path (browserHandleExternalNavigation) and removing the isMainFrame guard that was silently swallowing subframe-triggered custom-scheme navigations.

  • Introduces browserExternalNavigationAction(for:) with two cases: .promptToOpenApp (shows a localized "Open External App?" sheet before handing off to macOS) and .browserFallback (parses a safe HTTPS S.browser_fallback_url from intent:// URLs and loads it in the WebView).
  • Rewires BrowserNavigationDelegate, BrowserUIDelegate, PopupNavigationDelegate, and PopupUIDelegate to use the shared helper; adds a requestNavigation path in BrowserPopupWindowController for popup intent-fallback loading.
  • Adds a localized failure dialog with a Copy Link action when NSWorkspace.shared.open() fails, and covers the new paths with targeted unit tests.

Confidence Score: 5/5

Safe to merge; the change is well-scoped to external-URL routing with no regressions to normal navigation policy and targeted regression tests added.

The external-nav consolidation is correct and the diff is internally consistent: decisionHandler is called exactly once on all code paths, intent fallback extraction validates scheme before loading, the failure dialog is localized and exposes no implementation details, and the old NSLog calls have been removed. The only gap is that the new UI presentation helpers interact with AppKit modal APIs without explicit isolation annotation, which is a code hygiene concern rather than a runtime defect given WebKit always delivers delegate callbacks on the main thread.

Sources/Panels/BrowserPanel.swift — the new private presentation helpers should be annotated @mainactor to express their isolation contract explicitly.

Important Files Changed

Filename Overview
Sources/Panels/BrowserPanel.swift Adds shared external-navigation helpers (intent fallback parsing, "Open App?" prompt, failure dialog with Copy Link) and rewires BrowserNavigationDelegate and BrowserUIDelegate to use them; removes the isMainFrame guard so subframe external URLs are now intercepted. New UI-heavy free functions are missing @mainactor annotation.
Sources/Panels/BrowserPopupWindowController.swift Wires PopupUIDelegate and PopupNavigationDelegate through browserHandleExternalNavigation, adds requestNavigation helper for intent fallback loading in popups, and moves the isMainFrame guard after the external-URL check to match the main browser behaviour.
Resources/Localizable.xcstrings Adds en/ja localizations for the new external-open prompt (title, message, openApp, stayInBrowser) and failure dialog (title, message, copyLink); copy is clean and exposes no implementation details.
cmuxTests/BrowserConfigTests.swift Adds targeted regression tests for subframe external routing, embedded-URL suppression, intent fallback URL extraction, and rejection of non-HTTPS fallback schemes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[WebKit navigation delegate\nany frame] --> B{URL in request?}
    B -- No --> C[decisionHandler .allow]
    B -- Yes --> D{browserShouldRouteExternalNavigation}
    D -- No --> E[Continue normal policy checks]
    D -- Yes --> F[browserHandleExternalNavigation]
    F --> G{browserExternalNavigationAction}
    G -- intent:// with HTTPS fallback --> H[.browserFallback\nload fallback URL in WebView]
    G -- non-embedded scheme --> I[.promptToOpenApp\nShow Open External App? sheet]
    H --> J[loadFallbackRequest\ncurrentTab / popup requestNavigation]
    I -- User: Stay in Browser --> K[No-op / DEBUG log]
    I -- User: Open App --> L[NSWorkspace.shared.open]
    L -- success --> M[DEBUG log opened=1]
    L -- failure --> N[Show failure dialog\nCannot Open Link]
    N --> O[User: Copy Link to NSPasteboard\nUser: OK to dismiss]
    F --> P[decisionHandler .cancel]
Loading

Reviews (3): Last reviewed commit: "fix: remove frame parameter from externa..." | Re-trigger Greptile

Comment thread Sources/Panels/BrowserPanel.swift Outdated
coderabbitai[bot]
coderabbitai Bot previously requested changes May 16, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Panels/BrowserPanel.swift`:
- Around line 1354-1357: The NSLog call in BrowserPanel that logs
url.absoluteString when NSWorkspace.shared.open(url) fails should not emit raw
external URLs; remove or replace that NSLog call to emit only sanitized metadata
(e.g., host, path components count, or a redacted URL) consistent with the debug
path, keep the logic that calls browserPresentExternalNavigationFailure(for:
url, in: webView) intact, and ensure the change touches the block where 'opened'
is checked after NSWorkspace.shared.open(url) to avoid logging sensitive data.
- Around line 1276-1311: The alert shown by
browserPresentExternalNavigationFailure(for:in:) exposes vendor/service names
via appName; change the sheet to use cmux-branded, generic copy only (remove
appName from alert.informativeText), e.g. set a localized generic message like
"cmux could not open this link. You can copy the link and open it in another
app." and ensure any user-facing buttons/labels likewise avoid vendor names;
keep browserExternalApplicationName(for:) unused in the sheet body and still use
browserCopyExternalNavigationURL(_:) for the copy action.
- Around line 1331-1366: The function browserHandleExternalNavigation currently
takes an optional loadFallbackRequest and directly calls browserLoadRequest when
nil, which bypasses normal navigation checks; change the signature of
browserHandleExternalNavigation to require a non-optional loadFallbackRequest:
((URLRequest) -> Void) so all fallback requests are routed through the panel’s
standard navigation path, remove the fallback to browserLoadRequest inside
browserHandleExternalNavigation, and update all callers (the delegate sites
referenced) to pass their existing panel navigation handler into
loadFallbackRequest (instead of relying on the default) so intent:// fallbacks
produced by browserIntentFallbackURL go through the same insecure-HTTP prompt
and request-rewriting logic as ordinary navigations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7ae1865f-51fb-4a6e-8dd0-73b49f248ccb

📥 Commits

Reviewing files that changed from the base of the PR and between df72e51 and 45e5afb.

📒 Files selected for processing (4)
  • Resources/Localizable.xcstrings
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/BrowserPopupWindowController.swift
  • cmuxTests/BrowserConfigTests.swift

Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment thread Sources/Panels/BrowserPanel.swift
Comment thread Sources/Panels/BrowserPanel.swift Outdated

@cursor cursor 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.

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 9a2e1cc. Configure here.

Comment thread Sources/Panels/BrowserPanel.swift
@austinywang
austinywang dismissed coderabbitai[bot]’s stale review May 17, 2026 02:22

Stale CodeRabbit changes-requested review. The referenced inline findings have been addressed/resolved, current CodeRabbit check is passing, and CI/CD is green.

@austinywang
austinywang merged commit 3ae2ee0 into main May 17, 2026
26 checks passed
@austinywang austinywang mentioned this pull request May 19, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — f87b55b4 Deployed May 16, 2026 by vercel[bot]
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.

Browser pane: 'Open in Slack/Discord/Zoom' deep-link popups don't launch the native app

1 participant