Skip to content

Commit f360385

Browse files
committed
fix: always listen for paste events even when not in focus
1 parent cf6213e commit f360385

File tree

1 file changed

+54
-2
lines changed

1 file changed

+54
-2
lines changed

cli/src/components/multiline-input.tsx

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { TextAttributes } from '@opentui/core'
2-
import { useKeyboard, useRenderer } from '@opentui/react'
2+
import { useAppContext, useKeyboard, useRenderer } from '@opentui/react'
33
import {
44
forwardRef,
55
useCallback,
@@ -20,6 +20,7 @@ import type { InputValue } from '../types/store'
2020
import 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

Comments
 (0)