diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx
index 8ae21247e9..741632af85 100644
--- a/packages/cli/src/gemini.tsx
+++ b/packages/cli/src/gemini.tsx
@@ -67,7 +67,6 @@ import {
type InitializationResult,
} from './core/initializer.js';
import { validateAuthMethod } from './config/auth.js';
-import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js';
import { runZedIntegration } from './zed-integration/zedIntegration.js';
import { cleanupExpiredSessions } from './utils/sessionCleanup.js';
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
@@ -562,7 +561,6 @@ export async function main() {
await setupTerminalAndTheme(config, settings);
- setMaxSizedBoxDebugging(isDebugMode);
const initAppHandle = startupProfiler.start('initialize_app');
const initializationResult = await initializeApp(config, settings);
initAppHandle?.end();
diff --git a/packages/cli/src/ui/components/MainContent.test.tsx b/packages/cli/src/ui/components/MainContent.test.tsx
index 4bd823503c..63cbdce790 100644
--- a/packages/cli/src/ui/components/MainContent.test.tsx
+++ b/packages/cli/src/ui/components/MainContent.test.tsx
@@ -5,6 +5,7 @@
*/
import { render } from '../../test-utils/render.js';
+import { waitFor } from '../../test-utils/async.js';
import { MainContent } from './MainContent.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Box, Text } from 'ink';
@@ -41,9 +42,21 @@ vi.mock('../hooks/useAlternateBuffer.js', () => ({
}));
vi.mock('./HistoryItemDisplay.js', () => ({
- HistoryItemDisplay: ({ item }: { item: { content: string } }) => (
+ HistoryItemDisplay: ({
+ item,
+ availableTerminalHeight,
+ }: {
+ item: { content: string };
+ availableTerminalHeight?: number;
+ }) => (
- HistoryItem: {item.content}
+
+ HistoryItem: {item.content} (height:{' '}
+ {availableTerminalHeight === undefined
+ ? 'undefined'
+ : availableTerminalHeight}
+ )
+
),
}));
@@ -81,23 +94,32 @@ describe('MainContent', () => {
vi.mocked(useAlternateBuffer).mockReturnValue(false);
});
- it('renders in normal buffer mode', () => {
+ it('renders in normal buffer mode', async () => {
const { lastFrame } = render();
+ await waitFor(() => expect(lastFrame()).toContain('AppHeader'));
const output = lastFrame();
- expect(output).toContain('AppHeader');
- expect(output).toContain('HistoryItem: Hello');
- expect(output).toContain('HistoryItem: Hi there');
+ expect(output).toContain('HistoryItem: Hello (height: 20)');
+ expect(output).toContain('HistoryItem: Hi there (height: 20)');
});
- it('renders in alternate buffer mode', () => {
+ it('renders in alternate buffer mode', async () => {
vi.mocked(useAlternateBuffer).mockReturnValue(true);
const { lastFrame } = render();
+ await waitFor(() => expect(lastFrame()).toContain('ScrollableList'));
const output = lastFrame();
- expect(output).toContain('ScrollableList');
expect(output).toContain('AppHeader');
- expect(output).toContain('HistoryItem: Hello');
- expect(output).toContain('HistoryItem: Hi there');
+ expect(output).toContain('HistoryItem: Hello (height: undefined)');
+ expect(output).toContain('HistoryItem: Hi there (height: undefined)');
+ });
+
+ it('does not constrain height in alternate buffer mode', async () => {
+ vi.mocked(useAlternateBuffer).mockReturnValue(true);
+ const { lastFrame } = render();
+ await waitFor(() => expect(lastFrame()).toContain('HistoryItem: Hello'));
+ const output = lastFrame();
+
+ expect(output).toMatchSnapshot();
});
});
diff --git a/packages/cli/src/ui/components/MainContent.tsx b/packages/cli/src/ui/components/MainContent.tsx
index f46a9c0c2f..7f3982eec0 100644
--- a/packages/cli/src/ui/components/MainContent.tsx
+++ b/packages/cli/src/ui/components/MainContent.tsx
@@ -65,7 +65,9 @@ export const MainContent = () => {
{
[
pendingHistoryItems,
uiState.constrainHeight,
+ isAlternateBuffer,
availableTerminalHeight,
mainAreaWidth,
uiState.isEditorDialogOpen,
@@ -107,7 +110,7 @@ export const MainContent = () => {
return (
{
return pendingItems;
}
},
- [
- version,
- mainAreaWidth,
- staticAreaMaxItemHeight,
- uiState.slashCommands,
- pendingItems,
- ],
+ [version, mainAreaWidth, uiState.slashCommands, pendingItems],
);
if (isAlternateBuffer) {
diff --git a/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap
new file mode 100644
index 0000000000..ffbb0ab2d2
--- /dev/null
+++ b/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap
@@ -0,0 +1,9 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`MainContent > does not constrain height in alternate buffer mode 1`] = `
+"ScrollableList
+AppHeader
+HistoryItem: Hello (height: undefined)
+HistoryItem: Hi there (height: undefined)
+ShowMoreLines"
+`;
diff --git a/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx b/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx
index 9b1b93f25a..9063606146 100644
--- a/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx
+++ b/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx
@@ -6,6 +6,7 @@
import { OverflowProvider } from '../../contexts/OverflowContext.js';
import { renderWithProviders } from '../../../test-utils/render.js';
+import { waitFor } from '../../../test-utils/async.js';
import { DiffRenderer } from './DiffRenderer.js';
import * as CodeColorizer from '../../utils/CodeColorizer.js';
import { vi } from 'vitest';
@@ -23,7 +24,7 @@ describe('', () => {
describe.each([true, false])(
'with useAlternateBuffer = %s',
(useAlternateBuffer) => {
- it('should call colorizeCode with correct language for new file with known extension', () => {
+ it('should call colorizeCode with correct language for new file with known extension', async () => {
const newFileDiffContent = `
diff --git a/test.py b/test.py
new file mode 100644
@@ -43,17 +44,19 @@ index 0000000..e69de29
,
{ useAlternateBuffer },
);
- expect(mockColorizeCode).toHaveBeenCalledWith({
- code: 'print("hello world")',
- language: 'python',
- availableHeight: undefined,
- maxWidth: 80,
- theme: undefined,
- settings: expect.anything(),
- });
+ await waitFor(() =>
+ expect(mockColorizeCode).toHaveBeenCalledWith({
+ code: 'print("hello world")',
+ language: 'python',
+ availableHeight: undefined,
+ maxWidth: 80,
+ theme: undefined,
+ settings: expect.anything(),
+ }),
+ );
});
- it('should call colorizeCode with null language for new file with unknown extension', () => {
+ it('should call colorizeCode with null language for new file with unknown extension', async () => {
const newFileDiffContent = `
diff --git a/test.unknown b/test.unknown
new file mode 100644
@@ -73,17 +76,19 @@ index 0000000..e69de29
,
{ useAlternateBuffer },
);
- expect(mockColorizeCode).toHaveBeenCalledWith({
- code: 'some content',
- language: null,
- availableHeight: undefined,
- maxWidth: 80,
- theme: undefined,
- settings: expect.anything(),
- });
+ await waitFor(() =>
+ expect(mockColorizeCode).toHaveBeenCalledWith({
+ code: 'some content',
+ language: null,
+ availableHeight: undefined,
+ maxWidth: 80,
+ theme: undefined,
+ settings: expect.anything(),
+ }),
+ );
});
- it('should call colorizeCode with null language for new file if no filename is provided', () => {
+ it('should call colorizeCode with null language for new file if no filename is provided', async () => {
const newFileDiffContent = `
diff --git a/test.txt b/test.txt
new file mode 100644
@@ -99,17 +104,19 @@ index 0000000..e69de29
,
{ useAlternateBuffer },
);
- expect(mockColorizeCode).toHaveBeenCalledWith({
- code: 'some text content',
- language: null,
- availableHeight: undefined,
- maxWidth: 80,
- theme: undefined,
- settings: expect.anything(),
- });
+ await waitFor(() =>
+ expect(mockColorizeCode).toHaveBeenCalledWith({
+ code: 'some text content',
+ language: null,
+ availableHeight: undefined,
+ maxWidth: 80,
+ theme: undefined,
+ settings: expect.anything(),
+ }),
+ );
});
- it('should render diff content for existing file (not calling colorizeCode directly for the whole block)', () => {
+ it('should render diff content for existing file (not calling colorizeCode directly for the whole block)', async () => {
const existingFileDiffContent = `
diff --git a/test.txt b/test.txt
@@ -131,6 +138,7 @@ index 0000001..0000002 100644
{ useAlternateBuffer },
);
// colorizeCode is used internally by the line-by-line rendering, not for the whole block
+ await waitFor(() => expect(lastFrame()).toContain('new line'));
expect(mockColorizeCode).not.toHaveBeenCalledWith(
expect.objectContaining({
code: expect.stringContaining('old line'),
@@ -144,7 +152,7 @@ index 0000001..0000002 100644
expect(lastFrame()).toMatchSnapshot();
});
- it('should handle diff with only header and no changes', () => {
+ it('should handle diff with only header and no changes', async () => {
const noChangeDiff = `diff --git a/file.txt b/file.txt
index 1234567..1234567 100644
--- a/file.txt
@@ -160,22 +168,24 @@ index 1234567..1234567 100644
,
{ useAlternateBuffer },
);
+ await waitFor(() => expect(lastFrame()).toBeDefined());
expect(lastFrame()).toMatchSnapshot();
expect(mockColorizeCode).not.toHaveBeenCalled();
});
- it('should handle empty diff content', () => {
+ it('should handle empty diff content', async () => {
const { lastFrame } = renderWithProviders(
,
{ useAlternateBuffer },
);
+ await waitFor(() => expect(lastFrame()).toBeDefined());
expect(lastFrame()).toMatchSnapshot();
expect(mockColorizeCode).not.toHaveBeenCalled();
});
- it('should render a gap indicator for skipped lines', () => {
+ it('should render a gap indicator for skipped lines', async () => {
const diffWithGap = `
diff --git a/file.txt b/file.txt
@@ -200,10 +210,11 @@ index 123..456 100644
,
{ useAlternateBuffer },
);
+ await waitFor(() => expect(lastFrame()).toContain('added line'));
expect(lastFrame()).toMatchSnapshot();
});
- it('should not render a gap indicator for small gaps (<= MAX_CONTEXT_LINES_WITHOUT_GAP)', () => {
+ it('should not render a gap indicator for small gaps (<= MAX_CONTEXT_LINES_WITHOUT_GAP)', async () => {
const diffWithSmallGap = `
diff --git a/file.txt b/file.txt
@@ -233,6 +244,7 @@ index abc..def 100644
,
{ useAlternateBuffer },
);
+ await waitFor(() => expect(lastFrame()).toContain('context line 15'));
expect(lastFrame()).toMatchSnapshot();
});
@@ -270,7 +282,7 @@ index 123..789 100644
},
])(
'with terminalWidth $terminalWidth and height $height',
- ({ terminalWidth, height }) => {
+ async ({ terminalWidth, height }) => {
const { lastFrame } = renderWithProviders(
,
{ useAlternateBuffer },
);
+ await waitFor(() => expect(lastFrame()).toContain('anotherNew'));
const output = lastFrame();
expect(sanitizeOutput(output, terminalWidth)).toMatchSnapshot();
},
);
});
- it('should correctly render a diff with a SVN diff format', () => {
+ it('should correctly render a diff with a SVN diff format', async () => {
const newFileDiff = `
fileDiff Index: file.txt
@@ -315,10 +328,11 @@ fileDiff Index: file.txt
,
{ useAlternateBuffer },
);
+ await waitFor(() => expect(lastFrame()).toContain('newVar'));
expect(lastFrame()).toMatchSnapshot();
});
- it('should correctly render a new file with no file extension correctly', () => {
+ it('should correctly render a new file with no file extension correctly', async () => {
const newFileDiff = `
fileDiff Index: Dockerfile
@@ -341,6 +355,7 @@ fileDiff Index: Dockerfile
,
{ useAlternateBuffer },
);
+ await waitFor(() => expect(lastFrame()).toContain('RUN npm run build'));
expect(lastFrame()).toMatchSnapshot();
});
},
diff --git a/packages/cli/src/ui/components/messages/DiffRenderer.tsx b/packages/cli/src/ui/components/messages/DiffRenderer.tsx
index fdf1d26c91..83b205ac76 100644
--- a/packages/cli/src/ui/components/messages/DiffRenderer.tsx
+++ b/packages/cli/src/ui/components/messages/DiffRenderer.tsx
@@ -13,7 +13,6 @@ import { MaxSizedBox } from '../shared/MaxSizedBox.js';
import { theme as semanticTheme } from '../../semantic-colors.js';
import type { Theme } from '../../themes/theme.js';
import { useSettings } from '../../contexts/SettingsContext.js';
-import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
interface DiffLine {
type: 'add' | 'del' | 'context' | 'hunk' | 'other';
@@ -102,7 +101,6 @@ export const DiffRenderer: React.FC = ({
theme,
}) => {
const settings = useSettings();
- const isAlternateBuffer = useAlternateBuffer();
const screenReaderEnabled = useIsScreenReaderEnabled();
@@ -179,7 +177,6 @@ export const DiffRenderer: React.FC = ({
tabWidth,
availableTerminalHeight,
terminalWidth,
- !isAlternateBuffer,
);
}
}, [
@@ -192,7 +189,6 @@ export const DiffRenderer: React.FC = ({
terminalWidth,
theme,
settings,
- isAlternateBuffer,
tabWidth,
]);
@@ -205,7 +201,6 @@ const renderDiffContent = (
tabWidth = DEFAULT_TAB_WIDTH,
availableTerminalHeight: number | undefined,
terminalWidth: number,
- useMaxSizedBox: boolean,
) => {
// 1. Normalize whitespace (replace tabs with spaces) *before* further processing
const normalizedLines = parsedLines.map((line) => ({
@@ -283,22 +278,14 @@ const renderDiffContent = (
) {
acc.push(
- {useMaxSizedBox ? (
-
- {'═'.repeat(terminalWidth)}
-
- ) : (
- // We can use a proper separator when not using max sized box.
-
- )}
+
,
);
}
@@ -342,24 +329,15 @@ const renderDiffContent = (
: undefined;
acc.push(
- {useMaxSizedBox ? (
-
- {gutterNumStr.padStart(gutterWidth)}{' '}
-
- ) : (
-
- {gutterNumStr}
-
- )}
+
+ {gutterNumStr}
+
{line.type === 'context' ? (
<>
{prefixSymbol}
@@ -393,22 +371,14 @@ const renderDiffContent = (
[],
);
- if (useMaxSizedBox) {
- return (
-
- {content}
-
- );
- }
-
return (
-
+
{content}
-
+
);
};
diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
index 5be9169f02..6728100eff 100644
--- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
@@ -19,7 +19,6 @@ import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
import { useKeypress } from '../../hooks/useKeypress.js';
import { theme } from '../../semantic-colors.js';
-import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
import { useSettings } from '../../contexts/SettingsContext.js';
export interface ToolConfirmationMessageProps {
@@ -41,7 +40,6 @@ export const ToolConfirmationMessage: React.FC<
}) => {
const { onConfirm } = confirmationDetails;
- const isAlternateBuffer = useAlternateBuffer();
const settings = useSettings();
const allowPermanentApproval =
settings.merged.security?.enablePermanentToolApproval ?? false;
@@ -273,20 +271,14 @@ export const ToolConfirmationMessage: React.FC<
bodyContentHeight -= 2; // Account for padding;
}
- const commandBox = (
-
- {executionProps.command}
-
- );
-
- bodyContent = isAlternateBuffer ? (
- commandBox
- ) : (
+ bodyContent = (
- {commandBox}
+
+ {executionProps.command}
+
);
} else if (confirmationDetails.type === 'info') {
@@ -338,7 +330,6 @@ export const ToolConfirmationMessage: React.FC<
isDiffingEnabled,
availableTerminalHeight,
terminalWidth,
- isAlternateBuffer,
allowPermanentApproval,
]);
diff --git a/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx b/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx
index 98b0af9f40..41f79aab08 100644
--- a/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx
+++ b/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx
@@ -27,31 +27,6 @@ vi.mock('./DiffRenderer.js', () => ({
),
}));
-vi.mock('../../utils/MarkdownDisplay.js', () => ({
- MarkdownDisplay: ({ text }: { text: string }) => (
-
- MarkdownDisplay: {text}
-
- ),
-}));
-
-vi.mock('../AnsiOutput.js', () => ({
- AnsiOutputText: ({ data }: { data: unknown }) => (
-
- AnsiOutputText: {JSON.stringify(data)}
-
- ),
-}));
-
-vi.mock('../shared/MaxSizedBox.js', () => ({
- MaxSizedBox: ({ children }: { children: React.ReactNode }) => (
-
- MaxSizedBox:
- {children}
-
- ),
-}));
-
// Mock UIStateContext
const mockUseUIState = vi.fn();
vi.mock('../../contexts/UIStateContext.js', () => ({
@@ -64,6 +39,25 @@ vi.mock('../../hooks/useAlternateBuffer.js', () => ({
useAlternateBuffer: () => mockUseAlternateBuffer(),
}));
+// Mock useSettings
+vi.mock('../../contexts/SettingsContext.js', () => ({
+ useSettings: () => ({
+ merged: {
+ ui: {
+ useAlternateBuffer: false,
+ },
+ },
+ }),
+}));
+
+// Mock useOverflowActions
+vi.mock('../../contexts/OverflowContext.js', () => ({
+ useOverflowActions: () => ({
+ addOverflowingId: vi.fn(),
+ removeOverflowingId: vi.fn(),
+ }),
+}));
+
describe('ToolResultDisplay', () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -73,7 +67,7 @@ describe('ToolResultDisplay', () => {
it('renders string result as markdown by default', () => {
const { lastFrame } = render(
- ,
+ ,
);
const output = lastFrame();
@@ -83,7 +77,7 @@ describe('ToolResultDisplay', () => {
it('renders string result as plain text when renderOutputAsMarkdown is false', () => {
const { lastFrame } = render(
{
});
it('renders ANSI output result', () => {
- const ansiResult = {
- text: 'ansi content',
- };
+ const ansiResult: AnsiOutput = [
+ [
+ {
+ text: 'ansi content',
+ fg: 'red',
+ bg: 'black',
+ bold: false,
+ italic: false,
+ underline: false,
+ dim: false,
+ inverse: false,
+ },
+ ],
+ ];
const { lastFrame } = render(
{
expect(output).toMatchSnapshot();
});
- it('falls back to plain text if availableHeight is set and not in alternate buffer', () => {
+ it('does not fall back to plain text if availableHeight is set and not in alternate buffer', () => {
mockUseAlternateBuffer.mockReturnValue(false);
// availableHeight calculation: 20 - 1 - 5 = 14 > 3
const { lastFrame } = render(
,
);
const output = lastFrame();
-
- // Should force renderOutputAsMarkdown to false
expect(output).toMatchSnapshot();
});
@@ -178,7 +181,7 @@ describe('ToolResultDisplay', () => {
mockUseAlternateBuffer.mockReturnValue(true);
const { lastFrame } = render(
= ({
renderOutputAsMarkdown = true,
}) => {
const { renderMarkdown } = useUIState();
- const isAlternateBuffer = useAlternateBuffer();
const availableHeight = availableTerminalHeight
? Math.max(
@@ -51,13 +49,6 @@ export const ToolResultDisplay: React.FC = ({
)
: undefined;
- // Long tool call response in MarkdownDisplay doesn't respect availableTerminalHeight properly,
- // so if we aren't using alternate buffer mode, we're forcing it to not render as markdown when the response is too long, it will fallback
- // to render as plain text, which is contained within the terminal using MaxSizedBox
- if (availableHeight && !isAlternateBuffer) {
- renderOutputAsMarkdown = false;
- }
-
const combinedPaddingAndBorderWidth = 4;
const childWidth = terminalWidth - combinedPaddingAndBorderWidth;
@@ -72,56 +63,59 @@ export const ToolResultDisplay: React.FC = ({
if (!truncatedResultDisplay) return null;
+ let content: React.ReactNode;
+
+ if (typeof truncatedResultDisplay === 'string' && renderOutputAsMarkdown) {
+ content = (
+
+ );
+ } else if (
+ typeof truncatedResultDisplay === 'string' &&
+ !renderOutputAsMarkdown
+ ) {
+ content = (
+
+ {truncatedResultDisplay}
+
+ );
+ } else if (
+ typeof truncatedResultDisplay === 'object' &&
+ 'fileDiff' in truncatedResultDisplay
+ ) {
+ content = (
+
+ );
+ } else if (
+ typeof truncatedResultDisplay === 'object' &&
+ 'todos' in truncatedResultDisplay
+ ) {
+ // display nothing, as the TodoTray will handle rendering todos
+ return null;
+ } else {
+ content = (
+
+ );
+ }
+
return (
-
- {typeof truncatedResultDisplay === 'string' &&
- renderOutputAsMarkdown ? (
-
-
-
- ) : typeof truncatedResultDisplay === 'string' &&
- !renderOutputAsMarkdown ? (
- isAlternateBuffer ? (
-
-
- {truncatedResultDisplay}
-
-
- ) : (
-
-
-
- {truncatedResultDisplay}
-
-
-
- )
- ) : typeof truncatedResultDisplay === 'object' &&
- 'fileDiff' in truncatedResultDisplay ? (
-
- ) : typeof truncatedResultDisplay === 'object' &&
- 'todos' in truncatedResultDisplay ? (
- // display nothing, as the TodoTray will handle rendering todos
- <>>
- ) : (
-
- )}
-
+
+ {content}
+
);
};
diff --git a/packages/cli/src/ui/components/messages/__snapshots__/DiffRenderer.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/DiffRenderer.test.tsx.snap
index 38944657b1..03f55105a7 100644
--- a/packages/cli/src/ui/components/messages/__snapshots__/DiffRenderer.test.tsx.snap
+++ b/packages/cli/src/ui/components/messages/__snapshots__/DiffRenderer.test.tsx.snap
@@ -10,11 +10,11 @@ exports[` > with useAlterna
exports[` > with useAlternateBuffer = false > should correctly render a diff with multiple hunks and a gap indicator > with terminalWidth 30 and height 6 1`] = `
"... first 10 lines hidden ...
- ;
-21 + const anotherNew = 'test'
- ;
-22 console.log('end of
- second hunk');"
+ 'test';
+21 + const anotherNew =
+ 'test';
+22 console.log('end of second
+ hunk');"
`;
exports[` > with useAlternateBuffer = false > should correctly render a diff with multiple hunks and a gap indicator > with terminalWidth 80 and height 6 1`] = `
@@ -84,22 +84,13 @@ exports[` > with useAlterna
exports[` > with useAlternateBuffer = true > should correctly render a diff with a SVN diff format 1`] = `
" 1 - const oldVar = 1;
1 + const newVar = 1;
-═══════════════════════════════════════════════════════════════════════════════
+════════════════════════════════════════════════════════════════════════════════
20 - const anotherOld = 'test';
20 + const anotherNew = 'test';"
`;
exports[` > with useAlternateBuffer = true > should correctly render a diff with multiple hunks and a gap indicator > with terminalWidth 30 and height 6 1`] = `
-" 1 console.log('first hunk');
-
- 2 - const oldVar = 1;
- 2 + const newVar = 1;
- 3 console.log('end of first
- hunk');
-═════════════════════════════
-20 console.log('second
- hunk');
-21 - const anotherOld =
+"... first 10 lines hidden ...
'test';
21 + const anotherNew =
'test';
@@ -108,11 +99,8 @@ exports[` > with useAlterna
`;
exports[` > with useAlternateBuffer = true > should correctly render a diff with multiple hunks and a gap indicator > with terminalWidth 80 and height 6 1`] = `
-" 1 console.log('first hunk');
- 2 - const oldVar = 1;
- 2 + const newVar = 1;
- 3 console.log('end of first hunk');
-═══════════════════════════════════════════════════════════════════════════════
+"... first 4 lines hidden ...
+════════════════════════════════════════════════════════════════════════════════
20 console.log('second hunk');
21 - const anotherOld = 'test';
21 + const anotherNew = 'test';
@@ -124,7 +112,7 @@ exports[` > with useAlterna
2 - const oldVar = 1;
2 + const newVar = 1;
3 console.log('end of first hunk');
-═══════════════════════════════════════════════════════════════════════════════
+════════════════════════════════════════════════════════════════════════════════
20 console.log('second hunk');
21 - const anotherOld = 'test';
21 + const anotherNew = 'test';
@@ -164,7 +152,7 @@ exports[` > with useAlterna
" 1 context line 1
2 - deleted line
2 + added line
-═══════════════════════════════════════════════════════════════════════════════
+════════════════════════════════════════════════════════════════════════════════
10 context line 10
11 context line 11"
`;
diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolMessageRawMarkdown.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolMessageRawMarkdown.test.tsx.snap
index e61465aee6..5a0a17f7e9 100644
--- a/packages/cli/src/ui/components/messages/__snapshots__/ToolMessageRawMarkdown.test.tsx.snap
+++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolMessageRawMarkdown.test.tsx.snap
@@ -18,7 +18,7 @@ exports[` - Raw Markdown Display Snapshots > renders with renderM
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ✓ test-tool A tool for testing │
│ │
-│ Test **bold** and \`code\` markdown │"
+│ Test bold and code markdown │"
`;
exports[` - Raw Markdown Display Snapshots > renders with renderMarkdown=true, useAlternateBuffer=false '(default, regular buffer)' 1`] = `
diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap
index 2919124771..666a2f7fed 100644
--- a/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap
+++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap
@@ -1,326 +1,32 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-exports[`ToolResultDisplay > falls back to plain text if availableHeight is set and not in alternate buffer 1`] = `"MaxSizedBox:Some result"`;
+exports[`ToolResultDisplay > does not fall back to plain text if availableHeight is set and not in alternate buffer 1`] = `"Some result"`;
-exports[`ToolResultDisplay > keeps markdown if in alternate buffer even with availableHeight 1`] = `"MarkdownDisplay: Some result"`;
+exports[`ToolResultDisplay > keeps markdown if in alternate buffer even with availableHeight 1`] = `"Some result"`;
-exports[`ToolResultDisplay > renders ANSI output result 1`] = `"AnsiOutputText: {"text":"ansi content"}"`;
+exports[`ToolResultDisplay > renders ANSI output result 1`] = `"ansi content"`;
exports[`ToolResultDisplay > renders file diff result 1`] = `"DiffRenderer: test.ts - diff content"`;
exports[`ToolResultDisplay > renders nothing for todos result 1`] = `""`;
-exports[`ToolResultDisplay > renders string result as markdown by default 1`] = `"MarkdownDisplay: Some result"`;
+exports[`ToolResultDisplay > renders string result as markdown by default 1`] = `"Some result"`;
-exports[`ToolResultDisplay > renders string result as plain text when renderOutputAsMarkdown is false 1`] = `"MaxSizedBox:Some result"`;
+exports[`ToolResultDisplay > renders string result as plain text when renderOutputAsMarkdown is false 1`] = `"**Some result**"`;
exports[`ToolResultDisplay > truncates very long string results 1`] = `
-"MaxSizedBo...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+"... first 251 lines hidden ...
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+aaaaaaaaaaaaaaa"
`;
diff --git a/packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx b/packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx
index 6312e4816a..06501eca3e 100644
--- a/packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx
+++ b/packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx
@@ -5,19 +5,14 @@
*/
import { render } from '../../../test-utils/render.js';
+import { waitFor } from '../../../test-utils/async.js';
import { OverflowProvider } from '../../contexts/OverflowContext.js';
-import { MaxSizedBox, setMaxSizedBoxDebugging } from './MaxSizedBox.js';
+import { MaxSizedBox } from './MaxSizedBox.js';
import { Box, Text } from 'ink';
import { describe, it, expect } from 'vitest';
describe('', () => {
- // Make sure MaxSizedBox logs errors on invalid configurations.
- // This is useful for debugging issues with the component.
- // It should be set to false in production for performance and to avoid
- // cluttering the console if there are ignorable issues.
- setMaxSizedBoxDebugging(true);
-
- it('renders children without truncation when they fit', () => {
+ it('renders children without truncation when they fit', async () => {
const { lastFrame, unmount } = render(
@@ -27,53 +22,105 @@ describe('', () => {
,
);
- expect(lastFrame()).equals('Hello, World!');
+ await waitFor(() => expect(lastFrame()).toContain('Hello, World!'));
+ expect(lastFrame()).toMatchSnapshot();
unmount();
});
- it('hides lines when content exceeds maxHeight', () => {
+ it('hides lines when content exceeds maxHeight', async () => {
const { lastFrame, unmount } = render(
-
+
Line 1
-
-
Line 2
-
-
Line 3
,
);
- expect(lastFrame()).equals(`... first 2 lines hidden ...
-Line 3`);
+ await waitFor(() =>
+ expect(lastFrame()).toContain('... first 2 lines hidden ...'),
+ );
+ expect(lastFrame()).toMatchSnapshot();
unmount();
});
- it('hides lines at the end when content exceeds maxHeight and overflowDirection is bottom', () => {
+ it('hides lines at the end when content exceeds maxHeight and overflowDirection is bottom', async () => {
const { lastFrame, unmount } = render(
-
+
Line 1
-
-
Line 2
-
-
Line 3
,
);
- expect(lastFrame()).equals(`Line 1
-... last 2 lines hidden ...`);
+ await waitFor(() =>
+ expect(lastFrame()).toContain('... last 2 lines hidden ...'),
+ );
+ expect(lastFrame()).toMatchSnapshot();
unmount();
});
- it('wraps text that exceeds maxWidth', () => {
+ it('shows plural "lines" when more than one line is hidden', async () => {
+ const { lastFrame, unmount } = render(
+
+
+
+ Line 1
+ Line 2
+ Line 3
+
+
+ ,
+ );
+ await waitFor(() =>
+ expect(lastFrame()).toContain('... first 2 lines hidden ...'),
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ unmount();
+ });
+
+ it('shows singular "line" when exactly one line is hidden', async () => {
+ const { lastFrame, unmount } = render(
+
+
+
+ Line 1
+
+
+ ,
+ );
+ await waitFor(() =>
+ expect(lastFrame()).toContain('... first 1 line hidden ...'),
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ unmount();
+ });
+
+ it('accounts for additionalHiddenLinesCount', async () => {
+ const { lastFrame, unmount } = render(
+
+
+
+ Line 1
+ Line 2
+ Line 3
+
+
+ ,
+ );
+ await waitFor(() =>
+ expect(lastFrame()).toContain('... first 7 lines hidden ...'),
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ unmount();
+ });
+
+ it('wraps text that exceeds maxWidth', async () => {
const { lastFrame, unmount } = render(
@@ -84,325 +131,66 @@ Line 3`);
,
);
- expect(lastFrame()).equals(`This is a
-long line
-of text`);
+ await waitFor(() => expect(lastFrame()).toContain('This is a'));
+ expect(lastFrame()).toMatchSnapshot();
unmount();
});
- it('handles mixed wrapping and non-wrapping segments', () => {
- const multilineText = `This part will wrap around.
-And has a line break.
- Leading spaces preserved.`;
- const { lastFrame, unmount } = render(
-
-
-
- Example
-
-
- No Wrap:
- {multilineText}
-
-
- Longer No Wrap:
- This part will wrap around.
-
-
- ,
- );
-
- expect(lastFrame()).equals(
- `Example
-No Wrap: This part
- will wrap
- around.
- And has a
- line break.
- Leading
- spaces
- preserved.
-Longer No Wrap: This
- part
- will
- wrap
- arou
- nd.`,
- );
- unmount();
- });
-
- it('handles words longer than maxWidth by splitting them', () => {
- const { lastFrame, unmount } = render(
-
-
-
- Supercalifragilisticexpialidocious
-
-
- ,
- );
-
- expect(lastFrame()).equals(`... …
-istic
-expia
-lidoc
-ious`);
- unmount();
- });
-
- it('does not truncate when maxHeight is undefined', () => {
+ it('does not truncate when maxHeight is undefined', async () => {
const { lastFrame, unmount } = render(
-
+
Line 1
-
-
Line 2
,
);
- expect(lastFrame()).equals(`Line 1
-Line 2`);
+ await waitFor(() => expect(lastFrame()).toContain('Line 1'));
+ expect(lastFrame()).toMatchSnapshot();
unmount();
});
- it('shows plural "lines" when more than one line is hidden', () => {
- const { lastFrame, unmount } = render(
-
-
-
- Line 1
-
-
- Line 2
-
-
- Line 3
-
-
- ,
- );
- expect(lastFrame()).equals(`... first 2 lines hidden ...
-Line 3`);
- unmount();
- });
-
- it('shows plural "lines" when more than one line is hidden and overflowDirection is bottom', () => {
- const { lastFrame, unmount } = render(
-
-
-
- Line 1
-
-
- Line 2
-
-
- Line 3
-
-
- ,
- );
- expect(lastFrame()).equals(`Line 1
-... last 2 lines hidden ...`);
- unmount();
- });
-
- it('renders an empty box for empty children', () => {
+ it('renders an empty box for empty children', async () => {
const { lastFrame, unmount } = render(
,
);
- // Expect an empty string or a box with nothing in it.
- // Ink renders an empty box as an empty string.
- expect(lastFrame()).equals('');
+ // Use waitFor to ensure ResizeObserver has a chance to run
+ await waitFor(() => expect(lastFrame()).toBeDefined());
+ expect(lastFrame()?.trim()).equals('');
unmount();
});
- it('wraps text with multi-byte unicode characters correctly', () => {
- const { lastFrame, unmount } = render(
-
-
-
- 你好世界
-
-
- ,
- );
-
- // "你好" has a visual width of 4. "世界" has a visual width of 4.
- // With maxWidth=5, it should wrap after the second character.
- expect(lastFrame()).equals(`你好
-世界`);
- unmount();
- });
-
- it('wraps text with multi-byte emoji characters correctly', () => {
- const { lastFrame, unmount } = render(
-
-
-
- 🐶🐶🐶🐶🐶
-
-
- ,
- );
-
- // Each "🐶" has a visual width of 2.
- // With maxWidth=5, it should wrap every 2 emojis.
- expect(lastFrame()).equals(`🐶🐶
-🐶🐶
-🐶`);
- unmount();
- });
-
- it('falls back to an ellipsis when width is extremely small', () => {
- const { lastFrame, unmount } = render(
-
-
-
- No
- wrap
-
-
- ,
- );
-
- expect(lastFrame()).equals('N…');
- unmount();
- });
-
- it('truncates long non-wrapping text with ellipsis', () => {
- const { lastFrame, unmount } = render(
-
-
-
- ABCDE
- wrap
-
-
- ,
- );
-
- expect(lastFrame()).equals('AB…');
- unmount();
- });
-
- it('truncates non-wrapping text containing line breaks', () => {
- const { lastFrame, unmount } = render(
-
-
-
- {'A\nBCDE'}
- wrap
-
-
- ,
- );
-
- expect(lastFrame()).equals(`A\n…`);
- unmount();
- });
-
- it('truncates emoji characters correctly with ellipsis', () => {
- const { lastFrame, unmount } = render(
-
-
-
- 🐶🐶🐶
- wrap
-
-
- ,
- );
-
- expect(lastFrame()).equals(`🐶…`);
- unmount();
- });
-
- it('shows ellipsis for multiple rows with long non-wrapping text', () => {
- const { lastFrame, unmount } = render(
-
-
-
- AAA
- first
-
-
- BBB
- second
-
-
- CCC
- third
-
-
- ,
- );
-
- expect(lastFrame()).equals(`AA…\nBB…\nCC…`);
- unmount();
- });
-
- it('accounts for additionalHiddenLinesCount', () => {
- const { lastFrame, unmount } = render(
-
-
-
- Line 1
-
-
- Line 2
-
-
- Line 3
-
-
- ,
- );
- // 1 line is hidden by overflow, 5 are additionally hidden.
- expect(lastFrame()).equals(`... first 7 lines hidden ...
-Line 3`);
- unmount();
- });
-
- it('handles React.Fragment as a child', () => {
+ it('handles React.Fragment as a child', async () => {
const { lastFrame, unmount } = render(
- <>
-
+
+ <>
Line 1 from Fragment
-
-
Line 2 from Fragment
-
- >
-
+ >
Line 3 direct child
,
);
- expect(lastFrame()).equals(`Line 1 from Fragment
-Line 2 from Fragment
-Line 3 direct child`);
+ await waitFor(() => expect(lastFrame()).toContain('Line 1 from Fragment'));
+ expect(lastFrame()).toMatchSnapshot();
unmount();
});
- it('clips a long single text child from the top', () => {
+ it('clips a long single text child from the top', async () => {
const THIRTY_LINES = Array.from(
{ length: 30 },
(_, i) => `Line ${i + 1}`,
).join('\n');
-
const { lastFrame, unmount } = render(
-
+
{THIRTY_LINES}
@@ -410,21 +198,18 @@ Line 3 direct child`);
,
);
- const expected = [
- '... first 21 lines hidden ...',
- ...Array.from({ length: 9 }, (_, i) => `Line ${22 + i}`),
- ].join('\n');
-
- expect(lastFrame()).equals(expected);
+ await waitFor(() =>
+ expect(lastFrame()).toContain('... first 21 lines hidden ...'),
+ );
+ expect(lastFrame()).toMatchSnapshot();
unmount();
});
- it('clips a long single text child from the bottom', () => {
+ it('clips a long single text child from the bottom', async () => {
const THIRTY_LINES = Array.from(
{ length: 30 },
(_, i) => `Line ${i + 1}`,
).join('\n');
-
const { lastFrame, unmount } = render(
@@ -435,12 +220,10 @@ Line 3 direct child`);
,
);
- const expected = [
- ...Array.from({ length: 9 }, (_, i) => `Line ${i + 1}`),
- '... last 21 lines hidden ...',
- ].join('\n');
-
- expect(lastFrame()).equals(expected);
+ await waitFor(() =>
+ expect(lastFrame()).toContain('... last 21 lines hidden ...'),
+ );
+ expect(lastFrame()).toMatchSnapshot();
unmount();
});
});
diff --git a/packages/cli/src/ui/components/shared/MaxSizedBox.tsx b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx
index d8b15d104d..0e87d5a6cd 100644
--- a/packages/cli/src/ui/components/shared/MaxSizedBox.tsx
+++ b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx
@@ -4,15 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import React, { Fragment, useEffect, useId } from 'react';
-import { Box, Text } from 'ink';
-import stringWidth from 'string-width';
+import type React from 'react';
+import { useCallback, useEffect, useId, useRef, useState } from 'react';
+import { Box, Text, ResizeObserver, type DOMElement } from 'ink';
import { theme } from '../../semantic-colors.js';
-import { toCodePoints } from '../../utils/textUtils.js';
import { useOverflowActions } from '../../contexts/OverflowContext.js';
-import { debugLogger } from '@google/gemini-cli-core';
-
-let enableDebugLog = false;
/**
* Minimum height for the MaxSizedBox component.
@@ -21,42 +17,10 @@ let enableDebugLog = false;
*/
export const MINIMUM_MAX_HEIGHT = 2;
-export function setMaxSizedBoxDebugging(value: boolean) {
- enableDebugLog = value;
-}
-
-function debugReportError(message: string, element: React.ReactNode) {
- if (!enableDebugLog) return;
-
- if (!React.isValidElement(element)) {
- debugLogger.warn(
- message,
- `Invalid element: '${String(element)}' typeof=${typeof element}`,
- );
- return;
- }
-
- let sourceMessage = '';
- try {
- const elementWithSource = element as {
- _source?: { fileName?: string; lineNumber?: number };
- };
- const fileName = elementWithSource._source?.fileName;
- const lineNumber = elementWithSource._source?.lineNumber;
- sourceMessage = fileName ? `${fileName}:${lineNumber}` : '';
- } catch (error) {
- debugLogger.warn('Error while trying to get file name:', error);
- }
-
- debugLogger.warn(
- message,
- `${String(element.type)}. Source: ${sourceMessage}`,
- );
-}
interface MaxSizedBoxProps {
children?: React.ReactNode;
maxWidth?: number;
- maxHeight: number | undefined;
+ maxHeight?: number;
overflowDirection?: 'top' | 'bottom';
additionalHiddenLinesCount?: number;
}
@@ -64,41 +28,6 @@ interface MaxSizedBoxProps {
/**
* A React component that constrains the size of its children and provides
* content-aware truncation when the content exceeds the specified `maxHeight`.
- *
- * `MaxSizedBox` requires a specific structure for its children to correctly
- * measure and render the content:
- *
- * 1. **Direct children must be `` elements.** Each `` represents a
- * single row of content.
- * 2. **Row `` elements must contain only `` elements.** These
- * `` elements can be nested and there are no restrictions to Text
- * element styling other than that non-wrapping text elements must be
- * before wrapping text elements.
- *
- * **Constraints:**
- * - **Box Properties:** Custom properties on the child `` elements are
- * ignored. In debug mode, runtime checks will report errors for any
- * unsupported properties.
- * - **Text Wrapping:** Within a single row, `` elements with no wrapping
- * (e.g., headers, labels) must appear before any `` elements that wrap.
- * - **Element Types:** Runtime checks will warn if unsupported element types
- * are used as children.
- *
- * @example
- *
- *
- * This is the first line.
- *
- *
- * Non-wrapping Header:
- * This is the rest of the line which will wrap if it's too long.
- *
- *
- *
- * Line 3 with nested styled text inside of it.
- *
- *
- *
*/
export const MaxSizedBox: React.FC = ({
children,
@@ -109,49 +38,50 @@ export const MaxSizedBox: React.FC = ({
}) => {
const id = useId();
const { addOverflowingId, removeOverflowingId } = useOverflowActions() || {};
+ const observerRef = useRef(null);
+ const [contentHeight, setContentHeight] = useState(0);
- const laidOutStyledText: StyledText[][] = [];
- const targetMaxHeight = Math.max(
- Math.round(maxHeight ?? Number.MAX_SAFE_INTEGER),
- MINIMUM_MAX_HEIGHT,
+ const onRefChange = useCallback(
+ (node: DOMElement | null) => {
+ if (observerRef.current) {
+ observerRef.current.disconnect();
+ observerRef.current = null;
+ }
+
+ if (node && maxHeight !== undefined) {
+ const observer = new ResizeObserver((entries) => {
+ const entry = entries[0];
+ if (entry) {
+ setContentHeight(entry.contentRect.height);
+ }
+ });
+ observer.observe(node);
+ observerRef.current = observer;
+ }
+ },
+ [maxHeight],
);
- if (maxWidth === undefined) {
- throw new Error('maxWidth must be defined when maxHeight is set.');
- }
- function visitRows(element: React.ReactNode) {
- if (!React.isValidElement<{ children?: React.ReactNode }>(element)) {
- return;
- }
+ const effectiveMaxHeight =
+ maxHeight !== undefined
+ ? Math.max(Math.round(maxHeight), MINIMUM_MAX_HEIGHT)
+ : undefined;
- if (element.type === Fragment) {
- React.Children.forEach(element.props.children, visitRows);
- return;
- }
-
- if (element.type === Box) {
- layoutInkElementAsStyledText(element, maxWidth!, laidOutStyledText);
- return;
- }
-
- debugReportError('MaxSizedBox children must be elements', element);
- }
-
- React.Children.forEach(children, visitRows);
-
- const contentWillOverflow =
- (targetMaxHeight !== undefined &&
- laidOutStyledText.length > targetMaxHeight) ||
+ const isOverflowing =
+ (effectiveMaxHeight !== undefined && contentHeight > effectiveMaxHeight) ||
additionalHiddenLinesCount > 0;
+
+ // If we're overflowing, we need to hide at least 1 line for the message.
const visibleContentHeight =
- contentWillOverflow && targetMaxHeight !== undefined
- ? targetMaxHeight - 1
- : targetMaxHeight;
+ isOverflowing && effectiveMaxHeight !== undefined
+ ? effectiveMaxHeight - 1
+ : effectiveMaxHeight;
const hiddenLinesCount =
visibleContentHeight !== undefined
- ? Math.max(0, laidOutStyledText.length - visibleContentHeight)
+ ? Math.max(0, contentHeight - visibleContentHeight)
: 0;
+
const totalHiddenLines = hiddenLinesCount + additionalHiddenLinesCount;
useEffect(() => {
@@ -166,36 +96,40 @@ export const MaxSizedBox: React.FC = ({
};
}, [id, totalHiddenLines, addOverflowingId, removeOverflowingId]);
- const visibleStyledText =
- hiddenLinesCount > 0
- ? overflowDirection === 'top'
- ? laidOutStyledText.slice(hiddenLinesCount, laidOutStyledText.length)
- : laidOutStyledText.slice(0, visibleContentHeight)
- : laidOutStyledText;
+ if (effectiveMaxHeight === undefined) {
+ return (
+
+ {children}
+
+ );
+ }
- const visibleLines = visibleStyledText.map((line, index) => (
-
- {line.length > 0 ? (
- line.map((segment, segIndex) => (
-
- {segment.text}
-
- ))
- ) : (
-
- )}
-
- ));
+ const offset =
+ hiddenLinesCount > 0 && overflowDirection === 'top' ? -hiddenLinesCount : 0;
return (
-
+
{totalHiddenLines > 0 && overflowDirection === 'top' && (
... first {totalHiddenLines} line{totalHiddenLines === 1 ? '' : 's'}{' '}
hidden ...
)}
- {visibleLines}
+
+
+ {children}
+
+
{totalHiddenLines > 0 && overflowDirection === 'bottom' && (
... last {totalHiddenLines} line{totalHiddenLines === 1 ? '' : 's'}{' '}
@@ -205,423 +139,3 @@ export const MaxSizedBox: React.FC = ({
);
};
-
-// Define a type for styled text segments
-interface StyledText {
- text: string;
- props: Record;
-}
-
-/**
- * Single row of content within the MaxSizedBox.
- *
- * A row can contain segments that are not wrapped, followed by segments that
- * are. This is a minimal implementation that only supports the functionality
- * needed today.
- */
-interface Row {
- noWrapSegments: StyledText[];
- segments: StyledText[];
-}
-
-/**
- * Flattens the child elements of MaxSizedBox into an array of `Row` objects.
- *
- * This function expects a specific child structure to function correctly:
- * 1. The top-level child of `MaxSizedBox` should be a single ``. This
- * outer box is primarily for structure and is not directly rendered.
- * 2. Inside the outer ``, there should be one or more children. Each of
- * these children must be a `` that represents a row.
- * 3. Inside each "row" ``, the children must be `` components.
- *
- * The structure should look like this:
- *
- * // Row 1
- * ...
- * ...
- *
- * // Row 2
- * ...
- *
- *
- *
- * It is an error for a child without wrapping to appear after a
- * child with wrapping within the same row Box.
- *
- * @param element The React node to flatten.
- * @returns An array of `Row` objects.
- */
-function visitBoxRow(element: React.ReactNode): Row {
- if (
- !React.isValidElement<{ children?: React.ReactNode }>(element) ||
- element.type !== Box
- ) {
- debugReportError(
- `All children of MaxSizedBox must be elements`,
- element,
- );
- return {
- noWrapSegments: [{ text: '', props: {} }],
- segments: [],
- };
- }
-
- if (enableDebugLog) {
- const boxProps = element.props as {
- children?: React.ReactNode;
- readonly flexDirection?:
- | 'row'
- | 'column'
- | 'row-reverse'
- | 'column-reverse';
- };
- // Ensure the Box has no props other than the default ones and key.
- let maxExpectedProps = 4;
- if (boxProps.children !== undefined) {
- // Allow the key prop, which is automatically added by React.
- maxExpectedProps += 1;
- }
- if (
- boxProps.flexDirection !== undefined &&
- boxProps.flexDirection !== 'row'
- ) {
- debugReportError(
- 'MaxSizedBox children must have flexDirection="row".',
- element,
- );
- }
- if (Object.keys(boxProps).length > maxExpectedProps) {
- debugReportError(
- `Boxes inside MaxSizedBox must not have additional props. ${Object.keys(
- boxProps,
- ).join(', ')}`,
- element,
- );
- }
- }
-
- const row: Row = {
- noWrapSegments: [],
- segments: [],
- };
-
- let hasSeenWrapped = false;
-
- function visitRowChild(
- element: React.ReactNode,
- parentProps: Record | undefined,
- ) {
- if (element === null) {
- return;
- }
- if (typeof element === 'string' || typeof element === 'number') {
- const text = String(element);
- // Ignore empty strings as they don't need to be rendered.
- if (!text) {
- return;
- }
-
- const segment: StyledText = { text, props: parentProps ?? {} };
-
- // Check the 'wrap' property from the merged props to decide the segment type.
- if (parentProps === undefined || parentProps['wrap'] === 'wrap') {
- hasSeenWrapped = true;
- row.segments.push(segment);
- } else {
- if (!hasSeenWrapped) {
- row.noWrapSegments.push(segment);
- } else {
- // put in the wrapped segment as the row is already stuck in wrapped mode.
- row.segments.push(segment);
- debugReportError(
- 'Text elements without wrapping cannot appear after elements with wrapping in the same row.',
- element,
- );
- }
- }
- return;
- }
-
- if (!React.isValidElement<{ children?: React.ReactNode }>(element)) {
- debugReportError('Invalid element.', element);
- return;
- }
-
- if (element.type === Fragment) {
- React.Children.forEach(element.props.children, (child) =>
- visitRowChild(child, parentProps),
- );
- return;
- }
-
- if (element.type !== Text) {
- debugReportError(
- 'Children of a row Box must be elements.',
- element,
- );
- return;
- }
-
- // Merge props from parent elements. Child props take precedence.
- const { children, ...currentProps } = element.props;
- const mergedProps =
- parentProps === undefined
- ? currentProps
- : { ...parentProps, ...currentProps };
- React.Children.forEach(children, (child) =>
- visitRowChild(child, mergedProps),
- );
- }
-
- React.Children.forEach(element.props.children, (child) =>
- visitRowChild(child, undefined),
- );
-
- return row;
-}
-
-function layoutInkElementAsStyledText(
- element: React.ReactElement,
- maxWidth: number,
- output: StyledText[][],
-) {
- const row = visitBoxRow(element);
- if (row.segments.length === 0 && row.noWrapSegments.length === 0) {
- // Return a single empty line if there are no segments to display
- output.push([]);
- return;
- }
-
- const lines: StyledText[][] = [];
- const nonWrappingContent: StyledText[] = [];
- let noWrappingWidth = 0;
-
- // First, lay out the non-wrapping segments
- row.noWrapSegments.forEach((segment) => {
- nonWrappingContent.push(segment);
- noWrappingWidth += stringWidth(segment.text);
- });
-
- if (row.segments.length === 0) {
- // This is a bit of a special case when there are no segments that allow
- // wrapping. It would be ideal to unify.
- const lines: StyledText[][] = [];
- let currentLine: StyledText[] = [];
- nonWrappingContent.forEach((segment) => {
- const textLines = segment.text.split('\n');
- textLines.forEach((text, index) => {
- if (index > 0) {
- lines.push(currentLine);
- currentLine = [];
- }
- if (text) {
- currentLine.push({ text, props: segment.props });
- }
- });
- });
- if (
- currentLine.length > 0 ||
- (nonWrappingContent.length > 0 &&
- nonWrappingContent[nonWrappingContent.length - 1].text.endsWith('\n'))
- ) {
- lines.push(currentLine);
- }
- for (const line of lines) {
- output.push(line);
- }
- return;
- }
-
- const availableWidth = maxWidth - noWrappingWidth;
-
- if (availableWidth < 1) {
- // No room to render the wrapping segments. Truncate the non-wrapping
- // content and append an ellipsis so the line always fits within maxWidth.
-
- // Handle line breaks in non-wrapping content when truncating
- const lines: StyledText[][] = [];
- let currentLine: StyledText[] = [];
- let currentLineWidth = 0;
-
- for (const segment of nonWrappingContent) {
- const textLines = segment.text.split('\n');
- textLines.forEach((text, index) => {
- if (index > 0) {
- // New line encountered, finish current line and start new one
- lines.push(currentLine);
- currentLine = [];
- currentLineWidth = 0;
- }
-
- if (text) {
- const textWidth = stringWidth(text);
-
- // When there's no room for wrapping content, be very conservative
- // For lines after the first line break, show only ellipsis if the text would be truncated
- if (index > 0 && textWidth > 0) {
- // This is content after a line break - just show ellipsis to indicate truncation
- currentLine.push({ text: '…', props: {} });
- currentLineWidth = stringWidth('…');
- } else {
- // This is the first line or a continuation, try to fit what we can
- const maxContentWidth = Math.max(0, maxWidth - stringWidth('…'));
-
- if (textWidth <= maxContentWidth && currentLineWidth === 0) {
- // Text fits completely on this line
- currentLine.push({ text, props: segment.props });
- currentLineWidth += textWidth;
- } else {
- // Text needs truncation
- const codePoints = toCodePoints(text);
- let truncatedWidth = currentLineWidth;
- let sliceEndIndex = 0;
-
- for (const char of codePoints) {
- const charWidth = stringWidth(char);
- if (truncatedWidth + charWidth > maxContentWidth) {
- break;
- }
- truncatedWidth += charWidth;
- sliceEndIndex++;
- }
-
- const slice = codePoints.slice(0, sliceEndIndex).join('');
- if (slice) {
- currentLine.push({ text: slice, props: segment.props });
- }
- currentLine.push({ text: '…', props: {} });
- currentLineWidth = truncatedWidth + stringWidth('…');
- }
- }
- }
- });
- }
-
- // Add the last line if it has content or if the last segment ended with \n
- if (
- currentLine.length > 0 ||
- (nonWrappingContent.length > 0 &&
- nonWrappingContent[nonWrappingContent.length - 1].text.endsWith('\n'))
- ) {
- lines.push(currentLine);
- }
-
- // If we don't have any lines yet, add an ellipsis line
- if (lines.length === 0) {
- lines.push([{ text: '…', props: {} }]);
- }
-
- for (const line of lines) {
- output.push(line);
- }
- return;
- }
-
- // Now, lay out the wrapping segments
- let wrappingPart: StyledText[] = [];
- let wrappingPartWidth = 0;
-
- function addWrappingPartToLines() {
- if (lines.length === 0) {
- lines.push([...nonWrappingContent, ...wrappingPart]);
- } else {
- if (noWrappingWidth > 0) {
- lines.push([
- ...[{ text: ' '.repeat(noWrappingWidth), props: {} }],
- ...wrappingPart,
- ]);
- } else {
- lines.push(wrappingPart);
- }
- }
- wrappingPart = [];
- wrappingPartWidth = 0;
- }
-
- function addToWrappingPart(text: string, props: Record) {
- if (
- wrappingPart.length > 0 &&
- wrappingPart[wrappingPart.length - 1].props === props
- ) {
- wrappingPart[wrappingPart.length - 1].text += text;
- } else {
- wrappingPart.push({ text, props });
- }
- }
-
- row.segments.forEach((segment) => {
- const linesFromSegment = segment.text.split('\n');
-
- linesFromSegment.forEach((lineText, lineIndex) => {
- if (lineIndex > 0) {
- addWrappingPartToLines();
- }
-
- const words = lineText.split(/(\s+)/); // Split by whitespace
-
- words.forEach((word) => {
- if (!word) return;
- const wordWidth = stringWidth(word);
-
- if (
- wrappingPartWidth + wordWidth > availableWidth &&
- wrappingPartWidth > 0
- ) {
- addWrappingPartToLines();
- if (/^\s+$/.test(word)) {
- return;
- }
- }
-
- if (wordWidth > availableWidth) {
- // Word is too long, needs to be split across lines
- const wordAsCodePoints = toCodePoints(word);
- let remainingWordAsCodePoints = wordAsCodePoints;
- while (remainingWordAsCodePoints.length > 0) {
- let splitIndex = 0;
- let currentSplitWidth = 0;
- for (const char of remainingWordAsCodePoints) {
- const charWidth = stringWidth(char);
- if (
- wrappingPartWidth + currentSplitWidth + charWidth >
- availableWidth
- ) {
- break;
- }
- currentSplitWidth += charWidth;
- splitIndex++;
- }
-
- if (splitIndex > 0) {
- const part = remainingWordAsCodePoints
- .slice(0, splitIndex)
- .join('');
- addToWrappingPart(part, segment.props);
- wrappingPartWidth += stringWidth(part);
- remainingWordAsCodePoints =
- remainingWordAsCodePoints.slice(splitIndex);
- }
-
- if (remainingWordAsCodePoints.length > 0) {
- addWrappingPartToLines();
- }
- }
- } else {
- addToWrappingPart(word, segment.props);
- wrappingPartWidth += wordWidth;
- }
- });
- });
- // Split omits a trailing newline, so we need to handle it here
- if (segment.text.endsWith('\n')) {
- addWrappingPartToLines();
- }
- });
-
- if (wrappingPart.length > 0) {
- addWrappingPartToLines();
- }
- for (const line of lines) {
- output.push(line);
- }
-}
diff --git a/packages/cli/src/ui/components/shared/__snapshots__/MaxSizedBox.test.tsx.snap b/packages/cli/src/ui/components/shared/__snapshots__/MaxSizedBox.test.tsx.snap
new file mode 100644
index 0000000000..7725615aa2
--- /dev/null
+++ b/packages/cli/src/ui/components/shared/__snapshots__/MaxSizedBox.test.tsx.snap
@@ -0,0 +1,71 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[` > accounts for additionalHiddenLinesCount 1`] = `
+"... first 7 lines hidden ...
+Line 3"
+`;
+
+exports[` > clips a long single text child from the bottom 1`] = `
+"Line 1
+Line 2
+Line 3
+Line 4
+Line 5
+Line 6
+Line 7
+Line 8
+Line 9
+... last 21 lines hidden ..."
+`;
+
+exports[` > clips a long single text child from the top 1`] = `
+"... first 21 lines hidden ...
+Line 22
+Line 23
+Line 24
+Line 25
+Line 26
+Line 27
+Line 28
+Line 29
+Line 30"
+`;
+
+exports[` > does not truncate when maxHeight is undefined 1`] = `
+"Line 1
+Line 2"
+`;
+
+exports[` > handles React.Fragment as a child 1`] = `
+"Line 1 from Fragment
+Line 2 from Fragment
+Line 3 direct child"
+`;
+
+exports[` > hides lines at the end when content exceeds maxHeight and overflowDirection is bottom 1`] = `
+"Line 1
+... last 2 lines hidden ..."
+`;
+
+exports[` > hides lines when content exceeds maxHeight 1`] = `
+"... first 2 lines hidden ...
+Line 3"
+`;
+
+exports[` > renders children without truncation when they fit 1`] = `"Hello, World!"`;
+
+exports[` > shows plural "lines" when more than one line is hidden 1`] = `
+"... first 2 lines hidden ...
+Line 3"
+`;
+
+exports[` > shows singular "line" when exactly one line is hidden 1`] = `
+"... first 1 line hidden ...
+Line 1"
+`;
+
+exports[` > wraps text that exceeds maxWidth 1`] = `
+"This is a
+long line
+of text"
+`;
diff --git a/packages/cli/src/ui/utils/CodeColorizer.tsx b/packages/cli/src/ui/utils/CodeColorizer.tsx
index ae05eb8ea2..75571883f4 100644
--- a/packages/cli/src/ui/utils/CodeColorizer.tsx
+++ b/packages/cli/src/ui/utils/CodeColorizer.tsx
@@ -178,17 +178,8 @@ export function colorizeCode({
);
return (
-
- {/* We have to render line numbers differently depending on whether we are using MaxSizeBox or not */}
- {showLineNumbers && useMaxSizedBox && (
-
- {`${String(index + 1 + hiddenLinesCount).padStart(
- padWidth,
- ' ',
- )} `}
-
- )}
- {showLineNumbers && !useMaxSizedBox && (
+
+ {showLineNumbers && (
(
-
- {/* We have to render line numbers differently depending on whether we are using MaxSizeBox or not */}
- {showLineNumbers && useMaxSizedBox && (
-
- {`${String(index + 1).padStart(padWidth, ' ')} `}
-
- )}
- {showLineNumbers && !useMaxSizedBox && (
+
+ {showLineNumbers && (