Skip to content

Fix browser freeze after pane split - #1852

Merged
austinywang merged 1 commit into
manaflow-ai:mainfrom
Apptah:fix/browser-freeze-after-split
Mar 23, 2026
Merged

austinywang merged 1 commit into
manaflow-ai:mainfrom
Apptah:fix/browser-freeze-after-split

Conversation

@Apptah

@Apptah Apptah commented Mar 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Fix built-in browser freezing after a pane split by adding BrowserWindowPortalRegistry.refresh() calls after portal host rebinds
  • When a split occurs, SwiftUI recreates host views and the portal rebinds the WKWebView to a new container, but WebKit's rendering state (_exitInWindow/_enterInWindow) was never cycled — causing the web view to freeze
  • The refresh is a no-op when no reattach is needed, so normal rendering is unaffected

Root cause

In BrowserPanelView, the three portal bind paths (shouldBindNow, onDidMoveToWindow, onGeometryChanged) call BrowserWindowPortalRegistry.bind() but never follow up with refresh(). After a split reparents the WKWebView to a new container, the stale rendering state leaves the web view frozen.

Test plan

  • Open a browser tab (e.g. preview an HTML file)
  • Split the pane (horizontal or vertical)
  • Verify the browser in the original pane continues rendering (no freeze)
  • Verify popup windows (window.open / OAuth flows) still work correctly
  • Verify tab switches and workspace changes don't cause unnecessary redraws

🤖 Generated with Claude Code


Summary by cubic

Fixes the built-in browser freezing after a pane split by reattaching WKWebView rendering state when the portal rebinds to a new host. Keeps rendering alive after splits without affecting normal behavior.

  • Bug Fixes
    • Call BrowserWindowPortalRegistry.refresh(webView:reason:) after portal host bind in three paths: shouldBindNow, onDidMoveToWindow, and onGeometryChanged.
    • Cycles _exitInWindow / _enterInWindow after reparenting to prevent freezes.
    • No-op when no reattach is needed to avoid unnecessary redraws.

Written for commit 9323f12. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Improved stability of window portal rendering during lifecycle transitions by ensuring proper refresh behavior when window hosts are repositioned or reattached.

…g state

When a pane split occurs, SwiftUI recreates host views and the portal
system rebinds the WKWebView to a new container. However, the bind path
never called BrowserWindowPortalRegistry.refresh(), so WebKit's internal
rendering state (_exitInWindow/_enterInWindow) was never cycled. This
left the WKWebView frozen in the original pane after a split.

Add refresh() calls after every portal bind that changes the host, in
three code paths: the main update path (shouldBindNow), onDidMoveToWindow,
and onGeometryChanged. The refresh is a no-op when no reattach is needed
(browserPortalNeedsRenderingStateReattach == false), so normal rendering
is unaffected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Mar 20, 2026

Copy link
Copy Markdown

@busihoward-gpu is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Mar 20, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Added refresh calls to BrowserWindowPortalRegistry during WebViewRepresentable lifecycle events to maintain portal rendering state consistency after binding, geometry changes, and window movements.

Changes

Cohort / File(s) Summary
Portal Lifecycle Refresh
Sources/Panels/BrowserPanelView.swift
Added three BrowserWindowPortalRegistry.refresh(webView:reason:) calls during window-portal hosting lifecycle: after portal bind in host.onDidMoveToWindow, after rebinding in host.onGeometryChanged when anchor mismatch occurs, and after bind in updateUsingWindowPortal host-accepted path.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related PRs

Poem

🐰 A portal host binds with grace,
Each geometry finds its place,
Refresh calls whisper, "Stay aligned,"
So rendering stays intertwined! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix browser freeze after pane split' clearly and specifically summarizes the main change and matches the primary objective of the pull request.
Description check ✅ Passed The pull request description covers the required template sections with clear explanations of the change, root cause, and a comprehensive test plan.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

You can disable the changed files summary in the walkthrough.

Disable the reviews.changed_files_summary setting to disable the changed files summary in the walkthrough.

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


Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

@greptile-apps

greptile-apps Bot commented Mar 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a WKWebView rendering freeze that occurs after a pane split by adding BrowserWindowPortalRegistry.refresh() calls immediately after each BrowserWindowPortalRegistry.bind() call in WebViewRepresentable. The refresh cycles WebKit's internal _exitInWindow/_enterInWindow rendering state, which was never triggered when a pane split caused SwiftUI to reparent the web view to a new host container.

Key changes:

  • shouldBindNow path: refresh() added inside the existing if shouldBindNow guard — well-scoped, fires only when a rebind is actually required.
  • onGeometryChanged callback: refresh() added inside the if coordinator.lastPortalHostId != hostId || !isWebView(boundTo:) guard — correctly restricted to host-change events.
  • onDidMoveToWindow callback: refresh() added unconditionally after bind(), unlike the other two paths which guard with a lastPortalHostId check. This means every move-to-window event (not just host replacements) will schedule three rendering passes. The containerView.isHidden check in refreshHostedWebViewPresentation provides a partial guard, but when the container is visible the full 3-pass cycle runs.
  • The explanatory comment (describing the _exitInWindow/_enterInWindow root cause) is present only in the shouldBindNow path and is absent from the other two callsites.

Confidence Score: 4/5

  • Safe to merge with minor style improvements; the fix correctly targets the root cause and is guarded in two of three paths.
  • The fix is well-reasoned and correctly adds refresh() after every bind() call. The shouldBindNow and onGeometryChanged paths have proper host-change guards. The onDidMoveToWindow path is unconditional, which could cause unnecessary 3-pass refresh cycles on frequent window-entry events when the container is visible — however, this mirrors the existing unconditional bind() call and the PR's test plan specifically covers tab switches and workspace changes. No logic errors or regressions are introduced.
  • The onDidMoveToWindow handler in Sources/Panels/BrowserPanelView.swift warrants attention for the unconditional refresh behaviour.

Important Files Changed

Filename Overview
Sources/Panels/BrowserPanelView.swift Adds BrowserWindowPortalRegistry.refresh() after bind() in all three portal-host bind paths (shouldBindNow, onDidMoveToWindow, onGeometryChanged). The shouldBindNow and onGeometryChanged paths are well-guarded; the onDidMoveToWindow path calls refresh unconditionally on every window-entry event, which could trigger unnecessary 3-pass refresh cycles when the container is visible.

Sequence Diagram

sequenceDiagram
    participant SwiftUI
    participant WebViewRepresentable
    participant BrowserWindowPortalRegistry
    participant WKWebView

    Note over SwiftUI,WKWebView: Pane split — SwiftUI recreates host view

    SwiftUI->>WebViewRepresentable: updateNSView (shouldBindNow=true)
    WebViewRepresentable->>BrowserWindowPortalRegistry: bind(webView, to: newAnchor)
    BrowserWindowPortalRegistry->>WKWebView: reparent to new container
    WebViewRepresentable->>BrowserWindowPortalRegistry: refresh(reason: "portalHostBind") ✨ NEW
    BrowserWindowPortalRegistry->>WKWebView: _exitInWindow (immediate)
    BrowserWindowPortalRegistry->>WKWebView: _enterInWindow (immediate)
    BrowserWindowPortalRegistry-->>BrowserWindowPortalRegistry: schedule async pass (+0ms)
    BrowserWindowPortalRegistry-->>BrowserWindowPortalRegistry: schedule delayed pass (+30ms)

    Note over SwiftUI,WKWebView: Host view enters window

    SwiftUI->>WebViewRepresentable: onDidMoveToWindow fires
    WebViewRepresentable->>BrowserWindowPortalRegistry: bind(webView, to: anchor)
    WebViewRepresentable->>BrowserWindowPortalRegistry: refresh(reason: "portalHostBind.didMoveToWindow") ✨ NEW
    BrowserWindowPortalRegistry->>WKWebView: _exitInWindow/_enterInWindow (3 passes)

    Note over SwiftUI,WKWebView: Geometry changes with new host

    SwiftUI->>WebViewRepresentable: onGeometryChanged fires
    WebViewRepresentable->>WebViewRepresentable: check lastPortalHostId != hostId
    WebViewRepresentable->>BrowserWindowPortalRegistry: bind(webView, to: anchor)
    WebViewRepresentable->>BrowserWindowPortalRegistry: refresh(reason: "portalHostBind.geometryChanged") ✨ NEW
    BrowserWindowPortalRegistry->>WKWebView: _exitInWindow/_enterInWindow (3 passes)
Loading

Last reviewed commit: "Fix browser freeze a..."

Comment on lines +6067 to +6070
BrowserWindowPortalRegistry.refresh(
webView: webView,
reason: "portalHostBind.didMoveToWindow"
)

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 onDidMoveToWindow refresh fires unconditionally on every window-entry event

Unlike the onGeometryChanged handler (which guards with coordinator.lastPortalHostId != hostId || !isWebView(boundTo:) before calling bind + refresh), this path calls bind() + refresh() on every invocation that passes the generation/ownership/window guards — even when the host hasn't changed.

refreshHostedWebViewPresentation schedules three layout + browserPortalReattachRenderingState passes (immediate, async, and asyncAfter +30 ms), guarded only by containerView.isHidden. If the container is visible at the moment onDidMoveToWindow fires (e.g., after SwiftUI recreates the host view during a workspace transition while the pane is in view), all three passes run unnecessarily.

Consider adding an early-exit guard consistent with onGeometryChanged:

Suggested change
BrowserWindowPortalRegistry.refresh(
webView: webView,
reason: "portalHostBind.didMoveToWindow"
)
Self.installPortalAnchorView(portalAnchorView, in: host)
let hostId = ObjectIdentifier(host)
let needsRebind = coordinator.lastPortalHostId != hostId ||
!BrowserWindowPortalRegistry.isWebView(webView, boundTo: portalAnchorView)
BrowserWindowPortalRegistry.bind(
webView: webView,
to: portalAnchorView,
visibleInUI: coordinator.desiredPortalVisibleInUI,
zPriority: coordinator.desiredPortalZPriority
)
if needsRebind {
BrowserWindowPortalRegistry.refresh(
webView: webView,
reason: "portalHostBind.didMoveToWindow"
)
}

This mirrors the guard already present in onGeometryChanged and avoids the three-pass refresh cycle on ordinary re-presentations where the host–anchor binding is already correct.

Comment on lines +6102 to +6105
BrowserWindowPortalRegistry.refresh(
webView: webView,
reason: "portalHostBind.geometryChanged"
)

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 Missing explanatory comment in onGeometryChanged refresh path

The shouldBindNow path has a detailed comment explaining why refresh() is required (cycling _exitInWindow/_enterInWindow after a pane split reparents the WKWebView). The same rationale applies here — a new hostId means the web view was reparented — but there's no comment, which can make this call look like unnecessary churn to a future reader.

Suggested change
BrowserWindowPortalRegistry.refresh(
webView: webView,
reason: "portalHostBind.geometryChanged"
)
BrowserWindowPortalRegistry.refresh(
webView: webView,
reason: "portalHostBind.geometryChanged"
)
// Force a rendering-state reattach after portal host replacement
// (e.g. after a pane split). Without this, WKWebView can freeze
// because _exitInWindow/_enterInWindow are never cycled when the
// web view is reparented to a new container during bind.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@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.

🧹 Nitpick comments (1)
Sources/Panels/BrowserPanelView.swift (1)

6143-6150: Consider centralizing the bind() + refresh() sequence.

This fix is correct here, but the same invariant now lives in three call sites. A small helper for portal rebinding would make it much harder for a future bind path to miss the refresh and reintroduce this freeze.

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

In `@Sources/Panels/BrowserPanelView.swift` around lines 6143 - 6150, Extract the
repeated bind() + BrowserWindowPortalRegistry.refresh(...) sequence into a
single helper and replace the three call sites with it; for example add a method
(either a static on BrowserWindowPortalRegistry like
bindPortalHost(webView:reason:) or an instance/helper on BrowserPanelView such
as rebindPortal(for webView:reason:)) that performs the bind and then calls
BrowserWindowPortalRegistry.refresh(webView:reason:), and update all locations
that currently call bind() followed by refresh() to call this new helper to
ensure the refresh invariant is always applied.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Sources/Panels/BrowserPanelView.swift`:
- Around line 6143-6150: Extract the repeated bind() +
BrowserWindowPortalRegistry.refresh(...) sequence into a single helper and
replace the three call sites with it; for example add a method (either a static
on BrowserWindowPortalRegistry like bindPortalHost(webView:reason:) or an
instance/helper on BrowserPanelView such as rebindPortal(for webView:reason:))
that performs the bind and then calls
BrowserWindowPortalRegistry.refresh(webView:reason:), and update all locations
that currently call bind() followed by refresh() to call this new helper to
ensure the refresh invariant is always applied.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9c74c70e-ffbf-4a73-8984-bd885f002102

📥 Commits

Reviewing files that changed from the base of the PR and between 638f74f and 9323f12.

📒 Files selected for processing (1)
  • Sources/Panels/BrowserPanelView.swift

@austinywang
austinywang merged commit 5ced313 into manaflow-ai:main Mar 23, 2026
4 of 5 checks passed
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
…g state (manaflow-ai#1852)

When a pane split occurs, SwiftUI recreates host views and the portal
system rebinds the WKWebView to a new container. However, the bind path
never called BrowserWindowPortalRegistry.refresh(), so WebKit's internal
rendering state (_exitInWindow/_enterInWindow) was never cycled. This
left the WKWebView frozen in the original pane after a split.

Add refresh() calls after every portal bind that changes the host, in
three code paths: the main update path (shouldBindNow), onDidMoveToWindow,
and onGeometryChanged. The refresh is a no-op when no reattach is needed
(browserPortalNeedsRenderingStateReattach == false), so normal rendering
is unaffected.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

2 participants