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}>;
};