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 && (