diff --git a/packages/cli/src/ui/components/AskUserDialog.test.tsx b/packages/cli/src/ui/components/AskUserDialog.test.tsx
index 1bd29241db..cade23a918 100644
--- a/packages/cli/src/ui/components/AskUserDialog.test.tsx
+++ b/packages/cli/src/ui/components/AskUserDialog.test.tsx
@@ -1347,4 +1347,79 @@ describe('AskUserDialog', () => {
});
});
});
+
+ describe('Paste placeholder expansion', () => {
+ const largePasteText = 'line1\nline2\nline3\nline4\nline5\nline6';
+
+ it('expands paste placeholders in text question submission', async () => {
+ const textQuestion: Question[] = [
+ {
+ question: 'Describe the issue',
+ header: 'Description',
+ type: QuestionType.TEXT,
+ },
+ ];
+
+ const onSubmit = vi.fn();
+ const { stdin } = renderWithProviders(
+ ,
+ { width: 120 },
+ );
+
+ // Simulate a bracketed paste of large text (>5 lines triggers placeholder)
+ writeKey(stdin, `\x1B[200~${largePasteText}\x1B[201~`);
+
+ // Submit with Enter
+ writeKey(stdin, '\r');
+
+ await waitFor(async () => {
+ expect(onSubmit).toHaveBeenCalledWith({ '0': largePasteText });
+ });
+ });
+
+ it('expands paste placeholders in choice question custom "Other" input', async () => {
+ const choiceQuestion: Question[] = [
+ {
+ question: 'Which option?',
+ header: 'Option',
+ type: QuestionType.CHOICE,
+ options: [
+ { label: 'Option A', description: 'First option' },
+ { label: 'Option B', description: 'Second option' },
+ ],
+ multiSelect: false,
+ },
+ ];
+
+ const onSubmit = vi.fn();
+ const { stdin } = renderWithProviders(
+ ,
+ { width: 120 },
+ );
+
+ // Navigate down past options to the "Other" custom option
+ writeKey(stdin, '\x1b[B'); // Down past Option A
+ writeKey(stdin, '\x1b[B'); // Down past Option B to "Other"
+
+ // Simulate a bracketed paste of large text
+ writeKey(stdin, `\x1B[200~${largePasteText}\x1B[201~`);
+
+ // Submit with Enter
+ writeKey(stdin, '\r');
+
+ await waitFor(async () => {
+ expect(onSubmit).toHaveBeenCalledWith({ '0': largePasteText });
+ });
+ });
+ });
});
diff --git a/packages/cli/src/ui/components/AskUserDialog.tsx b/packages/cli/src/ui/components/AskUserDialog.tsx
index 9606513510..83f9b18b45 100644
--- a/packages/cli/src/ui/components/AskUserDialog.tsx
+++ b/packages/cli/src/ui/components/AskUserDialog.tsx
@@ -23,7 +23,10 @@ import { useKeypress, type Key } from '../hooks/useKeypress.js';
import { keyMatchers, Command } from '../keyMatchers.js';
import { checkExhaustive } from '@google/gemini-cli-core';
import { TextInput } from './shared/TextInput.js';
-import { useTextBuffer } from './shared/text-buffer.js';
+import {
+ useTextBuffer,
+ PASTED_TEXT_PLACEHOLDER_REGEX,
+} from './shared/text-buffer.js';
import { getCachedStringWidth } from '../utils/textUtils.js';
import { useTabbedNavigation } from '../hooks/useTabbedNavigation.js';
import { DialogFooter } from './shared/DialogFooter.js';
@@ -81,6 +84,21 @@ function autoBoldIfPlain(text: string): string {
return text;
}
+/**
+ * Expands paste placeholders like [Pasted Text: 6 lines] with the actual
+ * pasted content stored in the buffer's pastedContent map.
+ */
+function expandPastePlaceholders(
+ text: string,
+ pastedContent: Record,
+): string {
+ if (!pastedContent || Object.keys(pastedContent).length === 0) return text;
+ return text.replace(
+ PASTED_TEXT_PLACEHOLDER_REGEX,
+ (match) => pastedContent[match] || match,
+ );
+}
+
interface AskUserDialogState {
answers: { [key: string]: string };
isEditingCustomOption: boolean;
@@ -302,10 +320,11 @@ const TextQuestionView: React.FC = ({
const lastTextValueRef = useRef(textValue);
useEffect(() => {
if (textValue !== lastTextValueRef.current) {
- onSelectionChange?.(textValue);
+ const expanded = expandPastePlaceholders(textValue, buffer.pastedContent);
+ onSelectionChange?.(expanded);
lastTextValueRef.current = textValue;
}
- }, [textValue, onSelectionChange]);
+ }, [textValue, onSelectionChange, buffer.pastedContent]);
// Handle Ctrl+C to clear all text
const handleExtraKeys = useCallback(
@@ -588,11 +607,15 @@ const ChoiceQuestionView: React.FC = ({
}
});
if (includeCustomOption && customOption.trim()) {
- answers.push(customOption.trim());
+ const expanded = expandPastePlaceholders(
+ customOption.trim(),
+ customBuffer.pastedContent,
+ );
+ answers.push(expanded);
}
return answers.join(', ');
},
- [questionOptions],
+ [questionOptions, customBuffer.pastedContent],
);
// Synchronize selection changes with parent - only when it actually changes
@@ -757,7 +780,11 @@ const ChoiceQuestionView: React.FC = ({
} else if (itemValue.type === 'other') {
// In single select, selecting other submits it if it has text
if (customOptionText.trim()) {
- onAnswer(customOptionText.trim());
+ const expanded = expandPastePlaceholders(
+ customOptionText.trim(),
+ customBuffer.pastedContent,
+ );
+ onAnswer(expanded);
}
}
}
@@ -767,6 +794,7 @@ const ChoiceQuestionView: React.FC = ({
selectedIndices,
isCustomOptionSelected,
customOptionText,
+ customBuffer.pastedContent,
onAnswer,
buildAnswerString,
],
diff --git a/packages/cli/src/ui/components/shared/TextInput.test.tsx b/packages/cli/src/ui/components/shared/TextInput.test.tsx
index f12714e288..0868e70497 100644
--- a/packages/cli/src/ui/components/shared/TextInput.test.tsx
+++ b/packages/cli/src/ui/components/shared/TextInput.test.tsx
@@ -17,7 +17,8 @@ vi.mock('../../hooks/useKeypress.js', () => ({
useKeypress: vi.fn(),
}));
-vi.mock('./text-buffer.js', () => {
+vi.mock(import('./text-buffer.js'), async (importOriginal) => {
+ const actual = await importOriginal();
const mockTextBuffer = {
text: '',
lines: [''],
@@ -60,6 +61,7 @@ vi.mock('./text-buffer.js', () => {
};
return {
+ ...actual,
useTextBuffer: vi.fn(() => mockTextBuffer as unknown as TextBuffer),
TextBuffer: vi.fn(() => mockTextBuffer as unknown as TextBuffer),
};
@@ -366,4 +368,65 @@ describe('TextInput', () => {
expect(lastFrame()).toContain('line2');
unmount();
});
+
+ it('expands paste placeholders on submit', async () => {
+ const placeholder = '[Pasted Text: 6 lines]';
+ const actualContent = 'line1\nline2\nline3\nline4\nline5\nline6';
+ mockBuffer.text = placeholder;
+ mockBuffer.viewportVisualLines = [placeholder];
+ (
+ mockBuffer as unknown as { pastedContent: Record }
+ ).pastedContent = {
+ [placeholder]: actualContent,
+ };
+
+ const { waitUntilReady, unmount } = render(
+ ,
+ );
+ await waitUntilReady();
+ const keypressHandler = mockedUseKeypress.mock.calls[0][0];
+
+ await act(async () => {
+ keypressHandler({
+ name: 'return',
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ sequence: '',
+ });
+ });
+ await waitUntilReady();
+
+ expect(onSubmit).toHaveBeenCalledWith(actualContent);
+ unmount();
+ });
+
+ it('passes text through unchanged on submit when no paste placeholders exist', async () => {
+ mockBuffer.setText('regular text');
+ (
+ mockBuffer as unknown as { pastedContent: Record }
+ ).pastedContent = {};
+
+ const { waitUntilReady, unmount } = render(
+ ,
+ );
+ await waitUntilReady();
+ const keypressHandler = mockedUseKeypress.mock.calls[0][0];
+
+ await act(async () => {
+ keypressHandler({
+ name: 'return',
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ sequence: '',
+ });
+ });
+ await waitUntilReady();
+
+ expect(onSubmit).toHaveBeenCalledWith('regular text');
+ unmount();
+ });
});
diff --git a/packages/cli/src/ui/components/shared/TextInput.tsx b/packages/cli/src/ui/components/shared/TextInput.tsx
index 40f44cda53..1d6dbf2fad 100644
--- a/packages/cli/src/ui/components/shared/TextInput.tsx
+++ b/packages/cli/src/ui/components/shared/TextInput.tsx
@@ -11,7 +11,10 @@ import { Text, Box } from 'ink';
import { useKeypress } from '../../hooks/useKeypress.js';
import chalk from 'chalk';
import { theme } from '../../semantic-colors.js';
-import type { TextBuffer } from './text-buffer.js';
+import {
+ type TextBuffer,
+ PASTED_TEXT_PLACEHOLDER_REGEX,
+} from './text-buffer.js';
import { cpSlice, cpIndexToOffset } from '../../utils/textUtils.js';
import { keyMatchers, Command } from '../../keyMatchers.js';
@@ -47,14 +50,23 @@ export function TextInput({
}
if (keyMatchers[Command.SUBMIT](key) && onSubmit) {
- onSubmit(text);
+ // Expand paste placeholders so consumers receive actual content
+ let expandedText = text;
+ const { pastedContent } = buffer;
+ if (pastedContent && Object.keys(pastedContent).length > 0) {
+ expandedText = text.replace(
+ PASTED_TEXT_PLACEHOLDER_REGEX,
+ (match) => pastedContent[match] || match,
+ );
+ }
+ onSubmit(expandedText);
return true;
}
const handled = handleInput(key);
return handled;
},
- [handleInput, onCancel, onSubmit, text],
+ [handleInput, onCancel, onSubmit, text, buffer],
);
useKeypress(handleKeyPress, { isActive: focus, priority: true });