From c30f964fbacedb0fa99b6a9f2a9ec3efbb2ac6d7 Mon Sep 17 00:00:00 2001 From: Jerop Kipruto Date: Fri, 30 Jan 2026 15:40:05 -0500 Subject: [PATCH] fix(ui): prevent content leak in MaxSizedBox bottom overflow When using overflowDirection="bottom", content was bleeding through after the "... last N lines hidden ..." indicator. This was caused by the inner overflow Box not having a constrained height. Add maxHeight constraint to inner overflow Box when overflowing. --- .../ui/components/shared/MaxSizedBox.test.tsx | 30 ++++++++++++++++++- .../src/ui/components/shared/MaxSizedBox.tsx | 7 ++++- .../__snapshots__/MaxSizedBox.test.tsx.snap | 8 +++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx b/packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx index 06501eca3e..ff9035ec6d 100644 --- a/packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx +++ b/packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx @@ -4,10 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { render } from '../../../test-utils/render.js'; +import { render, renderWithProviders } from '../../../test-utils/render.js'; import { waitFor } from '../../../test-utils/async.js'; import { OverflowProvider } from '../../contexts/OverflowContext.js'; import { MaxSizedBox } from './MaxSizedBox.js'; +import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; import { Box, Text } from 'ink'; import { describe, it, expect } from 'vitest'; @@ -226,4 +227,31 @@ describe('', () => { expect(lastFrame()).toMatchSnapshot(); unmount(); }); + + it('does not leak content after hidden indicator with bottom overflow', async () => { + const markdownContent = Array.from( + { length: 20 }, + (_, i) => `- Step ${i + 1}: Do something important`, + ).join('\n'); + const { lastFrame } = renderWithProviders( + + + , + { width: 80 }, + ); + + await waitFor(() => expect(lastFrame()).toContain('... last')); + + const frame = lastFrame()!; + const lines = frame.split('\n'); + const lastLine = lines[lines.length - 1]; + + // The last line should only contain the hidden indicator, no leaked content + expect(lastLine).toMatch(/^\.\.\. last \d+ lines? hidden \.\.\.$/); + expect(lastFrame()).toMatchSnapshot(); + }); }); diff --git a/packages/cli/src/ui/components/shared/MaxSizedBox.tsx b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx index 0e87d5a6cd..aefd5fe50d 100644 --- a/packages/cli/src/ui/components/shared/MaxSizedBox.tsx +++ b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx @@ -120,7 +120,12 @@ export const MaxSizedBox: React.FC = ({ hidden ... )} - + > does not leak content after hidden indicator with bottom overflow 1`] = ` +"Plan + + - Step 1: Do something important + - Step 2: Do something important +... last 18 lines hidden ..." +`; + exports[` > does not truncate when maxHeight is undefined 1`] = ` "Line 1 Line 2"