Skip to content

Add cmux browser disable switch - #3256

Merged
lawrencecchen merged 9 commits into
mainfrom
task-disable-cmux-browser
Apr 29, 2026
Merged

lawrencecchen merged 9 commits into
mainfrom
task-disable-cmux-browser

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Apr 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add global browser availability setting outside settings.json.
  • Add command palette and CLI controls to disable, enable, and inspect browser availability.
  • Route terminal links, intercepted open commands, socket browser creation, sidebar links, and browser new-tab requests away from cmux when disabled.

Testing

  • jq empty Resources/Localizable.xcstrings
  • git diff --check
  • ./scripts/reload.sh --tag brdis
  • CMUX_BUNDLE_ID=com.cmuxterm.test.disablebrowser /tmp/cmux-cli disable-browser --json
  • CMUX_BUNDLE_ID=com.cmuxterm.test.disablebrowser /tmp/cmux-cli browser --json status
  • CMUX_BUNDLE_ID=com.cmuxterm.test.disablebrowser /tmp/cmux-cli enable-browser --json

Summary by cubic

Adds a global, per-bundle switch to disable the embedded cmux browser, overriding settings.json. When disabled, links open in the system browser, browser creation is blocked, and session restore preserves layout without attaching web views or DevTools; all fallback paths are hardened.

  • New Features

    • CLI: cmux disable-browser | enable-browser | browser-status and cmux browser disable | enable | status (supports --json); persisted as browserDisabledOverride in app defaults for the detected bundle (respects CMUX_BUNDLE_ID).
    • Settings: “Enable cmux Browser” toggle with clear on/off subtitles; writes browserDisabledOverride.
    • Command Palette: “Disable cmux Browser” / “Enable cmux Browser”; browser-related commands and splits are hidden when disabled.
    • Disabled behavior: terminal link interception, sidebar links, and Resources/bin/open fall back to the system browser; UI/socket/CLI browser create/split are blocked and socket/v2 return opened_externally: true with browser_disabled: true; session restore keeps browser panels/layout without attaching a web view or opening DevTools.
  • Bug Fixes

    • Hardened fallbacks when disabled: early guards in App/Workspace/TabManager prevent browser creation and cleanly open externally; terminal link opens preflight the state and defer safely to avoid reentrancy.
    • Resources/bin/open now honors browserDisabledOverride and forwards system open exit codes; stops processing after external opens.
    • Socket/v2 and CLI preserve invalid URL errors when disabled; CLI returns explicit external-open results (e.g., “external_browser_disabled”).
    • Session restore respects the disabled state: preserves URL/history but doesn’t render the web view; DevTools aren’t auto-opened when disabled.

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

Summary by CodeRabbit

  • New Features

    • CLI commands to enable/disable/check browser integration (top-level and browser subcommands) with --json output
    • Settings toggle in Preferences and command-palette commands to flip browser availability
    • Localization entries for new commands and settings (English/Japanese)
  • Behavior Changes

    • When disabled, in-app browser surfaces are prevented; URLs open in the system browser and related commands/shortcuts are hidden or no-op
  • Tests

    • Regression test added to verify wrapper honors the disable override and falls back to system open

@vercel

vercel Bot commented Apr 29, 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 Apr 29, 2026 2:46am
cmux-staging Building Building Preview, Comment Apr 29, 2026 2:46am

@coderabbitai

coderabbitai Bot commented Apr 29, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a centralized browser-availability toggle (UserDefaults + notification) controllable via CLI, Settings, and the shell wrapper; when disabled, browser creation and embedded routing short‑circuit across wrapper, app UI, RPC, and panels to fall back to the system browser or return external-open metadata.

Changes

Cohort / File(s) Summary
CLI & Localization
CLI/cmux.swift, Resources/Localizable.xcstrings
Add top-level and browser subcommands (disable-browser, enable-browser, browser-status) with --json; add localized strings for CLI and Settings UI.
Shell wrapper
Resources/bin/open
Add is_true_setting(); read browserDisabledOverride from the UserDefaults suite and, when truthy, delegate immediately to system open and exit with its status; ensure delegated paths exit $?.
Browser availability core & panel
Sources/Panels/BrowserPanel.swift
Introduce BrowserAvailabilitySettings (keys, default, notification); gate webview rendering and session restore via shouldRenderWebViewForSessionSnapshot; adjust link/open helpers to fallback externally when disabled.
Workspace & Tab lifecycle
Sources/Workspace.swift, Sources/TabManager.swift
Add BrowserPanelCreationPolicy; update newBrowserSplit/newBrowserSurface signatures to accept creationPolicy and early-return when disabled; prevent browser creation/reopen when disabled.
App-level gating & Settings UI
Sources/AppDelegate.swift, Sources/cmuxApp.swift
AppDelegate/shortcuts early-return when browser-disabled; add Settings toggle bound to BrowserAvailabilitySettings.disabledKey with inverted binding and dynamic subtitle; reset logic updated.
ContentView & Command palette
Sources/ContentView.swift
Expose browser.disabled context key; hide browser palette commands with when predicates; add palette.disableBrowser/palette.enableBrowser; query link-open settings at call time.
Terminal & RPC gating
Sources/TerminalController.swift
V2 JSON-RPC and V1 socket endpoints short-circuit when browser-disabled: V2 returns ok with external-open metadata; V1 uses helper to open externally and return external-open response.
Embedded routing in terminal view
Sources/GhosttyTerminalView.swift
Extract openEmbeddedBrowserLink (@MainActor) to preflight workspace/panel mapping, gate on availability, prefer reuse, fallback to external open; handler delegates asynchronously and returns true.
Tests
tests/test_open_wrapper.py
Add browser_disabled_setting to test harness; fake defaults recognize browserDisabledOverride; new regression test asserts wrapper delegates to system open when override is truthy.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant User as User / CLI
  participant OpenScript as Resources/bin/open
  participant CmuxRPC as cmux App (RPC)
  participant Settings as BrowserAvailabilitySettings
  participant System as System Browser (NSWorkspace)

  User->>OpenScript: run open <url>
  OpenScript->>Settings: read `browserDisabledOverride`
  alt override == true
    OpenScript->>System: system `open` <url> (bypass cmux)
    System-->>OpenScript: exit status
    OpenScript-->>User: exit status
  else override != true
    OpenScript->>CmuxRPC: forward to cmux
    CmuxRPC->>Settings: isDisabled()
    alt disabled == true
      CmuxRPC->>System: NSWorkspace.open(<url>) (fallback)
      CmuxRPC-->>User: respond ok + metadata (browser_disabled/opened_externally)
    else disabled == false
      CmuxRPC->>CmuxRPC: create browser surface/tab/split
      CmuxRPC-->>User: respond ok (embedded browser created)
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Poem

🐰 I twitched my whiskers and toggled a key,
CLI, prefs, and wrapper decide where links flee.
If cmux sleeps on browsers, the system opens the gate,
Webviews stay cozy while links find their fate.
A hop, a nibble, control nicely neat — carrot treat.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add cmux browser disable switch' directly and concisely summarizes the main change—a new feature to disable the cmux browser globally.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed PR description covers all required template sections: clear summary of what changed and why, comprehensive testing steps, and completed checklist with all items marked done.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 task-disable-cmux-browser

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 Apr 29, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a global "browser disabled" override that routes all terminal links, intercepted open commands, socket browser creation, sidebar links, and new-tab requests to the system browser instead of cmux. The setting is controlled via CLI subcommands (disable-browser, enable-browser, browser-status), a command palette toggle, and a new Settings UI row, all backed by a UserDefaults key (browserDisabledOverride).

  • Resources/bin/open: the new browser_disabled guard calls system_open \"$@\" but does not exit, so the script falls through to the cmux browser open loop; the app-side guard returns an error which adds the URL to failed_urls, and system_open fires again — every intercepted URL is opened twice when the browser is disabled.

Confidence Score: 3/5

Not safe to merge as-is — the missing exit in the open script causes every URL to be opened twice in the default browser when the feature is active.

One clear P1 defect (double URL open in Resources/bin/open) plus two P2 style notes. The Swift-side guards across TabManager, Workspace, TerminalController, AppDelegate, and ContentView are thorough and correctly implemented. The shell script bug is straightforward to fix.

Resources/bin/open — the browser_disabled guard block needs an exit $? after system_open.

Important Files Changed

Filename Overview
Resources/bin/open Adds browser-disabled early exit via system_open, but is missing exit $? after the call — causes every intercepted URL to be opened twice when the browser is disabled.
CLI/cmux.swift Adds disable-browser, enable-browser, and browser-status CLI subcommands with correct domain resolution and JSON output; logic looks sound.
Sources/Panels/BrowserPanel.swift Introduces BrowserAvailabilitySettings enum and gates all BrowserLinkOpenSettings helpers behind it; synchronize() on every read is deprecated but functional.
Sources/cmuxApp.swift Adds Settings UI toggle and reset support for browser availability; browserEnabledBinding setter has a harmless but redundant double-write to UserDefaults.
Sources/ContentView.swift Adds browserDisabled command palette context key and gates browser-related palette commands correctly; switches sidebar PR link check from @AppStorage to function call.
Sources/TabManager.swift Guards all browser-creation and reopen paths with BrowserAvailabilitySettings.isEnabled() early returns; changes are straightforward.
Sources/TerminalController.swift Adds browser_disabled error returns across all socket-level browser-creation handlers; browser-open fallback correctly calls NSWorkspace.shared.open with structured response.
Sources/Workspace.swift Guards newBrowserSplit and newBrowserSurface with availability check; clean and consistent with the rest of the change.
Sources/AppDelegate.swift Guards openBrowserAndFocusAddressBar and performBrowserSplitShortcut with availability check; debug logging included.
Resources/Localizable.xcstrings Adds English and Japanese localization strings for new enable/disable browser commands and settings toggle; translations look correct.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[URL open request] --> B{Source}

    B -->|bin/open script| C{settings_domain set?}
    C -->|yes| D{browserDisabledOverride = true?}
    D -->|yes| E[system_open ALL args]
    E -->|⚠️ no exit — falls through| F{open_in_cmux setting}
    D -->|no| F
    F -->|false| G[system_open]
    F -->|true / unset| H[cmux browser open URL]
    H -->|browser disabled: error| I[failed_urls list]
    I --> J[system_open failed URLs ← 2nd open!]

    B -->|CLI socket| K{BrowserAvailabilitySettings.isEnabled?}
    K -->|no| L[return browser_disabled error / NSWorkspace.open]
    K -->|yes| M[Create browser panel in app]

    B -->|Command Palette| N{browserDisabled context key}
    N -->|true| O[browser commands hidden]
    N -->|false| P[browser commands shown]

    B -->|Settings toggle| Q[BrowserAvailabilitySettings.setDisabled]
    Q --> R[UserDefaults write + notification]
Loading

Comments Outside Diff (2)

  1. Sources/Panels/BrowserPanel.swift, line 599-605 (link)

    P2 defaults.synchronize() is deprecated and called on every read

    UserDefaults.synchronize() has been deprecated since macOS 10.14 and the system now synchronizes automatically. Calling it on every isDisabled invocation is unnecessary; it also signals intent that reads need an explicit flush, which could confuse future readers. Consider removing it (or at minimum adding a comment explaining why cross-process sync requires it here).

  2. Sources/cmuxApp.swift, line 836-840 (link)

    P2 Redundant double-write in browserEnabledBinding setter

    The setter calls BrowserAvailabilitySettings.setDisabled(!newValue) (which writes to UserDefaults.standard and calls synchronize()) and then immediately sets browserDisabled = !newValue via @AppStorage, which writes the identical value a second time. The setDisabled call is needed to fire didChangeNotification; the @AppStorage assignment is needed to trigger SwiftUI. The intermediate defaults.set + synchronize() inside setDisabled is therefore redundant when called from this binding. Consider posting the notification separately so setDisabled doesn't also duplicate the write.

Reviews (1): Last reviewed commit: "Add cmux browser disable switch" | Re-trigger Greptile

Comment thread Resources/bin/open

@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: e7aed0d7bb

ℹ️ 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 Sources/Workspace.swift Outdated

@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

🧹 Nitpick comments (3)
Sources/AppDelegate.swift (2)

10976-10986: Optional security hardening: redactedDebugURL still includes URL path.

redactedDebugURL(_:) clears user, password, query, and fragment, but it leaves path. In some environments, the path can still contain sensitive tokens/identifiers, so the “blocked browser disabled” DEBUG log could leak more than intended.

Optional tightening: consider logging only scheme + host (and maybe port), or replacing path with a constant like "/<redacted>".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 10976 - 10986, The
redactedDebugURL(_:) function currently strips user, password, query, and
fragment but leaves the URL path which can leak sensitive tokens; update
redactedDebugURL(_:) to also remove or replace the path (e.g., set
components.path = "/<redacted>" or empty) and ensure scheme, host, and port (if
present) are retained; also handle URLs that lack a host by returning a safe
placeholder like "<redacted>" or "<invalid>" so no sensitive path data is ever
included in logs.

10446-10761: Potential UX/behavior bug: .focusBrowserAddressBar shortcut may fall through when browser is disabled.

Right now, the handler only returns true if openBrowserAndFocusAddressBar(...) != nil. When the browser is disabled, openBrowserAndFocusAddressBar(...) will return nil, and the function will likely continue checking later shortcuts / responder behavior instead of deterministically consuming the matched shortcut.

Suggested behavior: if the shortcut is matched and the browser is disabled, consume it (return true), so the keybinding doesn’t “act like unhandled”.

🛠️ Proposed fix
 if matchConfiguredShortcut(event: event, action: .focusBrowserAddressBar) {
     if let focusedPanel = tabManager?.focusedBrowserPanel {
         focusBrowserAddressBar(in: focusedPanel)
         return true
     }

     if let browserAddressBarFocusedPanelId,
        focusBrowserAddressBar(panelId: browserAddressBarFocusedPanelId) {
         return true
     }

-    if openBrowserAndFocusAddressBar(insertAtEnd: true) != nil {
-        return true
-    }
+    let openedPanelId = openBrowserAndFocusAddressBar(insertAtEnd: true)
+    if openedPanelId != nil {
+        return true
+    }
+    // If the browser feature is disabled, still consume the shortcut so it doesn't fall through.
+    if !BrowserAvailabilitySettings.isEnabled() {
+        return true
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 10446 - 10761, The
.focusBrowserAddressBar branch can match but currently only returns true when
focus succeeds; change the logic in the matchConfiguredShortcut(event: event,
action: .focusBrowserAddressBar) block so that after the existing early-success
returns (focusedBrowserPanel, browserAddressBarFocusedPanelId,
openBrowserAndFocusAddressBar != nil) you always consume the shortcut (return
true) even when openBrowserAndFocusAddressBar(...) returns nil (i.e., browser
disabled). Update the block that references matchConfiguredShortcut,
focusBrowserAddressBar(in:), focusBrowserAddressBar(panelId:), and
openBrowserAndFocusAddressBar(...) to ensure a final unconditional return true
when the shortcut matched.
Sources/Panels/BrowserPanel.swift (1)

696-711: Remove UserDefaults.synchronize() calls on lines 697 and 710.

Apple's documentation explicitly discourages calling synchronize() on every read/write—the method is "unnecessary and shouldn't be used." Calling it in isDisabled() (line 697) and setDisabled() (line 710) adds avoidable latency to frequently-checked settings. Only call synchronize() if you can prove a cross-process consistency gap; for typical preference reads/writes, rely on UserDefaults' automatic buffering.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Panels/BrowserPanel.swift` around lines 696 - 711, Remove the
unnecessary UserDefaults.synchronize() calls in the BrowserPanel helpers: delete
the call inside static func isDisabled(defaults: UserDefaults = .standard) and
the call inside static func setDisabled(_ disabled: Bool, defaults: UserDefaults
= .standard); keep reading with defaults.object(forKey:
disabledKey)/defaults.bool(forKey: disabledKey) and keep
defaults.set(...)/NotificationCenter.default.post(name: didChangeNotification,
object: nil) as-is, relying on UserDefaults' automatic buffering for
persistence.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Resources/Localizable.xcstrings`:
- Around line 63095-63112: Update the "settings.browser.enabled.subtitleOff"
localization entries to match the surfaces called out in
"settings.browser.enabled.subtitleOn": explicitly mention terminal link clicks
and intercepted open commands in both the "en" and "ja" stringUnit.value texts
so the off-state describes that terminal link clicks and intercepted open
commands are routed to the default browser (or not handled by the app) just as
subtitleOn references; modify the English and Japanese values under
settings.browser.enabled.subtitleOff accordingly to mirror the same user-facing
surfaces.

In `@Sources/Panels/BrowserPanel.swift`:
- Around line 618-621: Move the empty-URL validation to run before the
short-circuit that checks browser availability: ensure the code trims and checks
rawURL (the target computed from rawURL.trimmingCharacters(in:
.whitespacesAndNewlines) and the guard !target.isEmpty) before calling
BrowserAvailabilitySettings.isEnabled(defaults: defaults); this way blank input
is rejected (returns false) even when the browser is disabled instead of being
treated as "open externally."

---

Nitpick comments:
In `@Sources/AppDelegate.swift`:
- Around line 10976-10986: The redactedDebugURL(_:) function currently strips
user, password, query, and fragment but leaves the URL path which can leak
sensitive tokens; update redactedDebugURL(_:) to also remove or replace the path
(e.g., set components.path = "/<redacted>" or empty) and ensure scheme, host,
and port (if present) are retained; also handle URLs that lack a host by
returning a safe placeholder like "<redacted>" or "<invalid>" so no sensitive
path data is ever included in logs.
- Around line 10446-10761: The .focusBrowserAddressBar branch can match but
currently only returns true when focus succeeds; change the logic in the
matchConfiguredShortcut(event: event, action: .focusBrowserAddressBar) block so
that after the existing early-success returns (focusedBrowserPanel,
browserAddressBarFocusedPanelId, openBrowserAndFocusAddressBar != nil) you
always consume the shortcut (return true) even when
openBrowserAndFocusAddressBar(...) returns nil (i.e., browser disabled). Update
the block that references matchConfiguredShortcut, focusBrowserAddressBar(in:),
focusBrowserAddressBar(panelId:), and openBrowserAndFocusAddressBar(...) to
ensure a final unconditional return true when the shortcut matched.

In `@Sources/Panels/BrowserPanel.swift`:
- Around line 696-711: Remove the unnecessary UserDefaults.synchronize() calls
in the BrowserPanel helpers: delete the call inside static func
isDisabled(defaults: UserDefaults = .standard) and the call inside static func
setDisabled(_ disabled: Bool, defaults: UserDefaults = .standard); keep reading
with defaults.object(forKey: disabledKey)/defaults.bool(forKey: disabledKey) and
keep defaults.set(...)/NotificationCenter.default.post(name:
didChangeNotification, object: nil) as-is, relying on UserDefaults' automatic
buffering for persistence.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6891259c-71a2-4870-baa1-accc8191f41c

📥 Commits

Reviewing files that changed from the base of the PR and between 5c2677a and e7aed0d.

📒 Files selected for processing (10)
  • CLI/cmux.swift
  • Resources/Localizable.xcstrings
  • Resources/bin/open
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/TabManager.swift
  • Sources/TerminalController.swift
  • Sources/Workspace.swift
  • Sources/cmuxApp.swift

Comment thread Resources/Localizable.xcstrings
Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment thread Sources/TerminalController.swift Outdated

@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: d6ac4cd2c2

ℹ️ 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 Sources/GhosttyTerminalView.swift

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/Panels/BrowserPanel.swift`:
- Around line 696-711: Remove all calls to UserDefaults.synchronize() in
BrowserPanel: delete synchronize() from isDisabled(defaults:) and
setDisabled(_:defaults:), and keep isEnabled(defaults:) as the negation of
isDisabled; additionally, since didChangeNotification has no observers and state
is managed via `@AppStorage` in cmuxApp.swift, remove the
NotificationCenter.default.post(name: didChangeNotification, object: nil) call
from setDisabled(_:defaults:) as well so reads/writes rely on UserDefaults
without deprecated synchronization or unused notifications (referencing
isDisabled, isEnabled, setDisabled, disabledKey, and didChangeNotification).

In `@Sources/TerminalController.swift`:
- Around line 3330-3359: v2BrowserDisabledExternalOpenResult currently collapses
malformed-but-nonempty URLs into a browser-disabled error because it only
receives URL?; change the helper to accept the original input string (e.g., add
parameter originalURLString: String?) or an enum indicating empty vs malformed,
and implement logic: if originalURLString != nil && url == nil return a distinct
malformed-URL error (e.g., .err(code: "malformed_url", message: "Invalid URL",
data: ["url": originalURLString])), but only return the browser_disabled error
when the caller actually omitted the URL (originalURLString == nil and url ==
nil); update callers of v2BrowserDisabledExternalOpenResult (and the analogous
helper used at the other location) to pass the original string so malformed URLs
are preserved.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 561f0959-a6bd-4100-8805-c5de1acd0080

📥 Commits

Reviewing files that changed from the base of the PR and between 8a3c66a and 80bf91f.

📒 Files selected for processing (7)
  • Resources/Localizable.xcstrings
  • Resources/bin/open
  • Sources/ContentView.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/TerminalController.swift
  • Sources/Workspace.swift
  • tests/test_open_wrapper.py
✅ Files skipped from review due to trivial changes (1)
  • Resources/Localizable.xcstrings
🚧 Files skipped from review as they are similar to previous changes (2)
  • Sources/Workspace.swift
  • Sources/ContentView.swift

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

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/Workspace.swift (1)

821-828: ⚠️ Potential issue | 🟠 Major

Honor shouldRenderWebView before reopening DevTools.

browserSnapshot.shouldRenderWebView is now persisted, but this restore path still reopens DevTools whenever developerToolsVisible is true. That can force browser-side restore work even when the snapshot explicitly says not to attach/render the web view yet.

Suggested fix
-            if browserSnapshot.developerToolsVisible && BrowserAvailabilitySettings.isEnabled() {
+            if browserSnapshot.developerToolsVisible &&
+                browserSnapshot.shouldRenderWebView &&
+                BrowserAvailabilitySettings.isEnabled() {
                 _ = browserPanel.showDeveloperTools()
                 browserPanel.requestDeveloperToolsRefreshAfterNextAttach(reason: "session_restore")
             } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` around lines 821 - 828, When restoring a snapshot,
don't unconditionally reopen DevTools — honor
browserSnapshot.shouldRenderWebView first: after calling
browserPanel.restoreSessionSnapshot(browserSnapshot) check
browserSnapshot.shouldRenderWebView and BrowserAvailabilitySettings.isEnabled()
before calling browserPanel.showDeveloperTools(); if shouldRenderWebView is
false, call browserPanel.hideDeveloperTools() and skip
requestDeveloperToolsRefreshAfterNextAttach(reason:), otherwise call
showDeveloperTools() and then
requestDeveloperToolsRefreshAfterNextAttach(reason: "session_restore"); keep the
existing hideDeveloperTools() path for the false case as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 3826-3834: The handler currently fires a detached Task that calls
Self.openEmbeddedBrowserLink(...) and then returns true immediately, hiding any
failure from openEmbeddedBrowserLink; replace the fire-and-forget Task with an
awaited call so the Bool result is observed and returned (or propagate the
failure), e.g. await Self.openEmbeddedBrowserLink(...) on the MainActor and
return that Bool instead of unconditionally returning true, ensuring the call
still executes on the MainActor.

In `@Sources/Workspace.swift`:
- Around line 10141-10143: The guard in the browser-creation helper (checking
BrowserAvailabilitySettings.isEnabled() ||
creationPolicy.permitsCreationWhenBrowserDisabled) returns nil before inspecting
url/initialRequest, which causes createBrowserToRight and
duplicateBrowserToRight to drop navigations when the internal browser is
disabled; change the helper so that if the browser is disabled it still detects
a provided url or initialRequest and triggers the external-open fallback (the
same fallback used in newBrowserSplit), while only returning the
browser_disabled path for creations with no URL/initialRequest; update logic
around BrowserAvailabilitySettings.isEnabled(),
creationPolicy.permitsCreationWhenBrowserDisabled, and the url/initialRequest
checks so URL-bearing calls always open externally and non-URL calls return the
disabled error.

---

Outside diff comments:
In `@Sources/Workspace.swift`:
- Around line 821-828: When restoring a snapshot, don't unconditionally reopen
DevTools — honor browserSnapshot.shouldRenderWebView first: after calling
browserPanel.restoreSessionSnapshot(browserSnapshot) check
browserSnapshot.shouldRenderWebView and BrowserAvailabilitySettings.isEnabled()
before calling browserPanel.showDeveloperTools(); if shouldRenderWebView is
false, call browserPanel.hideDeveloperTools() and skip
requestDeveloperToolsRefreshAfterNextAttach(reason:), otherwise call
showDeveloperTools() and then
requestDeveloperToolsRefreshAfterNextAttach(reason: "session_restore"); keep the
existing hideDeveloperTools() path for the false case as well.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 44113692-b884-48ac-bb02-430b3a575e2d

📥 Commits

Reviewing files that changed from the base of the PR and between f1bcb7e and af073ad.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (5)
  • Resources/Localizable.xcstrings
  • Sources/ContentView.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/TerminalController.swift
  • Sources/Workspace.swift
✅ Files skipped from review due to trivial changes (1)
  • Resources/Localizable.xcstrings
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/ContentView.swift

Comment thread Sources/GhosttyTerminalView.swift
Comment thread Sources/Workspace.swift Outdated

@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: af073adf52

ℹ️ 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 Sources/Workspace.swift Outdated

@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: c969c56dba

ℹ️ 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 on lines +8271 to +8272
if BrowserAvailabilitySettings.isDisabled() {
return v2BrowserDisabledExternalOpenResult(rawURL: urlStr, url: url, tabManager: tabManager)

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 Validate browser.open_split targets before external fallback

When browser mode is disabled, this early return skips all workspace/surface resolution in v2BrowserOpenSplit, so requests with stale or invalid workspace_id/surface_id still open the URL externally and can return success. In enabled mode the same invalid handles return not_found, so this changes scoped-command semantics and can trigger unintended external opens from bad refs. The disabled path should still validate routing context before performing side effects.

Useful? React with 👍 / 👎.

Comment thread Sources/TabManager.swift
preferredProfileID: UUID? = nil,
insertAtEnd: Bool = false
) -> UUID? {
guard BrowserAvailabilitySettings.isEnabled() else { return nil }

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 Keep URL fallback reachable in TabManager browser opens

This guard prevents TabManager.openBrowser from reaching Workspace.newBrowserSurface/newBrowserSplit, which now contain the disabled-mode external-open fallback for non-nil URLs. As a result, callers that only check for nil (for example AppDelegate.openDirectoryInInlineVSCode) now fail/beep instead of opening the generated URL in the default browser when browser mode is disabled.

Useful? React with 👍 / 👎.

@lawrencecchen
lawrencecchen merged commit e46e80b into main Apr 29, 2026
30 of 32 checks passed
@lawrencecchen
lawrencecchen deleted the task-disable-cmux-browser branch April 29, 2026 02:37

This branch was successfully deployed

1 active deployment
Preview – cmux — c969c56d Deployed Apr 29, 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.

1 participant