diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index e12730fe3..822a5c0a5 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -2244,6 +2244,67 @@ describe('InputPrompt', () => { unmount(); }); + it('should expand same-size placeholders correctly when #2 appears first', async () => { + const firstPaste = 'x'.repeat(1001); + const secondPaste = 'y'.repeat(1001); + + const { stdin, unmount } = renderWithProviders( + , + ); + await wait(); + + stdin.write(`\x1b[200~${firstPaste}\x1b[201~`); + await wait(); + stdin.write(`\x1b[200~${secondPaste}\x1b[201~`); + await wait(); + + mockBuffer.text = + '[Pasted Content 1001 chars] #2\n[Pasted Content 1001 chars]'; + mockBuffer.lines = mockBuffer.text.split('\n'); + mockBuffer.cursor = [1, '[Pasted Content 1001 chars]'.length]; + + // Wait for paste protection to expire + await new Promise((resolve) => setTimeout(resolve, 600)); + + stdin.write('\r'); + await wait(); + + expect(props.onSubmit).toHaveBeenCalledWith( + `${secondPaste}\n${firstPaste}`, + ); + + unmount(); + }); + + it('should write expanded placeholder content to shell history', async () => { + props.shellModeActive = true; + const largeContent = 'x'.repeat(1001); + mockBuffer.text = '[Pasted Content 1001 chars]'; + mockBuffer.lines = [mockBuffer.text]; + mockBuffer.cursor = [0, mockBuffer.text.length]; + + const { stdin, unmount } = renderWithProviders( + , + ); + await wait(); + + stdin.write(`\x1b[200~${largeContent}\x1b[201~`); + await wait(); + + // Wait for paste protection to expire + await new Promise((resolve) => setTimeout(resolve, 600)); + + stdin.write('\r'); + await wait(); + + expect(mockShellHistory.addCommandToHistory).toHaveBeenCalledWith( + largeContent, + ); + expect(props.onSubmit).toHaveBeenCalledWith(largeContent); + + unmount(); + }); + it('should delete entire placeholder on backspace', async () => { const placeholderText = '[Pasted Content 1001 chars]'; mockBuffer.text = placeholderText; diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index f56a7c4a9..2e4cc1636 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -88,6 +88,10 @@ export const calculatePromptWidths = (terminalWidth: number) => { } as const; }; +// Large paste placeholder thresholds +const LARGE_PASTE_CHAR_THRESHOLD = 1000; +const LARGE_PASTE_LINE_THRESHOLD = 10; + export const InputPrompt: React.FC = ({ buffer, onSubmit, @@ -121,7 +125,6 @@ export const InputPrompt: React.FC = ({ const pasteTimeoutRef = useRef(null); // Large paste placeholder handling - const LARGE_PASTE_CHAR_THRESHOLD = 1000; const [pendingPastes, setPendingPastes] = useState>( new Map(), ); @@ -255,16 +258,26 @@ export const InputPrompt: React.FC = ({ const handleSubmitAndClear = useCallback( (submittedValue: string) => { - 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); - }); + const placeholders = Array.from(pendingPastes.keys()).sort( + (a, b) => b.length - a.length, + ); + const escapedPlaceholders = placeholders.map((placeholderValue) => + placeholderValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), + ); + const placeholderRegex = new RegExp(escapedPlaceholders.join('|'), 'g'); + finalValue = finalValue.replace( + placeholderRegex, + (matchedPlaceholder) => + pendingPastes.get(matchedPlaceholder) ?? matchedPlaceholder, + ); setPendingPastes(new Map()); + activePlaceholderIds.current.clear(); + } + if (shellModeActive) { + shellHistory.addCommandToHistory(finalValue); } // 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. @@ -397,7 +410,6 @@ export const InputPrompt: React.FC = ({ const pasted = key.sequence.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); const charCount = [...pasted].length; // Proper Unicode char count const lineCount = pasted.split('\n').length; - const LARGE_PASTE_LINE_THRESHOLD = 10; if ( charCount > LARGE_PASTE_CHAR_THRESHOLD || lineCount > LARGE_PASTE_LINE_THRESHOLD @@ -852,7 +864,6 @@ export const InputPrompt: React.FC = ({ uiState, uiActions, pasteWorkaround, - LARGE_PASTE_CHAR_THRESHOLD, nextLargePastePlaceholder, pendingPastes, parsePlaceholder,