Skip to content

Make file drops default to path text - #3684

Merged
lawrencecchen merged 16 commits into
mainfrom
task-shift-drop-files-as-text
May 7, 2026
Merged

lawrencecchen merged 16 commits into
mainfrom
task-shift-drop-files-as-text

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented May 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Default file URL drops over terminals, browsers, editable text inputs, and text file-preview editors insert shell-escaped path text. Hold Shift to open the file preview or split target instead.
  • Non-text pane surfaces always use the shared blue Bonsplit preview routing, so PDFs/images/other non-editable content show the same split animation and destination math.
  • Browser, terminal, hosted terminal, standalone pane targets, and internal file-preview drags now share file-drop payload resolution, including image-pane drags into terminals.
  • Successful text drops now focus the destination terminal, browser web view, or file-preview text editor through the shared workspace focus path. This covers Finder drops and right-sidebar/internal drags.
  • The App Settings picker controls the default file-drop behavior. The localized hint badge is neutral gray and centers in the hovered droppable pane.

Testing

  • jq empty Resources/Localizable.xcstrings
  • git diff --check
  • xcodebuild test -quiet -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-dropdry-unit -only-testing:cmuxTests/FinderFileDropRegressionTests -only-testing:cmuxTests/BrowserPaneDropRoutingTests -only-testing:cmuxTests/FileDropOverlayViewTests -only-testing:cmuxTests/PortalTabDragRoutingTests\n- ./scripts/reload.sh --tag dropdry\n\n## Issues\n- Task: default file drops should insert path text into interactive destinations, Shift should create the preview/split, non-interactive destinations should show the blue split target, dropped text should focus its destination, and the behavior should be configurable in Settings.\n\n\n\n\n## Summary by CodeRabbit\n\n* New Features\n * Configurable "File Drops" setting to choose default handling.\n * Option to route file drops as plain-text paths into editor or terminal; Shift toggles behavior.\n * Drag hint badge shows resolved drop destination (editor vs terminal).\n * File paths inserted from explorer are normalized to macOS-friendly display paths.\n\n* Documentation\n * Localized hint strings added for English and Japanese.\n\n* Tests\n * New tests covering file-drop routing, Shift behavior, modifier merging, and path insertion.\n\n

Note

Medium Risk
Moderate risk because it rewires drag-and-drop hit-testing/routing across terminal panes, browser panes, and the window overlay, which can easily regress drop handling and focus behavior.

Overview
Changes file-drop behavior to default to inserting shell-escaped path text into interactive destinations (terminal surfaces, browser web views/editable inputs, and text file-preview editors), with Shift inverting to the preview/split behavior.

This refactors drag/drop plumbing by extracting shared pane drop routing utilities (PaneDropRoutingSupport), adding a dedicated BrowserPaneDropTargetView, and reworking FileDropOverlayView to route between pane targets and web views, show a new hint badge, and restore focus after successful text drops.

Adds an app setting (File Drops) with new localizations, extends file-preview drag registry/pasteboard handling to resolve URLs, normalizes file path display (/private/... to /...), and updates/expands unit tests for the new routing and web-view lifecycle behavior.

Reviewed by Cursor Bugbot for commit 59e88e0. Bugbot is set up for automated code reviews on this repo. Configure here.

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

@vercel

vercel Bot commented May 7, 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 7, 2026 0:19am
cmux-staging Building Building Preview, Comment May 7, 2026 0:19am

@coderabbitai

coderabbitai Bot commented May 7, 2026 •

Copy link
Copy Markdown

Review Change Stack

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

When files are dragged, a new routing policy can route file URLs into text insertion (editor or terminal) instead of preview/transfer. An overlay hint badge shows the resolved destination kind; insertion routes use shell-escaped paths via TerminalImageTransfer and JS insertion for web editors. Settings, localization, and tests were added.

Changes

Shift+file-drop-to-text routing

Layer / File(s) Summary
Routing policy and text conversion
Sources/DragOverlayRoutingPolicy.swift, Sources/TerminalImageTransfer.swift
Adds FileDropResolvedBehavior/FileDropDefaultBehavior, merged modifier-flag helpers, FileDropTextInsertion JS insertion, payload helpers, and public insertedText(forFileURLs:) with the URL implementation moved behind a private helper.
Terminal file-drop-as-text integration
Sources/GhosttyTerminalView.swift, Sources/Panels/FilePreviewPanel.swift
Adds handleDroppedFileURLsAsText / handleDroppedURLsAsText to convert dropped URLs into shell-escaped text and forward to the terminal surface; FilePreviewPanel supports text-mode insertion from drops and registry lookup.
Pane drop target routing and delegation
Sources/TerminalPaneDropTargetView.swift
Registers file-URL pasteboard types, short-circuits update/perform flows when routing-to-text is active, and delegates handling to hosted views or file-preview panels in text mode; refactors overlay animator/frame logic.
Browser portal & pane hit-testing
Sources/BrowserWindowPortal.swift
Expands browser pane hit-testing to include file URL types, delegates zone sizing to PaneDropRouting, supports file→text routing in perform/update flows, and exposes drop-target lookup APIs on WindowBrowserPortal/Registry.
Overlay UI hint badge & text drop handling
Sources/ContentView.swift
Adds FileDropHintBadgeView, shows/hides badge during drag, determines editor vs terminal targets, and performs file-drop-as-text insertion into NSTextView, WKWebView (JS), or terminal views; updates drag lifecycle methods and JS helpers.
Path display, settings, and search index
Sources/FileExplorerTerminalPathInsertion.swift, Sources/cmuxApp.swift, Sources/SettingsNavigation.swift
Normalizes filesystem paths for macOS display (/private/... → /...), adds persisted default file-drop behavior in Settings (AppStorage + picker), and registers a settings search entry.
Localization strings and regression tests
Resources/Localizable.xcstrings, cmuxTests/FinderFileDropRegressionTests.swift, cmuxTests/BrowserPaneDropRoutingTests.swift
Adds fileDrop.holdShiftDropIntoEditor and fileDrop.holdShiftDropIntoTerminal (en/ja) and tests for routing rules, merged modifier flags, hit-testing behavior, and URL→text insertion formatting.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant OverlayView
  participant RoutingPolicy
  participant TargetHost
  participant TerminalSurface
  User->>OverlayView: drag with file URLs
  OverlayView->>RoutingPolicy: shouldRouteFileDropToTextDestination(types, flags)
  RoutingPolicy-->>OverlayView: true/false
  alt text routing
    OverlayView->>TargetHost: performFileDropAsText(urls)
    TargetHost->>TerminalSurface: sendText(shell-escaped paths) // or
    TargetHost->>WKWebView: evaluate JS to insert text
  else preview/transfer
    OverlayView->>Workspace: handle preview/transfer flow
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hop with a shift and a careful mind,
Files turn to paths, shell-escaped and kind,
A badge whispers "editor" or "terminal" bright,
I tuck the text in, tidy and light,
Hooray — the drop lands just right.


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift File And Package Boundaries ❌ Error ContentView.swift adds 318 lines to 15,956-line file (exceeds 250-line threshold). DragOverlayRoutingPolicy.swift is 405-line new file mixing values, persistence, WebKit DOM, and routing logic. Split DragOverlayRoutingPolicy.swift by responsibility. Extract FileDropHintBadgeView and text helpers from ContentView.swift to reduce it by >200 lines. Keep new files under 400 with single responsibility.
Cmux Architecture Rethink ❌ Error concludeDragOperation re-evaluates shouldRouteFileDropToTextDestination from live state, allowing decision to diverge from performDragOperation and skip paneDropTarget conclude callback. Track routing decision in performDragOperation via flag; use in concludeDragOperation instead of re-evaluating live state to ensure consistent drag lifecycle.
Docstring Coverage ⚠️ Warning Docstring coverage is 1.57% 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 clearly summarizes the main change: file drops now default to inserting path text instead of showing preview.
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 No Swift 6 actor isolation violations found. New enums are pure value types without shared mutable state or background context access. String(localized:) usage is safe from @MainActor contexts.
Cmux Swift Blocking Runtime ✅ Passed No new blocking/timing synchronization introduced. All blocking code found is pre-existing. New code uses async evaluateJavaScript and standard AppKit drag handler patterns.
Cmux No Hacky Sleeps ✅ Passed Check not applicable. Rule applies to TypeScript, JavaScript, shell, and non-Swift runtime scripts. This PR contains only Swift code and resources, covered by swift-blocking-runtime.md instead.
Cmux Swift Concurrency ✅ Passed No legacy async patterns. All new code is synchronous or properly uses AppKit/SwiftUI boundaries.
Cmux Swift @Concurrent ✅ Passed No concurrency violations. The evaluateJavaScript call in FileDropTextInsertion is intentional fire-and-forget in synchronous context. No misused @concurrent, no problematic async isolation.
Cmux Swift Logging ✅ Passed PR complies with Swift logging rules. No print(), debugPrint(), dump(), NSLog(), or Logger statements added to PR files. No secrets exposed.
Cmux Swiftui State Layout ✅ Passed No SwiftUI state violations. Uses @AppStorage (proper), value-type enums, computed properties, and AppKit bridges. No new @Published/@StateObject or problematic patterns.
Description check ✅ Passed The pull request description is comprehensive, following the template structure with a clear Summary section, Testing section with specific commands, and an Issues section linking to the task.
✨ 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-shift-drop-files-as-text

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 7, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR changes the default file-drop behavior to insert shell-escaped path text into interactive destinations (terminals, browser web views, file-preview text editors), with Shift inverting to preview/split. It extracts the monolithic FileDropOverlayView from ContentView.swift into dedicated files, introduces BrowserPaneDropTargetView, consolidates routing geometry into PaneDropRoutingSupport, and adds a configurable "File Drops" App Settings picker.

  • New files (FileDropOverlayView.swift, FileDropOverlayViewHitTesting.swift, FileDropHintBadgeView.swift, PaneDropRoutingSupport.swift, DragOverlayRoutingPolicy.swift, BrowserPaneDropTargetView.swift) extract and restructure the drag/drop layer with clear single responsibilities; FileDropHintBadgeView and PaneDropZoneOverlayAnimator are correctly marked @MainActor.
  • Text drop routing is wired through a shared FileDropTextDropController that posts focus after successful insertion; didPerformDragAsText is set only on success, fixing the previously-flagged conclude-phase orphan.
  • DragOverlayRoutingPolicy.fileURLs adds a new fallback for file-preview-transfer drags using standardizedFileURL, which resolves /tmp symlinks and can produce /private/tmp/\u2026 paths in the inserted text.

Confidence Score: 4/5

Safe to merge with the /private/tmp path normalization fixed for file-preview-tab text drops.

The refactoring and new routing logic are well-structured and most previously-flagged issues have been addressed. One confirmed defect in the new file-preview-drag text-insertion path: DragOverlayRoutingPolicy.fileURLs uses standardizedFileURL which resolves /tmp to /private/tmp, so terminal insertions for files under /tmp get the wrong path. The macOSDisplayPath normalization present in FileExplorerTerminalPathInsertion is absent here.

Sources/DragOverlayRoutingPolicy.swift — the fileURLs(from:) helper needs macOSDisplayPath normalization for the file-preview-transfer fallback branch.

Important Files Changed

Filename Overview
Sources/DragOverlayRoutingPolicy.swift New routing policy, behavior enums, and fileURLs helper — standardizedFileURL in the file-preview-drag branch will insert /private/… paths into the terminal for files under /tmp.
Sources/FileDropOverlayView.swift Extracted from ContentView.swift; multi-phase drag state (7 tracking fields) correctly follows AppKit protocol. didPerformDragAsText is now set only on success as required.
Sources/FileDropOverlayViewHitTesting.swift Hit-testing and routing helpers. paneDropTargetForTextDrop/SavingTextView logic is correct for the happy path; silent fallback when pane target is stale could leave panel model out of sync.
Sources/PaneDropRoutingSupport.swift Consolidated shared routing geometry, transfer decoding, and overlay animator — PaneDropZoneOverlayAnimator is correctly @MainActor; Task { @MainActor in } re-entry in animation completion is present.
Sources/BrowserPaneDropTargetView.swift New dedicated view for browser-pane drops; shouldCaptureHitTesting is now @MainActor; hasFileURL guard prevents file-preview-only transfers from entering the hosted WKWebView path.
Sources/FileDropHintBadgeView.swift New @MainActor badge view. Animation generation guard prevents stale hide completions. Fade-in/fade-out are balanced; rapid show→hide→show correctly replaces in-progress Core Animation.
Sources/FileExplorerTerminalPathInsertion.swift macOSDisplayPath normalization correctly rewrites /private/{tmp,var,etc} prefixes for file-explorer drops. standardizedFileURL.path call resolves symlinks as expected.
Sources/Panels/FilePreviewPanel.swift New handleDroppedFileURLsAsText and entry(id:) helpers. Class is @MainActor so AppKit access is safe. register(id:) default-UUID parameter enables test ID seeding without breaking production callers.
Sources/cmuxApp.swift Settings picker wired correctly; fileDropDefaultBehavior reset in resetToDefaults is consistent with peer settings that don't emit change notifications.
Sources/BrowserWindowPortal.swift Routing geometry extracted to PaneDropRouting. New paneDropTargetForDrop and browserPaneDropTargetAtWindowPoint lookups follow existing portal pattern correctly.

Sequence Diagram

sequenceDiagram
    participant Finder
    participant FileDropOverlayView
    participant DragOverlayRoutingPolicy
    participant PaneDropTargetView
    participant BrowserPaneDropTargetView
    participant FileDropTextDropController

    Finder->>FileDropOverlayView: draggingEntered/Updated
    FileDropOverlayView->>DragOverlayRoutingPolicy: resolvedFileDropBehavior(modifierFlags)
    alt "behavior == .text (default)"
        FileDropOverlayView->>FileDropOverlayView: paneDropTargetForTextDrop
        FileDropOverlayView->>PaneDropTargetView: fileDropDraggingEntered
        PaneDropTargetView->>DragOverlayRoutingPolicy: shouldRouteFileDropToTextDestination
        FileDropOverlayView->>FileDropOverlayView: updateHintBadge (show Shift hint)
    else "behavior == .preview (Shift held)"
        FileDropOverlayView->>PaneDropTargetView: fileDropDraggingEntered (split UI)
    end

    Finder->>FileDropOverlayView: performDragOperation
    alt text path
        FileDropOverlayView->>PaneDropTargetView: fileDropPerformDragOperation
        PaneDropTargetView->>PaneDropTargetView: handleFileDropAsText
        PaneDropTargetView->>FileDropTextDropController: performPanelTextDrop
        FileDropTextDropController->>FileDropTextDropController: insert() + focusPanel
    else browser web view
        FileDropOverlayView->>BrowserPaneDropTargetView: fileDropPerformDragOperation
        BrowserPaneDropTargetView->>BrowserPaneDropTargetView: shouldRouteFileDropToHostedWebView
        BrowserPaneDropTargetView->>FileDropTextDropController: focusBrowserPanel
    else preview path
        FileDropOverlayView->>PaneDropTargetView: fileDropPerformDragOperation (split)
    end

    Finder->>FileDropOverlayView: concludeDragOperation
    FileDropOverlayView->>PaneDropTargetView: fileDropConcludeDragOperation
Loading

Reviews (14): Last reviewed commit: "Fix browser drop review issues" | Re-trigger Greptile

Comment thread Sources/ContentView.swift Outdated
Comment on lines +719 to +720
webView.evaluateJavaScript(script)
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 WebView text insert silently succeeds even when JS finds no editable target

evaluateJavaScript(_:) is the fire-and-forget overload — its return value (the IIFE's true/false) is discarded, and the Swift function unconditionally returns true. If the cursor lands over a non-editable element in the web view, the JS IIFE returns false, no text is inserted, but AppKit receives a success signal and animates the drag image away as though the drop worked. The user loses the file path with no feedback. Use evaluateJavaScript(_:completionHandler:) and propagate the JS boolean back to the caller (the completion runs on the main thread so a @MainActor callback is safe here).

Comment thread Sources/TerminalImageTransfer.swift Outdated
Comment on lines +291 to +293
private static func insertedText(for fileURLs: [URL]) -> String {
insertedText(forFileURLs: fileURLs)
}

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 The private insertedText(for:) wrapper now only delegates to insertedText(forFileURLs:) and has no remaining callers. It was kept to preserve existing internal call sites, but after the rename those sites should use forFileURLs directly so the private shim can be removed.

Suggested change
private static func insertedText(for fileURLs: [URL]) -> String {
insertedText(forFileURLs: fileURLs)
}

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!

Comment thread Sources/ContentView.swift Outdated
Comment on lines +90 to +94
func hide() {
guard !isHidden else { return }
alphaValue = 0
isHidden = 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.

P2 The hide() path clears isHidden immediately without any animation, making the badge pop away abruptly. show() fades in via animator(), so a matching fade-out on hide() would keep the transition consistent and less jarring when Shift is released mid-drag.

Suggested change
func hide() {
guard !isHidden else { return }
alphaValue = 0
isHidden = true
}
func hide() {
guard !isHidden else { return }
NSAnimationContext.runAnimationGroup { context in
context.duration = 0.12
animator().alphaValue = 0
} completionHandler: { [weak self] in
self?.isHidden = true
}
}

coderabbitai[bot]
coderabbitai Bot previously requested changes May 7, 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 `@Sources/ContentView.swift`:
- Around line 559-570: The fallback that treats a window firstResponder field
editor as an editor in textDropDestinationKindUnderPoint is causing mis-routed
drops and incorrect hints; update textDropDestinationKindUnderPoint (and the
related fallback logic used by editableTextViewUnderPoint) to only return
.editor when the resolved field editor/view's frame actually contains the
supplied windowPoint (i.e., hit-test the field editor's convertToWindow/bounds
or use hitTest on its superview) or remove the fallback entirely so only direct
hit-tested editableTextViews/webViews/terminals (editableTextViewUnderPoint,
webViewUnderPoint, terminalUnderPoint) are considered; ensure
performFileDropAsText uses the same point-validated target to avoid inserting
into an off-point firstResponder.
- Around line 348-353: When routing the drop to text
(shouldRouteFileDropToTextDestination(sender)) ensure we call draggingExited on
any activeDragWebView and activePaneDropTarget before nulling them and
returning; update the early-return paths in prepareForDragOperation and
performDragOperation to invoke activeDragWebView?.draggingExited(sender) and
activePaneDropTarget?.draggingExited(sender) (mirroring the cleanup in
draggingUpdated), then clear
preparedDragWebView/preparedPaneDropTarget/activePaneDropTarget and return;
likewise, in concludeDragOperation call
activeDragWebView?.draggingExited(sender) and
activePaneDropTarget?.draggingExited(sender) before the defer that clears
activeDragWebView so the web view receives a matching draggingExited/conclude
sequence.
- Around line 10-132: The ContentView file has ~120 lines of self-contained
AppKit code that should be extracted: move the FileDropHintBadgeView class and
FileDropTextDestinationKind enum into their own Swift source file, update
imports if needed, and remove the file-private/private modifier so they are
internal by omission (or explicitly internal) in the new file; ensure references
to FileDropHintBadgeView and FileDropTextDestinationKind in ContentView remain
unchanged and build by adding the new file to the target.

In `@Sources/GhosttyTerminalView.swift`:
- Around line 9254-9259: The method handleDroppedFileURLsAsText currently
returns true even if terminalSurface is nil and sendText no-ops; update it to
return false when there's no active terminal surface: after computing text
(TerminalImageTransferPlanner.insertedText(forFileURLs:)), ensure
terminalSurface is present (e.g., guard let surface = terminalSurface else {
return false }) before calling sendText on that surface and then return true;
reference the handleDroppedFileURLsAsText function and terminalSurface property
when making the change.
🪄 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: 2e06a849-23ed-4a30-ae42-b12ed2592903

📥 Commits

Reviewing files that changed from the base of the PR and between 65e0948 and 078bde5.

📒 Files selected for processing (7)
  • Resources/Localizable.xcstrings
  • Sources/ContentView.swift
  • Sources/DragOverlayRoutingPolicy.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/TerminalImageTransfer.swift
  • Sources/TerminalPaneDropTargetView.swift
  • cmuxTests/FinderFileDropRegressionTests.swift

Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/GhosttyTerminalView.swift
Comment thread Sources/ContentView.swift Outdated
@lawrencecchen lawrencecchen changed the title Add Shift file drops as text Make file drops default to path text May 7, 2026
Comment thread Sources/DragOverlayRoutingPolicy.swift
coderabbitai[bot]
coderabbitai Bot previously requested changes May 7, 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: 3

Caution

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

⚠️ Outside diff range comments (1)
Sources/FileExplorerTerminalPathInsertion.swift (1)

12-22: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

relativePath fallback returns un-normalized path, defeating the new macOSDisplayPath rewrite

In the non-matching branch (line 21), the function returns the caller's original path rather than normalizedPath. Before this PR, normalizedFileSystemPath was nearly idempotent on real absolute paths (just resolves ./..), so the gap was negligible. Now that macOSDisplayPath can materially rewrite strings (e.g. /private/tmp/foo → /tmp/foo), the fallback silently yields the /private/… form even though the matching branch would have yielded the display-normalized form.

🐛 Proposed fix
     if normalizedPath.hasPrefix(normalizedRoot) {
         return String(normalizedPath.dropFirst(normalizedRoot.count))
     }
-    return path
+    return normalizedPath
 }
🤖 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/FileExplorerTerminalPathInsertion.swift` around lines 12 - 22, The
fallback in relativePath(for:rootPath:) returns the original caller `path` which
bypasses the `normalizedFileSystemPath`/`macOSDisplayPath` rewrite; change the
non-matching return to return `normalizedPath` (and likewise return
`normalizedPath` when `rootPath` is empty) so all branches consistently use the
normalized/display path produced by
`normalizedFileSystemPath`/`macOSDisplayPath` (refer to relativePath,
normalizedFileSystemPath, normalizedPath, normalizedRootPath).
🤖 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 `@Resources/Localizable.xcstrings`:
- Around line 19-20: The Japanese text uses the ambiguous term "パステキスト" which
can be read as "paste text"; update the Japanese localizations for the resource
keys settings.app.fileDrop.defaultBehavior.preview.subtitle and
settings.app.fileDrop.defaultBehavior.text to use a clearer translation for
"path text" such as "パス文字列" (or "パスのテキスト") in both entries so the intent is
unambiguous.

In `@Sources/ContentView.swift`:
- Around line 575-576: Replace the unconditional WebView-based drop routing with
a cached editable-target probe: stop returning .editor based solely on
webViewUnderPoint(...) (the code in webViewUnderPoint(...) and the drop-routing
branch); instead, during drag update use the preparedDragWebView pattern to call
evaluateJavaScript(script, completionHandler:) that runs elementFromPoint(...)
and determines whether the element is editable, store that boolean on
preparedDragWebView (or a related cached flag), and in the actual drop handler
consult that cached editable flag to decide .editor routing; also ensure the
evaluateJavaScript completion handler updates the cache before the drop and
remove reliance on immediate synchronous evaluateJavaScript calls at drop time.

In `@Sources/SettingsNavigation.swift`:
- Line 299: The settingsPathAnchorIDs map is missing an entry for the new "File
Drops" setting, so add a mapping from the settings key (likely
"app.fileDropDefaultBehavior") to the anchor id produced by settingID(for: .app,
idSuffix: "file-drops") in the settingsPathAnchorIDs collection (and update any
related search/index wiring if there is a parallel mapping), ensuring the new
entry mirrors how other settings are wired so deep-links and jump-to behavior
work correctly.

---

Outside diff comments:
In `@Sources/FileExplorerTerminalPathInsertion.swift`:
- Around line 12-22: The fallback in relativePath(for:rootPath:) returns the
original caller `path` which bypasses the
`normalizedFileSystemPath`/`macOSDisplayPath` rewrite; change the non-matching
return to return `normalizedPath` (and likewise return `normalizedPath` when
`rootPath` is empty) so all branches consistently use the normalized/display
path produced by `normalizedFileSystemPath`/`macOSDisplayPath` (refer to
relativePath, normalizedFileSystemPath, normalizedPath, normalizedRootPath).
🪄 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: 1d44aee2-1f53-4bfd-82c1-ee77723dd25f

📥 Commits

Reviewing files that changed from the base of the PR and between 078bde5 and 85f2cf9.

📒 Files selected for processing (8)
  • Resources/Localizable.xcstrings
  • Sources/ContentView.swift
  • Sources/DragOverlayRoutingPolicy.swift
  • Sources/FileExplorerTerminalPathInsertion.swift
  • Sources/SettingsNavigation.swift
  • Sources/TerminalPaneDropTargetView.swift
  • Sources/cmuxApp.swift
  • cmuxTests/FinderFileDropRegressionTests.swift

Comment thread Resources/Localizable.xcstrings Outdated
Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/SettingsNavigation.swift
Comment thread Sources/DragOverlayRoutingPolicy.swift
Comment thread Sources/GhosttyTerminalView.swift
Comment thread Sources/TerminalPaneDropTargetView.swift
Comment thread Sources/BrowserWindowPortal.swift Outdated
Comment on lines +541 to +542
case .browser:
return .editor

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 Browser pane accepted as text destination but handleFileDropAsText has no browser branch

fileDropTextDestinationKind returns .editor for .browser panels (line 541–542), causing shouldRouteFileDropToTextDestination to return true and performDragOperation to call handleFileDropAsText. But handleFileDropAsText only has branches for TerminalPanel and FilePreviewPanel; a browser panel falls through to return false (line 521). The drag operation is accepted with .copy at the updateDragState level, then silently dropped — the user loses the file path with no feedback.

Either add a BrowserPanel branch in handleFileDropAsText that routes through WindowBrowserSlotView.handleDroppedFileURLsAsText, or return nil from fileDropTextDestinationKind for .browser here since browser panes already have their own BrowserPaneDropTargetView path that handles text insertion directly.

coderabbitai[bot]
coderabbitai Bot previously requested changes May 7, 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

Caution

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

⚠️ Outside diff range comments (1)
Sources/BrowserWindowPortal.swift (1)

1418-1881: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Split this pane-drop block into a dedicated Swift file before merge.

CI is already red on the Swift file-length budget (4362 > 4305), and this new pane-drop target / slot-helper code is cohesive enough to move without changing behavior. Pulling it into its own file under Sources/ should unblock the budget and keep future portal changes reviewable. 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/BrowserWindowPortal.swift` around lines 1418 - 1881, This file is
over the Swift file-length budget; extract the cohesive pane-drop/slot helper
classes into a new Swift source file: move the final class
BrowserPaneDropTargetView and the final class WindowBrowserSlotView (including
their private vars, methods like setPaneDropContext(_:),
paneDropTargetForDrop(_:), setPortalDragDropZone(_:), setDropZoneOverlay(zone:),
handleDroppedFileURLsAsText(_:at:), and any DEBUG-only helpers such as
logHitTestDecision and clearDragState) into a new Sources/Swift file, preserve
all access levels, imports, and conditional compilation blocks, remove their
definitions from the original file, ensure the new file is added to the build
target, and run a build to fix any missing references (update any
fileprivate/internal usage if needed to maintain visibility across files).
♻️ Duplicate comments (6)
Resources/Localizable.xcstrings (1)

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

Use unambiguous Japanese for “path text” in this label

パステキスト is ambiguous in Japanese; this should match the clearer terminology already used in Line 19 (for example, パス文字列).

🤖 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 `@Resources/Localizable.xcstrings` at line 20, The Japanese localization for
"settings.app.fileDrop.defaultBehavior.text" uses the ambiguous term "パステキスト";
update the Japanese "value" to the clearer term "パス文字列" to match the adjacent
localization (line 19) so the label is unambiguous for Japanese users.
Sources/GhosttyTerminalView.swift (1)

9254-9259: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Return false when text insertion has no target.

On Line 9257, terminalSurface?.sendText(text) can no-op while the method still returns true, which misreports drop handling success.

Suggested fix
 func handleDroppedFileURLsAsText(_ urls: [URL]) -> Bool {
     let text = TerminalImageTransferPlanner.insertedText(forFileURLs: urls)
     guard !text.isEmpty else { return false }
-    terminalSurface?.sendText(text)
+    guard let terminalSurface else { return false }
+    terminalSurface.sendText(text)
     return true
 }
🤖 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/GhosttyTerminalView.swift` around lines 9254 - 9259, The method
handleDroppedFileURLsAsText currently returns true even when terminalSurface is
nil and sendText is a no-op; change it to verify a real target before claiming
success: compute text with
TerminalImageTransferPlanner.insertedText(forFileURLs:), guard that text is
non-empty and that terminalSurface is non-nil (or otherwise call a
sendText-returning API and propagate its Bool), then call
terminalSurface.sendText(text) and return true only when a valid surface handled
the text—return false if terminalSurface is missing or the send did not actually
occur.
Sources/ContentView.swift (4)

358-363: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Shift-routing early-returns still leave activeDragWebView / activePaneDropTarget with an unbalanced draggingEntered.

draggingUpdated (hunk 10, 474–483) correctly calls prev.draggingExited(sender) on both activeDragWebView and activePaneDropTarget when transitioning into text-routing mode. However, if the user only adds Shift at the moment of drop (i.e., the last draggingUpdated ran in non-text mode and set up an activeDragWebView/activePaneDropTarget), then:

  • prepareForDragOperation (358–363) clears preparedPaneDropTarget and activePaneDropTarget = nil but never notifies them via draggingExited, and ignores activeDragWebView entirely.
  • performDragOperation (400–406) has the same gap.
  • concludeDragOperation (443–462) defers the nil-out but does not call draggingExited on either before clearing.

Net effect: the previously hovered web view / pane target receives an unbalanced draggingEntered and can leak stale drop indicators or hover state. Mirror the cleanup that draggingUpdated already performs at all three sites:

🛠 Apply at all three early-return sites
 if shouldRouteFileDropToTextDestination(sender) {
+    if let prev = activeDragWebView {
+        prev.draggingExited(sender)
+        activeDragWebView = nil
+    }
+    if let prev = activePaneDropTarget {
+        prev.fileDropDraggingExited(sender)
+        activePaneDropTarget = nil
+    }
     preparedDragWebView = nil
     preparedPaneDropTarget = nil
-    activePaneDropTarget = nil
     ...
 }

In concludeDragOperation, perform the same draggingExited calls before the defer clears the references.

Also applies to: 400-406, 443-462

🤖 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 358 - 363, When early-returning to
route the drop to text in prepareForDragOperation, performDragOperation, and
concludeDragOperation (where you currently set
preparedDragWebView/preparedPaneDropTarget/activePaneDropTarget to nil), first
call draggingExited(sender) on any existing activeDragWebView and
activePaneDropTarget (and on preparedPaneDropTarget if set) to mirror the
cleanup done in draggingUpdated; then clear those references (and only then
return). This ensures any prior draggingEntered is balanced and avoids leaking
hover state; locate the cleanup in prepareForDragOperation,
performDragOperation, and the defer block in concludeDragOperation and add the
draggingExited(sender) calls before nil-ing
activeDragWebView/activePaneDropTarget/preparedPaneDropTarget.

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

CI blocker: Swift file length budget exceeded — extract FileDropHintBadgeView, FileDropPaneTarget, and the pane-target extensions to dedicated files.

The workflow-guard-tests job is now failing with actual=16182, budget=15956 (226 lines over). The newly added FileDropHintBadgeView (~105 lines, 10–114), FileDropPaneTarget protocol (116–124), and the PaneDropTargetView / BrowserPaneDropTargetView conformance extensions (126–142) together account for more than the overage and are entirely self-contained AppKit types with no need to live in ContentView.swift.

Move them into dedicated files under Sources/ (e.g., Sources/FileDropHintBadgeView.swift and Sources/FileDropPaneTarget.swift). Drop the private modifiers on the moved declarations so they default to internal visibility, since FileDropOverlayView (still in this file) needs to reference them.

Based on learnings from PR 3502: extract helper subviews into a dedicated Swift file under Sources to stay under the CI file-length budget; mark them internal (omit private) so other files can reference them.

🤖 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 10 - 142, The file is over budget;
extract FileDropHintBadgeView, FileDropPaneTarget protocol, and the
PaneDropTargetView/BrowserPaneDropTargetView extensions into separate source
files (e.g., Sources/FileDropHintBadgeView.swift and
Sources/FileDropPaneTarget.swift), remove the leading private modifiers so they
are internal (omit private) so FileDropOverlayView and other types can access
them, and ensure the new files import AppKit if needed and preserve all method
and type names (FileDropHintBadgeView, configureNativeGlassIfNeeded,
FileDropPaneTarget, and the two extensions) exactly so existing references
continue to compile.

564-566: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

textDropDestinationKindUnderPoint returns .editor for any WKWebView regardless of editability.

Line 564 returns .editor whenever a WKWebView is under the cursor. This drives three behaviors that all assume a real editable target exists:

  1. updateHintBadge (542–558) — shows a "drop into editor" hint over web views with no editable focus.
  2. shouldRouteFileDropToTextDestination (533–540) — tells prepareForDragOperation to consume the drop instead of letting the web view's native drop handling run.
  3. performFileDropAsText (583–585) — delegates to FileDropTextInsertion.insert(...), but since the upstream gate already returned true, the drop is consumed even when the JS finds no editable element.

Probe editability during draggingUpdated (using the preparedDragWebView caching pattern that already exists for the non-text route) by running evaluateJavaScript with a completion handler that checks whether elementFromPoint is editable, cache that boolean, and consult the cache in textDropDestinationKindUnderPoint so non-editable web content falls through to terminal/native drop handling.

🤖 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 564 - 566,
textDropDestinationKindUnderPoint currently returns .editor whenever
webViewUnderPoint(windowPoint) != nil, causing editor-specific flows to run for
non-editable WKWebView content; change the logic to consult a cached
“webViewIsEditable” boolean that you populate during draggingUpdated by using
the existing preparedDragWebView pattern and calling
preparedDragWebView.evaluateJavaScript("document.elementFromPoint(x,y)...") (or
equivalent elementFromPoint check) with a completion handler to determine if the
element is editable, store that result on the drag state (cache), and then have
textDropDestinationKindUnderPoint return .editor only when
webViewUnderPoint(...) != nil AND the cached webViewIsEditable is true; update
related callers (updateHintBadge, shouldRouteFileDropToTextDestination,
performFileDropAsText / FileDropTextInsertion.insert) to rely on that cached
value so non-editable web content falls through to native/terminal drop
handling.

614-619: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

First-responder fallback in editableTextViewUnderPoint still routes drops to off-cursor field editors.

The fallback at 614–618 returns the window's firstResponder field editor whenever the hit-test walk finds nothing, regardless of where the cursor actually is. Because textDropDestinationKindUnderPoint (560–571) is the gate for both the floating Shift hint badge and the entire shift-routing path:

  • The badge can show "drop into editor" when the cursor is over a non-editable area while a search field elsewhere holds focus.
  • performFileDropAsText will insert the path into that off-cursor field editor — a surprising outcome for a positional gesture.

Either drop the fallback entirely (rely on the hit-test walk), or gate it on the field editor's frame actually containing windowPoint:

🛠 Proposed fix
-        if let fieldEditor = window?.firstResponder as? NSTextView,
-           fieldEditor.isFieldEditor,
-           fieldEditor.isEditable {
-            return fieldEditor
-        }
-        return nil
+        if let fieldEditor = window?.firstResponder as? NSTextView,
+           fieldEditor.isFieldEditor,
+           fieldEditor.isEditable,
+           let frameInWindow = fieldEditor.superview?.convert(fieldEditor.frame, to: nil),
+           frameInWindow.contains(windowPoint) {
+            return fieldEditor
+        }
+        return nil
🤖 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 614 - 619, The fallback in
editableTextViewUnderPoint currently returns the window's firstResponder field
editor unconditionally, which routes drops to off-cursor editors; change the
fallback to only return the field editor if its frame actually contains the
windowPoint (i.e., convert the fieldEditor’s bounds to window coordinates and
check contains(windowPoint)), otherwise return nil—this keeps
textDropDestinationKindUnderPoint and performFileDropAsText driven by the
hit-test walk and prevents inserting into an off-cursor NSTextView.
🤖 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/BrowserWindowPortal.swift`:
- Around line 1445-1457: The current guard treats any file-URL drag with
fileDropBehavior == .copy as captured, but handleDroppedFileURLsAsText(...) can
still fail for non-editable targets or unreadable payloads; change the capture
logic so that when fileDropBehavior == .copy you actually attempt
handleDroppedFileURLsAsText(...) and, if it returns false, treat the drop as not
captured (so it can fall back to preview/split handling). Update the
variables/branching around fileDropBehavior/resolvedFileDropBehavior,
shouldCaptureFileDrop and the guard in BrowserWindowPortal (and the analogous
blocks at the other locations) to consult the result of
handleDroppedFileURLsAsText(...) and only mark the drop captured when that call
succeeds; otherwise allow
shouldCaptureFilePreviewTransfer/shouldCaptureBonsplitTransfer to proceed.

In `@Sources/TerminalPaneDropTargetView.swift`:
- Around line 515-521: The browser pane case is missing so drops routed as text
are incorrectly rejected; update TerminalPaneDropTargetView to handle browser
panes by adding a branch that checks for the browser pane type (e.g.
BrowserPanel or your BrowserPane class used for browser tabs) before the
FilePreviewPanel branch and return false (or call an appropriate
browser-specific handler if wired) so browser drops advertised as text do not
block fallback behavior; apply the same change to the other occurrence around
the 538-543 region where handleFileDropAsText is dispatched.
- Line 125: Add explicit deinit implementations for both
PaneDropZoneOverlayAnimator and PaneDropTargetView to satisfy the
required_deinit lint rule; locate the class declarations for
PaneDropZoneOverlayAnimator and PaneDropTargetView and add a deinit { } block
(perform any necessary teardown there or leave empty if none), ensuring any
retained resources or observers are cleaned up and superclass deinit is
implicitly handled.
- Around line 125-236: The file is too large for the repo guard—extract the
PaneDropZoneOverlayAnimator and its helper methods (class
PaneDropZoneOverlayAnimator, static func applyStyle, func setZone, func
hideImmediately, private func applyFrame, private static func
rectApproximatelyEqual) into a new Sources/<DescriptiveName>.swift file; keep
the class and method signatures unchanged (same access level and final
modifier), import AppKit if needed, remove the moved class from the original
file, and ensure the new file is included in the Sources target so builds and
tests remain unchanged.

---

Outside diff comments:
In `@Sources/BrowserWindowPortal.swift`:
- Around line 1418-1881: This file is over the Swift file-length budget; extract
the cohesive pane-drop/slot helper classes into a new Swift source file: move
the final class BrowserPaneDropTargetView and the final class
WindowBrowserSlotView (including their private vars, methods like
setPaneDropContext(_:), paneDropTargetForDrop(_:), setPortalDragDropZone(_:),
setDropZoneOverlay(zone:), handleDroppedFileURLsAsText(_:at:), and any
DEBUG-only helpers such as logHitTestDecision and clearDragState) into a new
Sources/Swift file, preserve all access levels, imports, and conditional
compilation blocks, remove their definitions from the original file, ensure the
new file is added to the build target, and run a build to fix any missing
references (update any fileprivate/internal usage if needed to maintain
visibility across files).

---

Duplicate comments:
In `@Resources/Localizable.xcstrings`:
- Line 20: The Japanese localization for
"settings.app.fileDrop.defaultBehavior.text" uses the ambiguous term "パステキスト";
update the Japanese "value" to the clearer term "パス文字列" to match the adjacent
localization (line 19) so the label is unambiguous for Japanese users.

In `@Sources/ContentView.swift`:
- Around line 358-363: When early-returning to route the drop to text in
prepareForDragOperation, performDragOperation, and concludeDragOperation (where
you currently set
preparedDragWebView/preparedPaneDropTarget/activePaneDropTarget to nil), first
call draggingExited(sender) on any existing activeDragWebView and
activePaneDropTarget (and on preparedPaneDropTarget if set) to mirror the
cleanup done in draggingUpdated; then clear those references (and only then
return). This ensures any prior draggingEntered is balanced and avoids leaking
hover state; locate the cleanup in prepareForDragOperation,
performDragOperation, and the defer block in concludeDragOperation and add the
draggingExited(sender) calls before nil-ing
activeDragWebView/activePaneDropTarget/preparedPaneDropTarget.
- Around line 10-142: The file is over budget; extract FileDropHintBadgeView,
FileDropPaneTarget protocol, and the
PaneDropTargetView/BrowserPaneDropTargetView extensions into separate source
files (e.g., Sources/FileDropHintBadgeView.swift and
Sources/FileDropPaneTarget.swift), remove the leading private modifiers so they
are internal (omit private) so FileDropOverlayView and other types can access
them, and ensure the new files import AppKit if needed and preserve all method
and type names (FileDropHintBadgeView, configureNativeGlassIfNeeded,
FileDropPaneTarget, and the two extensions) exactly so existing references
continue to compile.
- Around line 564-566: textDropDestinationKindUnderPoint currently returns
.editor whenever webViewUnderPoint(windowPoint) != nil, causing editor-specific
flows to run for non-editable WKWebView content; change the logic to consult a
cached “webViewIsEditable” boolean that you populate during draggingUpdated by
using the existing preparedDragWebView pattern and calling
preparedDragWebView.evaluateJavaScript("document.elementFromPoint(x,y)...") (or
equivalent elementFromPoint check) with a completion handler to determine if the
element is editable, store that result on the drag state (cache), and then have
textDropDestinationKindUnderPoint return .editor only when
webViewUnderPoint(...) != nil AND the cached webViewIsEditable is true; update
related callers (updateHintBadge, shouldRouteFileDropToTextDestination,
performFileDropAsText / FileDropTextInsertion.insert) to rely on that cached
value so non-editable web content falls through to native/terminal drop
handling.
- Around line 614-619: The fallback in editableTextViewUnderPoint currently
returns the window's firstResponder field editor unconditionally, which routes
drops to off-cursor editors; change the fallback to only return the field editor
if its frame actually contains the windowPoint (i.e., convert the fieldEditor’s
bounds to window coordinates and check contains(windowPoint)), otherwise return
nil—this keeps textDropDestinationKindUnderPoint and performFileDropAsText
driven by the hit-test walk and prevents inserting into an off-cursor
NSTextView.

In `@Sources/GhosttyTerminalView.swift`:
- Around line 9254-9259: The method handleDroppedFileURLsAsText currently
returns true even when terminalSurface is nil and sendText is a no-op; change it
to verify a real target before claiming success: compute text with
TerminalImageTransferPlanner.insertedText(forFileURLs:), guard that text is
non-empty and that terminalSurface is non-nil (or otherwise call a
sendText-returning API and propagate its Bool), then call
terminalSurface.sendText(text) and return true only when a valid surface handled
the text—return false if terminalSurface is missing or the send did not actually
occur.
🪄 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: c8b56e3f-a016-4e12-b3c1-b3d818160ae5

📥 Commits

Reviewing files that changed from the base of the PR and between 85f2cf9 and cb4eb89.

📒 Files selected for processing (9)
  • Resources/Localizable.xcstrings
  • Sources/BrowserWindowPortal.swift
  • Sources/ContentView.swift
  • Sources/DragOverlayRoutingPolicy.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/Panels/FilePreviewPanel.swift
  • Sources/TerminalPaneDropTargetView.swift
  • cmuxTests/BrowserPaneDropRoutingTests.swift
  • cmuxTests/FinderFileDropRegressionTests.swift

Comment thread Sources/BrowserWindowPortal.swift Outdated
Comment thread Sources/TerminalPaneDropTargetView.swift Outdated
Comment thread Sources/TerminalPaneDropTargetView.swift Outdated
Comment thread Sources/TerminalPaneDropTargetView.swift
coderabbitai[bot]
coderabbitai Bot previously requested changes May 7, 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: 2

🤖 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 361-366: Cache the result of textDropDestinationKindUnderPoint for
each drag tick and thread it into callers instead of calling it twice;
specifically, compute let cachedKind =
textDropDestinationKindUnderPoint(sender.draggingLocation) once in
prepareForDragOperation and in draggingUpdated, pass cachedKind into
shouldRouteFileDropToTextDestination and updateHintBadge (or add an optional
parameter/overload to shouldRouteFileDropToTextDestination and updateHintBadge
to accept a precomputed drop kind) so they use cachedKind rather than invoking
textDropDestinationKindUnderPoint again; ensure the side effect that toggles
isHidden and the hitTest traversal only runs once per tick by using the cached
value everywhere within that drag event.
- Around line 109-114: The call to the private-API IMP via unsafeBitCast
(selector "setCornerRadius:" / CornerRadiusSetter) is redundant or unsafe;
either remove the block that looks up and calls the IMP in
configureNativeGlassIfNeeded (rely on layer?.cornerRadius = 13 set earlier) or,
if empirical testing shows NSGlassEffectView requires the direct setter, keep
the IMP path but add a short comment above it explaining why layer.cornerRadius
is insufficient for NSGlassEffectView rendering and that the `@convention`(c)
(AnyObject, Selector, CGFloat) signature is a best-effort assumption for 64-bit
macOS so future maintainers understand the risk.
🪄 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: 45ab3403-b850-4f13-a560-ce2a304a3565

📥 Commits

Reviewing files that changed from the base of the PR and between cb4eb89 and 247c40a.

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

Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/ContentView.swift Outdated
coderabbitai[bot]
coderabbitai Bot previously requested changes May 7, 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: 1

♻️ Duplicate comments (4)
Sources/ContentView.swift (4)

109-115: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Redundant private-API setCornerRadius: IMP path.

effectView.layer?.cornerRadius = 13 is already applied at line 39 immediately before configureNativeGlassIfNeeded(_:). If NSGlassEffectView honors the backing layer's cornerRadius, the unsafeBitCast-of-IMP block is dead code with a fragile signature assumption (@convention(c) (AnyObject, Selector, CGFloat) -> Void). If empirical testing actually requires it, leave a one-line comment explaining why the layer property is insufficient so this isn't accidentally removed later.

🤖 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 109 - 115, The IMP invocation for
"setCornerRadius:" in Sources/ContentView.swift is redundant and fragile because
effectView.layer?.cornerRadius = 13 is already set (see
configureNativeGlassIfNeeded(_:)) and the unsafeBitCast assumes a specific C
calling convention and signature; remove the entire
NSSelectorFromString/unsafeBitCast block (including the CornerRadiusSetter usage
and setter call). If testing shows the layer property truly doesn't affect
NSGlassEffectView, keep the IMP path but replace it with a single-line comment
above the block explaining why the layer setter is insufficient and why the
private-API IMP call is required.

565-576: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

textDropDestinationKindUnderPoint returns .editor for any WKWebView, regardless of whether the element under the cursor is editable.

Already raised previously: WKWebView.evaluateJavaScript(_:completionHandler:) is asynchronous, so FileDropTextInsertion.insert cannot synchronously confirm editability. As written, both the hint badge and performFileDropAsText will treat any browser-pane web view (e.g., a non-editable page) as an editor, consuming the drop and breaking the page's native drop handling. Probe editability during draggingUpdated via evaluateJavaScript(_:completionHandler:) and cache the result on preparedDragWebView (or a sibling flag), then use the cached editability at drop time instead of re-running webViewUnderPoint(...).

🤖 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 565 - 576,
textDropDestinationKindUnderPoint currently treats any WKWebView as editable
because webViewUnderPoint is synchronous; update the logic to rely on a cached
editability flag set during draggingUpdated instead of calling webViewUnderPoint
directly: in draggingUpdated, call
WKWebView.evaluateJavaScript(_:completionHandler:) to probe document
activeElement or isContentEditable and store the result on preparedDragWebView
(or a sibling Bool), then change textDropDestinationKindUnderPoint to check
editableTextViewUnderPoint(windowPoint), the cached preparedDragWebView
editability flag for the web view under point, and terminalUnderPoint in that
order; also ensure FileDropTextInsertion.insert uses the same cached flag rather
than re-evaluating editability synchronously.

10-117: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

File-length budget — extract FileDropHintBadgeView to its own file.

This was raised previously and the badge view (plus its private-API glass setup) is still inlined here. Per the established pattern for Sources/ContentView.swift, move FileDropHintBadgeView (and FileDropTextDestinationKind if file-private here) into a dedicated Sources/FileDropHintBadgeView.swift to stay under the CI file-length threshold.

Based on learnings: this repo's CI enforces a Swift file-length budget for large view files (e.g., Sources/ContentView.swift); helper subviews/components should be extracted to a dedicated file under Sources/ (PR 3502).

🤖 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 10 - 117, Extract the
FileDropHintBadgeView class (and any related file-private types like
FileDropTextDestinationKind if present) into a new
Sources/FileDropHintBadgeView.swift file: create the new file, add the necessary
imports (AppKit/Foundation as used), copy the full FileDropHintBadgeView
implementation including configureNativeGlassIfNeeded and preserve its access
level and `@available` annotations, and remove the inlined class from
Sources/ContentView.swift so ContentView references the type from the new file;
ensure any file-private helpers used only by that class remain file-private in
the new file and update any unresolved references in ContentView (none if you
removed only the class) so the project builds.

361-366: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

textDropDestinationKindUnderPoint is still invoked multiple times per drag tick.

Already raised previously and still present:

  • prepareForDragOperation (lines 361 and 365) hit-tests twice.
  • draggingUpdated calls updateHintBadge (line 475) — which hit-tests at line 551 — and then shouldRouteFileDropToTextDestination (line 477) — which hit-tests again at line 539.
  • The nil-vs-non-nil branch on line 486 redundantly re-runs the same hit-test, and is unreachable in the nil branch (the routing predicate already required canDropAsText).

Each invocation toggles isHidden on the overlay and walks contentView.hitTest at ~60 Hz. Cache the resolved FileDropTextDestinationKind? once per call and thread it into shouldRouteFileDropToTextDestination and updateHintBadge (e.g., overloads accepting a precomputed canDropAsText / kind).

Also applies to: 475-489, 538-545

🤖 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 361 - 366, The hit-test
FileDropTextDestinationKind? is being computed multiple times per drag tick
(textDropDestinationKindUnderPoint called from prepareForDragOperation,
draggingUpdated, shouldRouteFileDropToTextDestination, and updateHintBadge);
compute it once at the start of the drag-handling path (e.g., in
prepareForDragOperation/draggingUpdated), store it in a local
FileDropTextDestinationKind? variable, and thread that cached value into
shouldRouteFileDropToTextDestination and updateHintBadge by adding overloads or
optional parameters that accept the precomputed kind (or canDropAsText Bool),
then remove the duplicate calls to textDropDestinationKindUnderPoint and stop
toggling overlay.isHidden based on redundant hit-tests. Ensure all call sites
(prepareForDragOperation, draggingUpdated, and any branches that previously
re-run the test) use the cached value.
🤖 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 446-465: concludeDragOperation is recomputing the routing decision
and can diverge from what performDragOperation actually did; fix by having
performDragOperation record the routing outcome (e.g. set a Bool property like
didRouteFileDropToTextDestination or an enum on the view/controller) when it
chooses the text vs pane path, and then have concludeDragOperation consume that
stored decision instead of calling shouldRouteFileDropToTextDestination(sender)
again; update performDragOperation to set the flag when it calls text drop
handling or paneDropTarget.fileDropPerformDragOperation, and change
concludeDragOperation to guard/branch on that stored flag and then call
paneDropTarget.fileDropConcludeDragOperation(sender) only if the stored decision
indicates the pane path was used, clearing the stored flag as part of the
existing defer cleanup.

---

Duplicate comments:
In `@Sources/ContentView.swift`:
- Around line 109-115: The IMP invocation for "setCornerRadius:" in
Sources/ContentView.swift is redundant and fragile because
effectView.layer?.cornerRadius = 13 is already set (see
configureNativeGlassIfNeeded(_:)) and the unsafeBitCast assumes a specific C
calling convention and signature; remove the entire
NSSelectorFromString/unsafeBitCast block (including the CornerRadiusSetter usage
and setter call). If testing shows the layer property truly doesn't affect
NSGlassEffectView, keep the IMP path but replace it with a single-line comment
above the block explaining why the layer setter is insufficient and why the
private-API IMP call is required.
- Around line 565-576: textDropDestinationKindUnderPoint currently treats any
WKWebView as editable because webViewUnderPoint is synchronous; update the logic
to rely on a cached editability flag set during draggingUpdated instead of
calling webViewUnderPoint directly: in draggingUpdated, call
WKWebView.evaluateJavaScript(_:completionHandler:) to probe document
activeElement or isContentEditable and store the result on preparedDragWebView
(or a sibling Bool), then change textDropDestinationKindUnderPoint to check
editableTextViewUnderPoint(windowPoint), the cached preparedDragWebView
editability flag for the web view under point, and terminalUnderPoint in that
order; also ensure FileDropTextInsertion.insert uses the same cached flag rather
than re-evaluating editability synchronously.
- Around line 10-117: Extract the FileDropHintBadgeView class (and any related
file-private types like FileDropTextDestinationKind if present) into a new
Sources/FileDropHintBadgeView.swift file: create the new file, add the necessary
imports (AppKit/Foundation as used), copy the full FileDropHintBadgeView
implementation including configureNativeGlassIfNeeded and preserve its access
level and `@available` annotations, and remove the inlined class from
Sources/ContentView.swift so ContentView references the type from the new file;
ensure any file-private helpers used only by that class remain file-private in
the new file and update any unresolved references in ContentView (none if you
removed only the class) so the project builds.
- Around line 361-366: The hit-test FileDropTextDestinationKind? is being
computed multiple times per drag tick (textDropDestinationKindUnderPoint called
from prepareForDragOperation, draggingUpdated,
shouldRouteFileDropToTextDestination, and updateHintBadge); compute it once at
the start of the drag-handling path (e.g., in
prepareForDragOperation/draggingUpdated), store it in a local
FileDropTextDestinationKind? variable, and thread that cached value into
shouldRouteFileDropToTextDestination and updateHintBadge by adding overloads or
optional parameters that accept the precomputed kind (or canDropAsText Bool),
then remove the duplicate calls to textDropDestinationKindUnderPoint and stop
toggling overlay.isHidden based on redundant hit-tests. Ensure all call sites
(prepareForDragOperation, draggingUpdated, and any branches that previously
re-run the test) use the cached value.
🪄 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: 927a39b4-184f-44b1-8562-14fe7ed71a5b

📥 Commits

Reviewing files that changed from the base of the PR and between 247c40a and f2c3a14.

📒 Files selected for processing (6)
  • Sources/BrowserWindowPortal.swift
  • Sources/ContentView.swift
  • Sources/DragOverlayRoutingPolicy.swift
  • Sources/Panels/FilePreviewPanel.swift
  • Sources/TerminalPaneDropTargetView.swift
  • cmuxTests/FinderFileDropRegressionTests.swift

Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/BrowserPaneDropTargetView.swift
Comment thread Sources/BrowserPaneDropTargetView.swift
Comment thread Sources/BrowserPaneDropTargetView.swift
@lawrencecchen
lawrencecchen dismissed stale reviews from coderabbitai[bot], coderabbitai[bot], coderabbitai[bot], coderabbitai[bot], and coderabbitai[bot] May 7, 2026 12:18

Stale bot review addressed in later commits on #3684; current checks are green and current-head review threads are resolved.

@lawrencecchen
lawrencecchen merged commit 3887dd4 into main May 7, 2026
25 of 26 checks passed
@lawrencecchen
lawrencecchen deleted the task-shift-drop-files-as-text branch May 7, 2026 12:32

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 59e88e0. Configure here.

// The window overlay delegates Finder/sidebar files to pane-level Bonsplit targets.
_ = hasLocalDraggingSource
guard hasFileURL(pasteboardTypes) else { return false }
guard hasFileDropPayload(pasteboardTypes) else { return false }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Overlay captures file preview drags it cannot process

Medium Severity

shouldCaptureFileDropDestination now uses hasFileDropPayload which returns true for filePreviewTransferType, but FileDropOverlayView only registers for PasteboardFileURLReader.fileURLPasteboardTypes. When a pure internal file-preview drag (no real file URLs) is active, shouldCaptureFileDropOverlay returns true (capturing the hit test), but AppKit won't deliver drag destination methods to the overlay since it's unregistered for that type. This can block BrowserPaneDropTargetView from receiving the drag, potentially causing file-preview drags to silently fail when the overlay is installed.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 59e88e0. Configure here.

if let terminal = terminalUnderPoint(windowPoint) {
return insert(urls, into: terminal)
}
return false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant text computation in performFileDropAsText terminal path

Low Severity

performFileDropAsText computes text via TerminalImageTransferPlanner.insertedText(forFileURLs:) and guards on it being non-empty, but for the terminal path it calls insert(urls, into: terminal) which calls handleDroppedFileURLsAsText that recomputes the same text internally. The pre-computed text variable is unused in the terminal branch, wasting the string allocation and shell-escaping work.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 59e88e0. Configure here.

Comment on lines +179 to +184
guard let dragId = FilePreviewDragPasteboardWriter.dragID(from: pasteboard),
let entry = FilePreviewDragRegistry.shared.entry(id: dragId) else {
return []
}
return [URL(fileURLWithPath: entry.filePath).standardizedFileURL]
}

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 File preview drag URLs get /private/… paths inserted into the terminal

fileURLs(from:) constructs file-preview-transfer URLs as URL(fileURLWithPath: entry.filePath).standardizedFileURL. standardizedFileURL resolves the /tmp → /private/tmp symlink, so a file at /tmp/foo.txt yields .path == "/private/tmp/foo.txt". That URL is then passed straight to TerminalImageTransferPlanner.insertedText(forFileURLs:) which uses url.path without any display-path normalization. The result is that dragging a file-preview tab with a /tmp/… path into a terminal inserts /private/tmp/foo.txt instead of the user-visible /tmp/foo.txt. This is a new code path introduced by this PR (file-preview drags previously never inserted text). The same macOSDisplayPath normalization already applied in FileExplorerTerminalPathInsertion should be applied here, either inside fileURLs(from:) or inside insertedText(forFileURLs:).

This branch was successfully deployed

1 active deployment
Preview – cmux — 59e88e05 Deployed May 7, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant