From 69813b72c243e759a4084595d101361e7e00d6d5 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Fri, 27 Mar 2026 13:28:37 -0500 Subject: [PATCH] Add image attachment picker to chat composer - Add a hidden file input and attach-images button - Support selecting multiple images and re-selecting the same file - Show a drag-and-drop overlay when images are dropped --- apps/web/src/components/ChatView.tsx | 48 +++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index d19c9427c..e1979639b 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -96,9 +96,11 @@ import { ChevronLeftIcon, ChevronRightIcon, CircleAlertIcon, + ImagePlusIcon, ListTodoIcon, LockIcon, LockOpenIcon, + PaperclipIcon, XIcon, } from "lucide-react"; import { Button } from "./ui/button"; @@ -414,6 +416,7 @@ export default function ChatView({ threadId }: ChatViewProps) { const attachmentPreviewHandoffTimeoutByMessageIdRef = useRef>({}); const sendInFlightRef = useRef(false); const dragDepthRef = useRef(0); + const fileInputRef = useRef(null); const terminalOpenByThreadRef = useRef>({}); const setMessagesScrollContainerRef = useCallback((element: HTMLDivElement | null) => { messagesScrollRef.current = element; @@ -2339,6 +2342,19 @@ export default function ChatView({ threadId }: ChatViewProps) { focusComposer(); }; + const onFileInputChange = (event: React.ChangeEvent) => { + const files = Array.from(event.target.files ?? []); + if (files.length > 0) { + addComposerImages(files); + } + // Reset so the same file can be selected again + event.target.value = ""; + }; + + const openFilePicker = () => { + fileInputRef.current?.click(); + }; + const onRevertToTurnCount = useCallback( async (turnCount: number) => { const api = readNativeApi(); @@ -3776,9 +3792,18 @@ export default function ChatView({ threadId }: ChatViewProps) { className="mx-auto w-full min-w-0 max-w-3xl" data-chat-composer-form="true" > +
+ {isDragOverComposer && ( +
+
+ + Drop images to attach +
+
+ )}
+ {pendingUserInputs.length === 0 && ( + + )} {activeContextWindow ? ( ) : null}