From 04431751ceeaec7d5e6fe443fdd05902e7629161 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Fri, 20 Feb 2026 18:26:23 -0800 Subject: [PATCH] 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 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 * 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 --- Sources/ContentView.swift | 66 ++++++++++++++++++++++++++++--- Sources/Panels/BrowserPanel.swift | 16 ++++++++ Sources/Panels/CmuxWebView.swift | 17 ++++++-- 3 files changed, 91 insertions(+), 8 deletions(-) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index e5b5bea3..7e77f333 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -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, diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index 59c23491..f2b86d0c 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -2183,4 +2183,20 @@ private class BrowserUIDelegate: NSObject, WKUIDelegate { } return nil } + + /// Handle 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) + } + } } diff --git a/Sources/Panels/CmuxWebView.swift b/Sources/Panels/CmuxWebView.swift index e0131b9f..08843c0f 100644 --- a/Sources/Panels/CmuxWebView.swift +++ b/Sources/Panels/CmuxWebView.swift @@ -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 = [ + .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) {