Skip to content

Add configurable link-click modifiers (#797) - #4022

Open
austinywang wants to merge 1 commit into
mainfrom
issue-797-link-click-modifier
Open

austinywang wants to merge 1 commit into
mainfrom
issue-797-link-click-modifier

Conversation

@austinywang

@austinywang austinywang commented May 12, 2026 •

Copy link
Copy Markdown
Contributor

Closes #797

Summary

  • Add configurable terminal link-click modifiers for system default browser vs cmux browser pane.
  • Wire cmux.json support for browser keys plus link.click.* aliases, schema/docs, and Settings rows.
  • Add behavior coverage for modifier parsing, URL routing, settings parsing, and search anchors.

Testing

  • Not run locally per repo policy. CI will run on the PR.

Note

Medium Risk
Changes how terminal URL clicks are routed (embedded vs external) and how modifier keys are interpreted, which can affect core navigation behavior and input handling. Scope is contained to browser/link settings with added tests and schema/docs updates.

Overview
Adds configurable terminal link-click modifiers so specific modifier+click chords can force opening URLs in either the system default browser or a cmux browser pane, with defaults of cmd+click (external) and opt+click (cmux).

Refactors terminal URL opening to resolve a new TerminalOpenURLRoute that applies external URL patterns, per-modifier overrides, the existing terminal-link toggle, and host-whitelist logic (explicit cmux-modifier clicks bypass the host whitelist but not external patterns). Modifier handling is updated so Ghostty receives the correct link trigger while cmux captures the actual modifier flags for routing.

Wires the new settings through Settings UI, UserDefaults, cmux.json parsing (including link.click.* aliases), settings search index/aliases, localization strings, and updates the web schema/docs; adds unit tests covering parsing, routing, search anchors, and settings-file ingestion.

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


Summary by cubic

Adds configurable link-click modifiers for terminal URLs so users can choose which modifier opens links in the system browser vs the built‑in cmux browser. Updates settings, schema, routing, and tests to support explicit routing and clearer defaults. Closes #797.

  • New Features

    • New settings: browser.terminalLinkDefaultBrowserModifier and browser.terminalLinkCmuxBrowserModifier (defaults: cmd+click → system browser, opt+click → cmux pane).
    • New config aliases: link.click.defaultBrowser and link.click.builtInBrowser.
    • Settings UI adds pickers for both modifiers, with localization and search integration.
    • Routing: explicit modifiers override automatic behavior; cmux‑modifier clicks bypass the host whitelist but still honor external URL patterns.
    • Updated terminal click handling so modifier chords are detected without interfering with Ghostty’s link trigger.
    • openTerminalLinksInCmuxBrowser now applies to unmatched clicks; copy updated in UI/docs.
    • Schema and docs updated; behavior tests added for parsing, routing, settings, and search.
  • Migration

    • No action required. Existing clicks behave the same unless a modifier matches.
    • Defaults: cmd+click opens in the system browser; opt+click opens in the cmux browser pane.

Written for commit 52bb500. Summary will update on new commits.

@vercel

vercel Bot commented May 12, 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 12, 2026 7:49pm
cmux-staging Building Building Preview, Comment May 12, 2026 7:49pm

@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@austinywang has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 57 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1bd17d82-b39b-43db-b176-dd8edbe57911

📥 Commits

Reviewing files that changed from the base of the PR and between 5829da2 and 52bb500.

📒 Files selected for processing (15)
  • Resources/Localizable.xcstrings
  • Sources/CmuxSettingsJSONPathSupport.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/KeyboardShortcutSettingsFileStore+Template.swift
  • Sources/KeyboardShortcutSettingsFileStore.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/SettingsNavigation.swift
  • Sources/SettingsSearchAliases.swift
  • Sources/cmuxApp.swift
  • cmuxTests/BrowserConfigTests.swift
  • cmuxTests/KeyboardShortcutSettingsFileStoreStartupTests.swift
  • cmuxTests/SettingsSearchIndexTests.swift
  • cmuxTests/TerminalAndGhosttyTests.swift
  • web/app/[locale]/docs/configuration/page.tsx
  • web/data/cmux.schema.json
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-797-link-click-modifier

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.

@greptile-apps

greptile-apps Bot commented May 12, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds configurable modifier-key routing for terminal link clicks, letting users choose separate modifiers for opening links in the system default browser vs. the cmux browser pane. The feature is wired end-to-end: new TerminalLinkClickModifierBinding parsing, cmux.json support with browser.terminalLink* keys and link.click.* top-level aliases, Settings UI picker rows, schema and docs updates, and a new resolveTerminalOpenURLRoute dispatch layer in GhosttyTerminalView.

  • parseBrowserSection guard/return bug: both new modifier-parsing blocks use guard … else { return }, which exits the entire function on an invalid value, silently dropping all browser settings that follow (hostsToOpenInEmbeddedBrowser, interceptTerminalOpenCommandInCmuxBrowser, etc.). The top-level link.click.* aliases in the same PR use the correct if/else pattern.
  • Side-channel timing dependency: currentTerminalOpenURLModifierFlags is set before and cleared via defer after ghostty_surface_mouse_button. This works today because the Ghostty link callback fires synchronously while holding an internal lock (noted as an undocumented implementation detail at line ~4093); a future Ghostty change to async dispatch would silently break modifier routing.

Confidence Score: 3/5

Safe to merge after fixing the parseBrowserSection early-return bug; the side-channel timing concern is low risk given current Ghostty behavior but worth a follow-up.

The guard/return defect in parseBrowserSection is a real config-parsing regression: any user who writes an invalid modifier string loses their host whitelist, intercept, and external-patterns settings silently for that config file load. The modifier logic, parser, UI, and tests are otherwise correct and well-covered.

Sources/KeyboardShortcutSettingsFileStore.swift (guard/return in parseBrowserSection) and Sources/GhosttyTerminalView.swift (currentTerminalOpenURLModifierFlags side-channel timing contract).

Important Files Changed

Filename Overview
Sources/KeyboardShortcutSettingsFileStore.swift Adds cmux.json parsing for two new modifier keys (browser section and top-level link.click.* aliases); the browser-section blocks use guard/return which exits the entire parse function early on invalid values, silently dropping all subsequent browser settings.
Sources/GhosttyTerminalView.swift Introduces TerminalOpenURLRoute enum and resolveTerminalOpenURLRoute function; reworks link-click routing to consult modifier flags via a new fileprivate side-channel var; relies on synchronous Ghostty callback dispatch — an undocumented implementation detail called out in a nearby comment.
Sources/Panels/BrowserPanel.swift Adds TerminalLinkClickModifierBinding value type and three new BrowserLinkOpenSettings helpers; parser handles symbols, text aliases, and multi-modifier chords cleanly with correct fallback defaults.
Sources/cmuxApp.swift Wires two new @AppStorage bindings and picker rows into SettingsView; host-whitelist visibility condition correctly extended to include explicit cmux-browser modifier; subtitle copy updated.
cmuxTests/TerminalAndGhosttyTests.swift Tests resolveTerminalOpenURLRoute directly; does not exercise the end-to-end modifier capture via mouseDown/mouseUp, so the synchronous-dispatch contract is untested at integration level.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[mouseDown / mouseUp\nGhosttyNSView] -->|set currentTerminalOpenURLModifierFlags| B[ghostty_surface_mouse_button\nC call]
    B -->|synchronous link callback| C[GhosttyApp link handler]
    C --> D[resolveTerminalOpenURLRoute\ntarget + modifierFlags]
    D --> E{modifier\nmatches?}
    E -->|defaultBrowser modifier| F[.external - NSWorkspace.open]
    E -->|cmuxBrowser modifier\nbypasses host whitelist| G[.embeddedBrowser - open in pane]
    E -->|no match - automatic| H{openTerminalLinksInCmuxBrowser\n+ host whitelist check}
    H -->|pass| G
    H -->|fail| F
    B -->|defer clears flag| I[currentTerminalOpenURLModifierFlags = nil]
Loading

Comments Outside Diff (1)

  1. Sources/GhosttyTerminalView.swift, line 6163 (link)

    P2 Side-channel mutable state relies on undocumented synchronous Ghostty dispatch

    currentTerminalOpenURLModifierFlags is a new mutable field written by mouseDown/mouseUp just before calling ghostty_surface_mouse_button, then cleared by defer. The link-open callback in GhosttyApp reads it via surfaceView.currentTerminalOpenURLModifierFlags. This works today because a comment at line ~4093 documents that Ghostty dispatches its Swift handler synchronously while holding an internal lock — an explicitly undocumented Ghostty implementation detail. A future Ghostty update that defers dispatch asynchronously would make modifier flags always nil and silently revert all modifier-based routing to .automatic with no visible failure signal.

Reviews (1): Last reviewed commit: "Add configurable terminal link click mod..." | Re-trigger Greptile

Comment on lines +751 to +768
if let raw = jsonString(section["terminalLinkDefaultBrowserModifier"]) {
guard let binding = TerminalLinkClickModifierBinding.parse(raw) else {
logInvalid("browser.terminalLinkDefaultBrowserModifier", sourcePath: sourcePath)
return
}
snapshot.managedUserDefaults[BrowserLinkOpenSettings.terminalLinkDefaultBrowserModifierKey] = .string(
binding.configString
)
}
if let raw = jsonString(section["terminalLinkCmuxBrowserModifier"]) {
guard let binding = TerminalLinkClickModifierBinding.parse(raw) else {
logInvalid("browser.terminalLinkCmuxBrowserModifier", sourcePath: sourcePath)
return
}
snapshot.managedUserDefaults[BrowserLinkOpenSettings.terminalLinkCmuxBrowserModifierKey] = .string(
binding.configString
)
}

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.

P1 return in guard exits parseBrowserSection entirely on bad modifier values

Both new guard blocks call return on parse failure, which exits parseBrowserSection altogether — not just the current if block. If a user writes "terminalLinkDefaultBrowserModifier": "typo" in their cmux.json, every browser setting that appears after this block (interceptTerminalOpenCommandInCmuxBrowser, hostsToOpenInEmbeddedBrowser, urlsToAlwaysOpenExternally, etc.) is silently dropped for that file load. The same PR's top-level link.click.* parsing correctly uses if/else and avoids early exit — apply the same pattern here.

Suggested change
if let raw = jsonString(section["terminalLinkDefaultBrowserModifier"]) {
guard let binding = TerminalLinkClickModifierBinding.parse(raw) else {
logInvalid("browser.terminalLinkDefaultBrowserModifier", sourcePath: sourcePath)
return
}
snapshot.managedUserDefaults[BrowserLinkOpenSettings.terminalLinkDefaultBrowserModifierKey] = .string(
binding.configString
)
}
if let raw = jsonString(section["terminalLinkCmuxBrowserModifier"]) {
guard let binding = TerminalLinkClickModifierBinding.parse(raw) else {
logInvalid("browser.terminalLinkCmuxBrowserModifier", sourcePath: sourcePath)
return
}
snapshot.managedUserDefaults[BrowserLinkOpenSettings.terminalLinkCmuxBrowserModifierKey] = .string(
binding.configString
)
}
if let raw = jsonString(section["terminalLinkDefaultBrowserModifier"]) {
if let binding = TerminalLinkClickModifierBinding.parse(raw) {
snapshot.managedUserDefaults[BrowserLinkOpenSettings.terminalLinkDefaultBrowserModifierKey] = .string(
binding.configString
)
} else {
logInvalid("browser.terminalLinkDefaultBrowserModifier", sourcePath: sourcePath)
}
}
if let raw = jsonString(section["terminalLinkCmuxBrowserModifier"]) {
if let binding = TerminalLinkClickModifierBinding.parse(raw) {
snapshot.managedUserDefaults[BrowserLinkOpenSettings.terminalLinkCmuxBrowserModifierKey] = .string(
binding.configString
)
} else {
logInvalid("browser.terminalLinkCmuxBrowserModifier", sourcePath: sourcePath)
}
}

@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 52bb500. Configure here.

snapshot.managedUserDefaults[BrowserLinkOpenSettings.terminalLinkCmuxBrowserModifierKey] = .string(
binding.configString
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Invalid modifier string aborts entire browser section parsing

Medium Severity

The guard ... else { return } blocks for terminalLinkDefaultBrowserModifier and terminalLinkCmuxBrowserModifier in parseBrowserSection exit the entire function when the freeform string fails to parse. This silently drops all subsequent browser settings (interceptTerminalOpenCommandInCmuxBrowser, hostsToOpenInEmbeddedBrowser, urlsToAlwaysOpenExternally, insecureHttpHostsAllowedInEmbeddedBrowser, etc.). The top-level link.click.* aliases for the same settings correctly use if-else to log and continue parsing. The browser-section path is inconsistent with that approach.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 52bb500. Configure here.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

cmux-reconcile: useful

Usefulness verdict: Use as the configurable-link candidate, adapted to the current shared link coordinator.

The inspected diff adds modifier parsing/settings, per-destination preferences and tests. The current current terminal-link coordinator is now a dedicated shared routing path, whereas the patch introduces routing helpers around older GhosttyTerminalView/BrowserPanel code. Preserve the capability, rebase it onto that current path, and reconcile #1470’s link hit-testing rather than merging two overlapping policies.

Reviewed patch head: 52bb5007f00e267c8997a8dd5c096704549c2078. Source/diff triage on September 18, 2026; no new build or runtime validation. No issue state, label, or merge decision changed.

Older issue/PR tracking index — remaining scope and competing implementations are recorded there.

@lawrencecchen

Copy link
Copy Markdown
Contributor

Mac fleet instructions for head 52bb5007f00e267c8997a8dd5c096704549c2078. Planned tag: pr-4022-52bb5007; this is not yet a published build.

JOB_JSON=$(~/.local/bin/cmux-ci submit --kind cmux --command 'CMUX_FLEET_BUILD_TAG=pr-4022-52bb5007 /Users/Shared/cmux-build-fleet/recipes/cmux.sh https://github.com/manaflow-ai/cmux.git 52bb5007f00e267c8997a8dd5c096704549c2078' --artifact artifacts/cmux.app.zip --workspace https://github.com/manaflow-ai/cmux/pull/4022 --source-digest 52bb5007f00e267c8997a8dd5c096704549c2078 --cache-key cmux:pr-4022 --min-free-bytes 268435456000 --label cmux --label ram48)
JOB_ID=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"$JOB_JSON")
~/.local/bin/cmux-ci wait "$JOB_ID" --receipt artifacts/fleet/$JOB_ID.json
~/.local/bin/cmux-ci publish-hq "$JOB_ID"

Use an existing campaign job ID if one is already posted; do not submit a duplicate. A wait timeout leaves the remote job running. Published results will include an exact-head artifact link and timing/disk receipt. This recipe validates the macOS app only, not iOS or tests. Never use maclease or put credentials in a PR comment.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Reviewed. This has gone stale against main: terminal link routing moved into TerminalLinkOpenCoordinator / TerminalPointerGestureState (#12343, abffeec), so the GhosttyTerminalView routing here targets code that no longer works that way, and 7 files conflict.

There's also an unaddressed bug that Greptile and Bugbot both flagged: KeyboardShortcutSettingsFileStore.swift:752/761 does guard … parse(raw) else { return }, so one invalid modifier string silently drops every browser setting parsed after it.

And a product question: an ⌥-click default collides with macos-option-as-alt. If #797 is still wanted, it's probably best redone on the coordinator, with the modifiers carried on TerminalLinkOpenRequest.

@teamleaderleo teamleaderleo added area: browser The embedded browser, web surfaces, inline VS Code S3: minor Wrong behavior with a workaround labels Sep 30, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — 52bb5007 Deployed May 12, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: browser The embedded browser, web surfaces, inline VS Code S3: minor Wrong behavior with a workaround

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Configurable modifier keys for link click behavior (default browser vs built-in browser)

3 participants