diff --git a/packages/cli/src/ui/components/AskUserDialog.test.tsx b/packages/cli/src/ui/components/AskUserDialog.test.tsx
index 645321dfc0..a30fb9b4af 100644
--- a/packages/cli/src/ui/components/AskUserDialog.test.tsx
+++ b/packages/cli/src/ui/components/AskUserDialog.test.tsx
@@ -10,6 +10,7 @@ import { renderWithProviders } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { AskUserDialog } from './AskUserDialog.js';
import { QuestionType, type Question } from '@google/gemini-cli-core';
+import { UIStateContext, type UIState } from '../contexts/UIStateContext.js';
// Helper to write to stdin with proper act() wrapping
const writeKey = (stdin: { write: (data: string) => void }, key: string) => {
@@ -42,7 +43,6 @@ describe('AskUserDialog', () => {
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -108,7 +108,6 @@ describe('AskUserDialog', () => {
onSubmit={onSubmit}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -129,7 +128,6 @@ describe('AskUserDialog', () => {
onSubmit={onSubmit}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -159,33 +157,49 @@ describe('AskUserDialog', () => {
});
});
- it('shows scroll arrows when options exceed available height', async () => {
- const questions: Question[] = [
- {
- question: 'Choose an option',
- header: 'Scroll Test',
- options: Array.from({ length: 15 }, (_, i) => ({
- label: `Option ${i + 1}`,
- description: `Description ${i + 1}`,
- })),
- multiSelect: false,
- },
- ];
+ describe.each([
+ { useAlternateBuffer: true, expectedArrows: false },
+ { useAlternateBuffer: false, expectedArrows: true },
+ ])(
+ 'Scroll Arrows (useAlternateBuffer: $useAlternateBuffer)',
+ ({ useAlternateBuffer, expectedArrows }) => {
+ it(`shows scroll arrows correctly when useAlternateBuffer is ${useAlternateBuffer}`, async () => {
+ const questions: Question[] = [
+ {
+ question: 'Choose an option',
+ header: 'Scroll Test',
+ options: Array.from({ length: 15 }, (_, i) => ({
+ label: `Option ${i + 1}`,
+ description: `Description ${i + 1}`,
+ })),
+ multiSelect: false,
+ },
+ ];
- const { lastFrame } = renderWithProviders(
- ,
- );
+ const { lastFrame } = renderWithProviders(
+ ,
+ { useAlternateBuffer },
+ );
- await waitFor(() => {
- expect(lastFrame()).toMatchSnapshot();
- });
- });
+ await waitFor(() => {
+ if (expectedArrows) {
+ expect(lastFrame()).toContain('▲');
+ expect(lastFrame()).toContain('▼');
+ } else {
+ expect(lastFrame()).not.toContain('▲');
+ expect(lastFrame()).not.toContain('▼');
+ }
+ expect(lastFrame()).toMatchSnapshot();
+ });
+ });
+ },
+ );
it('navigates to custom option when typing unbound characters (Type-to-Jump)', async () => {
const { stdin, lastFrame } = renderWithProviders(
@@ -194,7 +208,6 @@ describe('AskUserDialog', () => {
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -246,7 +259,6 @@ describe('AskUserDialog', () => {
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -261,7 +273,6 @@ describe('AskUserDialog', () => {
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -276,7 +287,6 @@ describe('AskUserDialog', () => {
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -308,7 +318,6 @@ describe('AskUserDialog', () => {
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -351,7 +360,6 @@ describe('AskUserDialog', () => {
onSubmit={onSubmit}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -420,7 +428,6 @@ describe('AskUserDialog', () => {
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -450,7 +457,6 @@ describe('AskUserDialog', () => {
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -496,7 +502,6 @@ describe('AskUserDialog', () => {
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -533,7 +538,6 @@ describe('AskUserDialog', () => {
onSubmit={onSubmit}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -567,7 +571,6 @@ describe('AskUserDialog', () => {
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -590,7 +593,6 @@ describe('AskUserDialog', () => {
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -613,7 +615,6 @@ describe('AskUserDialog', () => {
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -649,7 +650,6 @@ describe('AskUserDialog', () => {
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -681,7 +681,6 @@ describe('AskUserDialog', () => {
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -729,7 +728,6 @@ describe('AskUserDialog', () => {
onSubmit={onSubmit}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -780,7 +778,6 @@ describe('AskUserDialog', () => {
onSubmit={onSubmit}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -807,7 +804,6 @@ describe('AskUserDialog', () => {
onSubmit={vi.fn()}
onCancel={onCancel}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -854,7 +850,6 @@ describe('AskUserDialog', () => {
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -914,7 +909,6 @@ describe('AskUserDialog', () => {
onSubmit={onSubmit}
onCancel={vi.fn()}
width={120}
- availableHeight={20}
/>,
{ width: 120 },
);
@@ -946,4 +940,72 @@ describe('AskUserDialog', () => {
});
});
});
+
+ it('uses availableTerminalHeight from UIStateContext if availableHeight prop is missing', () => {
+ const questions: Question[] = [
+ {
+ question: 'Choose an option',
+ header: 'Context Test',
+ options: Array.from({ length: 10 }, (_, i) => ({
+ label: `Option ${i + 1}`,
+ description: `Description ${i + 1}`,
+ })),
+ multiSelect: false,
+ },
+ ];
+
+ const mockUIState = {
+ availableTerminalHeight: 5, // Small height to force scroll arrows
+ } as UIState;
+
+ const { lastFrame } = renderWithProviders(
+
+
+ ,
+ { useAlternateBuffer: false },
+ );
+
+ // With height 5 and alternate buffer disabled, it should show scroll arrows (▲)
+ expect(lastFrame()).toContain('▲');
+ expect(lastFrame()).toContain('▼');
+ });
+
+ it('does NOT truncate the question when in alternate buffer mode even with small height', () => {
+ const longQuestion =
+ 'This is a very long question ' + 'with many words '.repeat(10);
+ const questions: Question[] = [
+ {
+ question: longQuestion,
+ header: 'Alternate Buffer Test',
+ options: [{ label: 'Option 1', description: 'Desc 1' }],
+ multiSelect: false,
+ },
+ ];
+
+ const mockUIState = {
+ availableTerminalHeight: 5,
+ } as UIState;
+
+ const { lastFrame } = renderWithProviders(
+
+
+ ,
+ { useAlternateBuffer: true },
+ );
+
+ // Should NOT contain the truncation message
+ expect(lastFrame()).not.toContain('hidden ...');
+ // Should contain the full long question (or at least its parts)
+ expect(lastFrame()).toContain('This is a very long question');
+ });
});
diff --git a/packages/cli/src/ui/components/AskUserDialog.tsx b/packages/cli/src/ui/components/AskUserDialog.tsx
index e2892feade..ba4c14510f 100644
--- a/packages/cli/src/ui/components/AskUserDialog.tsx
+++ b/packages/cli/src/ui/components/AskUserDialog.tsx
@@ -5,7 +5,14 @@
*/
import type React from 'react';
-import { useCallback, useMemo, useRef, useEffect, useReducer } from 'react';
+import {
+ useCallback,
+ useMemo,
+ useRef,
+ useEffect,
+ useReducer,
+ useContext,
+} from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import type { Question } from '@google/gemini-cli-core';
@@ -21,6 +28,8 @@ import { getCachedStringWidth } from '../utils/textUtils.js';
import { useTabbedNavigation } from '../hooks/useTabbedNavigation.js';
import { DialogFooter } from './shared/DialogFooter.js';
import { MaxSizedBox } from './shared/MaxSizedBox.js';
+import { UIStateContext } from '../contexts/UIStateContext.js';
+import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
interface AskUserDialogState {
answers: { [key: string]: string };
@@ -121,7 +130,7 @@ interface AskUserDialogProps {
/**
* Height constraint for scrollable content.
*/
- availableHeight: number;
+ availableHeight?: number;
}
interface ReviewViewProps {
@@ -199,7 +208,7 @@ interface TextQuestionViewProps {
onSelectionChange?: (answer: string) => void;
onEditingCustomOption?: (editing: boolean) => void;
availableWidth: number;
- availableHeight: number;
+ availableHeight?: number;
initialAnswer?: string;
progressHeader?: React.ReactNode;
keyboardHints?: React.ReactNode;
@@ -216,6 +225,7 @@ const TextQuestionView: React.FC = ({
progressHeader,
keyboardHints,
}) => {
+ const isAlternateBuffer = useAlternateBuffer();
const prefix = '> ';
const horizontalPadding = 1; // 1 for cursor
const bufferWidth =
@@ -279,13 +289,20 @@ const TextQuestionView: React.FC = ({
const INPUT_HEIGHT = 2; // TextInput + margin
const FOOTER_HEIGHT = 2; // DialogFooter + margin
const overhead = HEADER_HEIGHT + INPUT_HEIGHT + FOOTER_HEIGHT;
- const questionHeight = Math.max(1, availableHeight - overhead);
+ const questionHeight =
+ availableHeight && !isAlternateBuffer
+ ? Math.max(1, availableHeight - overhead)
+ : undefined;
return (
{progressHeader}
-
+
{question.question}
@@ -389,7 +406,7 @@ interface ChoiceQuestionViewProps {
onSelectionChange?: (answer: string) => void;
onEditingCustomOption?: (editing: boolean) => void;
availableWidth: number;
- availableHeight: number;
+ availableHeight?: number;
initialAnswer?: string;
progressHeader?: React.ReactNode;
keyboardHints?: React.ReactNode;
@@ -406,6 +423,7 @@ const ChoiceQuestionView: React.FC = ({
progressHeader,
keyboardHints,
}) => {
+ const isAlternateBuffer = useAlternateBuffer();
const numOptions =
(question.options?.length ?? 0) + (question.type !== 'yesno' ? 1 : 0);
const numLen = String(numOptions).length;
@@ -711,18 +729,27 @@ const ChoiceQuestionView: React.FC = ({
const TITLE_MARGIN = 1;
const FOOTER_HEIGHT = 2; // DialogFooter + margin
const overhead = HEADER_HEIGHT + TITLE_MARGIN + FOOTER_HEIGHT;
- const listHeight = Math.max(1, availableHeight - overhead);
- const questionHeight = Math.min(3, Math.max(1, listHeight - 4));
- const maxItemsToShow = Math.max(
- 1,
- Math.floor((listHeight - questionHeight) / 2),
- );
+ const listHeight = availableHeight
+ ? Math.max(1, availableHeight - overhead)
+ : undefined;
+ const questionHeight =
+ listHeight && !isAlternateBuffer
+ ? Math.min(15, Math.max(1, listHeight - 4))
+ : undefined;
+ const maxItemsToShow =
+ listHeight && questionHeight
+ ? Math.max(1, Math.floor((listHeight - questionHeight) / 2))
+ : selectionItems.length;
return (
{progressHeader}
-
+
{question.question}
{question.multiSelect && (
@@ -824,8 +851,15 @@ export const AskUserDialog: React.FC = ({
onCancel,
onActiveTextInputChange,
width,
- availableHeight,
+ availableHeight: availableHeightProp,
}) => {
+ const uiState = useContext(UIStateContext);
+ const availableHeight =
+ availableHeightProp ??
+ (uiState?.constrainHeight !== false
+ ? uiState?.availableTerminalHeight
+ : undefined);
+
const [state, dispatch] = useReducer(askUserDialogReducerLogic, initialState);
const { answers, isEditingCustomOption, submitted } = state;
diff --git a/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap
index fdb34f4adb..7f5d630bc1 100644
--- a/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap
@@ -1,5 +1,56 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: false) > shows scroll arrows correctly when useAlternateBuffer is false 1`] = `
+"Choose an option
+
+▲
+● 1. Option 1
+ Description 1
+ 2. Option 2
+ Description 2
+▼
+
+Enter to select · ↑/↓ to navigate · Esc to cancel"
+`;
+
+exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: true) > shows scroll arrows correctly when useAlternateBuffer is true 1`] = `
+"Choose an option
+
+● 1. Option 1
+ Description 1
+ 2. Option 2
+ Description 2
+ 3. Option 3
+ Description 3
+ 4. Option 4
+ Description 4
+ 5. Option 5
+ Description 5
+ 6. Option 6
+ Description 6
+ 7. Option 7
+ Description 7
+ 8. Option 8
+ Description 8
+ 9. Option 9
+ Description 9
+ 10. Option 10
+ Description 10
+ 11. Option 11
+ Description 11
+ 12. Option 12
+ Description 12
+ 13. Option 13
+ Description 13
+ 14. Option 14
+ Description 14
+ 15. Option 15
+ Description 15
+ 16. Enter a custom value
+
+Enter to select · ↑/↓ to navigate · Esc to cancel"
+`;
+
exports[`AskUserDialog > Text type questions > renders text input for type: "text" 1`] = `
"What should we name this component?
@@ -104,19 +155,6 @@ Which database should we use?
Enter to select · ←/→ to switch questions · Esc to cancel"
`;
-exports[`AskUserDialog > shows scroll arrows when options exceed available height 1`] = `
-"Choose an option
-
-▲
-● 1. Option 1
- Description 1
- 2. Option 2
- Description 2
-▼
-
-Enter to select · ↑/↓ to navigate · Esc to cancel"
-`;
-
exports[`AskUserDialog > shows warning for unanswered questions on Review tab 1`] = `
"← □ License │ □ README │ ≡ Review →
diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
index 2272c1a4dd..a50669bd40 100644
--- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
@@ -271,7 +271,7 @@ export const ToolConfirmationMessage: React.FC<
handleConfirm(ToolConfirmationOutcome.Cancel);
}}
width={terminalWidth}
- availableHeight={availableBodyContentHeight() ?? 10}
+ availableHeight={availableBodyContentHeight()}
/>
);
return { question: '', bodyContent, options: [] };