From 601c4f70a509c7c25fdca803e746b5371a4240c1 Mon Sep 17 00:00:00 2001 From: shaman shetty Date: Mon, 5 Oct 2026 01:50:22 +0530 Subject: [PATCH] fix(cli): cap pending plain text height to reduce streaming flicker While a response streams in the default (non-alternate-buffer) mode, plain text such as long lists or paragraphs was not height-limited; only code blocks were. The frame could grow taller than the terminal, which makes Ink clear the screen and rewrite the full history on every update. Wrap pending markdown in MaxSizedBox so only the most recent lines that fit in availableTerminalHeight are shown. Completed messages and alternate-buffer mode are unchanged. --- .../cli/src/ui/utils/MarkdownDisplay.test.tsx | 39 +++++++++++++++++++ packages/cli/src/ui/utils/MarkdownDisplay.tsx | 20 ++++++++++ 2 files changed, 59 insertions(+) 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}; };