Skip to content

Fix browser panes reloading when switching workspaces - #1136

Merged
austinywang merged 2 commits into
mainfrom
issue-1132-browser-refresh-workspace-switch
Mar 10, 2026
Merged

austinywang merged 2 commits into
mainfrom
issue-1132-browser-refresh-workspace-switch

Conversation

@austinywang

@austinywang austinywang commented Mar 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add a regression test for the workspace handoff sequence where a browser pane is hidden, unmounted, and later rebound
  • preserve hidden browser portal entries when their SwiftUI anchor disappears so the same WKWebView is reused on workspace return
  • keep the existing explicit detach paths for panel close, window teardown, and web view replacement

Testing

  • regression test added for hidden portal anchor loss/rebind (not run locally per repo policy)
  • ./scripts/reload.sh --tag issue-1132-browser-refresh

Fixes #1132


Summary by cubic

Prevents browser panes from reloading when switching workspaces by preserving hidden portal entries and reusing their WKWebView on return. Keeps workspace switches fast and seamless.

  • Bug Fixes
    • Preserve hidden browser portals when their SwiftUI anchor temporarily unmounts during workspace handoff, so rebinds reuse the existing WKWebView.
    • Keep explicit detach behavior for panel close, window teardown, and web view replacement.
    • Add a regression test for the hide → anchor removal → rebind flow to ensure no reloads on workspace switch.

Written for commit 57eeaed. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced portal lifecycle handling: hidden portals now remain preserved when their anchors are removed, eliminating unnecessary web view reloads during anchor transitions and improving performance during workspace changes.

@vercel

vercel Bot commented Mar 10, 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 Mar 10, 2026 0:44am

@coderabbitai

coderabbitai Bot commented Mar 10, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR modifies BrowserWindowPortal to preserve hidden portal entries when their anchor is missing or invalid, returning nil instead of detaching them. It adds corresponding test coverage verifying that hidden portals survive anchor removal and are properly rebound when the anchor is restored.

Changes

Cohort / File(s) Summary
Portal Lifecycle Logic
Sources/BrowserWindowPortal.swift
Modified dead-annotation logic to unconditionally return nil when anchorView is nil or anchor is invalid for the host, preserving hidden/off-tree portals instead of detaching them and allowing rebinding without WebKit reload.
Portal Lifecycle Test Coverage
cmuxTests/CmuxWebViewKeyEquivalentTests.swift
Added comprehensive test testHiddenPortalEntrySurvivesAnchorRemovalUntilWorkspaceRebind() verifying that hidden portal entries remain alive during anchor removal, reuse existing slots upon rebinding, and maintain correct display/refresh counts.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A portal's home, once thought to flee,
Now stays put, preserved and free,
No reload when anchors roam,
Just waiting for their way back home! 🏠✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 accurately reflects the main objective of the PR: fixing browser panes that reload when switching workspaces, which is the core issue addressed by preserving hidden portal entries.
Description check ✅ Passed The PR description covers the Summary and Testing sections required by the template, explaining what changed and how it was tested, though it lacks explicit checklist completion details and demo video.
Linked Issues check ✅ Passed The code changes directly address issue #1132 by preserving hidden browser portal entries to prevent WKWebView reloading, matching the expected behavior of retaining state across workspace switches.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the workspace reload issue: BrowserWindowPortal logic changes preserve hidden portals, and the new test validates the fix without introducing unrelated modifications.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-1132-browser-refresh-workspace-switch

Comment @coderabbitai help to get the list of available commands and usage tips.

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 2 files

@greptile-apps

greptile-apps Bot commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes browser panes reloading when switching workspaces by preserving hidden portal entries in pruneDeadEntries even after their SwiftUI anchor view is unmounted. A regression test covering the full hide → anchor-removal → rebind lifecycle is also added.

Key changes:

  • In pruneDeadEntries, two guard branches that previously pruned hidden entries (return entry.visibleInUI ? nil : webViewId) now unconditionally return nil, keeping every entry alive regardless of visibility until an explicit detach (panel close, window teardown, or web view replacement) removes it.
  • A new testHiddenPortalEntrySurvivesAnchorRemovalUntilWorkspaceRebind test uses TrackingPortalWebView to assert slot identity, hidden state, entry count, and displayIfNeededCount after rebind — good direct coverage of the regression.
  • One edge-case concern: the anchorInvalidForCurrentHost path now also preserves entries whose anchor has moved to a different NSWindow (anchor.window !== currentWindow). In a multi-window setup this could silently retain an orphaned hidden entry indefinitely if the explicit detach path is never triggered for that scenario.

Confidence Score: 4/5

  • Safe to merge; the fix is correct for the target scenario with one minor edge-case concern in multi-window configurations.
  • The logic change is small, well-understood, and directly tested. The only concern is that the blanket "always preserve" policy extends to the anchor.window !== currentWindow sub-case, which could silently accumulate orphaned hidden entries in multi-window setups if explicit detach paths don't cover that path. This is a style/robustness concern rather than a correctness bug for the described fix.
  • Sources/BrowserWindowPortal.swift — specifically the anchorInvalidForCurrentHost branch and the deallocated-anchor (guard let anchor) branch in pruneDeadEntries.

Important Files Changed

Filename Overview
Sources/BrowserWindowPortal.swift Two targeted changes to pruneDeadEntries flip the prune decision for hidden entries from "prune when anchor-less/invalid" to "always preserve". Correct for same-window workspace switching, but the anchor.window !== currentWindow sub-case now silently keeps orphaned entries in multi-window configurations.
cmuxTests/CmuxWebViewKeyEquivalentTests.swift Well-structured regression test that exercises the full hide → anchor-removal → rebind lifecycle using TrackingPortalWebView to verify displayIfNeededCount is incremented on rebind. Assertions cover slot identity, hidden state, entry count, and display refresh — good coverage of the happy path.

Sequence Diagram

sequenceDiagram
    participant SwiftUI
    participant Portal as WindowBrowserPortal
    participant Entry as PortalEntry (hidden)
    participant WKWebView

    Note over SwiftUI,WKWebView: Workspace deactivation
    SwiftUI->>Portal: updateEntryVisibility(visibleInUI: false)
    SwiftUI->>Portal: synchronizeWebViewForAnchor(oldAnchor)
    Portal->>Entry: mark slot hidden
    Entry->>WKWebView: slot.isHidden = true

    Note over SwiftUI,WKWebView: SwiftUI unmounts anchor
    SwiftUI->>SwiftUI: oldAnchor.removeFromSuperview()
    SwiftUI->>Portal: synchronizeWebViewForAnchor(oldAnchor)
    Portal->>Portal: pruneDeadEntries()
    Note over Portal: anchorInvalidForCurrentHost = true<br/>BEFORE: hidden → return webViewId (prune ❌)<br/>AFTER:  return nil (preserve ✅)
    Portal-->>Entry: entry kept alive, WKWebView stays attached

    Note over SwiftUI,WKWebView: Workspace reactivation
    SwiftUI->>Portal: bind(webView, to: newAnchor, visibleInUI: true)
    Portal->>Entry: anchorView = newAnchor, visibleInUI = true
    Portal->>Entry: slot.isHidden = false
    Entry->>WKWebView: displayIfNeeded() — no reload
Loading

Last reviewed commit: 57eeaed

Comment on lines 2816 to 2821
if anchorInvalidForCurrentHost {
return entry.visibleInUI ? nil : webViewId
// Hidden browser portals can legitimately be off-tree between workspace
// deactivation and the next rebind. Preserve them until an explicit detach
// (panel close, window teardown, or web view replacement) says otherwise.
return nil
}

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.

Multi-window anchor migration may leak entries permanently

The anchorInvalidForCurrentHost condition covers three sub-cases: anchor.window !== currentWindow, anchor.superview == nil, and the reference-view check. The first sub-case — anchor migrated to a different NSWindow — is not the same as the "workspace switch on the same window" scenario the comment describes.

In a multi-window configuration, if a hidden portal's anchorView ends up on a different window and the corresponding workspace is later closed without going back to that window, the entry will never be cleaned up unless a direct detachWebView call is made. The previous code at least pruned hidden entries in that state.

Consider narrowing the "always-preserve" logic to the specific conditions that workspace switching triggers:

if anchorInvalidForCurrentHost {
    // Hidden portals whose anchor is just off-tree (superview == nil or outside the reference
    // view) are legitimately mid-transition during workspace deactivation; preserve them.
    // If the anchor has moved to a different NSWindow, the portal is truly orphaned and
    // should still be pruned while hidden.
    let anchorOnDifferentWindow = anchor.window !== currentWindow
    if anchorOnDifferentWindow && !entry.visibleInUI {
        return webViewId
    }
    return nil
}

Same consideration applies to the guard let anchor = entry.anchorView else path above — a fully-deallocated anchor (weak ref went nil) is an equally strong signal that the entry is orphaned.

@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/BrowserWindowPortal.swift (1)

2804-2820: ⚠️ Potential issue | 🟠 Major

Avoid making orphaned hidden portals immortal.

Both branches now unconditionally return nil, so pruneDeadEntries() will never reclaim an entry once its anchor has been deallocated or moved off-tree. Because the hidden WindowBrowserSlotView stays attached to hostView, that can retain the WKWebView and its WebKit process indefinitely unless some external path happens to call detach. The workspace-switch fix needs a bounded grace/rebind policy here, not permanent retention.

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

In `@Sources/BrowserWindowPortal.swift` around lines 2804 - 2820, The current
logic in pruneDeadEntries()/the block handling WindowBrowserSlotView always
returns nil when anchorInvalidForCurrentHost, preventing pruning and leaking
WKWebView; change it to enforce a bounded grace/rebind window instead of
permanent retention: record a timestamp (e.g. lastHiddenAt) on
WindowBrowserSlotView when it becomes off-tree/anchorInvalidForCurrentHost, and
only return nil (preserve) if that timestamp is within a short grace period;
otherwise return webViewId so pruneDeadEntries() can reclaim it (also ensure
detach() still forces immediate reclamation). Use the existing symbols anchor,
container, hostView, webViewId, pruneDeadEntries(), WindowBrowserSlotView and
detach to locate and implement this timed-grace policy.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@Sources/BrowserWindowPortal.swift`:
- Around line 2804-2820: The current logic in pruneDeadEntries()/the block
handling WindowBrowserSlotView always returns nil when
anchorInvalidForCurrentHost, preventing pruning and leaking WKWebView; change it
to enforce a bounded grace/rebind window instead of permanent retention: record
a timestamp (e.g. lastHiddenAt) on WindowBrowserSlotView when it becomes
off-tree/anchorInvalidForCurrentHost, and only return nil (preserve) if that
timestamp is within a short grace period; otherwise return webViewId so
pruneDeadEntries() can reclaim it (also ensure detach() still forces immediate
reclamation). Use the existing symbols anchor, container, hostView, webViewId,
pruneDeadEntries(), WindowBrowserSlotView and detach to locate and implement
this timed-grace policy.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 54807481-7f56-4058-be8e-5d317618d27a

📥 Commits

Reviewing files that changed from the base of the PR and between 18bb11d and 57eeaed.

📒 Files selected for processing (2)
  • Sources/BrowserWindowPortal.swift
  • cmuxTests/CmuxWebViewKeyEquivalentTests.swift

This branch was successfully deployed

1 active deployment
Preview — 57eeaedc Deployed Mar 10, 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 panes fully refresh/reload when switching workspaces

1 participant