From 486a1b759a6c91831f3f983e89526dd74bf5f39c Mon Sep 17 00:00:00 2001 From: daraadedeji07 Date: Wed, 16 Sep 2026 00:04:42 +0000 Subject: [PATCH] fix(web): skip dropped folders instead of staging failed attachments Dropping folders in Chromium staged an attachment that failed to upload and blocked Send. Filter directory entries from dropped files before passing them to the composer while preserving the files fallback for browsers without DataTransfer items. Made with Devin (Claude) via Devin. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../components/chat/workspaceFileDrop.test.ts | 40 +++++++++++++++++++ .../src/components/chat/workspaceFileDrop.ts | 19 ++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/chat/workspaceFileDrop.test.ts b/apps/web/src/components/chat/workspaceFileDrop.test.ts index ec5d074a3eb7..3a81e35c2a2d 100644 --- a/apps/web/src/components/chat/workspaceFileDrop.test.ts +++ b/apps/web/src/components/chat/workspaceFileDrop.test.ts @@ -8,6 +8,7 @@ import { function makeDragEvent(options?: { types?: string[]; files?: File[]; + items?: NonNullable; movedWithinTarget?: boolean; }) { const preventDefault = vi.fn(); @@ -16,6 +17,7 @@ function makeDragEvent(options?: { types: options?.types ?? ["Files"], files: options?.files ?? [], dropEffect: "none", + ...(options?.items === undefined ? {} : { items: options.items }), }, relatedTarget: options?.movedWithinTarget ? ({} as EventTarget) : null, currentTarget: { @@ -75,4 +77,42 @@ describe("makeWorkspaceFileDropHandlers", () => { expect(setDragActive).toHaveBeenCalledWith(false); expect(addFiles).toHaveBeenCalledWith([file]); }); + + it("skips dropped directories in mixed drops", () => { + const file = new File(["contents"], "example.txt", { type: "text/plain" }); + const directory = { + kind: "file", + getAsFile: () => null, + webkitGetAsEntry: () => ({ isDirectory: true }), + }; + const { host, addFiles } = makeHost(); + const { event } = makeDragEvent({ + items: [ + directory, + { + kind: "file", + getAsFile: () => file, + webkitGetAsEntry: () => ({ isDirectory: false }), + }, + ], + }); + + makeWorkspaceFileDropHandlers(host).onDrop(event); + + expect(addFiles).toHaveBeenCalledWith([file]); + }); + + it("passes an empty list when a drop only contains a directory", () => { + const directory = { + kind: "file", + getAsFile: () => null, + webkitGetAsEntry: () => ({ isDirectory: true }), + }; + const { host, addFiles } = makeHost(); + const { event } = makeDragEvent({ items: [directory] }); + + makeWorkspaceFileDropHandlers(host).onDrop(event); + + expect(addFiles).toHaveBeenCalledWith([]); + }); }); diff --git a/apps/web/src/components/chat/workspaceFileDrop.ts b/apps/web/src/components/chat/workspaceFileDrop.ts index 132a8051e159..3e6dbf3570f4 100644 --- a/apps/web/src/components/chat/workspaceFileDrop.ts +++ b/apps/web/src/components/chat/workspaceFileDrop.ts @@ -2,6 +2,11 @@ export interface WorkspaceFileDragEvent { readonly dataTransfer: { readonly types: ReadonlyArray; readonly files: Iterable; + readonly items?: Iterable<{ + readonly kind: string; + getAsFile(): File | null; + webkitGetAsEntry(): { readonly isDirectory: boolean } | null; + }>; dropEffect: string; }; readonly relatedTarget: EventTarget | null; @@ -24,6 +29,18 @@ function movedWithinDropTarget(event: WorkspaceFileDragEvent): boolean { return event.relatedTarget !== null && event.currentTarget.contains(event.relatedTarget as Node); } +function droppedFiles(dataTransfer: WorkspaceFileDragEvent["dataTransfer"]): File[] { + if (dataTransfer.items === undefined) return Array.from(dataTransfer.files); + + const files: File[] = []; + for (const item of dataTransfer.items) { + if (item.kind !== "file" || item.webkitGetAsEntry()?.isDirectory === true) continue; + const file = item.getAsFile(); + if (file !== null) files.push(file); + } + return files; +} + export function makeWorkspaceFileDropHandlers(host: WorkspaceFileDropHost) { return { onDragEnter(event: WorkspaceFileDragEvent) { @@ -48,7 +65,7 @@ export function makeWorkspaceFileDropHandlers(host: WorkspaceFileDropHost) { if (!isFileDrag(event)) return; event.preventDefault(); host.setDragActive(false); - host.addFiles(Array.from(event.dataTransfer.files)); + host.addFiles(droppedFiles(event.dataTransfer)); }, }; }