Skip to content

Fix terminal portal tab drop routing - #3299

Merged
lawrencecchen merged 9 commits into
mainfrom
task-blue-drop-target-indicators
Apr 30, 2026
Merged

lawrencecchen merged 9 commits into
mainfrom
task-blue-drop-target-indicators

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Apr 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Route Bonsplit tab drops over portal-hosted terminal panes through pane-local drop targets.
  • Keep Bonsplit tab-strip drag and reorder events passing through terminal/browser portal overlays.
  • Keep the Bonsplit tab drag E2E using XCTest native drag gestures on CI.

Tests

  • Local: xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug -destination "platform=macOS" -derivedDataPath /tmp/cmux-dropfix-unit -only-testing:cmuxTests/WindowTerminalHostViewTests/testHostViewPassesThroughUnderlyingTabStripDuringMouseDrag -only-testing:cmuxTests/WindowTerminalHostViewTests/testTabStripPassThroughTreatsAppKitDragRoutingAsPointerEvents -only-testing:cmuxTests/WindowTerminalHostViewTests/testTerminalPaneDropTargetDefersToUnderlyingTabStrip test
  • Local: xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -destination "platform=macOS" -derivedDataPath /tmp/cmux-dropfix-ui-build -only-testing:cmuxUITests/BonsplitTabDragUITests/testMinimalModeKeepsTabReorderWorking build-for-testing
  • CI: https://github.com/manaflow-ai/cmux/actions/runs/25104895502
  • CI artifact issue: https://github.com/manaflow-ai/cmux-dev-artifacts/issues/1126

Dogfood

  • Tagged build: dropfix5

Summary by cubic

Fixes Bonsplit tab drop routing over portal-hosted terminal panes by adding a pane-local drop target and unifying minimal tab-strip pass-through across terminal and browser portals. Switches the Bonsplit tab drag E2E to XCTest native drag for CI stability.

  • Bug Fixes

    • Added TerminalPaneDropTargetView to capture internal tab drags, render zones, and delegate moves/splits via Workspace.performPortalPaneDrop.
    • Terminal and browser portals (and drop targets) defer to the minimal tab strip; AppKit drag‑routing events count as pointer input; terminal host returns the drop overlay when hit.
    • External drags (.fileURL/.URL/.png/.tiff/.html) bypass portal overlays; WKWebView keeps these types registered and receives the full drag lifecycle.
    • Normalized edge‑adjacent drops and same‑pane center no‑ops; E2E uses XCTest drag gestures instead of raw CGEvents.
  • Refactors

    • Extracted pass‑through helpers to BonsplitTabBarPassThrough; moved terminal helpers to GhosttyTerminalViewSupport; split portal debug utils to TerminalWindowPortalDebug.
    • Centralized pane‑drop logic in WorkspacePortalPaneDrop and exposed Workspace.handleExternalTabDrop; GhosttyTerminalView now hosts the drop‑target overlay and wires its context.
    • Split drag‑routing tests into PortalTabDragRoutingTests, BrowserPaneDropRoutingTests, and CmuxWebViewDragRoutingTests.

Written for commit 416a2f2. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved pointer event routing and drag-and-drop handling between terminal panes and browser windows.
    • Enhanced tab bar interaction detection to more accurately identify when drag operations should target the tab bar.
  • Refactor

    • Reorganized pass-through decision logic for improved maintainability and consistency.
  • Tests

    • Added comprehensive test coverage for drag-and-drop routing behavior and event sequencing.
    • Expanded external pasteboard handling tests for browser drag operations.

@vercel

vercel Bot commented Apr 29, 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 Apr 30, 2026 1:57am
cmux-staging Ready Ready Preview, Comment Apr 30, 2026 1:57am

@coderabbitai

coderabbitai Bot commented Apr 29, 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

The changes refactor drag-and-drop handling for portal-hosted panes by introducing dedicated TerminalPaneDropTargetView and BrowserPaneDropTargetView classes that capture and route pane-to-pane tab transfers. Portal hosts now short-circuit hit-testing and drag handling when pointer events target pane tab bars, delegating pass-through decisions to a new BonsplitTabBarPassThrough utility. Workspace gains portal-pane drop zone resolution and execution methods to map drag proposals to drop operations.

Changes

Cohort / File(s) Summary
Portal Drop Target Views
Sources/TerminalPaneDropTargetView.swift, Sources/BrowserWindowPortal.swift
Introduce TerminalPaneDropTargetView with context/transfer handling and drop-zone computation (+276 lines). Replace BonsplitTabBarPassThrough integration in BrowserWindowPortal with new shouldDeferToPaneTabBar(at:) method that short-circuits hit-testing to return nil when tab bar is targeted (-91 net lines).
Workspace Drop Integration
Sources/Workspace.swift, Sources/WorkspacePortalPaneDrop.swift
Expose SurfaceKind and handleExternalTabDrop(_:) as module-visible (remove private). Add portalPaneDropZone and performPortalPaneDrop extensions to compute zone rewrites and invoke external drop requests (+60 lines).
Portal Host Routing
Sources/TerminalWindowPortal.swift, Sources/GhosttyTerminalView.swift
Update TerminalWindowPortal.performHitTest to use currentEvent?.type predicate and detect TerminalPaneDropTargetView hits via super.hitTest. Integrate TerminalPaneDropTargetView into GhosttySurfaceScrollView with frame-sync and context update methods; remove passthrough visual effect view and focus gating (-3 net lines).
Pass-Through Utilities
Sources/BonsplitTabBarPassThrough.swift
New utility providing event-type predicates, titlebar band calculation, and registry/view-tree scanning logic to determine tab-bar pass-through eligibility (+115 lines).
Support & Debug Helpers
Sources/GhosttyTerminalViewSupport.swift, Sources/TerminalWindowPortalDebug.swift
Add GhosttyPassthroughVisualEffectView (non-interactive) and shouldAllowEnsureFocusWindowActivation focus-gating helper (+31 lines). Add debug token/frame formatting utilities (+19 lines).
Build Configuration
GhosttyTabs.xcodeproj/project.pbxproj
Register five new Swift source files and one test file (PortalTabDragRoutingTests.swift) in app and cmuxTests targets (+24 lines).
Test Coverage
cmuxTests/PortalTabDragRoutingTests.swift, cmuxTests/BrowserConfigTests.swift, cmuxTests/BrowserPanelTests.swift
Add new PortalTabDragRoutingTests verifying hit-test pass-through and deferral logic (+173 lines). Expand browser config tests with swizzled WebKit drag lifecycle instrumentation and broader pasteboard-type matrices (+228 lines). Broaden browser panel drag-routing assertions to cover multiple external payload types (+38 lines).

Sequence Diagram(s)

sequenceDiagram
    actor User as User (Drag)
    participant Host as Portal Host
    participant DDTarget as DropTargetView
    participant Registry as TabBar Registry
    participant TabBarView as TabBar View
    participant Workspace as Workspace
    
    User->>Host: Pointer Event (drag)
    activate Host
    Host->>Host: hitTest(_:) called
    Host->>DDTarget: Check shouldDeferToPaneTabBar(at:)
    activate DDTarget
    DDTarget->>Registry: Check registry hit
    alt Registry Hit
        Registry-->>DDTarget: true
    else No Registry Hit
        DDTarget->>TabBarView: Scan for TabBarBackground below host
        TabBarView-->>DDTarget: Found/Not Found
    end
    DDTarget-->>Host: Return defer decision
    deactivate DDTarget
    
    alt Should Defer to Tab Bar
        Host->>Host: Return nil (pass through)
        Note over Host: Tab bar receives event
    else Should Capture in Pane
        Host->>DDTarget: Begin drag capture
        activate DDTarget
        DDTarget->>DDTarget: Decode transfer from pasteboard
        DDTarget->>Workspace: Request drop zone
        Workspace-->>DDTarget: Resolved zone (center/left/right/etc)
        DDTarget->>DDTarget: Update overlay on surface
        User-->>DDTarget: Drop
        DDTarget->>Workspace: performPortalPaneDrop(zone)
        Workspace-->>DDTarget: Execute drop
        deactivate DDTarget
    end
    deactivate Host
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 A dropsy draggy tale we tell,
Where panes find homes through zones to dwell,
Defer or capture, scan or glide—
The portal paths now open wide!
~✨ 🎯✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.15% 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 and concisely summarizes the main change: fixing tab drop routing for terminal portals.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description comprehensively covers the summary of changes, testing approach with specific command examples, and a detailed checklist, matching the template structure well.

✏️ 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 task-blue-drop-target-indicators

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.

@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

🧹 Nitpick comments (3)
Sources/Workspace.swift (1)

12452-12483: Consider normalizing zone inside performPortalPaneDrop too.

Right now this method trusts callers to pass a pre-normalized zone. Re-normalizing here would make execution behavior self-contained and harder to misuse from future call sites.

♻️ Optional hardening diff
 `@discardableResult`
 func performPortalPaneDrop(
     tabId: UUID,
     sourcePaneId: UUID,
     targetPane paneId: PaneID,
     zone: DropZone
 ) -> Bool {
     let sourcePane = PaneID(id: sourcePaneId)
-    if zone == .center, sourcePane == paneId {
+    let normalizedZone = portalPaneDropZone(
+        tabId: tabId,
+        sourcePaneId: sourcePaneId,
+        targetPane: paneId,
+        proposedZone: zone
+    )
+    if normalizedZone == .center, sourcePane == paneId {
         return true
     }

     let destination: BonsplitController.ExternalTabDropRequest.Destination
-    switch zone {
+    switch normalizedZone {
     case .center:
         destination = .insert(targetPane: paneId, targetIndex: nil)
     case .left:
         destination = .split(targetPane: paneId, orientation: .horizontal, insertFirst: true)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` around lines 12452 - 12483, Normalize the incoming
DropZone inside performPortalPaneDrop rather than trusting callers: at the top
of the function compute a normalizedZone (e.g., let normalizedZone =
zone.normalized() or let normalizedZone = DropZone.normalized(zone) depending on
the existing API) and then use normalizedZone for the early return check and in
the switch (replace references to zone with normalizedZone); keep the rest of
the function (sourcePane creation, destination construction, and
handleExternalTabDrop call) unchanged.
Sources/BrowserWindowPortal.swift (1)

1724-1735: Consider reusing the shared tab-bar pass-through helper here.

This method currently re-scans window.contentView recursively each call and can drift from BonsplitTabBarPassThrough.shouldPassThroughToPaneTabBar(...) behavior. Reusing the shared helper (via the portal host ancestor) would keep routing semantics consistent and cheaper on drag-hover hot paths.

♻️ Suggested refactor
 func shouldDeferToPaneTabBar(at point: NSPoint) -> Bool {
     guard let window else { return false }
     let windowPoint = convert(point, to: nil)
-    if BonsplitTabBarHitRegionRegistry.containsWindowPoint(windowPoint, in: window) {
-        return true
-    }
-    guard let contentView = window.contentView else { return false }
-    return BonsplitTabBarPassThrough.hasBonsplitTabBarBackground(
-        at: windowPoint,
-        in: contentView
-    )
+    if let portalHost = slotView?.superview {
+        return BonsplitTabBarPassThrough
+            .shouldPassThroughToPaneTabBar(windowPoint: windowPoint, below: portalHost)
+            .result
+    }
+    if BonsplitTabBarHitRegionRegistry.containsWindowPoint(windowPoint, in: window) {
+        return true
+    }
+    guard let contentView = window.contentView else { return false }
+    return BonsplitTabBarPassThrough.hasBonsplitTabBarBackground(
+        at: windowPoint,
+        in: contentView
+    )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/BrowserWindowPortal.swift` around lines 1724 - 1735, The
shouldDeferToPaneTabBar(at:) implementation re-scans window.contentView and can
diverge from the shared routing logic; replace the manual contentView recursion
with a call into the shared helper (e.g.,
BonsplitTabBarPassThrough.shouldPassThroughToPaneTabBar(...) or the portal host
ancestor helper) so routing semantics and performance match the hot-path helper.
Concretely, in shouldDeferToPaneTabBar(at:) keep the initial window checks,
obtain the portal host ancestor (or content host) for the window, and call the
shared BonsplitTabBarPassThrough.shouldPassThroughToPaneTabBar(windowPoint, in:
portalHost) (or equivalent portal-host method) instead of calling
BonsplitTabBarPassThrough.hasBonsplitTabBarBackground(...); add guards for nil
portal host and return false if missing.
cmuxTests/TerminalAndGhosttyTests.swift (1)

2293-2298: Consider adding one negative event assertion to prevent over-broad pass-through logic.

Right now this test only proves selected events are included; it doesn’t guard against accidentally treating keyboard events as pointer pass-through.

Suggested test hardening
 func testTabStripPassThroughTreatsAppKitDragRoutingAsPointerEvents() {
     XCTAssertTrue(BonsplitTabBarPassThrough.isPassThroughPointerEvent(.appKitDefined))
     XCTAssertTrue(BonsplitTabBarPassThrough.isPassThroughPointerEvent(.applicationDefined))
     XCTAssertTrue(BonsplitTabBarPassThrough.isPassThroughPointerEvent(.systemDefined))
     XCTAssertTrue(BonsplitTabBarPassThrough.isPassThroughPointerEvent(.periodic))
+    XCTAssertFalse(BonsplitTabBarPassThrough.isPassThroughPointerEvent(.keyDown))
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/TerminalAndGhosttyTests.swift` around lines 2293 - 2298, The test
testTabStripPassThroughTreatsAppKitDragRoutingAsPointerEvents currently asserts
several pointer-related NSEvent types as pass-through but lacks a negative case;
add an assertion that a keyboard event type (e.g., .keyDown or .keyUp) is NOT
treated as pass-through by BonsplitTabBarPassThrough.isPassThroughPointerEvent
to prevent over-broad pass-through logic. Locate the test function and append
one XCTAssertFalse call using
BonsplitTabBarPassThrough.isPassThroughPointerEvent(.keyDown) (or .keyUp) so
keyboard events are explicitly rejected.
🤖 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 13308-13312: The current code calls
hostedView.setPaneDropContext(...) even when claimPortalHost fails, which allows
stale HostContainerView to overwrite dropContext; modify the logic so you only
call hostedView.setPaneDropContext(TerminalPaneDropContext(...)) when
hostOwnsPortalNow is true (i.e., after a successful claimPortalHost check).
Locate the claimPortalHost/hostOwnsPortalNow branch around the
TerminalPaneDropContext creation (references: hostedView.setPaneDropContext,
TerminalPaneDropContext, claimPortalHost, hostOwnsPortalNow, terminalSurface,
paneId) and wrap or gate the setPaneDropContext call so it runs only when
hostOwnsPortalNow is true.

---

Nitpick comments:
In `@cmuxTests/TerminalAndGhosttyTests.swift`:
- Around line 2293-2298: The test
testTabStripPassThroughTreatsAppKitDragRoutingAsPointerEvents currently asserts
several pointer-related NSEvent types as pass-through but lacks a negative case;
add an assertion that a keyboard event type (e.g., .keyDown or .keyUp) is NOT
treated as pass-through by BonsplitTabBarPassThrough.isPassThroughPointerEvent
to prevent over-broad pass-through logic. Locate the test function and append
one XCTAssertFalse call using
BonsplitTabBarPassThrough.isPassThroughPointerEvent(.keyDown) (or .keyUp) so
keyboard events are explicitly rejected.

In `@Sources/BrowserWindowPortal.swift`:
- Around line 1724-1735: The shouldDeferToPaneTabBar(at:) implementation
re-scans window.contentView and can diverge from the shared routing logic;
replace the manual contentView recursion with a call into the shared helper
(e.g., BonsplitTabBarPassThrough.shouldPassThroughToPaneTabBar(...) or the
portal host ancestor helper) so routing semantics and performance match the
hot-path helper. Concretely, in shouldDeferToPaneTabBar(at:) keep the initial
window checks, obtain the portal host ancestor (or content host) for the window,
and call the shared
BonsplitTabBarPassThrough.shouldPassThroughToPaneTabBar(windowPoint, in:
portalHost) (or equivalent portal-host method) instead of calling
BonsplitTabBarPassThrough.hasBonsplitTabBarBackground(...); add guards for nil
portal host and return false if missing.

In `@Sources/Workspace.swift`:
- Around line 12452-12483: Normalize the incoming DropZone inside
performPortalPaneDrop rather than trusting callers: at the top of the function
compute a normalizedZone (e.g., let normalizedZone = zone.normalized() or let
normalizedZone = DropZone.normalized(zone) depending on the existing API) and
then use normalizedZone for the early return check and in the switch (replace
references to zone with normalizedZone); keep the rest of the function
(sourcePane creation, destination construction, and handleExternalTabDrop call)
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3922fc56-ccd3-45f6-b0cf-ea0303c20337

📥 Commits

Reviewing files that changed from the base of the PR and between e34cf14 and 5a74203.

📒 Files selected for processing (6)
  • Sources/BrowserWindowPortal.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/TerminalWindowPortal.swift
  • Sources/Workspace.swift
  • cmuxTests/TerminalAndGhosttyTests.swift
  • cmuxUITests/BonsplitTabDragUITests.swift

Comment thread Sources/GhosttyTerminalView.swift Outdated
@greptile-apps

greptile-apps Bot commented Apr 29, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes tab-drop routing for terminal panes hosted behind portal overlays. It introduces TerminalPaneDropTargetView (mirroring the existing BrowserPaneDropTargetView) that intercepts Bonsplit tab drops onto portal-hosted terminal panes, routes the drop through Workspace.performPortalPaneDrop, and defers to the Bonsplit tab strip when the cursor is over it. WindowTerminalHostView.performHitTest is refactored to delegate pointer-event classification to BonsplitTabBarPassThrough.isPassThroughPointerEvent and to selectively return the TerminalPaneDropTargetView when a pane-local drop should be honoured. The UI test is simplified from raw CGEvent injection to XCTest's native press(forDuration:thenDragTo:) drag gesture.

Confidence Score: 4/5

Safe to merge; all findings are non-blocking style suggestions.

No P0 or P1 issues found. The new TerminalPaneDropTargetView closely mirrors the proven BrowserPaneDropTargetView pattern, zone/split mappings are consistent, and three targeted unit tests cover the critical pass-through paths. Only P2 style nits around code duplication and a redundant branch in bringPaneDropTargetToFrontIfNeeded.

Sources/GhosttyTerminalView.swift — shouldDeferToPaneTabBar duplication and bringPaneDropTargetToFrontIfNeeded branch simplification.

Important Files Changed

Filename Overview
Sources/GhosttyTerminalView.swift Adds TerminalPaneDropTargetView (+278 lines) that parallels BrowserPaneDropTargetView; view is embedded into GhosttySurfaceScrollView and kept at the front of the z-order. Minor duplication in shouldDeferToPaneTabBar and bringPaneDropTargetToFrontIfNeeded.
Sources/BrowserWindowPortal.swift Adds dragged mouse events (.leftMouseDragged etc.) and AppKit routing events to BonsplitTabBarPassThrough; adds shouldDeferToPaneTabBar guard in BrowserPaneDropTargetView.hitTest and updateDragState; moves location conversion before the tab-bar defer check.
Sources/TerminalWindowPortal.swift Refactors isPointerEvent detection to reuse BonsplitTabBarPassThrough.isPassThroughPointerEvent; adds intercept for TerminalPaneDropTargetView during shouldPassThrough=true path so pane-local drops are routed correctly.
Sources/Workspace.swift Adds portalPaneDropZone (adjacency-aware zone resolution) and performPortalPaneDrop (translates DropZone to ExternalTabDropRequest) for terminal pane drops through the portal.
cmuxTests/TerminalAndGhosttyTests.swift Adds three unit tests covering drag pass-through during leftMouseDragged, AppKit routing event classification, and TerminalPaneDropTargetView tab-strip deference; generalises makeMouseDownEvent into makeMouseEvent.
cmuxUITests/BonsplitTabDragUITests.swift Replaces fragile raw CGEvent drag implementation with XCTest native press(forDuration:thenDragTo:); removes AppKit import and the dropIndicator wait that was unreliable on CI.

Sequence Diagram

sequenceDiagram
    participant AppKit
    participant WTHV as WindowTerminalHostView
    participant TPDTV as TerminalPaneDropTargetView
    participant TBBPT as BonsplitTabBarPassThrough
    participant WS as Workspace

    AppKit->>WTHV: performHitTest(point, event)
    WTHV->>TBBPT: isPassThroughPointerEvent(eventType)
    TBBPT-->>WTHV: true (drag event)
    WTHV->>WTHV: shouldPassThroughToPaneTabBar?
    alt Point over tab strip
        WTHV-->>AppKit: nil (pass-through to tab strip)
    else Point over terminal pane
        WTHV->>WTHV: super.hitTest(point)
        WTHV-->>WTHV: hitView = TerminalPaneDropTargetView
        WTHV-->>AppKit: TerminalPaneDropTargetView
        AppKit->>TPDTV: draggingUpdated(sender)
        TPDTV->>TPDTV: shouldDeferToPaneTabBar?
        alt Cursor moved over tab strip
            TPDTV-->>AppKit: [] (no-op)
        else Cursor over pane body
            TPDTV->>WS: portalPaneDropZone(tabId, sourcePaneId, targetPane, proposedZone)
            WS-->>TPDTV: resolvedZone
            TPDTV-->>AppKit: .move
            AppKit->>TPDTV: performDragOperation(sender)
            TPDTV->>WS: performPortalPaneDrop(tabId, sourcePaneId, targetPane, zone)
            WS-->>TPDTV: true/false
            TPDTV-->>AppKit: handled
        end
    end
Loading

Comments Outside Diff (2)

  1. Sources/GhosttyTerminalView.swift, line 300-311 (link)

    P2 shouldDeferToPaneTabBar duplicated from BrowserPaneDropTargetView

    TerminalPaneDropTargetView.shouldDeferToPaneTabBar(at:) is a byte-for-byte copy of the same method in BrowserPaneDropTargetView (added in the same PR). If the deferred-hit-test logic ever needs to change (e.g. a new registry call or an extra nil guard), it would need to be updated in both classes. Extracting to a shared free function or a protocol extension on NSView — matching the pattern already used for BonsplitTabBarPassThrough — would keep the two drop-target classes in sync automatically.

  2. Sources/GhosttyTerminalView.swift, line 10403-10409 (link)

    P2 Both branches of bringPaneDropTargetToFrontIfNeeded execute the same call

    The if and else if branches are logically distinct, but both call addSubview(paneDropTargetView, positioned: .above, relativeTo: nil). This is correct — addSubview handles both "not yet a child" and "reorder to top" — but the code can be collapsed to a single condition to make the intent clearer and avoid the visual duplication.

    private func bringPaneDropTargetToFrontIfNeeded() {
        guard paneDropTargetView.superview === self, subviews.last === paneDropTargetView else {
            addSubview(paneDropTargetView, positioned: .above, relativeTo: nil)
            return
        }
    }

    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!

Reviews (1): Last reviewed commit: "Fix terminal portal tab drop routing" | Re-trigger Greptile

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

♻️ Duplicate comments (1)
Sources/GhosttyTerminalView.swift (1)

13005-13009: ⚠️ Potential issue | 🟠 Major

Gate pane drop-context updates to the owning host.

setPaneDropContext(...) at Line 13005 currently runs even when hostOwnsPortalNow is false. During host churn, a stale host can overwrite routing context and misroute drops.

🔧 Suggested fix
-        hostedView.setPaneDropContext(TerminalPaneDropContext(
-            workspaceId: terminalSurface.tabId,
-            panelId: terminalSurface.id,
-            paneId: paneId
-        ))
+        if hostOwnsPortalNow {
+            hostedView.setPaneDropContext(TerminalPaneDropContext(
+                workspaceId: terminalSurface.tabId,
+                panelId: terminalSurface.id,
+                paneId: paneId
+            ))
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 13005 - 13009, Only set the
pane drop context when the current host actually owns the portal: wrap the call
to hostedView.setPaneDropContext(...) with a guard that ensures
hostOwnsPortalNow is true (and optionally verify the hostedView is the same host
if you have a host identifier), and only then construct
TerminalPaneDropContext(workspaceId: terminalSurface.tabId, panelId:
terminalSurface.id, paneId: paneId) and call setPaneDropContext; do not call
setPaneDropContext when hostOwnsPortalNow is false to avoid stale-host
overwrites.
🧹 Nitpick comments (1)
Sources/BrowserWindowPortal.swift (1)

1611-1622: Prefer delegating tab-bar deferral to the shared helper.

This local implementation duplicates logic and bypasses BonsplitTabBarPassThrough.shouldPassThroughToPaneTabBar(...)’s optimized path. Reusing the helper will keep browser/terminal behavior in sync and reduce drift risk.

♻️ Suggested refactor
     func shouldDeferToPaneTabBar(at point: NSPoint) -> Bool {
-        guard let window else { return false }
         let windowPoint = convert(point, to: nil)
-        if BonsplitTabBarHitRegionRegistry.containsWindowPoint(windowPoint, in: window) {
-            return true
-        }
-        guard let contentView = window.contentView else { return false }
-        return BonsplitTabBarPassThrough.hasBonsplitTabBarBackground(
-            at: windowPoint,
-            in: contentView
-        )
+        return BonsplitTabBarPassThrough
+            .shouldPassThroughToPaneTabBar(windowPoint: windowPoint, below: self)
+            .result
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/BrowserWindowPortal.swift` around lines 1611 - 1622, The method
shouldDeferToPaneTabBar(at:) duplicates logic and should delegate to the shared
helper; replace its body so it calls
BonsplitTabBarPassThrough.shouldPassThroughToPaneTabBar(...) (passing the point
converted to window coordinates and the window) instead of manually calling
BonsplitTabBarHitRegionRegistry.containsWindowPoint and
BonsplitTabBarPassThrough.hasBonsplitTabBarBackground; keep the guard for window
and content view presence and ensure you convert the incoming point with
convert(point, to: nil) before calling the shared helper so browser and terminal
behavior remain in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 13005-13009: Only set the pane drop context when the current host
actually owns the portal: wrap the call to hostedView.setPaneDropContext(...)
with a guard that ensures hostOwnsPortalNow is true (and optionally verify the
hostedView is the same host if you have a host identifier), and only then
construct TerminalPaneDropContext(workspaceId: terminalSurface.tabId, panelId:
terminalSurface.id, paneId: paneId) and call setPaneDropContext; do not call
setPaneDropContext when hostOwnsPortalNow is false to avoid stale-host
overwrites.

---

Nitpick comments:
In `@Sources/BrowserWindowPortal.swift`:
- Around line 1611-1622: The method shouldDeferToPaneTabBar(at:) duplicates
logic and should delegate to the shared helper; replace its body so it calls
BonsplitTabBarPassThrough.shouldPassThroughToPaneTabBar(...) (passing the point
converted to window coordinates and the window) instead of manually calling
BonsplitTabBarHitRegionRegistry.containsWindowPoint and
BonsplitTabBarPassThrough.hasBonsplitTabBarBackground; keep the guard for window
and content view presence and ensure you convert the incoming point with
convert(point, to: nil) before calling the shared helper so browser and terminal
behavior remain in sync.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e9ca31f0-9fa8-49a7-b37b-02bb7942419e

📥 Commits

Reviewing files that changed from the base of the PR and between 5a74203 and 308411d.

📒 Files selected for processing (12)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/BonsplitTabBarPassThrough.swift
  • Sources/BrowserWindowPortal.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/GhosttyTerminalViewSupport.swift
  • Sources/TerminalPaneDropTargetView.swift
  • Sources/TerminalWindowPortal.swift
  • Sources/TerminalWindowPortalDebug.swift
  • Sources/Workspace.swift
  • Sources/WorkspacePortalPaneDrop.swift
  • cmuxTests/PortalTabDragRoutingTests.swift
  • cmuxUITests/BonsplitTabDragUITests.swift
✅ Files skipped from review due to trivial changes (1)
  • Sources/Workspace.swift

@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

♻️ Duplicate comments (1)
Sources/GhosttyTerminalView.swift (1)

13005-13009: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate pane drop-context updates to the owning host.

Line 13005 updates drop context even when hostOwnsPortalNow is false. During portal host churn, a stale host can overwrite paneId and misroute drops.

🔧 Proposed fix
-        hostedView.setPaneDropContext(TerminalPaneDropContext(
-            workspaceId: terminalSurface.tabId,
-            panelId: terminalSurface.id,
-            paneId: paneId
-        ))
+        if hostOwnsPortalNow {
+            hostedView.setPaneDropContext(TerminalPaneDropContext(
+                workspaceId: terminalSurface.tabId,
+                panelId: terminalSurface.id,
+                paneId: paneId
+            ))
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 13005 - 13009, Only set the
pane drop context when the current host actually owns the portal: guard the call
to hostedView.setPaneDropContext(...) with the hostOwnsPortalNow check (or
equivalent ownership check) so that you only construct
TerminalPaneDropContext(workspaceId: terminalSurface.tabId, panelId:
terminalSurface.id, paneId: paneId) and call setPaneDropContext when
hostOwnsPortalNow is true; this prevents stale hosts from overwriting paneId and
misrouting drops.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmuxTests/BrowserConfigTests.swift`:
- Around line 3343-3368: The test
testRegisterForDraggedTypesKeepsExternalFileImageAndURLTypes currently checks
external types and internal routing types but misses asserting that blocked text
types (e.g., NSPasteboard.PasteboardType.string) are filtered; update the test
to also assert that registeredDraggedTypes does NOT contain .string (and any
other blocked text types your registerForDraggedTypes implementation intends to
drop) so a regression that re-registers text drags will fail—use the existing
registeredTypes Set and add XCTAssertFalse(registeredTypes.contains(.string))
(and similar assertions for any other blocked text pasteboard types).

In `@Sources/TerminalPaneDropTargetView.swift`:
- Around line 224-235: The method shouldDeferToPaneTabBar currently calls
BonsplitTabBarPassThrough.hasBonsplitTabBarBackground(...) which scans the
entire contentView; change it to use the shared underlay-aware helper
BonsplitTabBarPassThrough.shouldPassThroughToPaneTabBar(windowPoint:below:)
instead: keep the existing guard for window and the conversion to windowPoint,
then call BonsplitTabBarPassThrough.shouldPassThroughToPaneTabBar(windowPoint:
windowPoint, below: self) (passing this view as the "below" host) and return
that result so the pass-through hit test is scoped and bounded.

---

Duplicate comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 13005-13009: Only set the pane drop context when the current host
actually owns the portal: guard the call to hostedView.setPaneDropContext(...)
with the hostOwnsPortalNow check (or equivalent ownership check) so that you
only construct TerminalPaneDropContext(workspaceId: terminalSurface.tabId,
panelId: terminalSurface.id, paneId: paneId) and call setPaneDropContext when
hostOwnsPortalNow is true; this prevents stale hosts from overwriting paneId and
misrouting drops.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5c523133-cf3a-4851-beec-1967aec0baaf

📥 Commits

Reviewing files that changed from the base of the PR and between 308411d and 9c791e3.

📒 Files selected for processing (14)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/BonsplitTabBarPassThrough.swift
  • Sources/BrowserWindowPortal.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/GhosttyTerminalViewSupport.swift
  • Sources/TerminalPaneDropTargetView.swift
  • Sources/TerminalWindowPortal.swift
  • Sources/TerminalWindowPortalDebug.swift
  • Sources/Workspace.swift
  • Sources/WorkspacePortalPaneDrop.swift
  • cmuxTests/BrowserConfigTests.swift
  • cmuxTests/BrowserPanelTests.swift
  • cmuxTests/PortalTabDragRoutingTests.swift
  • cmuxUITests/BonsplitTabDragUITests.swift
✅ Files skipped from review due to trivial changes (3)
  • Sources/TerminalWindowPortalDebug.swift
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/BrowserWindowPortal.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • Sources/GhosttyTerminalViewSupport.swift
  • cmuxTests/PortalTabDragRoutingTests.swift

Comment thread cmuxTests/BrowserConfigTests.swift Outdated
Comment thread Sources/TerminalPaneDropTargetView.swift

@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

♻️ Duplicate comments (1)
cmuxTests/BrowserConfigTests.swift (1)

3429-3454: ⚠️ Potential issue | 🟡 Minor

Keep asserting the blocked text types are filtered too.

Sources/Panels/CmuxWebView.swift filters .string, public.text, and public.plain-text in addition to the two internal routing UTTypes. This test still only proves the internal types are dropped, so a regression that re-registers text drags would continue to pass.

Suggested fix
         XCTAssertFalse(registeredTypes.contains(DragOverlayRoutingPolicy.bonsplitTabTransferType))
         XCTAssertFalse(registeredTypes.contains(DragOverlayRoutingPolicy.sidebarTabReorderType))
+        XCTAssertFalse(registeredTypes.contains(.string))
+        XCTAssertFalse(registeredTypes.contains(NSPasteboard.PasteboardType("public.text")))
+        XCTAssertFalse(registeredTypes.contains(NSPasteboard.PasteboardType("public.plain-text")))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/BrowserConfigTests.swift` around lines 3429 - 3454, Update the test
testRegisterForDraggedTypesKeepsExternalFileImageAndURLTypes to also assert that
blocked text pasteboard types are filtered: after building registeredTypes, add
XCTAssertFalse checks that registeredTypes does not contain .string and the text
UTTypes filtered in CmuxWebView (e.g. NSPasteboard.PasteboardType("public.text")
and NSPasteboard.PasteboardType("public.plain-text")), in addition to the
existing checks for DragOverlayRoutingPolicy.bonsplitTabTransferType and
DragOverlayRoutingPolicy.sidebarTabReorderType so the test verifies both
internal routing types and text types are not re-registered.
🤖 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/TerminalWindowPortal.swift`:
- Around line 126-130: The current nil-branch forces isPointerEvent = false when
currentEvent is nil, which bypasses the intentional pass-through behavior of
BonsplitTabBarPassThrough.isPassThroughPointerEvent(nil); change the logic in
TerminalWindowPortal (the code that sets isPointerEvent based on
currentEvent?.type) so that when currentEvent is absent you call
BonsplitTabBarPassThrough.isPassThroughPointerEvent(nil) and assign its result
to isPointerEvent instead of hard-coding false, preserving the shared no-event
pass-through path.

---

Duplicate comments:
In `@cmuxTests/BrowserConfigTests.swift`:
- Around line 3429-3454: Update the test
testRegisterForDraggedTypesKeepsExternalFileImageAndURLTypes to also assert that
blocked text pasteboard types are filtered: after building registeredTypes, add
XCTAssertFalse checks that registeredTypes does not contain .string and the text
UTTypes filtered in CmuxWebView (e.g. NSPasteboard.PasteboardType("public.text")
and NSPasteboard.PasteboardType("public.plain-text")), in addition to the
existing checks for DragOverlayRoutingPolicy.bonsplitTabTransferType and
DragOverlayRoutingPolicy.sidebarTabReorderType so the test verifies both
internal routing types and text types are not re-registered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c3fa0961-bab7-4282-87cb-ec4b81ef52ed

📥 Commits

Reviewing files that changed from the base of the PR and between 4686c2a and a4efc4f.

📒 Files selected for processing (6)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/BrowserWindowPortal.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/TerminalWindowPortal.swift
  • Sources/Workspace.swift
  • cmuxTests/BrowserConfigTests.swift
✅ Files skipped from review due to trivial changes (1)
  • GhosttyTabs.xcodeproj/project.pbxproj

Comment thread Sources/TerminalWindowPortal.swift Outdated
@lawrencecchen
lawrencecchen merged commit 75b4f76 into main Apr 30, 2026
19 checks passed
@lawrencecchen
lawrencecchen deleted the task-blue-drop-target-indicators branch April 30, 2026 02:07

This branch was successfully deployed

1 active deployment
Preview – cmux — 416a2f23 Deployed Apr 30, 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