From c7b681ef5d163d652428c48648ac21df7382339d Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Wed, 4 Feb 2026 15:18:20 +0800 Subject: [PATCH] feat(paste): add large paste placeholder and fix enter-submit on macOS - Add large paste placeholder feature: when pasting text > 1000 chars, show a placeholder like '[Pasted Content 1500 chars]' instead of the full content. The full text is stored and expanded on submit. - Fix enter-submit on macOS: only apply recentPasteTime protection when pasteWorkaround is enabled (Windows or Node < 20). On macOS/Linux with modern Node, bracketed paste markers work reliably so the protection is unnecessary and caused the first Enter after paste to be ignored. Co-authored-by: Qwen-Coder --- .../cli/src/ui/components/InputPrompt.tsx | 59 +++++++++++++++++-- .../cli/src/ui/contexts/KeypressContext.tsx | 5 +- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 0e3c43806..56d904bff 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -38,6 +38,7 @@ import { SCREEN_READER_USER_PREFIX } from '../textConstants.js'; import { useShellFocusState } from '../contexts/ShellFocusContext.js'; import { useUIState } from '../contexts/UIStateContext.js'; import { useUIActions } from '../contexts/UIActionsContext.js'; +import { useKeypressContext } from '../contexts/KeypressContext.js'; import { FEEDBACK_DIALOG_KEYS } from '../FeedbackDialog.js'; export interface InputPromptProps { buffer: TextBuffer; @@ -111,6 +112,7 @@ export const InputPrompt: React.FC = ({ const isShellFocused = useShellFocusState(); const uiState = useUIState(); const uiActions = useUIActions(); + const { pasteWorkaround } = useKeypressContext(); const [justNavigatedHistory, setJustNavigatedHistory] = useState(false); const [escPressCount, setEscPressCount] = useState(0); const [showEscapePrompt, setShowEscapePrompt] = useState(false); @@ -118,6 +120,13 @@ export const InputPrompt: React.FC = ({ const [recentPasteTime, setRecentPasteTime] = useState(null); const pasteTimeoutRef = useRef(null); + // Large paste placeholder handling + const LARGE_PASTE_CHAR_THRESHOLD = 1000; + const [pendingPastes, setPendingPastes] = useState>( + new Map(), + ); + const largePasteCounters = useRef>(new Map()); + const [dirs, setDirs] = useState( config.getWorkspaceContext().getDirectories(), ); @@ -186,6 +195,18 @@ export const InputPrompt: React.FC = ({ } }, [showEscapePrompt, onEscapePromptChange]); + // Helper to generate unique placeholder for large pastes + const nextLargePastePlaceholder = useCallback((charCount: number): string => { + const base = `[Pasted Content ${charCount} chars]`; + const currentCount = largePasteCounters.current.get(charCount) || 0; + const nextCount = currentCount + 1; + largePasteCounters.current.set(charCount, nextCount); + if (nextCount === 1) { + return base; + } + return `${base} #${nextCount}`; + }, []); + // Clear escape prompt timer on unmount useEffect( () => () => { @@ -204,10 +225,18 @@ export const InputPrompt: React.FC = ({ if (shellModeActive) { shellHistory.addCommandToHistory(submittedValue); } + // Expand any large paste placeholders to their full content before submitting + let finalValue = submittedValue; + if (pendingPastes.size > 0) { + pendingPastes.forEach((fullContent, placeholder) => { + finalValue = finalValue.replace(placeholder, fullContent); + }); + setPendingPastes(new Map()); + } // Clear the buffer *before* calling onSubmit to prevent potential re-submission // if onSubmit triggers a re-render while the buffer still holds the old value. buffer.setText(''); - onSubmit(submittedValue); + onSubmit(finalValue); resetCompletionState(); resetReverseSearchCompletionState(); }, @@ -218,6 +247,7 @@ export const InputPrompt: React.FC = ({ shellModeActive, shellHistory, resetReverseSearchCompletionState, + pendingPastes, ], ); @@ -330,8 +360,22 @@ export const InputPrompt: React.FC = ({ pasteTimeoutRef.current = null; }, 500); - // Ensure we never accidentally interpret paste as regular input. - buffer.handleInput(key); + // Handle large pastes by showing a placeholder + const pasted = key.sequence.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + const charCount = [...pasted].length; // Proper Unicode char count + if (charCount > LARGE_PASTE_CHAR_THRESHOLD) { + const placeholder = nextLargePastePlaceholder(charCount); + setPendingPastes((prev) => { + const next = new Map(prev); + next.set(placeholder, pasted); + return next; + }); + // Insert the placeholder as regular text + buffer.insert(placeholder, { paste: false }); + } else { + // Normal paste handling for small content + buffer.handleInput(key); + } return; } @@ -619,8 +663,10 @@ export const InputPrompt: React.FC = ({ if (keyMatchers[Command.SUBMIT](key)) { if (buffer.text.trim()) { - // Check if a paste operation occurred recently to prevent accidental auto-submission - if (recentPasteTime !== null) { + // Check if a paste operation occurred recently to prevent accidental auto-submission. + // Only applies when pasteWorkaround is enabled (Windows or Node < 20), where bracketed + // paste markers may not work reliably and Enter key events can leak from pasted text. + if (pasteWorkaround && recentPasteTime !== null) { // Paste occurred recently, ignore this submit to prevent auto-execution return; } @@ -719,6 +765,9 @@ export const InputPrompt: React.FC = ({ showShortcuts, uiState, uiActions, + pasteWorkaround, + LARGE_PASTE_CHAR_THRESHOLD, + nextLargePastePlaceholder, ], ); diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx index 0f01712cc..b574afc93 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.tsx @@ -60,6 +60,7 @@ export type KeypressHandler = (key: Key) => void; interface KeypressContextValue { subscribe: (handler: KeypressHandler) => void; unsubscribe: (handler: KeypressHandler) => void; + pasteWorkaround: boolean; } const KeypressContext = createContext( @@ -799,7 +800,9 @@ export function KeypressProvider({ ]); return ( - + {children} );