From 6b650c263b747f08c6cdfe309d7cf9d25ea49638 Mon Sep 17 00:00:00 2001 From: Jerop Kipruto Date: Wed, 28 Jan 2026 19:27:35 -0500 Subject: [PATCH] fix(ui): handle large content in AskUserDialog with MaxSizedBox Constrain content height using MaxSizedBox to prevent flicker when displaying large plan files. Content is truncated based on available terminal height with a "lines hidden" indicator. --- .../src/ui/components/AskUserDialog.test.tsx | 65 +++++++++++++++++++ .../cli/src/ui/components/AskUserDialog.tsx | 29 +++++++-- .../__snapshots__/AskUserDialog.test.tsx.snap | 37 +++++++++++ 3 files changed, 127 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/ui/components/AskUserDialog.test.tsx b/packages/cli/src/ui/components/AskUserDialog.test.tsx index 5810a9f922..320ba321f5 100644 --- a/packages/cli/src/ui/components/AskUserDialog.test.tsx +++ b/packages/cli/src/ui/components/AskUserDialog.test.tsx @@ -962,5 +962,70 @@ describe('AskUserDialog', () => { expect(lastFrame()).toMatchSnapshot(); }); + + it('truncates long content when availableTerminalHeight is small', async () => { + const longContent = Array.from( + { length: 30 }, + (_, i) => `Line ${i + 1}`, + ).join('\n'); + const questionWithLongContent: Question[] = [ + { + question: 'Approve this plan?', + header: 'Plan', + options: [{ label: 'Yes', description: '' }], + content: longContent, + }, + ]; + + const { lastFrame } = renderWithProviders( + , + { + width: 120, + uiState: { + availableTerminalHeight: 15, + }, + }, + ); + + await waitFor(() => { + expect(lastFrame()).toMatchSnapshot(); + }); + }); + + it('does not truncate content when availableTerminalHeight is undefined', () => { + const content = Array.from( + { length: 10 }, + (_, i) => `Line ${i + 1}`, + ).join('\n'); + const questionWithContent: Question[] = [ + { + question: 'Approve this plan?', + header: 'Plan', + options: [{ label: 'Yes', description: '' }], + content, + }, + ]; + + const { lastFrame } = renderWithProviders( + , + { + width: 120, + uiState: { + availableTerminalHeight: undefined, + }, + }, + ); + + expect(lastFrame()).not.toContain('lines hidden'); + expect(lastFrame()).toMatchSnapshot(); + }); }); }); diff --git a/packages/cli/src/ui/components/AskUserDialog.tsx b/packages/cli/src/ui/components/AskUserDialog.tsx index 886698a408..269f5e9353 100644 --- a/packages/cli/src/ui/components/AskUserDialog.tsx +++ b/packages/cli/src/ui/components/AskUserDialog.tsx @@ -31,6 +31,17 @@ import { useTabbedNavigation } from '../hooks/useTabbedNavigation.js'; import { DialogFooter } from './shared/DialogFooter.js'; import { MaxSizedBox } from './shared/MaxSizedBox.js'; +// Width reduction for content inside the dialog border/padding +const CONTENT_WIDTH_REDUCTION = 4; + +// Height consumed by dialog chrome surrounding the content area: +// - Border top/bottom: 2 +// - Question text + marginBottom: 2 +// - Footer (keyboard hints): 2 +// - Options/input minimum: 4 +// - Buffer for tab header when present: 2 +const DIALOG_CHROME_HEIGHT = 12; + interface AskUserDialogState { answers: { [key: string]: string }; isEditingCustomOption: boolean; @@ -283,11 +294,16 @@ const TextQuestionView: React.FC = ({ {progressHeader} {question.content && ( - + @@ -722,11 +738,16 @@ const ChoiceQuestionView: React.FC = ({ {progressHeader} {question.content && ( - + 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 53f1e9d821..36b53636ae 100644 --- a/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap @@ -11,6 +11,28 @@ exports[`AskUserDialog > Question content field > does not render content when c ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" `; +exports[`AskUserDialog > Question content field > does not truncate content when availableTerminalHeight is undefined 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Line 1 │ +│ Line 2 │ +│ Line 3 │ +│ Line 4 │ +│ Line 5 │ +│ Line 6 │ +│ Line 7 │ +│ Line 8 │ +│ Line 9 │ +│ Line 10 │ +│ │ +│ Approve this plan? │ +│ │ +│ ● 1. Yes │ +│ 2. Enter a custom value │ +│ │ +│ Enter to select · ↑/↓ to navigate · Esc to cancel │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + exports[`AskUserDialog > Question content field > renders content in a choice question 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ Plan Details │ @@ -44,6 +66,21 @@ exports[`AskUserDialog > Question content field > renders content in a text ques ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" `; +exports[`AskUserDialog > Question content field > truncates long content when availableTerminalHeight is small 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ ... first 28 lines hidden ... │ +│ Line 29 │ +│ Line 30 │ +│ │ +│ Approve this plan? │ +│ │ +│ ● 1. Yes │ +│ 2. Enter a custom value │ +│ │ +│ Enter to select · ↑/↓ to navigate · Esc to cancel │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + exports[`AskUserDialog > Question content field > uses customOptionPlaceholder for the Other option 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ Approve this? │