Skip to content

Fix browser issues - #1029

Merged
austinywang merged 1 commit into
mainfrom
issue-1024-terminal-drag-glitch
Mar 7, 2026
Merged

austinywang merged 1 commit into
mainfrom
issue-1024-terminal-drag-glitch

Conversation

@austinywang

@austinywang austinywang commented Mar 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • What changed?
  • Why?

Testing

  • How did you test this change?
  • What did you verify manually?

Demo Video

For UI or behavior changes, include a short demo video (GitHub upload, Loom, or other direct link).

  • Video URL or attachment:

Review Trigger (Copy/Paste as PR comment)

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

Checklist

  • I tested the change locally
  • I added or updated tests for behavior changes
  • I updated docs/changelog if needed
  • I requested bot reviews after my latest commit (copy/paste block above or equivalent)
  • All code review bot comments are resolved
  • All human review comments are resolved

Summary by cubic

Fixes a flicker where the browser portal detaches or hides when a visible webview’s anchor is temporarily reparented off-window during drag. Addresses Linear issue 1024 (terminal drag glitch) by keeping the slot visible until the anchor returns.

  • Bug Fixes
    • Detects off-window reparent (visible entry + anchor has superview but no window), defers detach via transient recovery, clears drop-zone overlay, and keeps the slot visible.
    • Adds a test to ensure the portal slot stays attached and is reused after the anchor is re-bound.

Written for commit 72c0f56. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of UI elements temporarily repositioned off-window, preventing unintended visibility flashing and enabling more stable recovery.
  • Tests

    • Added test coverage to verify portal visibility stability during anchor repositioning scenarios.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@vercel

vercel Bot commented Mar 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Building Building Preview, Comment Mar 7, 2026 2:25am

@austinywang
austinywang merged commit a635607 into main Mar 7, 2026
10 of 12 checks passed
@coderabbitai

coderabbitai Bot commented Mar 7, 2026 •

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1197b0a0-2dec-4722-ae92-0610787b9e61

📥 Commits

Reviewing files that changed from the base of the PR and between 0e7c695 and 72c0f56.

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

📝 Walkthrough

Walkthrough

The PR adds targeted recovery logic for off-window anchor reparenting in BrowserWindowPortal.swift. When a visible portal entry's anchor view moves off-window (superview exists but window is nil), the code schedules a transient recovery operation (anchorWindowMismatch) instead of immediate detachment, defers stabilization via retry, and clears the drop-zone overlay. A corresponding test verifies portal entries remain visible and bound during this scenario.

Changes

Cohort / File(s) Summary
Off-Window Anchor Reparent Recovery
Sources/BrowserWindowPortal.swift
Adds guard branch to detect when entry is visible but anchor's window is nil with existing superview; schedules transient recovery with anchorWindowMismatch reason, logs debug message, clears drop-zone overlay, and returns early for targeted retry-based stabilization.
Off-Window Reparent Test
cmuxTests/CmuxWebViewKeyEquivalentTests.swift
Adds test method testVisiblePortalEntryStaysVisibleDuringOffWindowAnchorReparentUntilRebind that verifies portal slots remain visible, unbound, and stable across rebinds during simulated off-window anchor reparenting.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • manaflow-ai/cmux#995: Modifies BrowserWindowPortal.swift anchor/window mismatch handling with transientRecoveryReason bookkeeping and bounded retry logic.
  • manaflow-ai/cmux#892: Modifies portal attachment/detach behavior for portal-hosted web views with related detach/rebind test coverage.

Poem

🐰 When anchors drift off-window wide,
No hasty detach, just a gentle glide,
Recovery waits with patient care,
Rebinding slots in their rightful air.
Off-window reparent, now tamed with grace!

✨ 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-1024-terminal-drag-glitch

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

@greptile-apps

greptile-apps Bot commented Mar 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a targeted guard in WindowBrowserPortal.synchronizeWebView to detect the "off-window anchor reparent" scenario — where a visible anchor view is detached from its window but still has a superview (e.g. during drag churn) — and keeps the browser portal alive instead of hiding it, deferring recovery via the existing scheduleTransientRecoveryRetryIfNeeded mechanism. A new lifecycle test covers the happy-path round-trip (off-window → rebind).

Key changes:

  • Sources/BrowserWindowPortal.swift: Inserts an isOffWindowReparent branch inside the anchorView.window !== window guard; when the condition is met the portal is kept visible, only the drop-zone overlay is cleared, and a deferred recovery retry is scheduled.
  • cmuxTests/CmuxWebViewKeyEquivalentTests.swift: Adds testVisiblePortalEntryStaysVisibleDuringOffWindowAnchorReparentUntilRebind covering the anchor detach → off-window reparent → rebind lifecycle.

Issues found:

  • When scheduleTransientRecoveryRetryIfNeeded returns false (retry budget exhausted), the isOffWindowReparent branch still returns early without hiding or cleaning up the container. This diverges from every other budget-exhaustion path in the function and can leave a zombie visible portal if the anchor stays off-window past the retry window.
  • The debug log key browser.portal.hidden.deferKeep is emitted when the container is visibly not hidden, making the name misleading for log analysis.

Confidence Score: 3/5

  • The change is low-risk for the happy path but introduces an untested budget-exhaustion edge case that can leave a zombie portal visible indefinitely.
  • The new branch is narrow and well-motivated, the test covers the primary scenario, and the existing transient-recovery machinery is reused correctly. However, the early-return behaviour when the retry budget is exhausted differs from every other budget-exhaustion site in the same function — those all fall through to hide and clean up the container — and this divergence is not tested. If the anchor stays off-window longer than the retry budget allows, the portal will remain visible with stale content and no scheduled recovery, which is a meaningful correctness concern.
  • Sources/BrowserWindowPortal.swift — specifically the isOffWindowReparent branch around line 2288 and the budget-exhaustion fallthrough (or lack thereof).

Important Files Changed

Filename Overview
Sources/BrowserWindowPortal.swift Adds an isOffWindowReparent fast-path that keeps the portal visible during drag churn, but leaves the container visible with no recovery when the retry budget is exhausted — a potential zombie-portal state not covered by the new test.
cmuxTests/CmuxWebViewKeyEquivalentTests.swift Adds a well-structured lifecycle test covering the happy-path off-window reparent scenario and subsequent rebind; does not cover the budget-exhaustion edge case.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[synchronizeWebView called] --> B{anchorView.window\n=== window?}
    B -- yes --> C[Normal layout & frame sync]
    B -- no --> D{isOffWindowReparent?\nvisibleInUI &&\nanchorView.window == nil &&\nanchorView.superview != nil}

    D -- yes --> E[scheduleTransientRecoveryRetryIfNeeded\nreason: anchorWindowMismatch]
    E --> F{didScheduleTransientRecovery?}
    F -- true --> G[setDropZoneOverlay nil\nreturn — portal stays VISIBLE]
    F -- false\nbudget exhausted --> H[⚠️ setDropZoneOverlay nil\nreturn — portal STILL VISIBLE\nno further recovery scheduled]

    D -- no --> I[scheduleTransientDetachRecovery\nreason: anchorWindowMismatch]
    I --> J{retry scheduled?}
    J -- yes --> K[hide container\nreturn]
    J -- no\nbudget exhausted --> L[hide container\nreturn]
Loading

Last reviewed commit: 72c0f56

Comment on lines +2288 to +2304
if isOffWindowReparent {
let didScheduleTransientRecovery = scheduleTransientRecoveryRetryIfNeeded(
forWebViewId: webViewId,
entry: &entry,
webView: webView,
reason: "anchorWindowMismatch"
)
#if DEBUG
if didScheduleTransientRecovery && !containerView.isHidden {
dlog(
"browser.portal.hidden.deferKeep web=\(browserPortalDebugToken(webView)) " +
"reason=anchorWindowMismatch.offWindow frame=\(browserPortalDebugFrame(containerView.frame))"
)
}
#endif
containerView.setDropZoneOverlay(zone: nil)
return

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.

Zombie portal when retry budget is exhausted

When scheduleTransientRecoveryRetryIfNeeded returns false (budget exhausted — i.e. the anchor has been off-window for more than transientRecoveryRetryBudget sync cycles), the code still returns early without hiding the container. No further deferred syncs are scheduled at that point, so the portal remains unconditionally visible with no remaining recovery path.

Compare this to the existing code immediately below: when the non-off-window anchor-window-mismatch path exhausts its budget, scheduleTransientDetachRecovery returns false and the code falls through to unconditionally hide and clean up the container.

In the off-window path the only escape from this state is an external call to synchronizeWebViewForAnchor (e.g. when the anchor is re-added to the window hierarchy). If that call never arrives — or arrives only after a long delay — the portal will remain visible indefinitely in a zombie state, showing stale content with no frame updates and only the drop-zone overlay cleared.

Consider hiding the container (and clearing the remaining overlays) when the budget is exhausted, analogous to the fallthrough behaviour in the non-off-window path:

if isOffWindowReparent {
    let didScheduleTransientRecovery = scheduleTransientRecoveryRetryIfNeeded(
        forWebViewId: webViewId,
        entry: &entry,
        webView: webView,
        reason: "anchorWindowMismatch"
    )
    if didScheduleTransientRecovery {
#if DEBUG
        if !containerView.isHidden {
            dlog(
                "browser.portal.hidden.deferKeep web=\(browserPortalDebugToken(webView)) " +
                "reason=anchorWindowMismatch.offWindow frame=\(browserPortalDebugFrame(containerView.frame))"
            )
        }
#endif
        containerView.setDropZoneOverlay(zone: nil)
        return
    }
    // Budget exhausted — fall through to hide/cleanup below
}

Comment on lines +2295 to +2302
#if DEBUG
if didScheduleTransientRecovery && !containerView.isHidden {
dlog(
"browser.portal.hidden.deferKeep web=\(browserPortalDebugToken(webView)) " +
"reason=anchorWindowMismatch.offWindow frame=\(browserPortalDebugFrame(containerView.frame))"
)
}
#endif

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.

Misleading debug log name

The log key browser.portal.hidden.deferKeep is emitted inside the guard !containerView.isHidden, meaning the container is currently visible and is being kept visible — it is not being hidden. The name hidden in the key will confuse log-analysis tooling or anyone grepping for hide events.

A name like browser.portal.visible.deferKeep or browser.portal.sync.deferKeep would better reflect what is actually happening at this point.

Suggested change
#if DEBUG
if didScheduleTransientRecovery && !containerView.isHidden {
dlog(
"browser.portal.hidden.deferKeep web=\(browserPortalDebugToken(webView)) " +
"reason=anchorWindowMismatch.offWindow frame=\(browserPortalDebugFrame(containerView.frame))"
)
}
#endif
dlog(
"browser.portal.visible.deferKeep web=\(browserPortalDebugToken(webView)) " +
"reason=anchorWindowMismatch.offWindow frame=\(browserPortalDebugFrame(containerView.frame))"
)

@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

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/BrowserWindowPortal.swift">

<violation number="1" location="Sources/BrowserWindowPortal.swift:2303">
P2: When the transient recovery budget is exhausted (`scheduleTransientRecoveryRetryIfNeeded` returns `false`), this block still returns early, leaving the container visible at a stale frame with no recovery scheduled. Gate the early return on the scheduling result so it falls through to the existing hide logic when retries are exhausted.</violation>
</file>

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

Comment on lines +2303 to +2304
containerView.setDropZoneOverlay(zone: nil)
return

@cubic-dev-ai cubic-dev-ai Bot Mar 7, 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: When the transient recovery budget is exhausted (scheduleTransientRecoveryRetryIfNeeded returns false), this block still returns early, leaving the container visible at a stale frame with no recovery scheduled. Gate the early return on the scheduling result so it falls through to the existing hide logic when retries are exhausted.

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

<comment>When the transient recovery budget is exhausted (`scheduleTransientRecoveryRetryIfNeeded` returns `false`), this block still returns early, leaving the container visible at a stale frame with no recovery scheduled. Gate the early return on the scheduling result so it falls through to the existing hide logic when retries are exhausted.</comment>

<file context>
@@ -2281,6 +2281,28 @@ final class WindowBrowserPortal: NSObject {
+                    )
+                }
+#endif
+                containerView.setDropZoneOverlay(zone: nil)
+                return
+            }
</file context>
Suggested change
containerView.setDropZoneOverlay(zone: nil)
return
containerView.setDropZoneOverlay(zone: nil)
if didScheduleTransientRecovery {
return
}
Fix with Cubic

This branch was successfully deployed

1 active deployment
Preview — 72c0f564 Deployed Mar 7, 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.

1 participant