@@ -14,7 +14,7 @@ import { useTranslation } from '../i18n/index.js';
1414import { getPlanModeManager } from '../../commands/plan.js' ;
1515import { TextBuffer } from '../textBuffer.js' ;
1616import { handleTextBufferKey , type KeyHandlerResult } from '../textBufferKeyHandler.js' ;
17- import { getPromptBlockWidth , isShiftEnterResidualSequence } from '../inputPrompt.js' ;
17+ import { getPromptBlockWidth , isShiftEnterResidualSequence , processImagesInText } from '../inputPrompt.js' ;
1818import { renderTerminalMarkdown } from '../../core/immediateCommandRouter.js' ;
1919
2020export interface AgentUIState {
@@ -44,6 +44,8 @@ export interface AgentUIProps {
4444 onToggleLiveCommandExpanded ?: ( ) => void ;
4545 onInputChange ?: ( input : string ) => void ;
4646 enableQueueInput ?: boolean ;
47+ /** Called when a dragged/dropped image is detected in the input */
48+ onImageDetected ?: ( data : Buffer , mimeType : string , filename ?: string ) => number ;
4749}
4850
4951interface TextBufferKeyInfo {
@@ -55,6 +57,8 @@ interface TextBufferKeyInfo {
5557}
5658
5759const INK_TEXTBUFFER_VIEWPORT_HEIGHT = 10 ;
60+ /** Debounce delay for image detection after input changes (ms) */
61+ const INK_IMAGE_SCAN_DELAY_MS = 150 ;
5862
5963function getInkTextBufferViewportWidth ( columns : number | undefined ) : number {
6064 return Math . max ( 1 , getPromptBlockWidth ( columns ) - 4 ) ;
@@ -133,14 +137,31 @@ export function getComposerHelpLine(
133137 return `${ contextDisplay } ${ contextDisplay ? ' · ' : '' } ${ commandHint } ` ;
134138}
135139
140+ /**
141+ * Check if text potentially contains an image path (quick heuristic).
142+ * Mirrors the logic from inputPrompt.ts.
143+ */
144+ function hasPotentialImagePath ( text : string ) : boolean {
145+ const imageExtPattern = / \. ( p n g | j p g | j p e g | g i f | w e b p ) $ / i;
146+ // Check for quoted paths, escaped paths, or simple paths
147+ if ( imageExtPattern . test ( text ) ) {
148+ return true ;
149+ }
150+ if ( / [ " ' ] .* \. ( p n g | j p g | j p e g | g i f | w e b p ) [ " ' ] / i. test ( text ) ) {
151+ return true ;
152+ }
153+ return false ;
154+ }
155+
136156export function AgentUI ( {
137157 state,
138158 onInstruction,
139159 onEscape,
140160 onCtrlC,
141161 onToggleLiveCommandExpanded,
142162 onInputChange,
143- enableQueueInput = true
163+ enableQueueInput = true ,
164+ onImageDetected,
144165} : AgentUIProps ) {
145166 const { exit } = useApp ( ) ;
146167 const { colors } = useTheme ( ) ;
@@ -158,6 +179,11 @@ export function AgentUI({
158179 )
159180 ) ;
160181
182+ // Track the last processed input to avoid re-processing the same text
183+ const lastProcessedInputRef = useRef < string > ( '' ) ;
184+ // Debounce timer for image scanning
185+ const imageScanTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
186+
161187 const syncInputFromBuffer = useCallback ( ( ) => {
162188 const buffer = textBufferRef . current ;
163189 setInput ( buffer . getText ( ) ) ;
@@ -219,6 +245,57 @@ export function AgentUI({
219245 }
220246 } , [ ctrlCCount ] ) ;
221247
248+ // Debounced image detection: when input changes and contains potential image paths,
249+ // process them through processImagesInText and update the input with [Image #N] placeholders.
250+ useEffect ( ( ) => {
251+ if ( ! onImageDetected ) {
252+ return ;
253+ }
254+
255+ // Clear any pending scan
256+ if ( imageScanTimerRef . current ) {
257+ clearTimeout ( imageScanTimerRef . current ) ;
258+ imageScanTimerRef . current = null ;
259+ }
260+
261+ // Skip if already processed (e.g., after a replacement)
262+ if ( input === lastProcessedInputRef . current ) {
263+ return ;
264+ }
265+
266+ // Quick heuristic check before scheduling the scan
267+ if ( ! hasPotentialImagePath ( input ) ) {
268+ lastProcessedInputRef . current = input ;
269+ return ;
270+ }
271+
272+ // Debounce: wait for typing to settle before scanning
273+ imageScanTimerRef . current = setTimeout ( ( ) => {
274+ imageScanTimerRef . current = null ;
275+
276+ const processed = processImagesInText ( input , onImageDetected , {
277+ announce : false ,
278+ } ) ;
279+
280+ if ( processed !== input ) {
281+ // Image was detected and replaced with [Image #N]
282+ lastProcessedInputRef . current = processed ;
283+ const buffer = textBufferRef . current ;
284+ buffer . setText ( processed ) ;
285+ syncInputFromBuffer ( ) ;
286+ } else {
287+ lastProcessedInputRef . current = input ;
288+ }
289+ } , INK_IMAGE_SCAN_DELAY_MS ) ;
290+
291+ return ( ) => {
292+ if ( imageScanTimerRef . current ) {
293+ clearTimeout ( imageScanTimerRef . current ) ;
294+ imageScanTimerRef . current = null ;
295+ }
296+ } ;
297+ } , [ input , onImageDetected , syncInputFromBuffer ] ) ;
298+
222299 useInput ( ( char , key ) => {
223300 syncBufferViewport ( ) ;
224301
0 commit comments