Skip to content

Add browser mute tab toggle for #4910 - #4911

Merged
austinywang merged 7 commits into
mainfrom
issue-4910-browser-mute-tab
May 30, 2026
Merged

austinywang merged 7 commits into
mainfrom
issue-4910-browser-mute-tab

Conversation

@austinywang

@austinywang austinywang commented May 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Move browser audio mute/unmute out of the address bar and into the Bonsplit pane-tab context menu.
  • Publish per-browser-pane mute state into Bonsplit tab metadata so the tab shows a speaker-slash indicator when muted.
  • Keep mute persistence/re-application across session restore, navigation, and WKWebView recreation.

Verification

  • Validated _setPageMuted: exists and accepts the audio bit on local WebKit with a focused Swift selector probe.
  • Ran jq empty Resources/Localizable.xcstrings.
  • Ran plutil -lint vendor/bonsplit/Sources/Bonsplit/Resources/en.lproj/Localizable.strings vendor/bonsplit/Sources/Bonsplit/Resources/ja.lproj/Localizable.strings.
  • Ran git diff --check and git -C vendor/bonsplit diff --check.
  • Did not run local app build, reload, or tests per workspace constraints.

Dependency

Closes #4910


Note

Medium Risk
Uses private WebKit _setPageMuted: via runtime selector invocation; behavior may vary by WebKit version, though failures are guarded and mute state still persists in the app.

Overview
Adds per-pane browser tab audio mute controlled from the Bonsplit pane-tab context menu (not the address bar), with a muted speaker-slash on the tab via isAudioMuted metadata.

BrowserPanel owns published isMuted, exposes setMuted / toggleMute, and reapplies mute whenever the web view is bound or navigation starts/commits/finishes/fails so intent survives WKWebView replacement. cmuxSetPageAudioMuted on WKWebView calls WebKit’s private _setPageMuted: when available.

Mute is persisted in SessionBrowserPanelSnapshot (backward-compatible decode), restored on session load, copied when duplicating a browser tab, and wired through workspace tab subscriptions and mirror tab drag payloads. Tests cover snapshot round-trip and missing isMuted key.

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

Summary by CodeRabbit

  • New Features

    • Added audio muting capability for browser panels, with muted state preserved across navigation and sessions
    • Context menu option to toggle audio muting for individual tabs
    • Muted state automatically preserved when duplicating panels
    • Visual indication of audio mute status in tab UI
  • Tests

    • Enhanced session persistence test coverage for mute state
  • Chores

    • Updated vendored dependency

Review Change Stack

@vercel

vercel Bot commented May 27, 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 30, 2026 12:03am
cmux-staging Building Building Preview, Comment May 30, 2026 12:03am

@coderabbitai

coderabbitai Bot commented May 27, 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

This PR implements per-browser-pane audio muting with WebKit private API, preserving mute state across navigation and app restart. BrowserPanel tracks mute state independently of web view replacement, applies mute via private selector on lifecycle events, persists state in session snapshots, and exposes toggle controls via workspace context menu and programmatic API.

Changes

Audio Mute Feature for Browser Pane

Layer / File(s) Summary
WebKit audio mute capability
Sources/Panels/CmuxWebView.swift
WKWebView extension adds cmuxSetPageAudioMuted(_:) that invokes WebKit's private _setPageMuted: selector, guarded by runtime availability checks.
BrowserPanel mute state and lifecycle
Sources/Panels/BrowserPanel.swift
@Published isMuted property tracks mute preference. applyMuteState(_:to:reason:) helper applies mute to web views. Mute re-application wired into lifecycle: immediately after web view bind, and on navigation delegate events (didStartProvisionalNavigation, didCommit, didFinish, didFailNavigation). Session snapshot restoration calls setMuted(snapshot.isMuted). Public setMuted(_:) and toggleMute() methods control mute state.
Session persistence schema and tests
Sources/SessionPersistence.swift, cmuxTests/SessionPersistenceTests.swift
SessionBrowserPanelSnapshot adds isMuted: Bool property with memberwise initializer parameter (default false). Codable support includes backward-compatible decoding that defaults to false when key is missing. Tests verify round-trip encoding and missing-key handling.
Workspace integration and UI sync
Sources/Workspace.swift
Workspace captures browserPanel.isMuted in session snapshots. Observes browserPanel.$isMuted and syncs to bonsplit tab UI via updateTab(... isAudioMuted: ...). Panel duplication applies source mute state via newPanel.setMuted(browser.isMuted). Tab context-menu adds .toggleAudioMute action routing to browser.toggleMute(). Bonsplit submodule updated to support audio mute UI.
Tab transfer payload update
Sources/SessionIndexView.swift
MirrorTabItem Codable schema adds isAudioMuted: Bool field. Drag/drop payload populated with false when constructing transfer data.

Sequence Diagrams

sequenceDiagram
  participant User
  participant BrowserPanel
  participant WKWebView
  User->>BrowserPanel: navigate to page
  BrowserPanel->>BrowserPanel: didStartProvisionalNavigation
  BrowserPanel->>BrowserPanel: applyMuteState(isMuted)
  BrowserPanel->>WKWebView: cmuxSetPageAudioMuted(isMuted)
  BrowserPanel->>BrowserPanel: didCommit
  BrowserPanel->>BrowserPanel: applyMuteState(isMuted)
  BrowserPanel->>WKWebView: cmuxSetPageAudioMuted(isMuted)
  BrowserPanel->>BrowserPanel: didFinish
  BrowserPanel->>BrowserPanel: applyMuteState(isMuted)
  BrowserPanel->>WKWebView: cmuxSetPageAudioMuted(isMuted)
Loading
sequenceDiagram
  participant User
  participant TabContextMenu
  participant Workspace
  participant BrowserPanel
  participant BonsplitController
  User->>TabContextMenu: click toggleAudioMute
  TabContextMenu->>BrowserPanel: toggleMute()
  BrowserPanel->>BrowserPanel: isMuted = !isMuted
  BrowserPanel-->>Workspace: publish $isMuted change
  Workspace->>Workspace: observe isMuted change
  Workspace->>BonsplitController: updateTab(isAudioMuted: ...)
  BonsplitController->>BonsplitController: update tab UI
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related PRs

  • manaflow-ai/cmux#4244: Modifies BrowserPanel web view replacement/restoration lifecycle, which interacts with mute re-application on web view lifecycle events.
  • manaflow-ai/cmux#4245: Introduces hidden WebView discard/restore behavior that performs web view replacement, affecting where mute-persistence logic must be reapplied.
  • manaflow-ai/cmux#4284: Refactors the browser-tab duplication path (Workspace.duplicateBrowserToRight), which is where this PR hooks in to copy source panel's isMuted state.

Poem

🔊 A browser pane now knows when to hush,
Through WebKit's whisper and session flush,
Toggle, persist, and navigate with grace—
Per-pane silence finds its place! 🤫


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (2 errors, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Full Internationalization ❌ Error Missing localized strings for browser audio mute. No browser.audioMute entries in Resources/Localizable.xcstrings for the 20 supported locales. Add browser.audioMute localization keys to Resources/Localizable.xcstrings with translations for all 20 locales.
Cmux Architecture Rethink ❌ Error setMuted() returns early when muting fails, never updating isMuted intent; violates invariant that session restore/tab duplication persist mute state. Always update isMuted regardless of applyMuteState success to persist pane-level intent separately from WebKit API availability.
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Swift File And Package Boundaries ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
✅ Passed checks (14 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly references the main objective (browser mute tab toggle) and links it to the specific issue #4910.
Description check ✅ Passed The description covers the main changes, implementation details, and verification steps. It includes a summary, verification approach, and closes the linked issue.
Linked Issues check ✅ Passed The PR successfully implements all primary coding objectives from #4910: per-pane mute state with persistence, application via WebKit's private API, state persistence across session restore and navigation, and Bonsplit tab metadata integration for UI indication.
Out of Scope Changes check ✅ Passed All changes are directly related to the browser mute feature requirements. The bonsplit submodule bump supports the feature's Bonsplit integration requirement, and test updates validate the new persistence functionality.
Cmux Swift Actor Isolation ✅ Passed @Published isMuted in @MainActor BrowserPanel, SessionBrowserPanelSnapshot Sendable, nonisolated WKWebView statics, MainActor-only mute operations.
Cmux Swift Blocking Runtime ✅ Passed No new blocking/timing synchronization introduced. Mute methods use synchronous calls, @Published properties, and safe function-pointer dispatch.
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift code changes and submodule updates. Check applies only to TypeScript, JavaScript, shell, and build scripts per runtime-no-hacky-sleeps.md.
Cmux Algorithmic Complexity ✅ Passed All mute operations are O(1): single-panel state changes, WKWebView selector calls, field additions, subscriber setup. No nested loops, rescans, or unbounded collection rebuilds found.
Cmux Swift Concurrency ✅ Passed PR extends existing Combine patterns without introducing new legacy async anti-patterns; @Published isMuted and subscription expansion follow established architecture.
Cmux Swift @Concurrent ✅ Passed All new functions are synchronous MainActor-isolated methods or simple synchronous wrappers. No nonisolated async work, invalid @concurrent annotations, or actor-crossing issues detected.
Cmux Swift Logging ✅ Passed PR contains only debug-guarded logging (cmuxDebugLog under #if DEBUG in Workspace.swift); no NSLog/print/dump in runtime code; complies with .github/review-bot-rules/swift-logging.md.
Cmux User-Facing Error Privacy ✅ Passed PR passes user-facing error privacy: no vendor names, credentials, or implementation details exposed; private API safely encapsulated; debug logs behind #if DEBUG.
Cmux Swiftui State Layout ✅ Passed BrowserPanel already conforms to ObservableObject; new @Published isMuted extends existing legacy state. Not accessed in SwiftUI view bodies, only in Workspace model code which is not a View.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds browser audio mute functionality via properties, extension methods, and state observation. No NSWindow, NSPanel, NSWindowController, or SwiftUI Window/WindowGroup are introduced or changed.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-4910-browser-mute-tab

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 27, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds per-browser-pane audio muting owned by BrowserPanel, surfaced through the Bonsplit pane-tab context menu. Mute intent is applied via WebKit's private _setPageMuted: SPI, re-enforced at every navigation lifecycle hook and on WKWebView rebind, and persisted in session snapshots with backward-compatible decode.

  • CmuxWebView.swift: New cmuxSetPageAudioMuted SPI wrapper uses nonisolated computed static properties and guards on responds(to:) before dispatch, returning false when the selector is absent.
  • BrowserPanel.swift: isMuted is only updated when the API call succeeds (if applied), keeping the tab indicator in sync with actual WebKit state; mute is re-applied at bindWebView and all four navigation events to survive WKWebView replacement.
  • SessionPersistence.swift / Workspace.swift: isMuted is encoded in snapshots and copied on tab duplication; the custom decoder defaults to false for backward compatibility with existing sessions.

Confidence Score: 5/5

Safe to merge — mute intent is correctly gated on API availability, tab indicator and session state stay in sync with actual WebKit state, and backward-compatible session decode is covered by tests.

All three issues raised in the prior review round (actor isolation on static constants, isMuted set before API confirmation, asymmetric unmute guard) have been addressed. The private SPI dispatch is guarded by responds(to:), isMuted is only published when the call succeeds, and the custom decoder defaults to false for old snapshots. No new logic or state defects were found.

No files require special attention.

Important Files Changed

Filename Overview
Sources/Panels/CmuxWebView.swift Adds cmuxSetPageAudioMuted SPI wrapper using nonisolated computed static properties for selector and audio-bit constants; guards on responds(to:) before dispatch.
Sources/Panels/BrowserPanel.swift Adds isMuted @published state, applyMuteState helper called on bindWebView and all four navigation events, and setMuted/toggleMute accessors; isMuted only updated when the SPI call returns true.
Sources/Workspace.swift Restructures installBrowserPanelSubscription to nest isMuted into the Combine pipeline, adds .toggleAudioMute delegate case, propagates mute on snapshot save and tab duplication.
Sources/SessionPersistence.swift Adds isMuted field to SessionBrowserPanelSnapshot with custom decoder that defaults to false, preserving backward compatibility with existing sessions.
Sources/SessionIndexView.swift Adds isAudioMuted: Bool to MirrorTabItem; hardcoded to false in sessionTabTransferData which is correct as those entries originate from session history, not live tabs.
cmuxTests/SessionPersistenceTests.swift Adds encode/decode round-trip tests for isMuted: true and backward-compat decode test asserting isMuted defaults to false when absent.

Sequence Diagram

sequenceDiagram
    participant User
    participant Bonsplit
    participant Workspace
    participant BrowserPanel
    participant WKWebView

    User->>Bonsplit: Tap Mute Tab in context menu
    Bonsplit->>Workspace: delegate .toggleAudioMute(tab)
    Workspace->>BrowserPanel: toggleMute()
    BrowserPanel->>BrowserPanel: setMuted(!isMuted)
    BrowserPanel->>WKWebView: cmuxSetPageAudioMuted(true)
    WKWebView-->>BrowserPanel: "applied = true"
    BrowserPanel->>BrowserPanel: "isMuted = true only if applied"
    BrowserPanel-->>Workspace: "@Published isMuted fires"
    Workspace->>Bonsplit: updateTab(isAudioMuted: true)
    Bonsplit-->>User: Speaker-slash indicator on tab
Loading

Reviews (6): Last reviewed commit: "fix: keep browser mute state aligned wit..." | Re-trigger Greptile

Comment thread Resources/Localizable.xcstrings
Comment thread Sources/Panels/CmuxWebView.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.

Caution

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

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

8-30: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix unsafeBitCast ABI for WKWebView private "_setPageMuted:" invocation (tie to _WKMediaMutedState)

  • WebKit declares - (void)_setPageMuted:(_WKMediaMutedState)mutedState (void return, single parameter).
  • The current implementation still casts/invokes with UInt (@convention(c) (AnyObject, Selector, UInt) -> Void); confirm _WKMediaMutedState’s underlying integer type matches that ABI (or adjust the cast to the correct underlying type) so “selector exists” doesn’t mask potential UB/crash on parameter representation drift.
🤖 Prompt for 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.

In `@Sources/Panels/CmuxWebView.swift` around lines 8 - 30, The unsafeBitCast is
using UInt which may not match the underlying C enum type for
_WKMediaMutedState; update the ABI to match that enum by changing the
SetPageMutedFunction signature in cmuxSetPageAudioMuted to use Int (Objective-C
NSInteger-sized type) instead of UInt, and change cmuxMediaMutedStateAudio to
return an Int so the passed value uses the same underlying integer width; keep
using Self.cmuxSetPageMutedSelector and the same call path in
cmuxSetPageAudioMuted but cast the implementation to `@convention`(c) (AnyObject,
Selector, Int) -> Void and pass muted ? Self.cmuxMediaMutedStateAudio : 0.
🤖 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.

Outside diff comments:
In `@Sources/Panels/CmuxWebView.swift`:
- Around line 8-30: The unsafeBitCast is using UInt which may not match the
underlying C enum type for _WKMediaMutedState; update the ABI to match that enum
by changing the SetPageMutedFunction signature in cmuxSetPageAudioMuted to use
Int (Objective-C NSInteger-sized type) instead of UInt, and change
cmuxMediaMutedStateAudio to return an Int so the passed value uses the same
underlying integer width; keep using Self.cmuxSetPageMutedSelector and the same
call path in cmuxSetPageAudioMuted but cast the implementation to `@convention`(c)
(AnyObject, Selector, Int) -> Void and pass muted ?
Self.cmuxMediaMutedStateAudio : 0.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 26ac5396-8328-4f10-b6a7-8ff3ff1d2d5f

📥 Commits

Reviewing files that changed from the base of the PR and between 3d891ae and 5951419.

📒 Files selected for processing (2)
  • Resources/Localizable.xcstrings
  • Sources/Panels/CmuxWebView.swift

@austinywang

Copy link
Copy Markdown
Contributor Author

Greptile noted that the vendor/bonsplit submodule bump is opaque from this parent PR. The Bonsplit implementation is in manaflow-ai/bonsplit#137, and its tests/review/security checks are passing; the parent PR body also links that dependency.

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

🤖 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 `@vendor/bonsplit`:
- Line 1: The vendor submodule is pinned to an unreachable commit that adds
isAudioMuted (seen in Sources/Bonsplit/Public/Types/Tab.swift and
BonsplitController.swift); update the submodule reference to a commit reachable
from upstream main (or push the branch containing
3427ca47c769558cc7f275e401bcf8f04a9fc745 to the remote) so the pin is stable,
then update any references to isAudioMuted in callers/tests if the chosen
upstream tip differs; ensure the submodule pointer in vendor/bonsplit is
advanced to the selected reachable commit and commit the updated gitlink.
🪄 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: 00c388fc-6cb3-465d-adef-6140004aec9d

📥 Commits

Reviewing files that changed from the base of the PR and between 5951419 and 06a078d.

📒 Files selected for processing (3)
  • Sources/SessionIndexView.swift
  • Sources/Workspace.swift
  • vendor/bonsplit

Comment thread vendor/bonsplit Outdated
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Actionable comments posted: 0

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

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

Copy link
Copy Markdown
Contributor Author

Updated the Bonsplit dependency branch and parent gitlink to 9166c3639fabb80bca050d57d7cbf3fe66248c2e, which merges Bonsplit origin/main with the browser audio mute action. Also addressed the remaining mute feedback by keeping isMuted tied to successful mute application, removing the unused capability wrapper, switching the WebKit SPI argument to Int, and removing the no-longer-used cmux Localizable.xcstrings entries.

Comment thread Sources/Panels/BrowserPanel.swift
@austinywang
austinywang merged commit 1c2fff7 into main May 30, 2026
21 checks passed
@austinywang
austinywang deleted the issue-4910-browser-mute-tab branch May 30, 2026 04:11

This branch was successfully deployed

1 active deployment
Preview – cmux — 2adc6cb7 Deployed May 30, 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: add a "mute tab" button to the toolbar

1 participant