Skip to content

Protect visible browser web views from discard - #6191

Closed
lawrencecchen wants to merge 3 commits into
mainfrom
issue-6189-browser-hibernation
Closed

lawrencecchen wants to merge 3 commits into
mainfrom
issue-6189-browser-hibernation

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add workspace-layout protection to browser hidden-webview discard blockers.
  • Feed browser panels from the same rendered-panel visibility set used by hibernation protection.
  • Add a regression test covering a workspace-visible browser whose WebView was transiently reported hidden.

Testing

  • xcodebuild test -project cmux.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-bhib-test -only-testing:cmuxTests/BrowserHiddenWebViewDiscardManagerTests/testWorkspaceVisibleBrowserPanelBlocksHiddenWebViewDiscard\n- ./scripts/reload-cloud.sh --tag bhib\n\n## Issues\n- Closes Prevent cmux browser web views from resource cleanup while active #6189

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Medium Risk
Touches browser memory lifecycle and workspace/portal visibility reconciliation; wrong gating could leave WebViews undiscarded or discard too aggressively, but behavior is scoped with explicit blockers and tests.

Overview
Adds a workspace_visible hidden-webview discard blocker so browser panels in the rendered layout are not memory-discarded when SwiftUI or the portal briefly reports the WebView hidden.

Workspace tracks presentation visibility (setWorkspacePresentationVisible from WorkspaceContentView) and drives setWorkspaceVisibilityProtectsHiddenWebViewDiscard during browser portal reconciliation, using the same rendered-panel set as layout visibility but only while the workspace is presented. Canvas inline hosting clears that protection on mount/render/occlude/unmount so discard follows canvas visibility instead.

Clearing protection while the WebView is still hidden resets the hidden-at timestamp so the discard timer does not fire immediately after a stale hide. Portal hide and workspace retire clear the blocker as well.

Regression coverage is in BrowserWorkspaceVisibleDiscardTests (visible panel + transient hide, clock restart, canvas-inline exclusion, background-mounted workspace).

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


Summary by cubic

Prevents discarding browser WebViews that are still visible by adding a workspace_visible blocker keyed off the presentation-visible rendered layout. Canvas-inline hosts are excluded, and clearing protection while hidden restarts the discard timer.

  • Bug Fixes
    • Added workspace_visible to discard blockers/lifecycle payload; toggling re-schedules; clearing while hidden resets the hidden-at timestamp.
    • Protection syncs from presentation-visible rendered panel IDs via Workspace.setWorkspacePresentationVisible; portal hide, canvas mount/render/unmount, and workspace retire clear it. Tests cover the blocker, clock restart, canvas-inline exclusion, workspace retire clearing, and background-mounted workspace exclusion.

Written for commit d43d404. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added workspace visibility-based protection mechanism to prevent hidden browser web views from being unexpectedly discarded during resource cleanup.
  • Tests

    • Added test suite verifying workspace visibility discard protection behavior, including scenarios for visible panels, protection state changes, and canvas inline panels.

@vercel

vercel Bot commented Jun 15, 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 Jun 16, 2026 3:53am
cmux-staging Building Building Preview, Comment Jun 16, 2026 3:53am

@coderabbitai

coderabbitai Bot commented Jun 15, 2026 •

Copy link
Copy Markdown

Review Change Stack

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 "workspace_visible" discard blocker to the hidden-webview discard pipeline. BlockerSnapshot gains an isProtectedByWorkspaceVisibility flag; BrowserPanel tracks it via a new setter that triggers discard-scheduling re-evaluation. Workspace reconciliation and hideBrowserPortalView call the setter; CanvasPaneContentMount clears it during canvas lifecycle transitions. Three new tests cover blocker presence, clock restart, and canvas-inline exclusion.

Changes

Workspace-visibility discard protection for hidden browser WebViews

Layer / File(s) Summary
BlockerSnapshot contract and blocker emission
Sources/Panels/BrowserHiddenWebViewDiscardManager.swift
Adds isProtectedByWorkspaceVisibility: Bool stored property and initializer parameter (default false) to BlockerSnapshot, and appends "workspace_visible" to blockers(for:) when the flag is true.
BrowserPanel protection flag, setter, and snapshot
Sources/Panels/BrowserPanel.swift
Adds isProtectedByWorkspaceVisibilityForResourceCleanup private state, setWorkspaceVisibilityProtectsHiddenWebViewDiscard(_:reason:now:) -> Bool which updates the flag on change and triggers discard-scheduling reevaluation with a workspace_visibility.<reason> label, and threads the flag into hiddenWebViewDiscardSnapshot.
Workspace reconciliation and portal visibility handling
Sources/Workspace.swift, Sources/Panels/BrowserPanel.swift
reconcileBrowserPortalVisibilityForCurrentRenderedLayout calls the new setter for all browser panels (canvas-inline panels always clear with "canvasInline.\(reason)" and skip portal work; others compute visibility before proceeding). hideBrowserPortalView(source:) clears protection with "portal.<source>" before hiding.
Canvas lifecycle workspace visibility protection
Sources/Canvas/CanvasPaneContent.swift
Calls setWorkspaceVisibilityProtectsHiddenWebViewDiscard(false, reason:) on BrowserPanel instances during mount (canvas.mount), rendering transitions (canvas.render/canvas.occlude), and unmount (canvas.unmount), immediately before the existing noteWebViewVisibility calls.
Test helper and new discard-blocker tests
cmuxTests/BrowserWorkspaceVisibleDiscardTests.swift, cmux.xcodeproj/project.pbxproj
Registers the new test file in the Xcode project. Adds withHiddenWebViewDiscardPolicyEnabled helper and three tests: blocker presence and discard suppression when protection is active; clock restart and blocker removal when protection is cleared; and canvas-inline panels not receiving the blocker.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • manaflow-ai/cmux#5412: Extends BrowserHiddenWebViewDiscardManager.BlockerSnapshot and blockers(for:) with a boolean-driven media_playback blocker, the same pattern this PR applies for workspace_visible.
  • manaflow-ai/cmux#5424: Adds a conditional visual_automation hidden-webview discard blocker to BlockerSnapshot, directly parallel to the workspace_visible blocker added here.
  • manaflow-ai/cmux#4244: Implements the core hidden-WebView discard lifecycle and blocker machinery in BrowserPanel and the discard scheduler that this PR extends.

Poem

🐇 Hop, hop, the web views stay alive,
Workspace eyes keep them fit to thrive.
A flag is raised, a blocker appears,
"workspace_visible" steadies the gears.
Canvas unmounts? The guard steps aside.
No surprise discards — the rabbit's inside! 🥕

🚥 Pre-merge checks | ✅ 20 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (20 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Protect visible browser web views from discard' directly describes the main change: adding workspace-layout protection for browser WebView discard prevention.
Linked Issues check ✅ Passed The implementation fully meets all four acceptance criteria from issue #6189: browser WebViews participate in hibernation-prevention lifecycle [6189], active panes are protected from resource cleanup [6189], inactive surfaces maintain intended cleanup behavior through canvas-inline exclusion [6189], and regression tests cover the behavior [6189].
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing workspace visibility protection for browser discard: manager snapshot extension, panel protection setter, workspace reconciliation, canvas lifecycle handling, and corresponding tests.
Cmux Swift Actor Isolation ✅ Passed All production code changes properly maintain Swift 6 actor isolation: MainActor classes/protocols are explicitly marked, new properties inherit MainActor isolation from their classes, BlockerSnaps...
Cmux Swift Blocking Runtime ✅ Passed No blocking/timing-based synchronization (Task.sleep, semaphores, locks, asyncAfter, polling) introduced in production code. All new methods use synchronous operations, with test code properly mark...
Cmux Expensive Synchronous Load ✅ Passed PR adds only lightweight state updates and scheduler calls to @MainActor methods; no expensive synchronous loaders (RestorableAgentSessionIndex.load, disk I/O, sysctl) introduced.
Cmux Cache Substitution Correctness ✅ Passed The new isProtectedByWorkspaceVisibilityForResourceCleanup flag is a transient runtime property fed from fresh layout reads; never persisted, restored, or cached for cold/stale scenarios.
Cmux No Hacky Sleeps ✅ Passed This rule applies only to TypeScript, JavaScript, shell, and non-Swift runtime code. All PR changes are in Swift (.swift), which is explicitly out of scope and covered by a separate check.
Cmux Algorithmic Complexity ✅ Passed PR uses O(1) Set lookups in a linear O(n) loop over bounded workspace panels; no nested scans, batch rescans, or algorithmic debt introduced.
Cmux Swift Concurrency ✅ Passed PR introduces only synchronous methods and properties; no legacy async patterns (DispatchQueue, Task fire-and-forget, Combine, completion handlers) detected in new cmux Swift code.
Cmux Swift @Concurrent ✅ Passed All Swift changes follow concurrent annotation rules: no new async functions, no invalid @concurrent annotations, all classes properly @MainActor-isolated, and no heavy operations in synchronous me...
Cmux Swift File And Package Boundaries ✅ Passed PR adds focused workspace visibility protection to existing discard manager: +44 lines to 324-line manager, +23 to 13381-line BrowserPanel, +15 to 13012-line Workspace—all under threshold. No new m...
Cmux Swift Logging ✅ Passed No logging violations found. PR adds production Swift code without introducing print, debugPrint, dump, NSLog, ad-hoc file/stdout logging, or improperly isolated Logger constants.
Cmux User-Facing Error Privacy ✅ Passed All new strings added are internal technical identifiers (blocker names, reason codes) used for discard scheduling and telemetry, not user-facing errors or alerts. No sensitive info exposed.
Cmux Full Internationalization ✅ Passed PR adds only internal technical identifiers (blocker names, reason codes) and test code; no user-facing text or string catalog changes requiring localization per i18n rules.
Cmux Swiftui State Layout ✅ Passed PR adds private state management for browser discard protection with no @Published properties, no SwiftUI layout issues, and all state mutations occur in lifecycle/event handlers, not view body code.
Cmux Architecture Rethink ✅ Passed PR adds workspace-visible discard blocker through single API in BrowserPanel driven by Workspace.reconcileBrowserPortalVisibilityForCurrentRenderedLayout(), with clear lifecycle exceptions and no t...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR contains no new or materially changed standalone cmux-owned windows; all changes are to discard-blocker logic in existing panels and managers.
Cmux Source Artifacts ✅ Passed All 6 changed files are intentional source, test, and config files (4 Swift source, 1 test, 1 Xcode config). No artifact patterns detected; no local tool output, logs, caches, or scratch directorie...
Description check ✅ Passed The PR description includes a clear summary of changes, testing approach, and linked issue.

✏️ 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 issue-6189-browser-hibernation

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.

@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 `@Sources/Workspace.swift`:
- Around line 10663-10669: The workspace-visibility discard-protection flag is
set to true when browser portals become visible through the
`setWorkspaceVisibilityProtectsHiddenWebViewDiscard` call, but this flag is not
being cleared when portals are hidden in `hideAllBrowserPortalViews()` or
`hideBrowserPortalView(source:)`. This causes inactive workspaces to retain the
"workspace_visible" blocker and prevents proper hidden-webview cleanup. Add
calls to `setWorkspaceVisibilityProtectsHiddenWebViewDiscard` with `false` in
both the `hideAllBrowserPortalViews()` method and the
`hideBrowserPortalView(source:)` method to clear the discard-protection flag
when hiding portals, ensuring the flag state matches the actual visibility state
of the browser portals.
🪄 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: 5d9725b9-b3f8-4dad-a3b8-ad27f477ea37

📥 Commits

Reviewing files that changed from the base of the PR and between 65eca62 and c7fd90d.

📒 Files selected for processing (4)
  • Sources/Panels/BrowserHiddenWebViewDiscardManager.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Workspace.swift
  • cmuxTests/BrowserPanelTests.swift

@greptile-apps

greptile-apps Bot commented Jun 15, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a workspace_visible discard blocker that prevents hidden browser WebViews from being memory-discarded when their workspace is still in the rendered layout of a presented workspace — fixing cases where SwiftUI transiently reports a WebView as hidden while the tab stays on screen.

  • Introduces workspacePresentationVisible on Workspace and isProtectedByWorkspaceVisibilityForResourceCleanup on BrowserPanel, fed by setWorkspacePresentationVisible (called from WorkspaceContentView) and reconcileBrowserPortalVisibilityForCurrentRenderedLayout, which now uses the presentation-gated panel set rather than the raw rendered set.
  • Canvas-inline browsers are explicitly excluded from the protection; hideBrowserPortalView, canvas mount/render/unmount, and workspace retire all clear it. Dropping protection while the WebView is hidden resets the discard clock to prevent immediate discard on stale timestamps.
  • Adds BrowserWorkspaceVisibleDiscardTests covering the blocker, clock restart, canvas-inline exclusion, workspace retire, and background-mounted workspace scenarios.

Confidence Score: 5/5

Safe to merge; the new protection mechanism is well-tested and the existing workspace retire path correctly clears per-panel flags through hideBrowserPortalView.

The core fix (blocking discard when workspacePresentationVisible is true) is correct and covered by the new test suite. The one noted concern — that reconcileBrowserPortalVisibilityForCurrentRenderedLayout doesn't guard against portalRenderingEnabled == false, so a layout command on the outgoing workspace during handoff could briefly re-add the blocker — is a narrow edge case whose impact is delaying memory discard rather than corrupting state, and onDisappear eventually clears it.

The interaction between portalRenderingEnabled and workspacePresentationVisible in Workspace.swift is worth a follow-up pass to ensure reconcileBrowserPortalVisibilityForCurrentRenderedLayout short-circuits when portal rendering is disabled.

Important Files Changed

Filename Overview
Sources/Panels/BrowserHiddenWebViewDiscardManager.swift Adds isProtectedByWorkspaceVisibility to BlockerSnapshot and the workspace_visible blocker string; adds an explicit init with a default value for backward compatibility. Clean, no issues.
Sources/Panels/BrowserPanel.swift Adds isProtectedByWorkspaceVisibilityForResourceCleanup flag, setWorkspaceVisibilityProtectsHiddenWebViewDiscard with clock-reset logic, and clears protection in hideBrowserPortalView. Logic is sound; the flag's correctness depends on all reconcile call sites being in sync with portalRenderingEnabled.
Sources/Workspace.swift Introduces workspacePresentationVisible, setWorkspacePresentationVisible, and presentationVisiblePanelIdsForCurrentLayout. The reconciler and follow-up check correctly use the new predicate, but portalRenderingEnabled is not checked inside the reconciler, allowing re-application of the blocker after portal rendering is disabled.
Sources/WorkspaceContentView.swift Wires setWorkspacePresentationVisible into updateAgentHibernationPresentationVisibility and onDisappear; straightforward and correct.
Sources/Canvas/CanvasPaneContent.swift Clears workspace visibility protection on canvas-inline mount, render, and unmount. Correct exclusion of canvas-inline browsers from the workspace_visible blocker.
cmuxTests/BrowserWorkspaceVisibleDiscardTests.swift New test suite covering protection blocker, clock restart, canvas-inline exclusion, workspace retire, and background-mounted workspace exclusion. Good coverage of the key scenarios.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant WCV as WorkspaceContentView
    participant WS as Workspace
    participant BP as BrowserPanel
    participant DM as DiscardManager

    WCV->>WS: setWorkspacePresentationVisible(true)
    WS->>WS: reconcileBrowserPortalVisibilityForCurrentRenderedLayout
    WS->>BP: setWorkspaceVisibilityProtectsHiddenWebViewDiscard(true)
    BP->>DM: reevaluateHiddenWebViewDiscardScheduling
    Note over DM: workspace_visible blocker → discard cancelled

    Note over BP: SwiftUI transiently hides WebView
    BP->>DM: noteWebViewVisibility(false)
    DM-->>DM: blockers present (workspace_visible) → no discard

    WCV->>WS: setWorkspacePresentationVisible(false)
    WS->>WS: reconcileBrowserPortalVisibilityForCurrentRenderedLayout
    WS->>BP: setWorkspaceVisibilityProtectsHiddenWebViewDiscard(false)
    BP->>BP: "webViewLastHiddenAt = now (clock reset)"
    BP->>DM: reevaluateHiddenWebViewDiscardScheduling
    Note over DM: No blockers → discard timer scheduled
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant WCV as WorkspaceContentView
    participant WS as Workspace
    participant BP as BrowserPanel
    participant DM as DiscardManager

    WCV->>WS: setWorkspacePresentationVisible(true)
    WS->>WS: reconcileBrowserPortalVisibilityForCurrentRenderedLayout
    WS->>BP: setWorkspaceVisibilityProtectsHiddenWebViewDiscard(true)
    BP->>DM: reevaluateHiddenWebViewDiscardScheduling
    Note over DM: workspace_visible blocker → discard cancelled

    Note over BP: SwiftUI transiently hides WebView
    BP->>DM: noteWebViewVisibility(false)
    DM-->>DM: blockers present (workspace_visible) → no discard

    WCV->>WS: setWorkspacePresentationVisible(false)
    WS->>WS: reconcileBrowserPortalVisibilityForCurrentRenderedLayout
    WS->>BP: setWorkspaceVisibilityProtectsHiddenWebViewDiscard(false)
    BP->>BP: "webViewLastHiddenAt = now (clock reset)"
    BP->>DM: reevaluateHiddenWebViewDiscardScheduling
    Note over DM: No blockers → discard timer scheduled
Loading

Reviews (7): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

private var pendingInteractiveBrowserPrompts: [PendingInteractiveBrowserPrompt] = []
private var isPresentingPendingInteractiveBrowserPrompt = false
private var isWebViewVisibleInUI: Bool = false
private var isProtectedByWorkspaceVisibilityForResourceCleanup: Bool = false

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.

P2 Stale flag if layout transitions don't call the reconciler

isProtectedByWorkspaceVisibilityForResourceCleanup is a cached copy of workspace-level state kept on BrowserPanel. Its correctness depends entirely on every layout transition eventually calling reconcileBrowserPortalVisibilityForCurrentRenderedLayout. There are currently three call sites (layout change, toggleSplitZoom, canvas layout), but if a future transition is added and the reconcile call is missed — or if a panel is moved between windows in a multi-window path that lacks its own reconcile call — the flag remains true and the panel is silently exempt from discard indefinitely. The source of truth (renderedVisiblePanelIdsForCurrentLayout) lives in Workspace, not in BrowserPanel, so the panel can represent the wrong visibility without any compile-time or runtime signal.

Rule Used: Flag Swift fixes that patch symptoms while leaving... (source)

@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 `@Sources/Workspace.swift`:
- Around line 10667-10673: The workspace-visibility blocker "workspace_visible"
is not being cleared when browser tabs are deselected, preventing hidden-WebView
discard for inactive tabs. In the `hideBrowserPortalsForDeselectedTabs` function
(and the related location at lines 11541-11547), when hiding deselected browser
portals, call `setWorkspaceVisibilityProtectsHiddenWebViewDiscard` with
`shouldBeVisible` set to false and an appropriate reason to clear the blocker,
ensuring that previously visible tabs don't retain the blocker after being
hidden.
🪄 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: 8509c4bb-e45f-42f0-ae27-f5dc49b00e3b

📥 Commits

Reviewing files that changed from the base of the PR and between c7fd90d and 8c8de46.

📒 Files selected for processing (4)
  • Sources/Panels/BrowserHiddenWebViewDiscardManager.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Workspace.swift
  • cmuxTests/BrowserPanelTests.swift

Comment thread Sources/Workspace.swift
Comment on lines +10667 to +10673
let shouldBeVisible = visiblePanelIds.contains(browserPanel.id)
if browserPanel.setWorkspaceVisibilityProtectsHiddenWebViewDiscard(
shouldBeVisible,
reason: reason
) {
didChange = true
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear the workspace-visibility blocker when a tab is deselected.

reconcileBrowserPortalVisibilityForCurrentRenderedLayout updates the new blocker, but normal tab selection hides deselected browser portals through hideBrowserPortalsForDeselectedTabs without clearing it. A previously visible browser tab can therefore keep the "workspace_visible" blocker after it is hidden, suppressing hidden-WebView discard for inactive tabs.

Proposed fix
     private func hideBrowserPortalsForDeselectedTabs(inPane pane: PaneID, selectedTabId: TabID) {
         for tab in bonsplitController.tabs(inPane: pane) {
             guard tab.id != selectedTabId else { continue }
             guard let panelId = panelIdFromSurfaceId(tab.id),
                   let browserPanel = panels[panelId] as? BrowserPanel else { continue }
+            _ = browserPanel.setWorkspaceVisibilityProtectsHiddenWebViewDiscard(
+                false,
+                reason: "tabDeselected"
+            )
             browserPanel.hideBrowserPortalView(source: "tabDeselected")
         }
     }

Also applies to: 11541-11547

🤖 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/Workspace.swift` around lines 10667 - 10673, The workspace-visibility
blocker "workspace_visible" is not being cleared when browser tabs are
deselected, preventing hidden-WebView discard for inactive tabs. In the
`hideBrowserPortalsForDeselectedTabs` function (and the related location at
lines 11541-11547), when hiding deselected browser portals, call
`setWorkspaceVisibilityProtectsHiddenWebViewDiscard` with `shouldBeVisible` set
to false and an appropriate reason to clear the blocker, ensuring that
previously visible tabs don't retain the blocker after being hidden.

@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 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 8442-8445: The call to
setWorkspaceVisibilityProtectsHiddenWebViewDiscard in the generic portal-hide
path at line 8442 is prematurely clearing the workspace protection during
transient hides. Remove this call from the transient portal-hide path in
BrowserPanel.swift, and instead add a call to
setWorkspaceVisibilityProtectsHiddenWebViewDiscard with
clearWorkspaceVisibilityProtection: true in the authoritative
workspace-retirement or workspace-removal code path to ensure protection is only
cleared when the workspace is actually being retired or removed, not during
temporary portal visibility changes.

In `@Sources/Workspace.swift`:
- Around line 10665-10672: The code at line 10665 intentionally bypasses portal
reconciliation for canvas-inline browsers, but the follow-up predicate
browserPortalVisibilityNeedsFollowUp() (around lines 10737-10750) still expects
those visible panels to be portal-bound, creating a mismatch. Add a check in the
browserPortalVisibilityNeedsFollowUp() predicate to skip canvas-inline browsers
(checking canvasInlineHostingActive) the same way they are skipped during the
main visibility update, so that layout follow-up converges properly instead of
timing out.
🪄 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: dfbb8121-0a46-4dfa-b2c7-1c8371926d0b

📥 Commits

Reviewing files that changed from the base of the PR and between 8c8de46 and 4ff16f5.

📒 Files selected for processing (5)
  • Sources/Canvas/CanvasPaneContent.swift
  • Sources/Panels/BrowserHiddenWebViewDiscardManager.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Workspace.swift
  • cmuxTests/BrowserPanelTests.swift

Comment on lines +8442 to +8445
_ = setWorkspaceVisibilityProtectsHiddenWebViewDiscard(
false,
reason: "portal.\(source)"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t clear workspace protection from the generic portal-hide path.

Line 8442 removes the new blocker before recording the webview as hidden, so a workspace-visible browser can still become discard-eligible during the transient hide this PR is meant to tolerate. Clear this only from the authoritative workspace-retirement/removal path, not every portal hide.

🐛 Proposed fix
-    func hideBrowserPortalView(source: String) {
-        _ = setWorkspaceVisibilityProtectsHiddenWebViewDiscard(
-            false,
-            reason: "portal.\(source)"
-        )
+    func hideBrowserPortalView(
+        source: String,
+        clearWorkspaceVisibilityProtection: Bool = false
+    ) {
+        if clearWorkspaceVisibilityProtection {
+            _ = setWorkspaceVisibilityProtectsHiddenWebViewDiscard(
+                false,
+                reason: "portal.\(source)"
+            )
+        }
         noteWebViewVisibility(
             false,
             reason: "portal.\(source)",
             recordIfUnchanged: true
         )

Update the workspace-retirement caller to pass clearWorkspaceVisibilityProtection: true; leave transient portal hides at the default false.

🤖 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/BrowserPanel.swift` around lines 8442 - 8445, The call to
setWorkspaceVisibilityProtectsHiddenWebViewDiscard in the generic portal-hide
path at line 8442 is prematurely clearing the workspace protection during
transient hides. Remove this call from the transient portal-hide path in
BrowserPanel.swift, and instead add a call to
setWorkspaceVisibilityProtectsHiddenWebViewDiscard with
clearWorkspaceVisibilityProtection: true in the authoritative
workspace-retirement or workspace-removal code path to ensure protection is only
cleared when the workspace is actually being retired or removed, not during
temporary portal visibility changes.

Comment thread Sources/Workspace.swift
Comment on lines +10665 to +10672
if browserPanel.canvasInlineHostingActive {
if browserPanel.setWorkspaceVisibilityProtectsHiddenWebViewDiscard(
false,
reason: "canvasInline.\(reason)"
) {
didChange = true
}
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Skip canvas-inline browsers in the follow-up predicate too.

Line 10665 now intentionally bypasses portal reconciliation for canvasInlineHostingActive, but browserPortalVisibilityNeedsFollowUp() still expects those visible panels to be portal-bound. That mismatch can leave layout follow-up non-converging until timeout.

Proposed fix
     private func browserPortalVisibilityNeedsFollowUp() -> Bool {
         let visiblePanelIds = renderedVisiblePanelIdsForCurrentLayout()
 
         for panel in panels.values {
             guard let browserPanel = panel as? BrowserPanel else { continue }
+            if browserPanel.canvasInlineHostingActive { continue }
             guard visiblePanelIds.contains(browserPanel.id) else { continue }
             let anchorView = browserPanel.portalAnchorView

Also applies to: 10737-10750

🤖 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/Workspace.swift` around lines 10665 - 10672, The code at line 10665
intentionally bypasses portal reconciliation for canvas-inline browsers, but the
follow-up predicate browserPortalVisibilityNeedsFollowUp() (around lines
10737-10750) still expects those visible panels to be portal-bound, creating a
mismatch. Add a check in the browserPortalVisibilityNeedsFollowUp() predicate to
skip canvas-inline browsers (checking canvasInlineHostingActive) the same way
they are skipped during the main visibility update, so that layout follow-up
converges properly instead of timing out.

@lawrencecchen
lawrencecchen force-pushed the issue-6189-browser-hibernation branch from 4a56cfa to 79c26e6 Compare June 16, 2026 02:32
@lawrencecchen
lawrencecchen force-pushed the issue-6189-browser-hibernation branch 2 times, most recently from b0123ca to eeef54c Compare June 16, 2026 02:52
@lawrencecchen
lawrencecchen force-pushed the issue-6189-browser-hibernation branch 2 times, most recently from 08c1252 to 93ce68b Compare June 16, 2026 03:04
…bernation

# Conflicts:
#	.github/swift-file-length-budget.tsv
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: d43d4043d4

ℹ️ 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".

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

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

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Prevent cmux browser web views from resource cleanup while active

2 participants