Settings > Browser > Extensions: import, add-unpacked, and live enable/disable for web extensions

Merged
KY
3mo ago
+13720-687

Summary

  • Adds opt-in browser web-extension management under Settings → Browser → Extensions.
  • Introduces the browser.webExtensions setting for Safari app extensions and unpacked extension folders, with import, add, enable/disable, remove, and toolbar-visibility controls.
  • Reconciles settings live with one shared WKWebExtensionController: enabled extensions load at startup and on change; disabled/removed extensions unload without discarding permission state unnecessarily.
  • Routes extension commands, toolbar actions, popup/popout windows, extension-owned navigation, passkeys, and active-tab/window lifecycle through the built-in browser.
  • Replaces the earlier hardcoded Bitwarden path with a generic, fail-closed model. Nothing loads unless the user adds it or automation supplies CMUX_BROWSER_EXTENSIONS.
  • Adds behavior coverage for settings observation and optimistic writes, discovery parsing, load/unload reconciliation and rollback, toolbar visibility, prewarmed WebView host scoping, shortcut precedence, navigation, metadata invalidation, and extension tab/window behavior.

This makes WebKit extension support user-manageable and safe to apply live instead of being tied to one extension or a fixed local path.

Screenshots

The Extensions card in Settings → Browser with the Bitwarden Safari extension imported and enabled:

Extensions card in Settings → Browser: Import Safari Extension menu, Add Unpacked… button, and an enabled Bitwarden row with a Remove button

Extensions showing in the browser:

Web extension actions in the cmux browser toolbar

Passkey flow working:

Passkey flow working through a browser web extension

Testing

Final-head verification target: 840428a64281.

  • Main/conflict integration:
    • merged current origin/main through 7fa5eabe26
    • resolved the Xcode project conflict by preserving both browser-extension ownership and main’s background-workspace mount policy wiring
    • vendor/bonsplit remains pinned to a0400b3533db21afda4a5a59e695cbff588a39b7 in the branch tree, origin/main tree, and clean detached submodule checkout; the unrelated newer bonsplit upstream commit was intentionally not introduced
  • Latest red/green navigation and identity proof:
    • test-only commits: 034829f134 and 0a8a21234a
    • implementation commit: 5b6d933c98
    • package path-identity regressions: 2 passed
    • exact final-head app-host regressions: repeatedReplacementForSameExtensionContextKeepsWebView() and extensionContextSwapPreservesLogicalNavigationHistory() both passed; Xcode reported ** TEST SUCCEEDED **
  • Earlier focused behavior validation retained across the review stack:
    • BrowserPrewarmedWebViewPoolTests: 13 passed
    • BrowserWebExtensionReviewRegressionTests: 6 passed
    • BrowserWebExtensionSupportTests: 14 non-WebKit-host tests passed
    • BrowserExternalNavigationSchemeTests: 6 passed
    • BrowserWebExtensionDiscoveryServiceTests: 1 passed
    • both affected Swift package suites: 6 tests each passed
  • Static validation on the final merged head:
    • git diff --check
    • Package.resolved policy
    • workspace package grouping
    • pbxproj normalization/check
    • test-target wiring (485 files)
    • diff-aware Swift file-length budget
    • no user-facing strings changed by the final conflict/navigation fixes; the prior 26 added localization keys retain nonempty coverage across all 20 supported locales
  • Tagged build:
    • ./scripts/reload.sh --tag browser-extension-management
    • succeeded on the exact final merged head
    • app: /Users/austinwang/Library/Developer/Xcode/DerivedData/cmux-browser-extension-management/Build/Products/Debug/cmux DEV browser-extension-management.app
  • Manual dogfood from the tagged build verified persistence, startup loading, live enable/disable, import/remove, toolbar popup, extension shortcut routing, and passkey sign-in.

Local runner note: this machine has no logged-in GUI console session, but the two targeted AppKit/WebKit regressions above completed successfully. CI remains authoritative for the broader key-window/WebKit-host matrix.

Demo Video

  • No new video was recorded. The screenshots above show the settings UI, toolbar integration, and passkey result; the tagged build is available for dogfood.

Review Trigger (Copy/Paste as PR comment)

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

Checklist

  • I tested the change locally
  • I added or updated tests for behavior changes
  • Docs/changelog update is not required for this settings/browser feature
  • I requested bot reviews after my latest commit (copy/paste block above or equivalent)
  • All code review bot comments are resolved
  • All human review comments are resolved

🤖 Generated with Claude Code and iterated with Codex


[!NOTE]
Medium Risk
Touches global keyboard routing, shared JSON config concurrency, and extension load/navigation policy in the embedded browser—high user impact but guarded by macOS version checks and extensive tests in the full PR.

Overview
Adds Settings → Browser → Extensions so users can import Safari .appex extensions, add unpacked folders, and enable/disable/remove entries persisted as browser.webExtensions in cmux.json. The UI uses optimistic writes with rollback, host discovery/load-error streams, and duplicate-resource handling; only enabled entries are meant to load in the browser (macOS 15.4+).

Config plumbing: JSONConfigStore gains an atomic update(_:mutation:) and skip-no-op writes via mutateRoot returning Bool. JSONValueModel serializes writes, exposes hasObservedValue and write completion IDs for the extensions card.

Runtime wiring (in this diff): AppDelegate strongly retains browserWebExtensionHost and passes it into new TabManager / DockSplitStore instances; dock/browser panel creation hooks register extensions and notify the host on focus/metadata changes. New shortcut routing lets manifest keyboard commands run when cmux shortcuts (and browser focus mode) would otherwise block them, including before stale menu-shortcut suppression. Browser navigation policy gains extension-specific block/route-in-current-tab hooks; external URL handling moves into shared BrowserExternalNavigationPolicy helpers.

Also: SettingsHostActions discovery/support APIs, settings search anchor for web extensions, centralized NSWindow.cmuxShouldOwnCloseShortcut, and localized strings for the extensions UI and extension permission/errors.

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

Summary by CodeRabbit

  • New Features
    • Added browser web extension support on macOS 15.4 and later.
    • Manage extensions from Browser settings, including importing Safari extensions and adding unpacked extensions.
    • Enable, disable, and remove configured extensions with duplicate and invalid-folder checks.
    • Extension toolbar actions, popups, navigation, and keyboard commands are supported.
  • Bug Fixes
    • Improved browser panel and prewarmed web view handling when extensions are enabled.
  • Documentation
    • Added localized labels, help text, search keywords, and unsupported-platform messaging.