From 92e481272aa17ee557f2f913d644ea9b4aef0824 Mon Sep 17 00:00:00 2001 From: "@taltas" <6816042+taltas@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:40:50 +0000 Subject: [PATCH 1/2] fix(webview): accept file drag-and-drop into chat input without Shift --- .../src/components/chat/ChatTextArea.tsx | 23 +++++++- .../chat/__tests__/ChatTextArea.spec.tsx | 58 +++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 61419487cc..4413152934 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -36,6 +36,22 @@ import { ZooCodeAuthBadge } from "./ZooCodeAuthBadge" import { usePromptHistory } from "./hooks/usePromptHistory" import { disabledChatControlClassName, enabledChatControlClassName } from "./chatControlStyles" +// Data transfer types that signal a file-oriented drag rather than an in-webview +// text selection drag. "Files" covers drags from the OS file manager; the +// remaining types are set by VS Code when dragging resources from the Explorer +// or editor tabs, in local and remote workspaces alike (see fillEditorsDragData +// in VS Code's workbench drag-and-drop code). +const FILE_DRAG_DATA_TRANSFER_TYPES = [ + "Files", + "text/uri-list", + "ResourceURIs", + "application/vnd.code.files", + "application/vnd.code.uri-list", +] + +const isFileDrag = (dataTransfer: DataTransfer): boolean => + FILE_DRAG_DATA_TRANSFER_TYPES.some((type) => dataTransfer.types.includes(type)) + interface ChatTextAreaProps { inputValue: string setInputValue: (value: string) => void @@ -967,8 +983,11 @@ export const ChatTextArea = forwardRef( className={cn("chat-text-area", !isEditMode && "relative", "flex", "flex-col", "outline-none")} onDrop={handleDrop} onDragOver={(e) => { - // Only allowed to drop images/files on shift key pressed. - if (!e.shiftKey) { + // Accept file drags from the OS file manager, the VS Code + // Explorer, and editor tabs without requiring Shift. In-webview + // text drags, such as moving a selection inside the input, keep + // the browser's native drop behavior unless Shift is held. + if (!e.shiftKey && !isFileDrag(e.dataTransfer)) { setIsDraggingOver(false) return } diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx index 5cced6a92f..dc0c624cd9 100644 --- a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx @@ -521,6 +521,64 @@ describe("ChatTextArea", () => { expect(setInputValue).not.toHaveBeenCalled() }) + describe("dragover acceptance", () => { + // jsdom has no DragEvent constructor, so build a plain cancelable + // Event and attach the drag payload the React handler expects. + const fireDragOver = (element: Element, types: string[], shiftKey = false) => { + const event = new Event("dragover", { bubbles: true, cancelable: true }) + Object.defineProperty(event, "dataTransfer", { + value: { types, files: [], dropEffect: "none" }, + }) + Object.defineProperty(event, "shiftKey", { value: shiftKey }) + fireEvent(element, event) + return event + } + + it("accepts VS Code Explorer file drags without holding Shift", () => { + const { container } = render() + + const event = fireDragOver( + container.querySelector(".chat-text-area")!, + ["text/plain", "ResourceURIs", "application/vnd.code.uri-list", "text/uri-list"], + false, + ) + + expect(event.defaultPrevented).toBe(true) + }) + + it("accepts OS file manager drags without holding Shift", () => { + const { container } = render() + + const event = fireDragOver(container.querySelector(".chat-text-area")!, ["Files"], false) + + expect(event.defaultPrevented).toBe(true) + }) + + it.each(["text/uri-list", "application/vnd.code.files"])("accepts %s drags without Shift", (type) => { + const { container } = render() + + const event = fireDragOver(container.querySelector(".chat-text-area")!, [type], false) + + expect(event.defaultPrevented).toBe(true) + }) + + it("keeps the browser's native behavior for plain text drags without Shift", () => { + const { container } = render() + + const event = fireDragOver(container.querySelector(".chat-text-area")!, ["text/plain"], false) + + expect(event.defaultPrevented).toBe(false) + }) + + it("still accepts plain text drags when Shift is held", () => { + const { container } = render() + + const event = fireDragOver(container.querySelector(".chat-text-area")!, ["text/plain"], true) + + expect(event.defaultPrevented).toBe(true) + }) + }) + describe("prompt history navigation", () => { const mockClineMessages = [ { type: "say", say: "user_feedback", text: "First prompt", ts: 1000 }, From 43ad496a73e9a5d08fb445d0f1a963a207bede57 Mon Sep 17 00:00:00 2001 From: Elliott de Launay Date: Sun, 25 Oct 2026 15:56:56 +0000 Subject: [PATCH 2/2] fix(chat): preserve native link drags and strengthen file drag tests --- .../src/components/chat/ChatTextArea.tsx | 14 ++-- .../chat/__tests__/ChatTextArea.spec.tsx | 66 ++++++++++--------- 2 files changed, 43 insertions(+), 37 deletions(-) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 4413152934..8ba0df7ab2 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -40,17 +40,21 @@ import { disabledChatControlClassName, enabledChatControlClassName } from "./cha // text selection drag. "Files" covers drags from the OS file manager; the // remaining types are set by VS Code when dragging resources from the Explorer // or editor tabs, in local and remote workspaces alike (see fillEditorsDragData -// in VS Code's workbench drag-and-drop code). +// in VS Code's workbench drag-and-drop code). Bare text/uri-list also +// identifies browser link drags, so it must retain native drop behavior. const FILE_DRAG_DATA_TRANSFER_TYPES = [ "Files", - "text/uri-list", - "ResourceURIs", - "application/vnd.code.files", + "ResourceURLs", + "CodeEditors", + "CodeFiles", "application/vnd.code.uri-list", ] +// Chromium lowercases custom drag types; compare without relying on casing. const isFileDrag = (dataTransfer: DataTransfer): boolean => - FILE_DRAG_DATA_TRANSFER_TYPES.some((type) => dataTransfer.types.includes(type)) + dataTransfer.types.some((type) => + FILE_DRAG_DATA_TRANSFER_TYPES.some((fileType) => fileType.toLowerCase() === type.toLowerCase()), + ) interface ChatTextAreaProps { inputValue: string diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx index dc0c624cd9..4668c39f71 100644 --- a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx @@ -526,56 +526,58 @@ describe("ChatTextArea", () => { // Event and attach the drag payload the React handler expects. const fireDragOver = (element: Element, types: string[], shiftKey = false) => { const event = new Event("dragover", { bubbles: true, cancelable: true }) - Object.defineProperty(event, "dataTransfer", { - value: { types, files: [], dropEffect: "none" }, - }) + const dataTransfer = { types, files: [], dropEffect: "none" } + Object.defineProperty(event, "dataTransfer", { value: dataTransfer }) Object.defineProperty(event, "shiftKey", { value: shiftKey }) fireEvent(element, event) - return event + return { event, dataTransfer } } - it("accepts VS Code Explorer file drags without holding Shift", () => { + it.each([ + "Files", + "resourceurls", + "codeeditors", + "codefiles", + "application/vnd.code.uri-list", + "RESOURCEURLS", + ])("accepts %s drags without Shift", (type) => { const { container } = render() - const event = fireDragOver( - container.querySelector(".chat-text-area")!, - ["text/plain", "ResourceURIs", "application/vnd.code.uri-list", "text/uri-list"], - false, - ) + const { event, dataTransfer } = fireDragOver(container.querySelector(".chat-text-area")!, [type]) expect(event.defaultPrevented).toBe(true) + expect(dataTransfer.dropEffect).toBe("copy") + expect(container.querySelector(".border-dashed")).not.toBeNull() }) - it("accepts OS file manager drags without holding Shift", () => { - const { container } = render() + it.each([["text/plain"], ["text/uri-list"], ["text/plain", "text/uri-list"]])( + "keeps native behavior and clears the highlight for drag types %j without Shift", + (...types) => { + const { container } = render() + const target = container.querySelector(".chat-text-area")! + fireDragOver(target, ["Files"]) + expect(container.querySelector(".border-dashed")).not.toBeNull() - const event = fireDragOver(container.querySelector(".chat-text-area")!, ["Files"], false) + const { event, dataTransfer } = fireDragOver(target, types) - expect(event.defaultPrevented).toBe(true) - }) - - it.each(["text/uri-list", "application/vnd.code.files"])("accepts %s drags without Shift", (type) => { - const { container } = render() - - const event = fireDragOver(container.querySelector(".chat-text-area")!, [type], false) - - expect(event.defaultPrevented).toBe(true) - }) - - it("keeps the browser's native behavior for plain text drags without Shift", () => { - const { container } = render() - - const event = fireDragOver(container.querySelector(".chat-text-area")!, ["text/plain"], false) - - expect(event.defaultPrevented).toBe(false) - }) + expect(event.defaultPrevented).toBe(false) + expect(dataTransfer.dropEffect).toBe("none") + expect(container.querySelector(".border-dashed")).toBeNull() + }, + ) it("still accepts plain text drags when Shift is held", () => { const { container } = render() - const event = fireDragOver(container.querySelector(".chat-text-area")!, ["text/plain"], true) + const { event, dataTransfer } = fireDragOver( + container.querySelector(".chat-text-area")!, + ["text/plain"], + true, + ) expect(event.defaultPrevented).toBe(true) + expect(dataTransfer.dropEffect).toBe("copy") + expect(container.querySelector(".border-dashed")).not.toBeNull() }) })