@@ -19,6 +19,7 @@ import { FilePicker } from "./FilePicker";
1919import { ImagePreview } from "./ImagePreview" ;
2020import { type FileEntry } from "@/lib/api" ;
2121import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow" ;
22+ import { invoke } from "@tauri-apps/api/core" ;
2223
2324interface FloatingPromptInputProps {
2425 /**
@@ -199,7 +200,8 @@ const FloatingPromptInputInner = (
199200 return currentPrompt ; // Image already added
200201 }
201202
202- const mention = `@${ imagePath } ` ;
203+ // Wrap path in quotes if it contains spaces
204+ const mention = imagePath . includes ( ' ' ) ? `@"${ imagePath } "` : `@${ imagePath } ` ;
203205 const newPrompt = currentPrompt + ( currentPrompt . endsWith ( ' ' ) || currentPrompt === '' ? '' : ' ' ) + mention + ' ' ;
204206
205207 // Focus the textarea
@@ -225,19 +227,49 @@ const FloatingPromptInputInner = (
225227 // Extract image paths from prompt text
226228 const extractImagePaths = ( text : string ) : string [ ] => {
227229 console . log ( '[extractImagePaths] Input text:' , text ) ;
228- const regex = / @ ( [ ^ \s ] + ) / g;
229- const matches = Array . from ( text . matchAll ( regex ) ) ;
230- console . log ( '[extractImagePaths] Regex matches:' , matches . map ( m => m [ 0 ] ) ) ;
230+
231+ // Updated regex to handle both quoted and unquoted paths
232+ // Pattern 1: @"path with spaces" - quoted paths
233+ // Pattern 2: @path - unquoted paths (continues until @ or end)
234+ const quotedRegex = / @ " ( [ ^ " ] + ) " / g;
235+ const unquotedRegex = / @ ( [ ^ @ \n \s ] + ) / g;
236+
231237 const pathsSet = new Set < string > ( ) ; // Use Set to ensure uniqueness
232-
238+
239+ // First, extract quoted paths
240+ let matches = Array . from ( text . matchAll ( quotedRegex ) ) ;
241+ console . log ( '[extractImagePaths] Quoted matches:' , matches . map ( m => m [ 0 ] ) ) ;
242+
233243 for ( const match of matches ) {
234- const path = match [ 1 ] ;
235- console . log ( '[extractImagePaths] Processing path:' , path ) ;
244+ const path = match [ 1 ] ; // No need to trim, quotes preserve exact path
245+ console . log ( '[extractImagePaths] Processing quoted path:' , path ) ;
246+
236247 // Convert relative path to absolute if needed
237248 const fullPath = path . startsWith ( '/' ) ? path : ( projectPath ? `${ projectPath } /${ path } ` : path ) ;
238249 console . log ( '[extractImagePaths] Full path:' , fullPath , 'Is image:' , isImageFile ( fullPath ) ) ;
250+
239251 if ( isImageFile ( fullPath ) ) {
240- pathsSet . add ( fullPath ) ; // Add to Set (automatically handles duplicates)
252+ pathsSet . add ( fullPath ) ;
253+ }
254+ }
255+
256+ // Remove quoted mentions from text to avoid double-matching
257+ let textWithoutQuoted = text . replace ( quotedRegex , '' ) ;
258+
259+ // Then extract unquoted paths
260+ matches = Array . from ( textWithoutQuoted . matchAll ( unquotedRegex ) ) ;
261+ console . log ( '[extractImagePaths] Unquoted matches:' , matches . map ( m => m [ 0 ] ) ) ;
262+
263+ for ( const match of matches ) {
264+ const path = match [ 1 ] . trim ( ) ;
265+ console . log ( '[extractImagePaths] Processing unquoted path:' , path ) ;
266+
267+ // Convert relative path to absolute if needed
268+ const fullPath = path . startsWith ( '/' ) ? path : ( projectPath ? `${ projectPath } /${ path } ` : path ) ;
269+ console . log ( '[extractImagePaths] Full path:' , fullPath , 'Is image:' , isImageFile ( fullPath ) ) ;
270+
271+ if ( isImageFile ( fullPath ) ) {
272+ pathsSet . add ( fullPath ) ;
241273 }
242274 }
243275
@@ -295,7 +327,14 @@ const FloatingPromptInputInner = (
295327 return currentPrompt ; // All dropped images are already in the prompt
296328 }
297329
298- const mentionsToAdd = newPaths . map ( p => `@${ p } ` ) . join ( ' ' ) ;
330+ // Wrap paths with spaces in quotes for clarity
331+ const mentionsToAdd = newPaths . map ( p => {
332+ // If path contains spaces, wrap in quotes
333+ if ( p . includes ( ' ' ) ) {
334+ return `@"${ p } "` ;
335+ }
336+ return `@${ p } ` ;
337+ } ) . join ( ' ' ) ;
299338 const newPrompt = currentPrompt + ( currentPrompt . endsWith ( ' ' ) || currentPrompt === '' ? '' : ' ' ) + mentionsToAdd + ' ' ;
300339
301340 setTimeout ( ( ) => {
@@ -438,6 +477,60 @@ const FloatingPromptInputInner = (
438477 }
439478 } ;
440479
480+ const handlePaste = async ( e : React . ClipboardEvent ) => {
481+ const items = e . clipboardData ?. items ;
482+ if ( ! items || ! projectPath ) return ;
483+
484+ for ( const item of items ) {
485+ if ( item . type . startsWith ( 'image/' ) ) {
486+ e . preventDefault ( ) ;
487+
488+ // Get the image blob
489+ const blob = item . getAsFile ( ) ;
490+ if ( ! blob ) continue ;
491+
492+ try {
493+ // Convert blob to base64
494+ const reader = new FileReader ( ) ;
495+ reader . onload = async ( ) => {
496+ const base64Data = reader . result as string ;
497+
498+ // Generate a session-specific ID for the image
499+ const sessionId = `paste-${ Date . now ( ) } ` ;
500+
501+ // Save the image via Tauri command
502+ const imagePath = await invoke < string > ( 'save_clipboard_image' , {
503+ projectPath,
504+ sessionId,
505+ imageData : base64Data ,
506+ mimeType : item . type
507+ } ) ;
508+
509+ // Add the image path as a mention to the prompt
510+ setPrompt ( currentPrompt => {
511+ // Wrap path in quotes if it contains spaces
512+ const mention = imagePath . includes ( ' ' ) ? `@"${ imagePath } "` : `@${ imagePath } ` ;
513+ const newPrompt = currentPrompt + ( currentPrompt . endsWith ( ' ' ) || currentPrompt === '' ? '' : ' ' ) + mention + ' ' ;
514+
515+ // Focus the textarea and move cursor to end
516+ setTimeout ( ( ) => {
517+ const target = isExpanded ? expandedTextareaRef . current : textareaRef . current ;
518+ target ?. focus ( ) ;
519+ target ?. setSelectionRange ( newPrompt . length , newPrompt . length ) ;
520+ } , 0 ) ;
521+
522+ return newPrompt ;
523+ } ) ;
524+ } ;
525+
526+ reader . readAsDataURL ( blob ) ;
527+ } catch ( error ) {
528+ console . error ( 'Failed to paste image:' , error ) ;
529+ }
530+ }
531+ }
532+ } ;
533+
441534 // Browser drag and drop handlers - just prevent default behavior
442535 // Actual file handling is done via Tauri's window-level drag-drop events
443536 const handleDrag = ( e : React . DragEvent ) => {
@@ -455,9 +548,19 @@ const FloatingPromptInputInner = (
455548 const handleRemoveImage = ( index : number ) => {
456549 // Remove the corresponding @mention from the prompt
457550 const imagePath = embeddedImages [ index ] ;
551+ const escapedPath = imagePath . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
552+ const escapedRelativePath = imagePath . replace ( projectPath + '/' , '' ) . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
553+
554+ // Create patterns for both quoted and unquoted mentions
458555 const patterns = [
459- new RegExp ( `@${ imagePath . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) } \\s?` , 'g' ) ,
460- new RegExp ( `@${ imagePath . replace ( projectPath + '/' , '' ) . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) } \\s?` , 'g' )
556+ // Quoted full path
557+ new RegExp ( `@"${ escapedPath } "\\s?` , 'g' ) ,
558+ // Unquoted full path
559+ new RegExp ( `@${ escapedPath } \\s?` , 'g' ) ,
560+ // Quoted relative path
561+ new RegExp ( `@"${ escapedRelativePath } "\\s?` , 'g' ) ,
562+ // Unquoted relative path
563+ new RegExp ( `@${ escapedRelativePath } \\s?` , 'g' )
461564 ] ;
462565
463566 let newPrompt = prompt ;
@@ -514,6 +617,7 @@ const FloatingPromptInputInner = (
514617 ref = { expandedTextareaRef }
515618 value = { prompt }
516619 onChange = { handleTextChange }
620+ onPaste = { handlePaste }
517621 placeholder = "Type your prompt here..."
518622 className = "min-h-[200px] resize-none"
519623 disabled = { disabled }
@@ -756,6 +860,7 @@ const FloatingPromptInputInner = (
756860 value = { prompt }
757861 onChange = { handleTextChange }
758862 onKeyDown = { handleKeyDown }
863+ onPaste = { handlePaste }
759864 placeholder = { dragActive ? "Drop images here..." : "Ask Claude anything..." }
760865 disabled = { disabled }
761866 className = { cn (
@@ -808,7 +913,7 @@ const FloatingPromptInputInner = (
808913 </ div >
809914
810915 < div className = "mt-2 text-xs text-muted-foreground" >
811- Press Enter to send, Shift+Enter for new line{ projectPath ?. trim ( ) && ", @ to mention files, drag & drop images" }
916+ Press Enter to send, Shift+Enter for new line{ projectPath ?. trim ( ) && ", @ to mention files, drag & drop or paste images" }
812917 </ div >
813918 </ div >
814919 </ div >
0 commit comments