diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx index ed68adb9c5d..f7f7dac54f2 100644 --- a/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx +++ b/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx @@ -87,6 +87,45 @@ describe('', () => { unmount(); }); + it('caps the height of pending plain text to the available height', async () => { + const text = Array.from( + { length: 30 }, + (_, i) => `${i + 1}. tip ${i + 1}`, + ).join(eol); + const { lastFrame, unmount } = await renderWithProviders( + , + ); + const frame = lastFrame() ?? ''; + expect(frame.trimEnd().split('\n').length).toBeLessThanOrEqual(10); + expect(frame).toContain('tip 30'); + expect(frame).not.toContain('tip 1\n'); + expect(frame).toContain('lines hidden'); + unmount(); + }); + + it('does not cap the height of completed plain text', async () => { + const text = Array.from( + { length: 30 }, + (_, i) => `${i + 1}. tip ${i + 1}`, + ).join(eol); + const { lastFrame, unmount } = await renderWithProviders( + , + ); + expect(lastFrame()).toContain('tip 1'); + expect(lastFrame()).toContain('tip 30'); + unmount(); + }); + it('renders unordered lists with different markers', async () => { const text = ` - item A diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.tsx index b3e88d9a012..d19cee8e845 100644 --- a/packages/cli/src/ui/utils/MarkdownDisplay.tsx +++ b/packages/cli/src/ui/utils/MarkdownDisplay.tsx @@ -12,6 +12,7 @@ import { TableRenderer } from './TableRenderer.js'; import { RenderInline } from './InlineMarkdownRenderer.js'; import { useSettings } from '../contexts/SettingsContext.js'; import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js'; +import { MaxSizedBox } from '../components/shared/MaxSizedBox.js'; interface MarkdownDisplayProps { text: string; @@ -311,6 +312,25 @@ const MarkdownDisplayInternal: React.FC = ({ ); } + // When not in alternate buffer mode a pending message that is taller than the + // terminal forces Ink to clear and redraw the whole screen on every update. + // Cap the height so streaming text keeps the frame shorter than the terminal. + if ( + !isAlternateBuffer && + isPending && + availableTerminalHeight !== undefined + ) { + return ( + + {contentBlocks} + + ); + } + return <>{contentBlocks}; };