Fix file drag-and-drop and file input in browser panel (#214)

* Fix file drag-and-drop and file input in browser panel (#194)

Two fixes for the browser panel:

1. File drag-and-drop from Finder: CmuxWebView previously suppressed ALL
   drag type registration as a no-op to prevent bonsplit tab drags from
   being intercepted. Now it selectively filters out only the text-based
   types that conflict with bonsplit (public.text, public.utf8-plain-text,
   public.plain-text) and the custom tab transfer types, while allowing
   file URL types through so Finder drops work.

2. File <input> elements: Added the WKUIDelegate runOpenPanelWith method
   to BrowserUIDelegate so clicking a file input opens the native macOS
   file picker (NSOpenPanel), with support for multiple selection and
   directory picking as specified by the HTML element.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(claude-opus-4-6): take a look at https://github.com/manaflow-ai/cmux/issues...

* ok

* wok

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Austin Wang 2026-02-20 18:26:23 -08:00 committed by GitHub
parent 6cb282bf09
commit 04431751ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 91 additions and 8 deletions

View file

@ -3,6 +3,7 @@ import Bonsplit
import SwiftUI
import ObjectiveC
import UniformTypeIdentifiers
import WebKit
struct ShortcutHintPillBackground: View {
var emphasis: Double = 1.0
@ -172,6 +173,9 @@ final class FileDropOverlayView: NSView {
/// Fallback handler when no terminal is found under the drop point.
var onDrop: (([URL]) -> Bool)?
private var isForwardingMouseEvent = false
/// The WKWebView currently receiving forwarded drag events, so we can
/// synthesize draggingExited/draggingEntered as the cursor moves.
private weak var activeDragWebView: WKWebView?
override var acceptsFirstResponder: Bool { false }
@ -248,30 +252,82 @@ final class FileDropOverlayView: NSView {
override func otherMouseDragged(with event: NSEvent) { forwardEvent(event) }
override func scrollWheel(with event: NSEvent) { forwardEvent(event) }
// MARK: NSDraggingDestination only accept file drops over terminal views.
// MARK: NSDraggingDestination accept file drops over terminal and browser views.
//
// AppKit sends draggingEntered once when the drag enters this overlay, then
// draggingUpdated as the cursor moves within it. We track which WKWebView (if
// any) is under the cursor and synthesize enter/exit calls so the browser's
// HTML5 drag events (dragenter, dragleave, drop) fire correctly.
override func draggingEntered(_ sender: any NSDraggingInfo) -> NSDragOperation {
return dragOperationForSender(sender)
return updateDragTarget(sender)
}
override func draggingUpdated(_ sender: any NSDraggingInfo) -> NSDragOperation {
return dragOperationForSender(sender)
return updateDragTarget(sender)
}
override func draggingExited(_ sender: (any NSDraggingInfo)?) {
if let prev = activeDragWebView {
prev.draggingExited(sender)
activeDragWebView = nil
}
}
override func performDragOperation(_ sender: any NSDraggingInfo) -> Bool {
let webView = activeDragWebView
activeDragWebView = nil
if let webView {
return webView.performDragOperation(sender)
}
guard let terminal = terminalUnderPoint(sender.draggingLocation) else { return false }
return terminal.performDragOperation(sender)
}
private func dragOperationForSender(_ sender: any NSDraggingInfo) -> NSDragOperation {
private func updateDragTarget(_ sender: any NSDraggingInfo) -> NSDragOperation {
let loc = sender.draggingLocation
let webView = webViewUnderPoint(loc)
// Cursor moved away from the previous web view.
if let prev = activeDragWebView, prev !== webView {
prev.draggingExited(sender)
activeDragWebView = nil
}
if let webView {
if activeDragWebView !== webView {
// Cursor entered a (new) web view send draggingEntered.
activeDragWebView = webView
return webView.draggingEntered(sender)
}
return webView.draggingUpdated(sender)
}
// Over a terminal (or nothing).
guard let types = sender.draggingPasteboard.types,
types.contains(.fileURL),
terminalUnderPoint(sender.draggingLocation) != nil else {
terminalUnderPoint(loc) != nil else {
return []
}
return .copy
}
/// Hit-tests the window to find a WKWebView (browser panel) under the cursor.
private func webViewUnderPoint(_ windowPoint: NSPoint) -> WKWebView? {
guard let window, let contentView = window.contentView else { return nil }
isHidden = true
defer { isHidden = false }
let point = contentView.convert(windowPoint, from: nil)
let hitView = contentView.hitTest(point)
var current: NSView? = hitView
while let view = current {
if let webView = view as? WKWebView { return webView }
current = view.superview
}
return nil
}
/// Hit-tests the window to find the GhosttyNSView under the cursor.
func terminalUnderPoint(_ windowPoint: NSPoint) -> GhosttyNSView? {
if let window,

View file

@ -2183,4 +2183,20 @@ private class BrowserUIDelegate: NSObject, WKUIDelegate {
}
return nil
}
/// Handle <input type="file"> elements by presenting the native file picker.
func webView(
_ webView: WKWebView,
runOpenPanelWith parameters: WKOpenPanelParameters,
initiatedByFrame frame: WKFrameInfo,
completionHandler: @escaping ([URL]?) -> Void
) {
let panel = NSOpenPanel()
panel.allowsMultipleSelection = parameters.allowsMultipleSelection
panel.canChooseDirectories = parameters.allowsDirectories
panel.canChooseFiles = true
panel.begin { result in
completionHandler(result == .OK ? panel.urls : nil)
}
}
}

View file

@ -117,10 +117,21 @@ final class CmuxWebView: WKWebView {
// of SwiftUI's sibling .onDrop overlays. Rejecting in draggingEntered doesn't help because
// AppKit only bubbles up through superviews, not siblings.
//
// Fix: prevent WKWebView from registering as a drag destination entirely. AppKit won't
// route drags here, so they reach the SwiftUI overlay drop zones as intended.
// Fix: filter out text-based types that conflict with bonsplit tab drags, but keep
// file URL types so Finder file drops and HTML drag-and-drop work.
private static let blockedDragTypes: Set<NSPasteboard.PasteboardType> = [
.string, // public.utf8-plain-text matches bonsplit's NSString tab drags
NSPasteboard.PasteboardType("public.text"),
NSPasteboard.PasteboardType("public.plain-text"),
NSPasteboard.PasteboardType("com.splittabbar.tabtransfer"),
NSPasteboard.PasteboardType("com.cmux.sidebar-tab-reorder"),
]
override func registerForDraggedTypes(_ newTypes: [NSPasteboard.PasteboardType]) {
// No-op: suppress WKWebView's automatic drag type registration.
let filtered = newTypes.filter { !Self.blockedDragTypes.contains($0) }
if !filtered.isEmpty {
super.registerForDraggedTypes(filtered)
}
}
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {