@@ -96,9 +96,11 @@ import {
9696 ChevronLeftIcon ,
9797 ChevronRightIcon ,
9898 CircleAlertIcon ,
99+ ImagePlusIcon ,
99100 ListTodoIcon ,
100101 LockIcon ,
101102 LockOpenIcon ,
103+ PaperclipIcon ,
102104 XIcon ,
103105} from "lucide-react" ;
104106import { Button } from "./ui/button" ;
@@ -414,6 +416,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
414416 const attachmentPreviewHandoffTimeoutByMessageIdRef = useRef < Record < string , number > > ( { } ) ;
415417 const sendInFlightRef = useRef ( false ) ;
416418 const dragDepthRef = useRef ( 0 ) ;
419+ const fileInputRef = useRef < HTMLInputElement > ( null ) ;
417420 const terminalOpenByThreadRef = useRef < Record < string , boolean > > ( { } ) ;
418421 const setMessagesScrollContainerRef = useCallback ( ( element : HTMLDivElement | null ) => {
419422 messagesScrollRef . current = element ;
@@ -2339,6 +2342,19 @@ export default function ChatView({ threadId }: ChatViewProps) {
23392342 focusComposer ( ) ;
23402343 } ;
23412344
2345+ const onFileInputChange = ( event : React . ChangeEvent < HTMLInputElement > ) => {
2346+ const files = Array . from ( event . target . files ?? [ ] ) ;
2347+ if ( files . length > 0 ) {
2348+ addComposerImages ( files ) ;
2349+ }
2350+ // Reset so the same file can be selected again
2351+ event . target . value = "" ;
2352+ } ;
2353+
2354+ const openFilePicker = ( ) => {
2355+ fileInputRef . current ?. click ( ) ;
2356+ } ;
2357+
23422358 const onRevertToTurnCount = useCallback (
23432359 async ( turnCount : number ) => {
23442360 const api = readNativeApi ( ) ;
@@ -3776,16 +3792,33 @@ export default function ChatView({ threadId }: ChatViewProps) {
37763792 className = "mx-auto w-full min-w-0 max-w-3xl"
37773793 data-chat-composer-form = "true"
37783794 >
3795+ < input
3796+ ref = { fileInputRef }
3797+ type = "file"
3798+ accept = "image/*"
3799+ multiple
3800+ className = "hidden"
3801+ onChange = { onFileInputChange }
3802+ tabIndex = { - 1 }
3803+ />
37793804 < div
37803805 className = { cn (
3781- "group rounded-[22px] p-px transition-colors duration-200" ,
3806+ "group relative rounded-[22px] p-px transition-colors duration-200" ,
37823807 composerProviderState . composerFrameClassName ,
37833808 ) }
37843809 onDragEnter = { onComposerDragEnter }
37853810 onDragOver = { onComposerDragOver }
37863811 onDragLeave = { onComposerDragLeave }
37873812 onDrop = { onComposerDrop }
37883813 >
3814+ { isDragOverComposer && (
3815+ < div className = "pointer-events-none absolute inset-0 z-10 flex items-center justify-center rounded-[22px] border-2 border-dashed border-primary/60 bg-primary/5" >
3816+ < div className = "flex items-center gap-2 rounded-lg bg-background/90 px-4 py-2 text-sm font-medium text-primary shadow-sm" >
3817+ < ImagePlusIcon className = "size-4" />
3818+ Drop images to attach
3819+ </ div >
3820+ </ div >
3821+ ) }
37893822 < div
37903823 className = { cn (
37913824 "rounded-[20px] border bg-card transition-colors duration-200 focus-within:border-ring/45" ,
@@ -4100,6 +4133,19 @@ export default function ChatView({ threadId }: ChatViewProps) {
41004133 data-chat-composer-actions = "right"
41014134 className = "flex shrink-0 items-center gap-2"
41024135 >
4136+ { pendingUserInputs . length === 0 && (
4137+ < Button
4138+ variant = "ghost"
4139+ size = "icon-sm"
4140+ type = "button"
4141+ className = "text-muted-foreground/70 hover:text-foreground/80"
4142+ onClick = { openFilePicker }
4143+ title = "Attach images"
4144+ aria-label = "Attach images"
4145+ >
4146+ < PaperclipIcon className = "size-4" />
4147+ </ Button >
4148+ ) }
41034149 { activeContextWindow ? (
41044150 < ContextWindowMeter usage = { activeContextWindow } />
41054151 ) : null }
0 commit comments