From 705d4b571995f46af74fd45a2231916a95330eb0 Mon Sep 17 00:00:00 2001 From: Khalid Abdi Date: Tue, 16 Jun 2026 21:09:15 +0300 Subject: [PATCH] frontend: fix file attach dropping the picked file (live FileList race) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The file pickers read event.target.files but reset event.target.value = "" before the file was actually used: in the AI composer the value reset ran before React's deferred setState updater read the (now-empty) live FileList, so no chip appeared; in Messages the reset ran before the length check, so it returned early. Snapshot the files into a plain array synchronously, before the reset. This is why attaching seemed to fail (no badge) — especially while typing. Co-Authored-By: Claude Opus 4.8 --- frontend/components/chat/chat-input.tsx | 13 +++++++++---- frontend/components/messages/messages-view.tsx | 9 ++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/frontend/components/chat/chat-input.tsx b/frontend/components/chat/chat-input.tsx index 480aa44..1fcc16f 100644 --- a/frontend/components/chat/chat-input.tsx +++ b/frontend/components/chat/chat-input.tsx @@ -85,12 +85,17 @@ export function ChatInput({ const handleFilesSelected = useCallback( (event: ChangeEvent) => { - const selected = event.target.files; - if (selected && selected.length > 0) { - setFiles((prev) => [...prev, ...Array.from(selected)]); - } + // Copy the files out NOW: `event.target.files` is a live FileList, and the + // `event.target.value = ""` reset below empties it. React runs the + // functional setState updater during a later render, so reading the + // FileList inside the updater would see it already cleared (no file added, + // no chip). Snapshot to a plain array first. + const picked = Array.from(event.target.files ?? []); // Reset so picking the same file again still fires onChange. event.target.value = ""; + if (picked.length > 0) { + setFiles((prev) => [...prev, ...picked]); + } }, [] ); diff --git a/frontend/components/messages/messages-view.tsx b/frontend/components/messages/messages-view.tsx index 6658ffe..ac194eb 100644 --- a/frontend/components/messages/messages-view.tsx +++ b/frontend/components/messages/messages-view.tsx @@ -283,12 +283,15 @@ export function MessagesView() { // Open the file picker; on select, upload each and stage it. const onPickFiles = async (event: ChangeEvent) => { - const files = event.target.files; + // Snapshot before resetting: `event.target.files` is a live FileList that + // `event.target.value = ""` empties, so reading it afterwards (or in a later + // tick) yields nothing. + const picked = Array.from(event.target.files ?? []); event.target.value = ""; - if (!files?.length) return; + if (picked.length === 0) return; setUploading(true); try { - for (const file of Array.from(files)) { + for (const file of picked) { if (file.size > MAX_ATTACHMENT_BYTES) { notify.error( t("messages.attach.tooLargeTitle"),