Skip to content

Fix file preview drop routing - #3539

Merged
lawrencecchen merged 5 commits into
mainfrom
fix-file-preview-text-drop
May 6, 2026
Merged

lawrencecchen merged 5 commits into
mainfrom
fix-file-preview-text-drop

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented May 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add a shared pane-level file drop adapter for file preview and markdown panes
  • keep browser uploads first, then route other Finder/sidebar file drops through Workspace.handleExternalFileDrop
  • let Bonsplit tabbar file drops insert previews through Support file drops on tab bar targets bonsplit#115
  • preserve requested working directories in restored terminal snapshots after the main merge

Tests

  • xcodebuild test -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug -destination platform=macOS -derivedDataPath /tmp/cmux-filedrop3-unit -only-testing:cmuxTests/PortalTabDragRoutingTests -only-testing:cmuxTests/FileDropOverlayViewTests
  • xcodebuild test -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug -destination platform=macOS -derivedDataPath /tmp/cmux-filedrop3-unit -only-testing:cmuxTests/WorkspaceSplitStartupCommandTests
  • ./scripts/reload.sh --tag filedrop3

Dogfood

  • Tagged build: filedrop3
  • Drag a text file from Finder or the file explorer onto an existing text preview pane.
  • Drag the same file onto the pane tab bar and confirm it inserts as a preview tab without breaking Bonsplit tab drags.

Summary by cubic

Fixes file preview drag‑and‑drop so all file URL drags (Finder, sidebar, and in‑app) land in the hovered pane with zone-based insert/split, while browser uploads still hit the WKWebView. Also themes the text preview editor and preserves effective working directories when restoring terminal tabs.

  • Bug Fixes

    • Route all file URL drags through shared pane drop targets: markdown/file preview panes install inline targets and terminals accept files via the portal; center/edge zones map to insert or split.
    • Defer to the pane tab bar so file drops there insert preview tabs without breaking bonsplit tab drags.
    • Keep browser payloads on the WKWebView and clear stale pane drop state when drags exit to prevent stuck overlays.
    • Apply workspace background/foreground colors to file preview text editors and keep requested/restorable working directories when restoring terminal snapshots.
  • Dependencies

    • Update bonsplit to support inserting preview tabs from tab‑bar file drops.

Written for commit a725315. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Pane-aware file drop targets and improved portal-hosted hit-testing so files can be dropped onto more panel types.
    • Themed file preview editor for consistent appearance across panels.
  • Bug Fixes

    • More reliable drag lifecycle and routing with improved hit-test deferral, destination selection, and overlay handling for internal and external drops.
  • Tests

    • New and updated tests validating overlay behavior, portal integration, and drop-routing scenarios.

@vercel

vercel Bot commented May 5, 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 May 6, 2026 1:01am
cmux-staging Building Building Preview, Comment May 6, 2026 1:01am

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented May 5, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Window-level file-URL drags can now route to pane-level drop targets; terminal-specific pane types were generalized to Pane* types. File-drop overlay hit-testing, portal-hosted resolution, pane-target lifecycle, drop routing, and file-preview theming/editor were added or extended.

Changes

Pane & File-Drop Routing, Portal Hit-Testing, and File-Preview Theming

Layer / File(s) Summary
Data Shape / Types
Sources/TerminalPaneDropTargetView.swift
Renamed terminal-specific types to generic Pane* (PaneDropContext, PaneDragTransfer, PaneDropRouting, PaneDropTargetView) with typealias aliases for backward compatibility.
Routing Policy & Pasteboard Helpers
Sources/DragOverlayRoutingPolicy.swift
New internal routing utility centralizing pasteboard-type constants and helpers (bonsplit tab transfer, file preview transfer, sidebar tab reorder, fileURL extraction, capture/pass-through heuristics).
Overlay Hit-testing & Discovery Helpers
Sources/ContentView.swift, Sources/GhosttyTerminalView.swift
FileDrop overlay hitTest can defer to Bonsplit tab bar; added helpers to find PaneDropTargetView under a window point and GhosttySurfaceScrollView.paneDropTargetForDrop(at:) for portal-hosted scroll views.
Window / Portal Integration
Sources/TerminalWindowPortal.swift
Added hosted scroll-view hit resolution and wrappers: hostedScrollViewAtWindowPoint, viewAtWindowPoint, terminalViewAtWindowPoint, terminalPaneDropTargetAtWindowPoint, and registry wrapper terminalPaneDropTargetAtWindowPoint(_:in:).
Overlay Drag Lifecycle & Routing
Sources/ContentView.swift
FileDropOverlayView now tracks activePaneDropTarget / preparedPaneDropTarget; draggingExited, prepareForDragOperation, performDragOperation, concludeDragOperation, and updateDragTarget route to pane targets when present and gate decisions using hasPaneTarget. Added shouldDeferFileDropOverlayToBonsplitTabBar.
Pane Target Behavior & Drop Handling
Sources/TerminalPaneDropTargetView.swift
PaneDropTargetView registers for both bonsplit transfer and .fileURL, decodes PaneDragTransfer for internal moves or extracts file URLs for external drops, computes drop zones, activates overlays (hosted or standalone), maps zones to destinations via PaneDropRouting.filePreviewDestination, and invokes workspace APIs for internal/external drops.
UI Wiring: Pane Overlay Install
Sources/Panels/PanelContentView.swift, Sources/WorkspaceContentView.swift
PanelContentView accepts workspaceId and conditionally installs a PaneDropTargetRepresentable overlay for .markdown and .filePreview panels; WorkspaceContentView passes workspaceId into PanelContentView.
File Preview Theming & Editor
Sources/Panels/FilePreviewPanel.swift, Sources/Panels/FilePreviewTextEditor.swift
FilePreviewPanelView gained appearance: PanelAppearance and derives themed background/foreground colors; introduced themed FilePreviewTextEditor NSViewRepresentable with SavingTextView (zoom, save chord, font scaling), theming, insets, and synchronization with panel content.
Workspace & API Visibility
Sources/Workspace.swift
sessionPanelSnapshot directory resolution no longer falls back to an effective restorable agent when nil; handleExternalFileDrop visibility widened from private to internal.
Tests & Coverage
cmuxTests/*
Added FileDropOverlayViewTests for overlay/portal delegation; updated portal/tab-routing tests to assert .fileURL capture and zone→destination mapping; updated drag-registration and Finder-regression tests to reflect prepared-registration and internal pane-drag acceptance.
Build / Project
GhosttyTabs.xcodeproj/project.pbxproj, vendor/bonsplit
Added new source files to Xcode project (DragOverlayRoutingPolicy.swift, FilePreviewTextEditor.swift, FileDropOverlayViewTests.swift) and updated vendor/bonsplit submodule pointer.
Minor Reformatting
Sources/Panels/TerminalPanelView.swift
PanelAppearance.fromConfig background color expression reformatted (no logic change).

Sequence Diagram

sequenceDiagram
    participant User as "User"
    participant Overlay as "FileDropOverlayView"
    participant Portal as "TerminalWindowPortal"
    participant Pane as "PaneDropTargetView"
    participant Workspace as "Workspace"
    participant Preview as "FilePreviewPanel"

    User->>Overlay: Drag file(s) over window
    Overlay->>Overlay: hitTest() / shouldDeferFileDropOverlayToBonsplitTabBar
    alt Defer to tab bar
        Overlay-->>User: no capture (deferred)
    else Capture file drop
        Overlay->>Portal: paneDropTargetUnderPoint(windowPoint)
        Portal->>Pane: paneDropTargetForDrop(localPoint)
        Pane-->>Portal: target or nil
        Portal-->>Overlay: resolved PaneDropTargetView

        User->>Overlay: draggingEntered / prepareForDragOperation
        Overlay->>Pane: prepareForDragOperation(sender)
        Pane->>Pane: extract fileURLs, compute zone, activate overlay

        User->>Overlay: performDragOperation (drop)
        Overlay->>Pane: performDragOperation(sender)
        Pane->>Workspace: handleExternalFileDrop(fileURLs, destination)
        Workspace-->>Pane: success/failure

        Overlay->>Pane: concludeDragOperation
        Pane->>Pane: clear overlay/state
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • manaflow-ai/cmux#3299: Modifies pane drop / portal drag-and-drop flow including pane drop-targets and hit-testing.
  • manaflow-ai/cmux#1843: Changes FileDropOverlayView drag lifecycle and forwarding of drag phases to pane targets.
  • manaflow-ai/cmux#3139: Touches file-preview and pane-level drop routing (PaneDropRouting / ContentView / Workspace integrations).

Poem

🐰 Hoppity-hop, a file finds its way,

Over portals and panes it softly will sway.
Tabs and previews now cue where to land,
Themes make the text feel warm and grand.
A rabbit applauds—drop routing, well planned!


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Swift @Concurrent ❌ Error File I/O async functions in FilePreviewPanel.swift (load, save, resolveMode) are called from @MainActor context without @concurrent annotation, violating the rule for heavy async work. Add @concurrent annotation to FilePreviewTextLoader.load, FilePreviewTextSaver.save, and FilePreviewMode.resolveMode to explicitly declare that these file I/O-heavy async functions escape the caller's actor.
Docstring Coverage ⚠️ Warning Docstring coverage is 1.98% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (11 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and specifically summarizes the main change: fixing file preview drop routing across panes and the tab bar.
Description check ✅ Passed The description covers all required sections with substantial detail: Summary explains the changes, Testing documents verification approach and specific test commands, Demo includes dogfood steps and tagged build info, Checklist is present.
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.
Cmux Swift Actor Isolation ✅ Passed Code maintains Swift 6 actor isolation. Key classes properly @MainActor marked. FilePreviewDragRegistry thread-safe, accessed from @MainActor only. No violations found.
Cmux Swift Blocking Runtime ✅ Passed No blocking patterns in new files. DragOverlayRoutingPolicy and FilePreviewTextEditor use standard patterns without semaphores, locks, sleep, or sync.
Cmux Swift Concurrency ✅ Passed No legacy async patterns introduced. New files are clean. Existing patterns remain unchanged or used appropriately at UI boundaries.
Cmux Swift File And Package Boundaries ✅ Passed New files within limits with clear responsibilities: DragOverlayRoutingPolicy (114 lines), FilePreviewTextEditor (204 lines). Large files unchanged. No violations detected.
Cmux Swift Logging ✅ Passed No new logging violations. Two new production files (318 lines total) have zero logging. All changes are structural refactoring without print/NSLog/debugPrint/dump in production code.
Cmux Swiftui State Layout ✅ Passed No violations found. New parameters use value types/closures. Existing ObservableObject only touched incidentally. AppKit bridge pattern is correct.
Cmux Architecture Rethink ✅ Passed Clean drag/drop implementation with clear ownership, unified paths, proper lifecycle cleanup, and no timing dependencies. No duplicate state ownership or split lifecycle management detected.
✨ 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 fix-file-preview-text-drop

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.

@greptile-apps

greptile-apps Bot commented May 5, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a shared pane-level file-drop adapter (PaneDropTargetView, generalised from TerminalPaneDropTargetView) that lets file-preview and markdown panes accept Finder/sidebar file drops with zone-based insert/split routing, while browser panes still receive drops through the existing WKWebView path. It also extracts FilePreviewTextEditor into its own file with workspace-theme colour support, adds bonsplit tab-bar deferral so tab drops insert preview tabs correctly, and preserves effective working directories when restoring terminal snapshots.

  • Drag routing overhaul: FileDropOverlayView now delegates file-URL drags to PaneDropTargetView instances found via an inline SwiftUI traversal (for file-preview/markdown panes) or the terminal portal (for terminal panes); DragOverlayRoutingPolicy.shouldCaptureFileDropDestination is simplified to capture all file-URL drags, dropping the previous local-source exclusion.
  • Theme-aware text editor: FilePreviewTextEditor is extracted to FilePreviewTextEditor.swift and applies PanelAppearance background/foreground colours through a static applyTheme helper called on every SwiftUI update pass.
  • Terminal snapshot fix: Workspace now checks effectiveRestorableAgent?.workingDirectory before TerminalPanel.requestedWorkingDirectory, preserving requested directories in restored snapshots.

Confidence Score: 4/5

Safe to merge for Finder-to-pane drops; in-app file-preview tab drags to non-pane targets (sidebar pins, new-window edges) may be silently blocked.

The core pane-level routing for Finder drops and the terminal snapshot fix look correct and are well-tested. The main concern is in DragOverlayRoutingPolicy.shouldCaptureFileDropDestination: the previous local-source guard that kept in-app file-preview tab drags out of the overlay has been removed, and the new bonsplit-tab-bar deferral only covers the tab bar. File-preview tabs dragged to the sidebar or window edges now hit the overlay, find no pane target, and the drop is silently absorbed.

Sources/DragOverlayRoutingPolicy.swift — the hasLocalDraggingSource parameter is now silently ignored; verify that in-app file-preview tab drags to sidebar and window-edge drop zones still work as expected.

Important Files Changed

Filename Overview
Sources/DragOverlayRoutingPolicy.swift Extracted from ContentView.swift; shouldCaptureFileDropDestination now ignores hasLocalDraggingSource, expanding overlay capture to all file-URL drags including in-app ones and potentially blocking file-preview tab drops to non-pane targets.
Sources/ContentView.swift FileDropOverlayView extended with pane drop target delegation; bonsplit tab-bar deferral added; inlinePaneDropTargetUnderPoint uses a fragile isHidden toggle without saving prior state.
Sources/TerminalPaneDropTargetView.swift Renamed to PaneDropTargetView with typealias; now handles both bonsplit tab transfers and file-URL drops; adds standalone drop-zone overlay for non-terminal panes.
Sources/Panels/FilePreviewTextEditor.swift Extracted to its own file; adds theme color support via applyTheme; typingAttributes[.foregroundColor] not updated on theme change, causing potential stale color on newly typed text.
Sources/Panels/FilePreviewPanel.swift Adds PanelAppearance param to FilePreviewPanelView and passes theme colors to FilePreviewTextEditor; mirrors transfer data on drag pasteboard.
Sources/Panels/PanelContentView.swift Installs PaneDropTargetRepresentable overlay for markdown and file-preview panels only; correctly guards with isVisibleInUI.
Sources/TerminalWindowPortal.swift Refactors viewAtWindowPoint and terminalViewAtWindowPoint into a shared hostedScrollViewAtWindowPoint helper; adds terminalPaneDropTargetAtWindowPoint for the new routing path.
Sources/Workspace.swift handleExternalFileDrop promoted from private to internal; effective restorable working directory checked before requestedWorkingDirectory in terminal snapshot restore.

Sequence Diagram

sequenceDiagram
    participant Finder as Finder / Sidebar
    participant Overlay as FileDropOverlayView
    participant Policy as DragOverlayRoutingPolicy
    participant PaneDT as PaneDropTargetView
    participant Portal as TerminalWindowPortal
    participant WS as Workspace

    Finder->>Overlay: file URL drag (hitTest)
    Overlay->>Policy: shouldCaptureFileDropDestination(types, hasLocal)
    Policy-->>Overlay: true (all file-URL drags captured)
    Overlay->>Overlay: shouldDeferToBonsplitTabBar?
    alt over tab bar
        Overlay-->>Finder: nil (defer to bonsplit)
    else over pane
        Overlay->>Overlay: inlinePaneDropTargetUnderPoint
        alt found SwiftUI PaneDropTargetView
            Overlay->>PaneDT: draggingEntered / draggingUpdated
            PaneDT-->>Overlay: .copy
        else terminal pane
            Overlay->>Portal: terminalPaneDropTargetAtWindowPoint
            Portal-->>Overlay: PaneDropTargetView
            Overlay->>PaneDT: draggingEntered / draggingUpdated
        end
        Overlay->>PaneDT: performDragOperation
        PaneDT->>WS: handleExternalFileDrop(urls, destination)
        WS-->>PaneDT: handled
    end
Loading

Reviews (4): Last reviewed commit: "Clear stale pane drop state" | Re-trigger Greptile

Comment thread Sources/ContentView.swift
Comment on lines 443 to +448
}
let webView = preparedDragWebView ?? activeDragWebView ?? webViewUnderPoint(sender.draggingLocation)
webView?.concludeDragOperation(sender)
if let paneDropTarget = preparedPaneDropTarget ?? activePaneDropTarget {
paneDropTarget.draggingExited(sender)
}

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.

P2 concludeDragOperation calls draggingExited on a semantically wrong path — and the path is dead in practice.

performDragOperation always nils out both preparedPaneDropTarget and activePaneDropTarget before delegating to the pane target, so preparedPaneDropTarget ?? activePaneDropTarget is always nil by the time concludeDragOperation runs after a successful drop. The draggingExited call is therefore unreachable via the normal AppKit lifecycle (prepare → perform → conclude). Even if the guard were reachable, draggingExited is the wrong signal here — the correct delegate call on conclusion of a successful drop is concludeDragOperation, not draggingExited. PaneDropTargetView already handles its own cleanup in performDragOperation via defer { clearDragState(...) }, so this block does nothing useful.

coderabbitai[bot]
coderabbitai Bot previously requested changes May 5, 2026

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Sources/Panels/FilePreviewPanel.swift (1)

928-1004: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Extract FilePreviewTextEditor into its own Swift file.

This file is now over the enforced CI budget (3965 > 3933). Moving the representable plus its theming helper out of FilePreviewPanel.swift keeps the feature intact and unblocks the PR.

Based on learnings: "This repo’s CI enforces a Swift file-length budget for large view files ... extract the subview into a dedicated Swift file under Sources ..."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Panels/FilePreviewPanel.swift` around lines 928 - 1004, Create a new
Swift file and move the entire FilePreviewTextEditor NSViewRepresentable
(including its nested Coordinator type and the private static applyTheme helper)
into it, adding required imports (SwiftUI, AppKit) and keeping the same access
level so references from FilePreviewPanel (e.g., panel: FilePreviewPanel,
SavingTextView, makeCoordinator, applyTheme) still compile; then remove the
struct from FilePreviewPanel.swift and ensure
FilePreviewPanel.attachTextView/textContent usages remain unchanged so the build
passes under the CI file-length budget.
cmuxTests/WindowAndDragTests.swift (1)

1-1: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

CI blocker: trim 4 lines to satisfy the Swift file-length budget.

The pipeline reports:

Swift file length budget exceeded for this file group: actual=2867, budget=2863 (+4).

The net line addition from this PR's two updated test segments is exactly 4 lines over the budget. The quickest fix is to collapse any redundant blank lines (the file has several consecutive double-blank separators between classes/test suites). For example, reducing the four double-blank separators immediately surrounding the new FileDropOverlayViewTests block to single-blank separators would recover the needed lines without changing any logic.

🔧 Example trimming approach (representative, not exhaustive)
 // ... end of FilePreviewDragPasteboardWriterTests
 }
-
 
 `@MainActor`
 final class FilePreviewPanelTextSavingTests: XCTestCase {

Apply the same pattern to the other double-blank separators in the file until 4 lines are recovered.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/WindowAndDragTests.swift` at line 1, Trim four redundant blank
lines around the new test block to satisfy the Swift file-length budget:
collapse the four double-blank separators immediately surrounding the
FileDropOverlayViewTests test suite into single blank lines (reduce consecutive
empty lines to a single empty line) so the file length drops by four lines
without changing any code or test logic; search for the FileDropOverlayViewTests
declaration and adjust the blank-line separators before and after that class
(and any immediately adjacent double-blank separators) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/ContentView.swift`:
- Around line 37-52: Extract the standalone drop-routing helpers (notably the
static func fileURLs(from:) and the related helpers referenced around the
676–712 region) out of ContentView.swift into their own Swift source file as a
same-type extension so ContentView.swift stays under the file-length budget;
move the functions verbatim, keep their signatures and behavior unchanged, and
do not mark them private if they must be accessed from other files.
- Around line 377-382: preparedPaneDropTarget and activePaneDropTarget are being
cleared too early and prepareForDragOperation(_:) is not being forwarded to the
pane target, breaking the NSDraggingDestination lifecycle; update the drag
handling so that when a paneDropTarget is found you call its
prepareForDragOperation(_:) (matching the webView path) and do NOT nil
preparedPaneDropTarget/activePaneDropTarget before invoking the pane target's
performDragOperation(_:) — instead defer clearing these references until after
concludeDragOperation(_:) completes (or defer/perform the target's cleanup
before you nil the refs) so draggingExited(sender) on TerminalPaneDropTargetView
will be called.

In `@Sources/Panels/PanelContentView.swift`:
- Around line 21-26: The pane-drop overlay (PaneDropTargetRepresentable) is
being mounted even for offscreen panels; update PanelContentView so the overlay
is only attached when the panel is visible by gating on isVisibleInUI — e.g.
replace the unconditional .overlay { paneDropTargetOverlay } with a conditional
overlay using if isVisibleInUI { paneDropTargetOverlay } (or return an EmptyView
otherwise). Apply the same visibility check to the other occurrence around lines
81–99 where PaneDropTargetRepresentable is mounted so hidden panels no longer
participate in AppKit drag hit-testing.

In `@Sources/TerminalWindowPortal.swift`:
- Around line 2119-2125: Move the newly added pane-drop-target logic out of
Sources/TerminalWindowPortal.swift into a new extension file (e.g.
Sources/TerminalWindowPortal+PaneDropTarget.swift) to satisfy the file-length
budget: create an extension on WindowTerminalPortal that implements
terminalPaneDropTargetAtWindowPoint(_:) (keeping the same implementation that
converts window point to hostView coordinates, iterates
hostView.subviews.reversed(), filters GhosttySurfaceScrollView entries via
entriesByHostedId, checks isHidden and frame.contains, converts to localPoint
and calls paneDropTargetForDrop(at:)), and add the static registry wrapper
extension on TerminalWindowPortalRegistry that delegates
terminalPaneDropTargetAtWindowPoint(_:in:) to portal(for:). Ensure imports
(AppKit) and symbol names remain unchanged so behavior is identical.

In `@Sources/WorkspaceContentView.swift`:
- Around line 235-238: WorkspaceContentView.swift exceeds the CI line-count by
one; extract a small nested helper into its own file to reduce length —
specifically move the PanelContentView declaration out of
WorkspaceContentView.swift into a new Sources/PanelContentView.swift, preserve
its signature and any used properties (workspaceId, paneId), update access
level/imports as needed, and remove the nested type from WorkspaceContentView so
all references (PanelContentView(...) usage) continue to compile.

In `@vendor/bonsplit`:
- Line 1: The submodule "bonsplit" is pinned to the unmerged PR HEAD commit
aa4f69723ea1a5a59df9dc0c9e7bbdfb1f96abc1 (PR `#115`) which is not reachable from
main; fix by repinning the submodule to a stable commit on the default branch
(main) or by first merging PR `#115` into main and then updating the submodule
reference, i.e., update the gitlink to a commit that exists on main (or merge
and re-run git submodule update --init && git add the submodule change and
commit) so the repository no longer references an open-PR-only SHA.

---

Outside diff comments:
In `@cmuxTests/WindowAndDragTests.swift`:
- Line 1: Trim four redundant blank lines around the new test block to satisfy
the Swift file-length budget: collapse the four double-blank separators
immediately surrounding the FileDropOverlayViewTests test suite into single
blank lines (reduce consecutive empty lines to a single empty line) so the file
length drops by four lines without changing any code or test logic; search for
the FileDropOverlayViewTests declaration and adjust the blank-line separators
before and after that class (and any immediately adjacent double-blank
separators) accordingly.

In `@Sources/Panels/FilePreviewPanel.swift`:
- Around line 928-1004: Create a new Swift file and move the entire
FilePreviewTextEditor NSViewRepresentable (including its nested Coordinator type
and the private static applyTheme helper) into it, adding required imports
(SwiftUI, AppKit) and keeping the same access level so references from
FilePreviewPanel (e.g., panel: FilePreviewPanel, SavingTextView,
makeCoordinator, applyTheme) still compile; then remove the struct from
FilePreviewPanel.swift and ensure FilePreviewPanel.attachTextView/textContent
usages remain unchanged so the build passes under the CI file-length budget.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b4601856-c665-4bb7-91f3-d49f1235d194

📥 Commits

Reviewing files that changed from the base of the PR and between 51fec04 and 93d2fb7.

📒 Files selected for processing (12)
  • Sources/ContentView.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/Panels/FilePreviewPanel.swift
  • Sources/Panels/PanelContentView.swift
  • Sources/Panels/TerminalPanelView.swift
  • Sources/TerminalPaneDropTargetView.swift
  • Sources/TerminalWindowPortal.swift
  • Sources/Workspace.swift
  • Sources/WorkspaceContentView.swift
  • cmuxTests/PortalTabDragRoutingTests.swift
  • cmuxTests/WindowAndDragTests.swift
  • vendor/bonsplit

Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/ContentView.swift
Comment thread Sources/Panels/PanelContentView.swift
Comment on lines +2119 to +2125
static func terminalPaneDropTargetAtWindowPoint(
_ windowPoint: NSPoint,
in window: NSWindow
) -> TerminalPaneDropTargetView? {
let portal = portal(for: window)
return portal.terminalPaneDropTargetAtWindowPoint(windowPoint)
}

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 | ⚡ Quick win

CI blocker: Swift file-length budget exceeded after adding this API.

The current CI run fails on file-length budget for Sources/TerminalWindowPortal.swift (+27 lines). Please move the newly added pane-drop-target methods into a separate extension file (same module) to unblock merge without changing behavior.

Suggested extraction sketch
--- a/Sources/TerminalWindowPortal.swift
+++ b/Sources/TerminalWindowPortal.swift
@@
-    func terminalPaneDropTargetAtWindowPoint(_ windowPoint: NSPoint) -> TerminalPaneDropTargetView? {
-        guard ensureInstalled() else { return nil }
-        let point = hostView.convert(windowPoint, from: nil)
-
-        for subview in hostView.subviews.reversed() {
-            guard let hostedView = subview as? GhosttySurfaceScrollView else { continue }
-            let hostedId = ObjectIdentifier(hostedView)
-            guard entriesByHostedId[hostedId] != nil else { continue }
-            guard !hostedView.isHidden else { continue }
-            guard hostedView.frame.contains(point) else { continue }
-            let localPoint = hostedView.convert(point, from: hostView)
-            if let target = hostedView.paneDropTargetForDrop(at: localPoint) {
-                return target
-            }
-        }
-
-        return nil
-    }
@@
-    static func terminalPaneDropTargetAtWindowPoint(
-        _ windowPoint: NSPoint,
-        in window: NSWindow
-    ) -> TerminalPaneDropTargetView? {
-        let portal = portal(for: window)
-        return portal.terminalPaneDropTargetAtWindowPoint(windowPoint)
-    }
// New file: Sources/TerminalWindowPortal+PaneDropTarget.swift
import AppKit

extension WindowTerminalPortal {
    func terminalPaneDropTargetAtWindowPoint(_ windowPoint: NSPoint) -> TerminalPaneDropTargetView? {
        guard ensureInstalled() else { return nil }
        let point = hostView.convert(windowPoint, from: nil)

        for subview in hostView.subviews.reversed() {
            guard let hostedView = subview as? GhosttySurfaceScrollView else { continue }
            let hostedId = ObjectIdentifier(hostedView)
            guard entriesByHostedId[hostedId] != nil else { continue }
            guard !hostedView.isHidden else { continue }
            guard hostedView.frame.contains(point) else { continue }
            let localPoint = hostedView.convert(point, from: hostView)
            if let target = hostedView.paneDropTargetForDrop(at: localPoint) {
                return target
            }
        }
        return nil
    }
}

extension TerminalWindowPortalRegistry {
    static func terminalPaneDropTargetAtWindowPoint(
        _ windowPoint: NSPoint,
        in window: NSWindow
    ) -> TerminalPaneDropTargetView? {
        let portal = portal(for: window)
        return portal.terminalPaneDropTargetAtWindowPoint(windowPoint)
    }
}

Based on learnings: “This repo’s CI enforces a Swift file-length budget for large view files… extract the subview/type into a dedicated Swift file under Sources.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/TerminalWindowPortal.swift` around lines 2119 - 2125, Move the newly
added pane-drop-target logic out of Sources/TerminalWindowPortal.swift into a
new extension file (e.g. Sources/TerminalWindowPortal+PaneDropTarget.swift) to
satisfy the file-length budget: create an extension on WindowTerminalPortal that
implements terminalPaneDropTargetAtWindowPoint(_:) (keeping the same
implementation that converts window point to hostView coordinates, iterates
hostView.subviews.reversed(), filters GhosttySurfaceScrollView entries via
entriesByHostedId, checks isHidden and frame.contains, converts to localPoint
and calls paneDropTargetForDrop(at:)), and add the static registry wrapper
extension on TerminalWindowPortalRegistry that delegates
terminalPaneDropTargetAtWindowPoint(_:in:) to portal(for:). Ensure imports
(AppKit) and symbol names remain unchanged so behavior is identical.

Comment thread Sources/WorkspaceContentView.swift
Comment thread vendor/bonsplit
@@ -1 +1 @@
Subproject commit f9b7618634677618c225120d5dd4d574ffaec2d3
Subproject commit aa4f69723ea1a5a59df9dc0c9e7bbdfb1f96abc1

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm the new bonsplit commit belongs to a merged PR (`#115`)
# and is reachable from the default branch.

# 1. Check PR `#115` state and merge commit
gh pr view 115 --repo manaflow-ai/bonsplit --json state,mergeCommit,headRefOid,mergedAt \
  | jq '{state, mergedAt, mergeCommit: .mergeCommit.oid, headSha: .headRefOid}'

# 2. Verify the pinned SHA exists in the repo and show its commit message
gh api repos/manaflow-ai/bonsplit/commits/aa4f69723ea1a5a59df9dc0c9e7bbdfb1f96abc1 \
  --jq '{sha: .sha, message: .commit.message, date: .commit.author.date}'

# 3. Check whether this SHA is reachable from the default branch
DEFAULT_BRANCH=$(gh api repos/manaflow-ai/bonsplit --jq '.default_branch')
echo "Default branch: $DEFAULT_BRANCH"
gh api "repos/manaflow-ai/bonsplit/compare/${DEFAULT_BRANCH}...aa4f69723ea1a5a59df9dc0c9e7bbdfb1f96abc1" \
  --jq '{status, ahead_by, behind_by}'

Repository: manaflow-ai/cmux

Length of output: 383


Submodule is pinned to an unmerged PR feature branch commit; this will break silently if the PR is rebased, force-pushed, or abandoned.

The new SHA aa4f69723ea1a5a59df9dc0c9e7bbdfb1f96abc1 is the HEAD of PR #115, which is still OPEN and not merged into the default branch (main). The commit is currently not reachable from main (diverged status). If the PR is later rebased, force-pushed, or abandoned, this submodule reference will point to a non-existent or unreachable commit, silently breaking builds.

Either merge PR #115 into manaflow-ai/bonsplit main first, or pin the submodule to a stable commit already on the default branch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vendor/bonsplit` at line 1, The submodule "bonsplit" is pinned to the
unmerged PR HEAD commit aa4f69723ea1a5a59df9dc0c9e7bbdfb1f96abc1 (PR `#115`) which
is not reachable from main; fix by repinning the submodule to a stable commit on
the default branch (main) or by first merging PR `#115` into main and then
updating the submodule reference, i.e., update the gitlink to a commit that
exists on main (or merge and re-run git submodule update --init && git add the
submodule change and commit) so the repository no longer references an
open-PR-only SHA.

coderabbitai[bot]
coderabbitai Bot previously requested changes May 6, 2026

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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/FileDropOverlayViewTests.swift`:
- Around line 179-182: This test binds a CmuxWebView to an anchor via
BrowserWindowPortalRegistry.bind but never detaches it, leaving global registry
state that can affect other tests; after
BrowserWindowPortalRegistry.synchronizeForAnchor(anchor) add a teardown call to
remove the binding—e.g., call BrowserWindowPortalRegistry.unbind(webView:
webView, from: anchor) (or the registry's appropriate remove/detach API) to
ensure the CmuxWebView binding is cleared at the end of the test.

In `@Sources/ContentView.swift`:
- Around line 286-289: The pane-target references (preparedPaneDropTarget and
activePaneDropTarget) are being cleared before delegation, so
concludeDragOperation has nothing to notify and the conclude path incorrectly
calls draggingExited; instead, preserve the paneDropTarget until the conclude
phase by not nil-ing preparedPaneDropTarget/activePaneDropTarget before calling
paneDropTarget.performDragOperation(sender), and ensure the conclude path
invokes paneDropTarget.concludeDragOperation (not draggingExited) so the
post-drop callback runs; apply the same change for the analogous block at the
later occurrence (the 313-315 area) referencing the same
preparedPaneDropTarget/activePaneDropTarget and paneDropTarget methods.
- Around line 244-246: In the prepareForDragOperation(_: ) branch, forward the
call to the pane destination instead of unconditionally marking it prepared:
call paneDropTarget.prepareForDragOperation(info) and use its boolean result to
decide whether to set preparedPaneDropTarget and return true; if the pane target
returns false, do not set preparedPaneDropTarget and return false. This uses the
existing symbols paneDropTarget, preparedPaneDropTarget and the
prepareForDragOperation(_:) method to match the WKWebView path.

In `@Sources/Panels/FilePreviewTextEditor.swift`:
- Around line 82-95: The Coordinator class and SavingTextView type are missing
explicit deinit methods required by the required_deinit rule; add an explicit
deinit to each (e.g., in final class Coordinator and in SavingTextView) —
implement deinit { /* cleanup if needed */ } for both, and if either holds
delegates or observers (e.g., NSTextView.delegate or NotificationCenter
observers) perform the appropriate removal inside their deinit to avoid leaks.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a25eb333-3e5c-4660-a944-dd484b532a46

📥 Commits

Reviewing files that changed from the base of the PR and between 93d2fb7 and 7763691.

📒 Files selected for processing (9)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/ContentView.swift
  • Sources/DragOverlayRoutingPolicy.swift
  • Sources/Panels/FilePreviewPanel.swift
  • Sources/Panels/FilePreviewTextEditor.swift
  • Sources/TerminalWindowPortal.swift
  • Sources/WorkspaceContentView.swift
  • cmuxTests/FileDropOverlayViewTests.swift
  • cmuxTests/WindowAndDragTests.swift

Comment thread cmuxTests/FileDropOverlayViewTests.swift
Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/Panels/FilePreviewTextEditor.swift
…-drop

# Conflicts:
#	Sources/ContentView.swift
#	Sources/Workspace.swift
#	cmuxTests/WindowAndDragTests.swift
#	vendor/bonsplit

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

235-238: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clear pane drop-target state when capture is declined

Line 235 and Line 277 return early on !shouldCapture, but pane-target state is left untouched. That can keep stale routing state alive across drag sessions.

💡 Suggested fix
         guard shouldCapture else {
             preparedDragWebView = nil
+            preparedPaneDropTarget = nil
+            activePaneDropTarget = nil
             return false
         }
         guard shouldCapture else {
             preparedDragWebView = nil
             activeDragWebView = nil
+            preparedPaneDropTarget = nil
+            activePaneDropTarget = nil
             return false
         }

Also applies to: 277-281

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/ContentView.swift` around lines 235 - 238, The early returns that
check shouldCapture currently only nil out preparedDragWebView but leave pane
drop-target routing state intact; update both guard branches (the one using
shouldCapture around preparedDragWebView and the other at lines ~277-281) to
also reset whatever variable(s) track the pane drop-target (clear the
pane-target routing state alongside preparedDragWebView) so stale drop-target
state is cleared when capture is declined.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Sources/ContentView.swift`:
- Around line 235-238: The early returns that check shouldCapture currently only
nil out preparedDragWebView but leave pane drop-target routing state intact;
update both guard branches (the one using shouldCapture around
preparedDragWebView and the other at lines ~277-281) to also reset whatever
variable(s) track the pane drop-target (clear the pane-target routing state
alongside preparedDragWebView) so stale drop-target state is cleared when
capture is declined.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4b18afd7-d5bb-4216-ba62-36b342a31c55

📥 Commits

Reviewing files that changed from the base of the PR and between 7763691 and 2918c82.

📒 Files selected for processing (9)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/ContentView.swift
  • Sources/DragOverlayRoutingPolicy.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/Panels/FilePreviewTextEditor.swift
  • Sources/Panels/PanelContentView.swift
  • Sources/Workspace.swift
  • cmuxTests/FileDropOverlayViewTests.swift
  • cmuxTests/FinderFileDropRegressionTests.swift

Comment on lines +37 to +39
_ = hasLocalDraggingSource
guard hasFileURL(pasteboardTypes) else { return false }
return true

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.

P1 Local file-URL drags silently blocked outside pane boundaries

_ = hasLocalDraggingSource removes the guard that previously prevented the overlay from capturing in-app drags. Any drag that carries .fileURL — including file-preview tab drags (which advertise filePreviewTransferType + bonsplitTransferType + .fileURL) — now always hits shouldCaptureFileDropDestination = true. The overlay then calls paneDropTargetUnderPoint, which returns nil for non-pane areas like the sidebar, title bar, or window edges. With no target found and shouldDeferFileDropOverlayToBonsplitTabBar only covering the tab bar, the overlay returns [] and swallows the drag. File-preview tabs dragged to the sidebar to pin them or to a window edge to create a new window will silently fail to drop instead of being routed through bonsplit's native handling.

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

551-587: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Extract these drop-routing helpers out of ContentView.swift.

This helper block is self-contained and adds more non-view glue code to an already oversized file. Please move it into a same-type extension under Sources/ so this PR doesn’t reintroduce the file-length-budget problem on ContentView.swift.

Based on learnings: “This repo’s CI enforces a Swift file-length budget for large view files (e.g., Sources/ContentView.swift). When adding helper views/small components, avoid bloating the existing file: extract the subview into a dedicated Swift file under Sources …”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/ContentView.swift` around lines 551 - 587, Extract the drop-routing
helpers (shouldDeferFileDropOverlayToBonsplitTabBar(at:),
paneDropTargetUnderPoint(_:), inlinePaneDropTargetUnderPoint(_:),
paneDropTarget(in:at:)) out of ContentView.swift into a new Swift file under
Sources as a same-type extension (e.g., extension ContentView { ... }),
preserving their access level (private) and any implicit use of
window/contentView; import AppKit if needed and keep the same function
signatures so callers compile; remove these methods from ContentView.swift and
run a build to fix any missing references.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@Sources/ContentView.swift`:
- Around line 551-587: Extract the drop-routing helpers
(shouldDeferFileDropOverlayToBonsplitTabBar(at:), paneDropTargetUnderPoint(_:),
inlinePaneDropTargetUnderPoint(_:), paneDropTarget(in:at:)) out of
ContentView.swift into a new Swift file under Sources as a same-type extension
(e.g., extension ContentView { ... }), preserving their access level (private)
and any implicit use of window/contentView; import AppKit if needed and keep the
same function signatures so callers compile; remove these methods from
ContentView.swift and run a build to fix any missing references.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b8e46534-da96-4893-8993-47e6cec971e4

📥 Commits

Reviewing files that changed from the base of the PR and between 2918c82 and a725315.

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

@lawrencecchen
lawrencecchen dismissed stale reviews from coderabbitai[bot] and coderabbitai[bot] May 6, 2026 01:12

Superseded by latest CodeRabbit pass on a725315 after addressing the comments.

@lawrencecchen
lawrencecchen merged commit a7d6d28 into main May 6, 2026
26 checks passed
@lawrencecchen
lawrencecchen deleted the fix-file-preview-text-drop branch May 6, 2026 01:13
@austinywang austinywang mentioned this pull request May 6, 2026
2 tasks
@svenrabe

svenrabe commented May 6, 2026

Copy link
Copy Markdown

how to upload a screenshot to claude now?!

austinywang added a commit that referenced this pull request May 6, 2026
Agent-hosted terminal panes were routed away from file preview, but their drops still flowed through the generic Ghostty file-drop handler. That handler pastes shell-escaped paths in a way that can leave Claude Code/Codex-style TUIs with literal path text instead of attachment state.

This gives agent terminals a distinct pane routing result and uses the existing file URL parsing and remote-upload planner while delivering the final path text as an explicit bracketed paste. Plain shell terminals and non-terminal panels keep the file-preview route from PR #3539.

Constraint: Issue #3615 requires agent terminal drops to attach images to the prompt while plain shell drops still open file preview
Constraint: Local tests and builds are disallowed before CI passes
Rejected: Reuse the generic terminal input route | dogfood showed it inserts literal paths instead of prompt attachments
Confidence: medium
Scope-risk: moderate
Directive: Agent terminal file drops must stay separate from plain shell/file-preview routing
Tested: git diff --check
Not-tested: Local tests/builds not run per task policy; CI pending

This branch was successfully deployed

1 active deployment
Preview – cmux — a725315b Deployed May 6, 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.

2 participants