Skip to content

Fix panel drag hover terminal flicker - #1213

Merged
austinywang merged 4 commits into
mainfrom
issue-1209-drag-hover-flicker
Mar 12, 2026
Merged

austinywang merged 4 commits into
mainfrom
issue-1209-drag-hover-flicker

Conversation

@austinywang

@austinywang austinywang commented Mar 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add a regression test that keeps the terminal pixel size stable while a drop-hover overlay is active
  • defer Ghostty surface resize updates while a panel drop-hover overlay is showing
  • let the deferred resize land immediately after the hover overlay clears

Closes #1209.


Summary by cubic

Stops terminal flicker during panel drag-hover by deferring terminal surface resize and rendering the hover overlay outside the hosted terminal so layout stays stable. Closes #1209.

  • Bug Fixes
    • Apply the pending terminal resize immediately after the hover overlay clears.
    • Add regression tests for terminal pixel stability during hover and for mounting the hover overlay in the parent container.

Written for commit 379150a. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Exposed a debug API to report the terminal's current pixel size for diagnostics.
  • Bug Fixes

    • Terminal resizing now correctly defers during drag-and-drop, preventing visual glitches.
    • Drop-hover overlays reliably attach to the parent container, maintain correct z-order, and animate show/hide for smoother drag-and-drop visuals.
  • Tests

    • Added coverage verifying drop-hover overlays attach to the parent container and hide correctly after hover.

@vercel

vercel Bot commented Mar 12, 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 12, 2026 1:31am

@coderabbitai

coderabbitai Bot commented Mar 12, 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: a9ed4012-e6ed-4e28-b4ca-0983ea8a3828

📥 Commits

Reviewing files that changed from the base of the PR and between 90ca636 and 379150a.

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

📝 Walkthrough

Walkthrough

Drop-hover and drag handling were updated so drop-zone overlays are attached to parent containers (not hosted terminal views), overlay lifecycle and logging were extended, and terminal surface resize deferral and a pixel-size debug helper were added to avoid surface re-layout during active drags/overlays.

Changes

Cohort / File(s) Summary
Terminal surface helpers & resize deferral
Sources/GhosttyTerminalView.swift
Added TerminalSurface.debugCurrentPixelSize() -> (width: UInt32, height: UInt32) and introduced/used deferral-reason logic for surface resize control.
NSView resize deferral & drag context
Sources/.../GhosttyNSView.swift
Added activeSurfaceResizeDeferralReason() to expose deferral cause (e.g., "tabDrag" or "dropOverlay"), and updated resize deferral logging/control flow to use it.
Drop-zone overlay lifecycle & drag logging
Sources/.../GhosttySurfaceScrollView.swift, Sources/.../GhosttySurfaceScrollView+Overlay*.swift
Added hasActiveDropZoneOverlay/hasActiveDropZoneOverlayPredicate, drop-zone overlay attach/remove/apply frame helpers, container-aware frame calculations, viewDidMoveToSuperview handling, extended debug state (log signatures, lastDragGeometryLogSignature, lastDropZoneOverlayLogSignature), and functions to ensure overlay attachment without affecting surface layout.
Tests: overlay attachment behavior
cmuxTests/CmuxWebViewKeyEquivalentTests.swift
Added testDropHoverOverlayAttachesToParentContainerInsteadOfHostedTerminalView() asserting the drop-hover overlay attaches to the parent container, has expected frame/origin/visibility, and hides after clearing.
Logging & debug instrumentation
Sources/.../Ghostty*.swift (multiple)
Expanded log signatures and debug helpers to include overlay container class, scroll/surface origin, and overlay attachment state for easier tracing of drag/overlay interactions.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant DragSource as Drag Source
participant GhosttyNSView as GhosttyNSView
participant ScrollView as GhosttySurfaceScrollView
participant Container as Parent Container
participant Terminal as TerminalSurface
DragSource->>GhosttyNSView: start drag / hover
GhosttyNSView->>ScrollView: notify active drag context
ScrollView->>Container: attachDropZoneOverlayIfNeeded()
Container-->>ScrollView: overlay attached (non-layout)
ScrollView->>Terminal: hasActiveDropZoneOverlay? -> true
Terminal-->>ScrollView: defer surface resize (deferral reason)
Note right of DragSource: while hovering, overlay visible; terminal not resized
DragSource->>GhosttyNSView: end drag / drop canceled
GhosttyNSView->>ScrollView: clear overlay
ScrollView->>Container: hide/remove overlay
ScrollView->>Terminal: clear deferral -> allow resize/reflow

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Poem

I’m a rabbit in the code, I hop with care, 🐰
I place the overlay so nothing shakes or tears.
Resize waits while hover lights the way,
Then settles back when you drop away.
Smooth panels, no flicker — happy devs today. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the primary fix: preventing terminal flicker during panel drag hover by deferring resize operations.
Description check ✅ Passed The description covers summary, closes the linked issue, and mentions testing/regression test, but lacks explicit demo video, testing methodology details, and missing checklist items.
Linked Issues check ✅ Passed Code changes directly address issue #1209: defer terminal resize during drop-hover overlay, apply deferred resize after overlay clears, and add regression test for stability.
Out of Scope Changes check ✅ Passed All changes are scoped to the stated objectives: surface resize deferral logic, drop overlay state predicate, debug helper, and a targeted regression test.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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-1209-drag-hover-flicker

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

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

🤖 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/GhosttyTerminalView.swift`:
- Around line 6730-6732: The predicate hasActiveDropZoneOverlay returns false as
soon as activeDropZone is cleared, but setDropZoneOverlay(zone: nil) only hides
the view and clears state after the hide animation completes, so keep the
predicate true until the hide completes: either delay clearing activeDropZone
until the completion handler in setDropZoneOverlay(zone: nil), or introduce a
transient flag (e.g. isHidingDropZoneOverlay) set when the hide animation starts
and cleared in its completion, and include that flag in hasActiveDropZoneOverlay
alongside activeDropZone and pendingDropZone; this ensures updateSurfaceSize()
retries won't think the overlay is gone while the hide animation is still
visible.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cfe74648-96c1-4193-878e-887cf2f83fe1

📥 Commits

Reviewing files that changed from the base of the PR and between 6849b83 and 90ca636.

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

Comment on lines +6730 to +6732
fileprivate var hasActiveDropZoneOverlay: Bool {
activeDropZone != nil || pendingDropZone != nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep the overlay predicate true until the hide animation finishes.

activeDropZone is cleared at the start of setDropZoneOverlay(zone: nil), but the overlay view remains visible until the completion handler runs. If a deferred updateSurfaceSize() retry lands during that gap, Ghostty can resize under a still-visible hover overlay and reintroduce the flicker this patch is trying to remove.

💡 Suggested fix
 fileprivate var hasActiveDropZoneOverlay: Bool {
-        activeDropZone != nil || pendingDropZone != nil
+        activeDropZone != nil || pendingDropZone != nil || !dropZoneOverlayView.isHidden
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 6730 - 6732, The predicate
hasActiveDropZoneOverlay returns false as soon as activeDropZone is cleared, but
setDropZoneOverlay(zone: nil) only hides the view and clears state after the
hide animation completes, so keep the predicate true until the hide completes:
either delay clearing activeDropZone until the completion handler in
setDropZoneOverlay(zone: nil), or introduce a transient flag (e.g.
isHidingDropZoneOverlay) set when the hide animation starts and cleared in its
completion, and include that flag in hasActiveDropZoneOverlay alongside
activeDropZone and pendingDropZone; this ensures updateSurfaceSize() retries
won't think the overlay is gone while the hide animation is still visible.

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

2 issues 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/GhosttyTerminalView.swift">

<violation number="1" location="Sources/GhosttyTerminalView.swift:3673">
P2: The new `dropOverlay` deferral path can spin a continuous main-queue retry loop while the overlay is active, causing avoidable CPU churn during drag hover.</violation>

<violation number="2" location="Sources/GhosttyTerminalView.swift:6731">
P2: Keep the drop-overlay deferral predicate true until the overlay view is actually hidden. As written, this can flip to false before the hide animation completes, allowing a surface resize under a still-visible overlay and causing flicker.</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
Comment thread Sources/GhosttyTerminalView.swift
@greptile-apps

greptile-apps Bot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a terminal content flicker/reflow bug (#1209) that occurred when dragging a panel over a Ghostty terminal: the drop-zone hover overlay caused layout changes that triggered unwanted Ghostty surface resizes mid-drag.

Key changes:

  • Adds hasActiveDropZoneOverlay (fileprivate) to GhosttySurfaceScrollView, returning true when either activeDropZone or pendingDropZone is non-nil.
  • Introduces activeSurfaceResizeDeferralReason() in GhosttyNSView, centralising the existing tab-drag deferral check alongside the new drop-overlay check. The drop overlay takes priority in the if/else chain.
  • updateSurfaceSize now calls the unified helper instead of the inline shouldDeferSurfaceResizeForActiveDrag() guard, preserving pendingSurfaceSize and scheduling a single-dispatch retry chain. When the overlay is cleared, the next retry fires, finds no deferral reason, and applies the buffered size.
  • Adds debugCurrentPixelSize() (#if DEBUG) on TerminalSurface to expose lastPixelWidth/Height to tests.
  • Adds a regression test that verifies pixel dimensions remain stable during an active drop overlay and then advance to the new dimensions once the overlay is dismissed.

Confidence Score: 5/5

  • This PR is safe to merge — the change is narrowly scoped, well-tested, and does not alter any existing deferral logic for the tab-drag path.
  • The diff is small (two files, ~80 net lines). The new hasActiveDropZoneOverlay property is fileprivate, correctly scoped to the single file. The activeSurfaceResizeDeferralReason() refactor preserves all existing tab-drag behaviour and only adds the drop-overlay branch. The retry chain and pendingSurfaceSize buffering were already in place; this PR merely extends the condition that activates them. A dedicated regression test covers the new code path end-to-end. No memory, threading, or access-control issues were found.
  • No files require special attention.

Important Files Changed

Filename Overview
Sources/GhosttyTerminalView.swift Adds hasActiveDropZoneOverlay property to GhosttySurfaceScrollView and refactors updateSurfaceSize to defer Ghostty surface resizes while a drop-zone overlay is active via the new activeSurfaceResizeDeferralReason() helper, which now checks both drop overlay and tab drag. A debugCurrentPixelSize() test helper is added inside #if DEBUG. Logic is sound and edge cases (nil terminalSurface, access control, retry chain) are handled correctly.
cmuxTests/CmuxWebViewKeyEquivalentTests.swift Adds testDropHoverOverlayDefersTerminalResizeUntilHoverEnds regression test that creates a real TerminalSurface, activates a drop-zone overlay, resizes the frame, and asserts the pixel dimensions stay frozen until the overlay is cleared. Test structure mirrors existing overlay tests in the same file.

Sequence Diagram

sequenceDiagram
    participant DragSession
    participant GhosttySurfaceScrollView
    participant GhosttyNSView
    participant TerminalSurface

    DragSession->>GhosttySurfaceScrollView: setDropZoneOverlay(zone: .left)
    Note over GhosttySurfaceScrollView: activeDropZone = .left<br/>hasActiveDropZoneOverlay = true

    GhosttySurfaceScrollView->>GhosttyNSView: layout() [frame resize]
    GhosttyNSView->>GhosttyNSView: updateSurfaceSize()
    GhosttyNSView->>GhosttyNSView: activeSurfaceResizeDeferralReason()
    GhosttyNSView-->>GhosttyNSView: "dropOverlay" (deferred)
    GhosttyNSView->>GhosttyNSView: scheduleDeferredSurfaceSizeRetryIfNeeded()
    Note over GhosttyNSView: pendingSurfaceSize = newSize<br/>resize blocked

    DragSession->>GhosttySurfaceScrollView: setDropZoneOverlay(zone: nil)
    Note over GhosttySurfaceScrollView: activeDropZone = nil<br/>hasActiveDropZoneOverlay = false

    GhosttyNSView->>GhosttyNSView: deferred retry fires (DispatchQueue.main.async)
    GhosttyNSView->>GhosttyNSView: updateSurfaceSize()
    GhosttyNSView->>GhosttyNSView: activeSurfaceResizeDeferralReason() → nil
    GhosttyNSView->>TerminalSurface: ghostty_surface_set_size(newWidth, newHeight)
    Note over TerminalSurface: lastPixelWidth/Height updated
Loading

Last reviewed commit: 90ca636

This branch was successfully deployed

1 active deployment
Preview — 379150a6 Deployed Mar 12, 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.

Panel drag hover causes terminal content to flicker and shift

1 participant