Skip to content

Fix browser pane portal anchor sizing - #1094

Merged
austinywang merged 6 commits into
mainfrom
issue-1093-browser-pane-fill
Mar 9, 2026
Merged

austinywang merged 6 commits into
mainfrom
issue-1093-browser-pane-fill

Conversation

@austinywang

@austinywang austinywang commented Mar 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • replace the browser portal anchor's frame/autoresizing setup with explicit edge constraints
  • keep the shared portal anchor pinned to all four edges of the SwiftUI host, even when it moves between hosts
  • force layout before portal sync so the hosted WKWebView binds against the full pane bounds

Testing

  • ./scripts/reload.sh --tag issue-1093-browser-pane-fill

Fixes #1093


Summary by cubic

Switches the browser pane to constraint-based layout so the portal anchor and hosted WKWebView always fill the pane, and preserves the portal frame during transient zero geometry to prevent flicker. Restricts portal ownership to the active pane to avoid stale hosts during splits and tab moves, improves drag/overlay behavior, and reduces jank during tab drags. Fixes #1093.

  • Bug Fixes
    • Constraint-based pinning for the portal anchor and WKWebView via pinHostedWebView; force layout on attach/update; ensure fill with .frame(.infinity), .layoutPriority(1), and address bar .fixedSize; preserve the visible frame across transient zero-geometry (clears overlays/context when preserving).
    • Gate attach/bind/visibility/focus/updates by pane ownership using paneId; compose the web view host .id with paneId to remount on pane moves; install the anchor only for the owner; release leases on dismantle.
    • Add host-lease guards for browser and terminal portals to prevent duplicate hosts, preferring in-window and larger-area hosts; defer terminal surface size updates while tab drag pasteboard is active.
    • Route drags through the portal so browser drop targets render above WKWebView (only sidebar tab reorders bypass); keep drop-zone overlays and top-chrome height in sync; remove the moved‑browser refresh path; fix browser panel pane ID unit tests.

Written for commit 4d86924. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Panels now carry stable pane identifiers to reduce remounts and improve pane-instance stability.
  • Bug Fixes

    • More reliable portal anchor handling when views move between hosts, with consistent constraint-based layout updates to avoid visual/positioning glitches.
    • Portal host lifecycle improvements to prevent stale hosts, ensure proper release on teardown, and stabilize portal ownership for correct rendering.
    • Refined hit-testing so only sidebar tab-reorder drags bypass the portal, preventing unintended hover/pass-through.
    • Guarded rendering and event processing so portal-related updates run only for the canonical pane instance.

@vercel

vercel Bot commented Mar 9, 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 9, 2026 8:25pm

@coderabbitai

coderabbitai Bot commented Mar 9, 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

Adds per-pane portal-host ownership and gating across BrowserPanel/BrowserPanelView, threads a new paneId through panel view creation, stabilizes WebView remounts via webViewSurfaceIdentity, tightens portal anchor attach/detach and constraint handling, and restricts portal pass-through hit-testing to sidebar-tab-reorder drags.

Changes

Cohort / File(s) Summary
Browser panel view & portal anchor
Sources/Panels/BrowserPanelView.swift
Adds paneId property and computed ownership/display/process flags; introduces webViewSurfaceIdentity; reworks portal anchor install/detach to reparent anchor, switch to Auto Layout, create/replace constraints with identifiers, and gate rendering/hit-testing/event handling to canonical pane instances.
Browser panel portal-host management
Sources/Panels/BrowserPanel.swift
Adds activePortalHostView tracking and portal-host lifecycle helpers: isEligiblePortalHost, claimPortalHostFromUpdate, claimPortalHostFromCallbackIfNeeded, isCurrentPortalHost, and releasePortalHost.
Panel composition wiring
Sources/Panels/PanelContentView.swift, Sources/WorkspaceContentView.swift
Threads paneId through PanelContentView and WorkspaceContentView so BrowserPanelView receives pane identifiers.
Portal/window pass-through & hit-testing
Sources/BrowserWindowPortal.swift
Narrows pass-through logic so only Bonsplit sidebar tab reorder drags bypass the portal (hasSidebarTabReorder gating); refines hit-test/drag-pass comments and branches.
WebView lifecycle & dismantle
.../WebViewRepresentable (integrations inside BrowserPanelView)
Replaces direct webViewInstanceID remount triggers with webViewSurfaceIdentity, synchronizes portal host ownership/visibility on lifecycle events, and deactivates portal anchoring on dismantle.

Sequence Diagram(s)

sequenceDiagram
    participant Panel as BrowserPanel
    participant View as BrowserPanelView
    participant Host as PortalHost(NSView)
    participant Registry as BrowserWindowPortalRegistry

    Panel->>View: notify host/geometry/window update
    View->>Panel: claimPortalHostFromUpdate(host)
    Panel->>Panel: isCurrentPortalHost(host)?
    alt host is current
        Panel->>View: request installPortalAnchorView(host)
        View->>Host: detach old anchor (if any) / reparent anchor
        View->>Host: set translatesAutoresizingMaskIntoConstraints = false
        View->>Host: create & add edge constraints (with identifiers)
        View->>Host: activate constraints / layout subtree
        View->>Registry: sync portal state & webViewSurfaceIdentity
        Registry->>Host: update overlay/pane chrome height / visibility
    else not current
        View->>Registry: update webview visibility / gating only
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #995: Adjusts portal anchoring and reparenting logic for panel portal hosts (closely related to anchor installation and host claims).
  • PR #892: Stabilizes WebView remounts and replaces webViewInstanceID semantics (directly related to webViewSurfaceIdentity changes).
  • PR #1068: Adds host/window guards and rebind/refresh logic for portal anchors and webview remount behavior (closely connected to lifecycle gating).

Poem

🐇 I hopped from host to host with care,
Anchors snug, constraints placed with flair,
I claimed my pane and guarded the gate,
Remounted steady, kept events straight,
A joyful hop — portals tidy and fair.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

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.
Description check ⚠️ Warning The pull request description provides a clear summary and implementation details, but lacks required Testing, Demo Video, Review Trigger, and Checklist sections per the repository template. Add the missing required sections: Testing verification details, demo video link (if applicable), review trigger comment block, and completion checklist with checkmarks for local testing, tests, documentation, and bot review requests.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix browser pane portal anchor sizing' is partially related to the main changeset; it addresses anchor sizing but omits broader fixes to portal ownership, pane instance gating, and drag routing that are central to resolving #1093.

✏️ 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-1093-browser-pane-fill

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

@greptile-apps

greptile-apps Bot commented Mar 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes issue #1093 where the browser pane's WKWebView would fail to fill its pane because the portal anchor view was sized incorrectly after split reparenting. The fix replaces the old frame/autoresizing-mask sizing approach in installPortalAnchorView with explicit Auto Layout edge constraints (top/bottom/leading/trailing), ensuring the shared anchor always stays pinned to all four edges of whatever SwiftUI host it currently lives in. A forced layoutSubtreeIfNeeded() call ensures the hosted WKWebView binds against the fully-resolved pane bounds rather than a stale zero or partial frame.

Key changes:

  • Added portalAnchorConstraintIdentifiers (static Set<String>) and portalAnchorConstraints(for:in:) helpers to locate and clean up the four edge constraints by identifier, correctly scoped to the anchor view so stale identically-named constraints on the host can't interfere.
  • installPortalAnchorView now deactivates old constraints before removing the anchor from a previous host, sets translatesAutoresizingMaskIntoConstraints = false, and activates the four edge constraints only when they aren't already present.
  • host.layoutSubtreeIfNeeded() is called unconditionally at the end to guarantee the anchor's frame is resolved before any portal sync takes place.
  • One minor leftover: anchorView.frame = host.bounds is still set before addSubview but is immediately made irrelevant by TAMIC being set to false.
  • The unconditional layoutSubtreeIfNeeded() call fires on every installPortalAnchorView invocation (including the hot onGeometryChanged path) even when no structural changes occurred; consider guarding it behind a layoutNeeded flag.

Confidence Score: 4/5

  • Safe to merge; the logic is correct and the fix directly addresses the sizing regression. Two minor style improvements are worth applying but don't block shipping.
  • The core constraint-based sizing approach is sound, the migration path (deactivate → remove → add → constrain) handles all interesting states (no superview, same host, different host), and the identifier-scoped lookup prevents cross-contamination. The only issues are a dead frame assignment and an unconditional synchronous layout pass on the geometry-change hot path — neither causes incorrect behaviour.
  • No files require special attention beyond the two style notes in Sources/Panels/BrowserPanelView.swift.

Important Files Changed

Filename Overview
Sources/Panels/BrowserPanelView.swift Replaces autoresizing-mask sizing of the portal anchor view with explicit Auto Layout edge constraints; adds helpers to identify/clean up those constraints when the anchor migrates between hosts, and forces a layout pass before portal sync. One redundant frame assignment and an unconditional layout pass on every call are worth addressing.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[installPortalAnchorView called] --> B{host.window != nil?}
    B -- No --> Z[return — guard host off-window]
    B -- Yes --> C{anchorView.superview !== host AND oldHost exists?}
    C -- Yes --> D[Deactivate old edge constraints on oldHost]
    D --> E[anchorView.removeFromSuperview]
    E --> F{anchorView.superview !== host?}
    C -- No --> F
    F -- Yes --> G[anchorView.frame = host.bounds\nhost.addSubview anchorView]
    F -- No --> H
    G --> H[translatesAutoresizingMaskIntoConstraints = false]
    H --> I[portalAnchorConstraints for anchorView in host]
    I --> J{Constraints count or identifiers mismatch?}
    J -- Yes --> K[Deactivate existing constraints\nCreate & activate top/bottom/leading/trailing]
    J -- No --> L
    K --> L[host.layoutSubtreeIfNeeded]
    L --> M[Portal sync proceeds against\nfully-resolved anchor frame]
Loading

Last reviewed commit: 86fdfae

Comment thread Sources/Panels/BrowserPanelView.swift Outdated
}

if anchorView.superview !== host {
anchorView.frame = host.bounds

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.

Redundant frame assignment

anchorView.frame = host.bounds is set here, but translatesAutoresizingMaskIntoConstraints is immediately set to false on line 4300 and explicit constraints are activated shortly after. Auto Layout completely ignores the frame when TAMIC is false, so this line has no effect on the final layout. It appears to be a leftover from the old autoresizing-mask approach and could be removed to avoid confusion.

Suggested change
anchorView.frame = host.bounds
if anchorView.superview !== host {
host.addSubview(anchorView)
}

NSLayoutConstraint.activate([top, bottom, leading, trailing])
}

host.layoutSubtreeIfNeeded()

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.

Unconditional layout pass on every call

host.layoutSubtreeIfNeeded() is called at the end of every invocation of installPortalAnchorView, including the common steady-state path where the anchor is already in the correct host with all four constraints already active (the if block above is skipped). This triggers a synchronous full-subtree layout pass even when nothing has changed.

installPortalAnchorView is called from at least three sites — including onGeometryChanged, which fires on every resize event. On a host with many subviews, or during animated resizing, this could pile up unnecessary synchronous layout work.

Consider guarding the call so the layout pass only happens when a structural change actually occurred (new subview added, or constraints re-created):

var layoutNeeded = false

if anchorView.superview !== host {
    anchorView.frame = host.bounds
    host.addSubview(anchorView)
    layoutNeeded = true
}
anchorView.translatesAutoresizingMaskIntoConstraints = false

let existingConstraints = portalAnchorConstraints(for: anchorView, in: host)
let existingConstraintIdentifiers = Set(existingConstraints.compactMap(\.identifier))
if existingConstraints.count != portalAnchorConstraintIdentifiers.count ||
    existingConstraintIdentifiers != portalAnchorConstraintIdentifiers {
    // ... activate constraints ...
    layoutNeeded = true
}

if layoutNeeded {
    host.layoutSubtreeIfNeeded()
}

@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 the current code and only fix it if needed.

Inline comments:
In `@Sources/Panels/BrowserPanelView.swift`:
- Around line 397-399: When shouldDisplayPaneInstance flips to false we must
clear any shared omnibar focus suppression left by this instance so the
canonical pane can restore WKWebView focus; detect the ownership transition
(shouldDisplayPaneInstance changing from true to false) and if addressBarFocused
is true call the counterpart cleanup
(panel.endSuppressWebViewFocusForAddressBar()) and update addressBarFocused
state as needed; integrate this cleanup alongside the existing visibility
changes and the shouldProcessViewEvents guards (also apply same fix in the other
affected block around the 501-537 region) so suppression is always unwound when
an instance stops being the canonical pane.
- Around line 4309-4324: The helper deactivatePortalHostIfNeeded currently calls
BrowserWindowPortalRegistry.updateEntryVisibility(for: webView, visibleInUI:
false, zPriority: 0) unconditionally, which hides the shared WKWebView even if
the web view has already been reattached to a new host; change the logic so that
you only call BrowserWindowPortalRegistry.updateEntryVisibility(...) and
BrowserWindowPortalRegistry.hide(webView: source:) when the panel's
portalAnchorView.superview === host (i.e., the host being deactivated still owns
the anchor). In other words, move the visibility update and hide calls so they
run inside the existing guard that checks panel.portalAnchorView.superview ===
host in deactivatePortalHostIfNeeded (and apply the same change in the other
occurrence around lines 4697-4704).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 42968e5e-cef0-4ad9-9181-2129aad7b3ff

📥 Commits

Reviewing files that changed from the base of the PR and between 46dfbf8 and 679910d.

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

Comment thread Sources/Panels/BrowserPanelView.swift Outdated
Comment thread Sources/Panels/BrowserPanelView.swift Outdated

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

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/GhosttyTerminalView.swift">

<violation number="1" location="Sources/GhosttyTerminalView.swift:7016">
P2: Releasing the portal host when `host.window == nil` makes the deferred visibility-update branch effectively unreachable, so portal entry visibility is not refreshed during deferred bind transitions.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/GhosttyTerminalView.swift Outdated

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

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/Panels/BrowserPanelView.swift">

<violation number="1" location="Sources/Panels/BrowserPanelView.swift:867">
P2: Remove the duplicate accessibility identifier `browserContentAccessibilityIdentifier` from `WebViewRepresentable` to avoid XCUITest "Multiple matches found" errors.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/Panels/BrowserPanelView.swift Outdated
.accessibilityIdentifier("BrowserWebViewSurface")
// Keep the host stable for normal pane churn, but force a remount when
// BrowserPanel replaces its underlying WKWebView after process termination.
.id(webViewSurfaceIdentity)

@cubic-dev-ai cubic-dev-ai Bot Mar 9, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Remove the duplicate accessibility identifier browserContentAccessibilityIdentifier from WebViewRepresentable to avoid XCUITest "Multiple matches found" errors.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/Panels/BrowserPanelView.swift, line 867:

<comment>Remove the duplicate accessibility identifier `browserContentAccessibilityIdentifier` from `WebViewRepresentable` to avoid XCUITest "Multiple matches found" errors.</comment>

<file context>
@@ -839,40 +840,46 @@ struct BrowserPanelView: View {
+                    .accessibilityIdentifier("BrowserWebViewSurface")
+                    // Keep the host stable for normal pane churn, but force a remount when
+                    // BrowserPanel replaces its underlying WKWebView after process termination.
+                    .id(webViewSurfaceIdentity)
+                    .contentShape(Rectangle())
+                    .accessibilityIdentifier(browserContentAccessibilityIdentifier)
</file context>
Fix with Cubic

bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
* Fix browser pane webview sizing

* Guard browser portal by pane ownership

* Keep browser portal frame during transient zero geometry

* Guard portal surfaces against duplicate hosts

* Defer terminal surface reflow during tab drags

* Fix browser panel pane ID unit tests

This branch was successfully deployed

1 active deployment
Preview — 4d869248 Deployed Mar 9, 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 does not fill its container — content renders only in bottom portion

1 participant