Skip to content

Fix #1357: hide stale terminal portal after restore churn - #2025

Merged
lawrencecchen merged 3 commits into
mainfrom
issue-1357-ghost-terminal-sidebar-bleed
Mar 25, 2026
Merged

lawrencecchen merged 3 commits into
mainfrom
issue-1357-ghost-terminal-sidebar-bleed

Conversation

@austinywang

@austinywang austinywang commented Mar 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add a regression test for restore-like portal churn where a visible terminal entry loses its anchor and must not remain rendered at its stale frame.
  • Hide terminal portal entries when sync-all encounters a missing anchor/window so stale terminal content cannot bleed into the sidebar after restart.

Testing

  • Added testDeferredSyncHidesVisibleHostedViewAfterAnchorDisappears in cmuxTests/TerminalAndGhosttyTests.swift.
  • Built and launched ./scripts/reload.sh --tag issue-1357-ghost-terminal-sidebar-bleed successfully.
  • Manual verification beyond the tagged app launch was not performed locally.

Demo Video

  • Video URL or attachment: N/A

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

Hide stale terminal portal views when their anchor/window disappears during restore churn to stop ghost terminal content from bleeding into the sidebar (fixes #1357). Clarifies deferred resync in tests to ensure stale views hide and hit regions clear.

  • Bug Fixes
    • On sync-all, hide a visible hosted terminal if its anchor/window is missing; keep it visible only when a transient recovery retry is scheduled to avoid a remount flash.
    • Tests: add and clarify a regression that removes the old anchor, binds a new one, and drains the queued full-sync to verify the stale view hides and stale hit-testing is cleared.

Written for commit 8c0aee3. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Improved terminal window visibility handling: transient anchor loss no longer immediately hides active terminals, preventing brief flicker; stale clickable regions are still cleared when anchors are replaced.
  • Tests
    • Added lifecycle tests to ensure portals hide and hit-testing/interaction regions update correctly after anchor changes.

@vercel

vercel Bot commented Mar 24, 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 24, 2026 7:05am

@chatgpt-codex-connector

Copy link
Copy Markdown

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

@coderabbitai

coderabbitai Bot commented Mar 24, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 08f410e2-9ecc-461c-8905-8cf4ed0c3095

📥 Commits

Reviewing files that changed from the base of the PR and between 836360d and 8c0aee3.

📒 Files selected for processing (1)
  • cmuxTests/TerminalAndGhosttyTests.swift

📝 Walkthrough

Walkthrough

Modified TerminalWindowPortal.synchronizeHostedView(withId:) to defer hiding a visible hosted view when its anchor or window is temporarily unavailable by scheduling a transient-recovery retry and returning early; added a regression test that verifies deferred sync hides retired hosted views and clears stale hit-testing regions.

Changes

Cohort / File(s) Summary
TerminalWindowPortal synchronization logic
Sources/TerminalWindowPortal.swift
Added transient-recovery preservation branch: when anchorView/window are missing but entry.visibleInUI == true, compute shouldPreserveVisibleOnTransient by checking hostedView.isHidden and scheduleTransientRecoveryRetryIfNeeded(...); if true, log portal.hidden.deferKeep and return early instead of hiding. Kept existing hide/reset behavior for other paths and adjusted debug logs.
Deferred sync regression test
cmuxTests/TerminalAndGhosttyTests.swift
Added testDeferredSyncHidesVisibleHostedViewAfterAnchorDisappears: constructs portal with retired and active anchors, binds hosted views, removes retired anchor, runs sync, asserts retired hosted view becomes hidden, stale hit-testing cleared after queued resync, and active anchor hit-testing resolves to active terminal.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Caller
    participant Portal as TerminalWindowPortal
    participant Anchor as AnchorView/Window
    participant Scheduler as TransientScheduler
    participant Hosted as HostedView

    Caller->>Portal: synchronizeHostedView(withId)
    Portal->>Anchor: check anchorView / window
    alt anchor/window missing
        Portal->>Hosted: check hostedView.isHidden
        Portal->>Scheduler: scheduleTransientRecoveryRetryIfNeeded(...)
        alt scheduler scheduled && hosted visible
            Portal->>Portal: log "portal.hidden.deferKeep"
            Portal-->>Caller: return (defer hide)
        else
            Portal->>Hosted: hide hosted view
            Portal-->>Caller: return (hidden)
        end
    else anchor/window present
        Portal->>Hosted: normal visibility sync / attach
        Portal-->>Caller: return
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I watched the portals pause and peep,
When anchors slipped away so steep,
"Hold fast!" I thumped, "don't vanish yet,"
Defer the hide — no quick reset.
Now stale ghosts fade when bonds reset. ✨

🚥 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 clearly summarizes the main change: fixing issue #1357 by hiding stale terminal portals after restore churn.
Description check ✅ Passed The description covers all required sections: summary of changes, testing approach, review trigger, and checklist with appropriate items marked.
Linked Issues check ✅ Passed The pull request implementation addresses the core requirements from #1357: preventing stale terminal surfaces from remaining visible by hiding hosted views when their anchor/window is missing during sync-all operations.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the issue: modifications to synchronization logic in TerminalWindowPortal and a focused regression test in TerminalAndGhosttyTests.

✏️ 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-1357-ghost-terminal-sidebar-bleed

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.

@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="cmuxTests/TerminalAndGhosttyTests.swift">

<violation number="1" location="cmuxTests/TerminalAndGhosttyTests.swift:2643">
P2: Avoid fixed 50ms RunLoop sleeps in this async test; they can make the regression test flaky on slower machines. Wait on a condition/expectation instead.

(Based on your team's feedback about avoiding fixed RunLoop sleeps in stale-delay tests.) [FEEDBACK_USED]</violation>
</file>

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

Comment thread cmuxTests/TerminalAndGhosttyTests.swift Outdated
@greptile-apps

greptile-apps Bot commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a sidebar bleed issue (#1357) where a terminal portal entry that was still marked visibleInUI=true could remain rendered at a stale screen position after its anchor view disappeared during a restore/restart churn. The root cause was an asymmetry in synchronizeHostedView: the anchorWindowMismatch guard branch already used a "hide unless transient recovery preserves it" pattern, but the missingAnchorOrWindow branch skipped hiding entirely for visibleInUI=true entries — leaving stale frames on screen indefinitely in normal (non-recovery) builds.

Key changes:

  • TerminalWindowPortal.swift: Rewrites the missingAnchorOrWindow guard branch to mirror the anchorWindowMismatch logic: compute shouldPreserveVisibleOnTransient = !hostedView.isHidden && scheduleTransientRecoveryRetryIfNeeded(...), return early only when the view should be preserved, otherwise hide and schedule a recovery retry for future restoration.
  • TerminalAndGhosttyTests.swift: Adds testDeferredSyncHidesVisibleHostedViewAfterAnchorDisappears, a regression test that binds and syncs a terminal, removes its anchor, binds a new one, and asserts the old hosted view is hidden and no longer hit-testable after the next full sync sweep.

Confidence Score: 5/5

  • Safe to merge; the fix is correct for normal production builds and the only feedback is minor test documentation.
  • The logic change is surgically precise and self-consistent — it brings missingAnchorOrWindow in line with the identical pattern already used for anchorWindowMismatch. The fix correctly hides stale entries in builds where transientRecoveryEnabled=false (normal production). In recovery-enabled builds, behavior is intentionally preserved (retry budget keeps the view alive while the anchor may return). The test validates the bug scenario end-to-end. The only comments are P2 style suggestions about a RunLoop wait that lacks an explanatory comment and a test assertion message that slightly misattributes when hiding occurs.
  • No files require special attention.

Important Files Changed

Filename Overview
Sources/TerminalWindowPortal.swift Fixes the missingAnchorOrWindow guard branch so a visible (visibleInUI=true) entry is now hidden when transient recovery is disabled, aligning it with the identical pattern already used for anchorWindowMismatch; no logic errors found.
cmuxTests/TerminalAndGhosttyTests.swift Adds a regression test for portal churn after restore; test logic is sound, but the assertion message for isHidden slightly misattributes the timing (synchronous vs. deferred), and the RunLoop wait lacks a comment explaining what it is flushing.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant Portal as WindowTerminalPortal
    participant Retired as retiredHosted entry
    participant Recovery as scheduleTransientRecovery

    Note over Caller,Recovery: Restore churn: old anchor removed, new anchor bound

    Caller->>Portal: bind(activeHosted, activeAnchor, visibleInUI:true)
    Portal->>Portal: synchronizeHostedView(activeHosted)
    Portal->>Portal: scheduleDeferredFullSynchronizeAll()

    Caller->>Portal: synchronizeHostedViewForAnchor(activeAnchor)
    Portal->>Portal: pruneDeadEntries()
    Note over Portal: retiredHosted kept because visibleInUI=true
    Portal->>Portal: synchronizeAllHostedViews(excluding: activeHosted)

    Portal->>Retired: synchronizeHostedView(retiredHosted)
    Note over Retired: anchorView==nil, visibleInUI==true

    alt transientRecoveryEnabled == false (normal build)
        Retired->>Recovery: scheduleTransientRecoveryRetryIfNeeded()
        Recovery-->>Retired: false
        Note over Retired: shouldPreserveVisibleOnTransient = false
        Retired->>Retired: hostedView.isHidden = true
    else transientRecoveryEnabled == true (recovery build)
        Retired->>Recovery: scheduleTransientRecoveryRetryIfNeeded()
        Recovery-->>Retired: true (budget remaining)
        Note over Retired: shouldPreserveVisibleOnTransient = true, return early
    end

    Portal-->>Caller: returns

    Note over Portal: DispatchQueue.main.async fires (deferred sync)
    Portal->>Portal: synchronizeAllHostedViews(excluding: nil)
    Note over Retired: already hidden in normal builds
Loading

Reviews (1): Last reviewed commit: "fix: hide stale terminal portal after re..." | Re-trigger Greptile

Comment thread cmuxTests/TerminalAndGhosttyTests.swift Outdated
Comment thread cmuxTests/TerminalAndGhosttyTests.swift Outdated

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

1214-1247: ⚠️ Potential issue | 🟠 Major

Don’t let the final transient retry keep the stale portal visible.

On Line 1215, scheduleTransientRecoveryRetryIfNeeded(...) still returns true when it decrements transientRecoveryRetriesRemaining from 1 to 0. That makes shouldPreserveVisibleOnTransient true, so this branch returns without hiding, but no follow-up sync is queued. The stale terminal can then stay rendered until some unrelated geometry event. The helper call after isHidden = true also re-arms the same retry budget again. Please base deferKeep on entry.transientRecoveryRetriesRemaining > 0 after the helper runs, and only invoke the helper once per sync attempt.

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

In `@Sources/TerminalWindowPortal.swift` around lines 1214 - 1247, Call
scheduleTransientRecoveryRetryIfNeeded(...) only once per sync and base the
deferKeep decision on the post-call retry budget: invoke
scheduleTransientRecoveryRetryIfNeeded(forHostedId:hostedId, entry:&entry,
hostedView:hostedView, reason:"missingAnchorOrWindow") a single time and store
its result and then compute shouldPreserveVisibleOnTransient as
!hostedView.isHidden && entry.transientRecoveryRetriesRemaining > 0 (not the raw
helper boolean which can be true when it just hit 0). Use the stored result to
avoid calling scheduleTransientRecoveryRetryIfNeeded again after setting
hostedView.isHidden and only schedule/re-arm retries once per sync attempt.
🤖 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/TerminalWindowPortal.swift`:
- Around line 1214-1247: Call scheduleTransientRecoveryRetryIfNeeded(...) only
once per sync and base the deferKeep decision on the post-call retry budget:
invoke scheduleTransientRecoveryRetryIfNeeded(forHostedId:hostedId,
entry:&entry, hostedView:hostedView, reason:"missingAnchorOrWindow") a single
time and store its result and then compute shouldPreserveVisibleOnTransient as
!hostedView.isHidden && entry.transientRecoveryRetriesRemaining > 0 (not the raw
helper boolean which can be true when it just hit 0). Use the stored result to
avoid calling scheduleTransientRecoveryRetryIfNeeded again after setting
hostedView.isHidden and only schedule/re-arm retries once per sync attempt.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 91468892-8b0c-4999-a910-a03ac0e5754d

📥 Commits

Reviewing files that changed from the base of the PR and between 142c62c and 836360d.

📒 Files selected for processing (2)
  • Sources/TerminalWindowPortal.swift
  • cmuxTests/TerminalAndGhosttyTests.swift

@lawrencecchen

Copy link
Copy Markdown
Contributor

Checked the CodeRabbit note on TerminalWindowPortal.swift. The retry-budget concern is real for the CMUX_ISSUE_483_PORTAL_RECOVERY branch, but that compile condition is not enabled in this target today. GhosttyTabs.xcodeproj only sets DEBUG in SWIFT_ACTIVE_COMPILATION_CONDITIONS, so the shipped and dogfood builds for this PR take the production path we already tested. I kept this PR scoped to the active restore bug and did not change the compile-gated recovery experiment without coverage.

This branch was successfully deployed

1 active deployment
Preview — 8c0aee31 Deployed Mar 24, 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.

Ghost terminal column remains rendered between sidebar and active terminal

2 participants