11import { TextAttributes } from '@opentui/core'
2- import { useKeyboard , useRenderer } from '@opentui/react'
2+ import { useAppContext , useKeyboard , useRenderer } from '@opentui/react'
33import {
44 forwardRef ,
55 useCallback ,
@@ -20,6 +20,7 @@ import type { InputValue } from '../types/store'
2020import type {
2121 KeyEvent ,
2222 MouseEvent ,
23+ PasteEvent ,
2324 ScrollBoxRenderable ,
2425 TextBufferView ,
2526 TextRenderable ,
@@ -189,6 +190,8 @@ export const MultilineInput = forwardRef<
189190) {
190191 const theme = useTheme ( )
191192 const renderer = useRenderer ( )
193+ const appContext = useAppContext ( )
194+ const { keyHandler } = appContext
192195 const hookBlinkValue = useChatStore ( ( state ) => state . isFocusSupported )
193196 const effectiveShouldBlinkCursor = shouldBlinkCursor ?? hookBlinkValue
194197
@@ -1005,6 +1008,50 @@ export const MultilineInput = forwardRef<
10051008 [ insertTextAtCursor ] ,
10061009 )
10071010
1011+ // Increase StdinParser timeout from default 10ms to 100ms.
1012+ // Some terminals (Ghostty, iTerm2, VS Code) split bracketed paste sequences
1013+ // across multiple stdin reads when drag-dropping files. The default 10ms
1014+ // timeout causes the parser to flush partial escape sequences as keypresses,
1015+ // corrupting paste detection. 100ms is still fast for keyboard input but
1016+ // gives enough time for split paste sequences to arrive.
1017+ useEffect ( ( ) => {
1018+ const cliRenderer = appContext . renderer as Record < string , unknown > | null
1019+ const stdinBuffer = cliRenderer ?. _stdinBuffer as Record < string , unknown > | undefined
1020+ if ( stdinBuffer && typeof stdinBuffer . timeoutMs === 'number' ) {
1021+ stdinBuffer . timeoutMs = 100
1022+ }
1023+ } , [ appContext ] )
1024+
1025+ // Global paste event listener — catches paste events (e.g. from drag-and-drop)
1026+ // at the global level, plus a scrollbox-level backup. Some terminals may not
1027+ // deliver paste events reliably via one mechanism alone, so we use both with
1028+ // dedup to prevent double-handling.
1029+ const onPasteRef = useRef ( onPaste )
1030+ onPasteRef . current = onPaste
1031+ const pasteHandledRef = useRef ( false )
1032+
1033+ // Always listen for paste events regardless of terminal focus state.
1034+ // Drag-and-drop inherently causes the terminal to lose focus (the file
1035+ // manager has focus during the drag), so the paste listener must stay
1036+ // active even when `focused` is false.
1037+ useEffect ( ( ) => {
1038+ if ( ! keyHandler ) return
1039+
1040+ const handlePaste = ( event : PasteEvent ) => {
1041+ pasteHandledRef . current = true
1042+ onPasteRef . current ( event . text )
1043+ // Reset dedup flag after microtask so scrollbox handler (which fires
1044+ // synchronously after global listeners) sees it as handled, but future
1045+ // paste events are not blocked.
1046+ queueMicrotask ( ( ) => { pasteHandledRef . current = false } )
1047+ }
1048+
1049+ keyHandler . on ( 'paste' , handlePaste )
1050+ return ( ) => {
1051+ keyHandler . off ( 'paste' , handlePaste )
1052+ }
1053+ } , [ keyHandler ] )
1054+
10081055 // Main keyboard handler - delegates to specialized handlers
10091056 useKeyboard (
10101057 useCallback (
@@ -1087,7 +1134,12 @@ export const MultilineInput = forwardRef<
10871134 visible : showScrollbar && layoutMetrics . isScrollable ,
10881135 trackOptions : { width : 1 } ,
10891136 } }
1090- onPaste = { ( event ) => onPaste ( event . text ) }
1137+ onPaste = { ( event ) => {
1138+ // Backup paste handler: fires if the global keyHandler listener
1139+ // didn't catch this event (dedup prevents double-handling)
1140+ if ( pasteHandledRef . current ) return
1141+ onPasteRef . current ( event . text )
1142+ } }
10911143 onMouseDown = { handleMouseDown }
10921144 style = { {
10931145 flexGrow : 0 ,
0 commit comments