From b7254905823119fac3ccb9c613dcd8237fc6f5ea Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Thu, 5 Feb 2026 01:51:20 -0800 Subject: [PATCH] fix(cli): fix cursor position after pasting text Use refs instead of state values in useInput callback to avoid stale closures. Also manually update textInputRef before calling setCursorPos so the bounds check uses the correct new text length. --- cli/src/components/ChatView.tsx | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/cli/src/components/ChatView.tsx b/cli/src/components/ChatView.tsx index f41818f959..fb7a6a0653 100644 --- a/cli/src/components/ChatView.tsx +++ b/cli/src/components/ChatView.tsx @@ -350,9 +350,11 @@ export const ChatView: React.FC = ({ insertText: insertTextAtCursor, } = useTextInput() - // Ref for text input (used by useHomeEndKeys) + // Refs for text input and cursor position (used by useHomeEndKeys and to avoid stale closures in useInput) const textInputRef = useRef(textInput) textInputRef.current = textInput + const cursorPosRef = useRef(cursorPos) + cursorPosRef.current = cursorPos const [fileResults, setFileResults] = useState([]) const [selectedIndex, setSelectedIndex] = useState(0) // For file menu @@ -1007,11 +1009,11 @@ export const ChatView: React.FC = ({ // 3. Handle Option+arrow via key.meta (backup - Ink sometimes parses these instead of passing raw sequence) if (key.meta) { if (key.leftArrow) { - setCursorPos(findWordStart(textInput, cursorPos)) + setCursorPos(findWordStart(textInputRef.current, cursorPosRef.current)) return } if (key.rightArrow) { - setCursorPos(findWordEnd(textInput, cursorPos)) + setCursorPos(findWordEnd(textInputRef.current, cursorPosRef.current)) return } } @@ -1261,10 +1263,10 @@ export const ChatView: React.FC = ({ } pasteUpdateTimeoutRef.current = setTimeout(() => { const newPlaceholder = `[Pasted text #${pasteNum} +${activePasteLinesRef.current} lines]` - setTextInput((prev) => { - const pattern = new RegExp(`\\[Pasted text #${pasteNum} \\+\\d+ lines\\]`) - return prev.replace(pattern, newPlaceholder) - }) + const pattern = new RegExp(`\\[Pasted text #${pasteNum} \\+\\d+ lines\\]`) + const newText = textInputRef.current.replace(pattern, newPlaceholder) + textInputRef.current = newText // Update ref immediately so setCursorPos bounds check works + setTextInput(newText) // Update cursor to be right after the placeholder setCursorPos(activePasteStartPosRef.current + newPlaceholder.length) Logger.info(`Paste #${pasteNum} complete: ${activePasteLinesRef.current} lines`) @@ -1277,7 +1279,8 @@ export const ChatView: React.FC = ({ pasteCounterRef.current += 1 const pasteNum = pasteCounterRef.current activePasteNumRef.current = pasteNum - activePasteStartPosRef.current = cursorPos // Track where placeholder starts + const currentCursorPos = cursorPosRef.current // Use ref to avoid stale closure + activePasteStartPosRef.current = currentCursorPos // Track where placeholder starts // Count line breaks in the pasted content (handle both \n and \r) const extraLines = input.match(/[\r\n]/g)?.length || 0 activePasteLinesRef.current = extraLines // Track total lines @@ -1289,8 +1292,10 @@ export const ChatView: React.FC = ({ return next }) - setTextInput((prev) => prev.slice(0, cursorPos) + placeholder + prev.slice(cursorPos)) - setCursorPos(cursorPos + placeholder.length) + const newText = textInputRef.current.slice(0, currentCursorPos) + placeholder + textInputRef.current.slice(currentCursorPos) + textInputRef.current = newText // Update ref immediately so setCursorPos bounds check works + setTextInput(newText) + setCursorPos(currentCursorPos + placeholder.length) return // Exit early - don't also add the raw input via normal handling below } @@ -1319,15 +1324,15 @@ export const ChatView: React.FC = ({ return } if (key.rightArrow && !inSlashMenu && !inFileMenu) { - setCursorPos((pos) => Math.min(textInput.length, pos + 1)) + setCursorPos((pos) => Math.min(textInputRef.current.length, pos + 1)) return } if (key.upArrow && !inSlashMenu && !inFileMenu) { - setCursorPos(moveCursorUp(textInput, cursorPos)) + setCursorPos(moveCursorUp(textInputRef.current, cursorPosRef.current)) return } if (key.downArrow && !inSlashMenu && !inFileMenu) { - setCursorPos(moveCursorDown(textInput, cursorPos)) + setCursorPos(moveCursorDown(textInputRef.current, cursorPosRef.current)) return } // Normal input (single char or short paste)