diff --git a/docs/cli/settings.md b/docs/cli/settings.md index 4a6b9a77b7..dbb3651a4f 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -75,7 +75,7 @@ they appear in the UI. | Show User Identity | `ui.showUserIdentity` | Show the signed-in user's identity (e.g. email) in the UI. | `true` | | Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` | | Render Process | `ui.renderProcess` | Enable Ink render process for the UI. | `true` | -| Terminal Buffer | `ui.terminalBuffer` | Use the new terminal buffer architecture for rendering. | `true` | +| Terminal Buffer | `ui.terminalBuffer` | Use the new terminal buffer architecture for rendering. | `false` | | Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` | | Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` | | Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` | diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 1955507c62..1fdbc755f0 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -346,7 +346,7 @@ their corresponding top-level category object in your `settings.json` file. - **`ui.terminalBuffer`** (boolean): - **Description:** Use the new terminal buffer architecture for rendering. - - **Default:** `true` + - **Default:** `false` - **Requires restart:** Yes - **`ui.useBackgroundColor`** (boolean): diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 730bd4b939..c041aaa8c3 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -757,7 +757,7 @@ const SETTINGS_SCHEMA = { label: 'Terminal Buffer', category: 'UI', requiresRestart: true, - default: true, + default: false, description: 'Use the new terminal buffer architecture for rendering.', showInDialog: true, }, diff --git a/packages/cli/src/interactiveCli.tsx b/packages/cli/src/interactiveCli.tsx index 418f58b193..965bc27693 100644 --- a/packages/cli/src/interactiveCli.tsx +++ b/packages/cli/src/interactiveCli.tsx @@ -156,8 +156,9 @@ export async function startInteractiveUI( useAlternateBuffer || config.getUseTerminalBuffer(), patchConsole: false, alternateBuffer: useAlternateBuffer, - renderProcess: config.getUseRenderProcess(), terminalBuffer: config.getUseTerminalBuffer(), + renderProcess: + config.getUseRenderProcess() && config.getUseTerminalBuffer(), incrementalRendering: settings.merged.ui.incrementalRendering !== false && useAlternateBuffer && diff --git a/packages/cli/src/ui/__snapshots__/App.test.tsx.snap b/packages/cli/src/ui/__snapshots__/App.test.tsx.snap index 94b1f9b1a4..611f2e0908 100644 --- a/packages/cli/src/ui/__snapshots__/App.test.tsx.snap +++ b/packages/cli/src/ui/__snapshots__/App.test.tsx.snap @@ -55,6 +55,12 @@ Footer Gemini CLI v1.2.3 + +Tips for getting started: +1. Create GEMINI.md files to customize your interactions +2. /help for more information +3. Ask coding questions, edit code or run commands +4. Be specific for the best results Composer " `; diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 4d40809837..3fdaa479cc 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -69,6 +69,7 @@ import { AppEvent, TransientMessageType, } from '../../utils/events.js'; +import '../../test-utils/customMatchers.js'; vi.mock('../hooks/useShellHistory.js'); vi.mock('../hooks/useCommandCompletion.js'); @@ -254,7 +255,7 @@ describe('InputPrompt', () => { setText: vi.fn( (newText: string, cursorPosition?: 'start' | 'end' | number) => { mockBuffer.text = newText; - mockBuffer.lines = [newText]; + mockBuffer.lines = newText.split('\n'); let col = 0; if (typeof cursorPosition === 'number') { col = cursorPosition; @@ -264,11 +265,18 @@ describe('InputPrompt', () => { col = newText.length; } mockBuffer.cursor = [0, col]; - mockBuffer.allVisualLines = [newText]; - mockBuffer.viewportVisualLines = [newText]; - mockBuffer.allVisualLines = [newText]; - mockBuffer.visualToLogicalMap = [[0, 0]]; + mockBuffer.allVisualLines = newText.split('\n'); + mockBuffer.viewportVisualLines = newText.split('\n'); + mockBuffer.visualToLogicalMap = newText + .split('\n') + .map((_, i) => [i, 0] as [number, number]); mockBuffer.visualCursor = [0, col]; + mockBuffer.visualScrollRow = 0; + mockBuffer.viewportHeight = 10; + mockBuffer.visualToTransformedMap = newText + .split('\n') + .map((_, i) => i); + mockBuffer.transformationsByLine = newText.split('\n').map(() => []); }, ), replaceRangeByOffset: vi.fn(), @@ -276,6 +284,7 @@ describe('InputPrompt', () => { allVisualLines: [''], visualCursor: [0, 0], visualScrollRow: 0, + viewportHeight: 10, handleInput: vi.fn((key: Key) => { if (defaultKeyMatchers[Command.CLEAR_INPUT](key)) { if (mockBuffer.text.length > 0) { @@ -409,6 +418,7 @@ describe('InputPrompt', () => { getTargetDir: () => path.join('test', 'project', 'src'), getVimMode: () => false, getUseBackgroundColor: () => true, + getUseTerminalBuffer: () => false, getTerminalBackground: () => undefined, getWorkspaceContext: () => ({ getDirectories: () => ['/test/project/src'], @@ -3779,11 +3789,7 @@ describe('InputPrompt', () => { ); it('should unfocus embedded shell on click', async () => { - props.buffer.text = 'hello'; - props.buffer.lines = ['hello']; - props.buffer.allVisualLines = ['hello']; - props.buffer.viewportVisualLines = ['hello']; - props.buffer.visualToLogicalMap = [[0, 0]]; + props.buffer.setText('hello'); props.isEmbeddedShellFocused = true; const { stdin, stdout, unmount } = await renderWithProviders( @@ -4291,11 +4297,7 @@ describe('InputPrompt', () => { describe('IME Cursor Support', () => { it('should report correct cursor position for simple ASCII text', async () => { const text = 'hello'; - mockBuffer.text = text; - mockBuffer.lines = [text]; - mockBuffer.allVisualLines = [text]; - mockBuffer.viewportVisualLines = [text]; - mockBuffer.visualToLogicalMap = [[0, 0]]; + mockBuffer.setText(text); mockBuffer.visualCursor = [0, 3]; // Cursor after 'hel' mockBuffer.visualScrollRow = 0; @@ -4322,11 +4324,7 @@ describe('InputPrompt', () => { it('should report correct cursor position for text with double-width characters', async () => { const text = 'πŸ‘hello'; - mockBuffer.text = text; - mockBuffer.lines = [text]; - mockBuffer.allVisualLines = [text]; - mockBuffer.viewportVisualLines = [text]; - mockBuffer.visualToLogicalMap = [[0, 0]]; + mockBuffer.setText(text); mockBuffer.visualCursor = [0, 2]; // Cursor after 'πŸ‘h' (Note: 'πŸ‘' is one code point but width 2) mockBuffer.visualScrollRow = 0; @@ -4352,11 +4350,7 @@ describe('InputPrompt', () => { it('should report correct cursor position for a line full of "πŸ˜€" emojis', async () => { const text = 'πŸ˜€πŸ˜€πŸ˜€'; - mockBuffer.text = text; - mockBuffer.lines = [text]; - mockBuffer.allVisualLines = [text]; - mockBuffer.viewportVisualLines = [text]; - mockBuffer.visualToLogicalMap = [[0, 0]]; + mockBuffer.setText(text); mockBuffer.visualCursor = [0, 2]; // Cursor after 2 emojis (each 1 code point, width 2) mockBuffer.visualScrollRow = 0; @@ -4501,12 +4495,12 @@ describe('InputPrompt', () => { mockBuffer.lines = [logicalLine]; mockBuffer.allVisualLines = [visualLine]; mockBuffer.viewportVisualLines = [visualLine]; - mockBuffer.allVisualLines = [visualLine]; mockBuffer.visualToLogicalMap = [[0, 0]]; mockBuffer.visualToTransformedMap = [0]; mockBuffer.transformationsByLine = [transformations]; mockBuffer.cursor = [0, cursorCol]; - mockBuffer.visualCursor = [0, 0]; + mockBuffer.visualCursor = [0, cursorCol]; + mockBuffer.visualScrollRow = 0; }; it('should snapshot collapsed image path', async () => { diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index c8d7efa1b4..7e59ab4d14 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -5,7 +5,14 @@ */ import type React from 'react'; -import { useCallback, useEffect, useState, useRef, useMemo } from 'react'; +import { + useCallback, + useEffect, + useState, + useRef, + useMemo, + Fragment, +} from 'react'; import clipboardy from 'clipboardy'; import { Box, Text, useStdout, type DOMElement } from 'ink'; import { SuggestionsDisplay, MAX_WIDTH } from './SuggestionsDisplay.js'; @@ -1820,24 +1827,45 @@ export const InputPrompt: React.FC = ({ height={Math.min(buffer.viewportHeight, scrollableData.length)} width="100%" > - 1} - keyExtractor={(item) => - item.type === 'visualLine' - ? `line-${item.absoluteVisualIdx}` - : `ghost-${item.index}` - } - width="100%" - backgroundColor={listBackgroundColor} - containerHeight={Math.min( - buffer.viewportHeight, - scrollableData.length, - )} - /> + {isAlternateBuffer ? ( + 1} + fixedItemHeight={true} + keyExtractor={(item) => + item.type === 'visualLine' + ? `line-${item.absoluteVisualIdx}` + : `ghost-${item.index}` + } + width={inputWidth} + backgroundColor={listBackgroundColor} + containerHeight={Math.min( + buffer.viewportHeight, + scrollableData.length, + )} + /> + ) : ( + scrollableData + .slice( + buffer.visualScrollRow, + buffer.visualScrollRow + buffer.viewportHeight, + ) + .map((item, index) => { + const actualIndex = buffer.visualScrollRow + index; + const key = + item.type === 'visualLine' + ? `line-${item.absoluteVisualIdx}` + : `ghost-${item.index}`; + return ( + + {renderItem({ item, index: actualIndex })} + + ); + }) + )} )} diff --git a/packages/cli/src/ui/components/__snapshots__/HistoryItemDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/HistoryItemDisplay.test.tsx.snap index 7d6fdeb42c..d237b30f99 100644 --- a/packages/cli/src/ui/components/__snapshots__/HistoryItemDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/HistoryItemDisplay.test.tsx.snap @@ -112,48 +112,7 @@ exports[` > gemini items (alternateBuffer=false) > should exports[` > gemini items (alternateBuffer=false) > should render a truncated gemini item 1`] = ` "✦ Example code block: - 1 Line 1 - 2 Line 2 - 3 Line 3 - 4 Line 4 - 5 Line 5 - 6 Line 6 - 7 Line 7 - 8 Line 8 - 9 Line 9 - 10 Line 10 - 11 Line 11 - 12 Line 12 - 13 Line 13 - 14 Line 14 - 15 Line 15 - 16 Line 16 - 17 Line 17 - 18 Line 18 - 19 Line 19 - 20 Line 20 - 21 Line 21 - 22 Line 22 - 23 Line 23 - 24 Line 24 - 25 Line 25 - 26 Line 26 - 27 Line 27 - 28 Line 28 - 29 Line 29 - 30 Line 30 - 31 Line 31 - 32 Line 32 - 33 Line 33 - 34 Line 34 - 35 Line 35 - 36 Line 36 - 37 Line 37 - 38 Line 38 - 39 Line 39 - 40 Line 40 - 41 Line 41 - 42 Line 42 + ... 42 hidden (Ctrl+O) ... 43 Line 43 44 Line 44 45 Line 45 @@ -167,48 +126,7 @@ exports[` > gemini items (alternateBuffer=false) > should exports[` > gemini items (alternateBuffer=false) > should render a truncated gemini_content item 1`] = ` " Example code block: - 1 Line 1 - 2 Line 2 - 3 Line 3 - 4 Line 4 - 5 Line 5 - 6 Line 6 - 7 Line 7 - 8 Line 8 - 9 Line 9 - 10 Line 10 - 11 Line 11 - 12 Line 12 - 13 Line 13 - 14 Line 14 - 15 Line 15 - 16 Line 16 - 17 Line 17 - 18 Line 18 - 19 Line 19 - 20 Line 20 - 21 Line 21 - 22 Line 22 - 23 Line 23 - 24 Line 24 - 25 Line 25 - 26 Line 26 - 27 Line 27 - 28 Line 28 - 29 Line 29 - 30 Line 30 - 31 Line 31 - 32 Line 32 - 33 Line 33 - 34 Line 34 - 35 Line 35 - 36 Line 36 - 37 Line 37 - 38 Line 38 - 39 Line 39 - 40 Line 40 - 41 Line 41 - 42 Line 42 + ... 42 hidden (Ctrl+O) ... 43 Line 43 44 Line 44 45 Line 45 diff --git a/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap index caa270d8c4..ab6fe9b928 100644 --- a/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap @@ -93,7 +93,7 @@ exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > exports[`InputPrompt > History Navigation and Completion Suppression > should not render suggestions during history navigation 1`] = ` "β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€ > second message - +β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„ " `; @@ -120,30 +120,30 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and c exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-collapsed-match 1`] = ` "β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€ (r:) commit - - +β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„ + git commit -m "feat: add search" in src/app " `; exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-expanded-match 1`] = ` "β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€ (r:) commit - - +β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„ + git commit -m "feat: add search" in src/app " `; exports[`InputPrompt > image path transformation snapshots > should snapshot collapsed image path 1`] = ` "β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€ > [Image ...reenshot2x.png] - +β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„ " `; exports[`InputPrompt > image path transformation snapshots > should snapshot expanded image path when cursor is on it 1`] = ` "β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€ > @/path/to/screenshots/screenshot2x.png - +β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„ " `; diff --git a/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx index 57c9050560..676051501c 100644 --- a/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx @@ -293,8 +293,8 @@ describe('', () => { await waitUntilReady(); const frame = lastFrame(); // Since it's Executing, it might still constrain to ACTIVE_SHELL_MAX_LINES (10) - // Actually let's just assert on the behaviour that happens right now (which is 10 lines) - expect(frame.match(/Line \d+/g)?.length).toBe(10); + // Actually let's just assert on the behaviour that happens right now (which is 100 lines because we removed the terminalBuffer check) + expect(frame.match(/Line \d+/g)?.length).toBe(100); unmount(); }); diff --git a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx index c7e5df8750..bdf9f207ed 100644 --- a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx @@ -444,8 +444,8 @@ describe('', () => { constrainHeight: true, }, width: 80, - config: makeFakeConfig({ useAlternateBuffer: false }), - settings: createMockSettings({ ui: { useAlternateBuffer: false } }), + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ ui: { useAlternateBuffer: true } }), }, ); const output = lastFrame(); diff --git a/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx b/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx index f30c309898..c273fa7f47 100644 --- a/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx @@ -5,6 +5,7 @@ */ import { renderWithProviders } from '../../../test-utils/render.js'; +import { waitFor } from '../../../test-utils/async.js'; import { createMockSettings } from '../../../test-utils/settings.js'; import { ToolResultDisplay } from './ToolResultDisplay.js'; import { describe, it, expect, vi } from 'vitest'; @@ -351,9 +352,10 @@ describe('ToolResultDisplay', () => { expect(output).not.toContain('Line 1'); expect(output).not.toContain('Line 2'); - expect(output).toContain('Line 3'); + expect(output).not.toContain('Line 3'); expect(output).toContain('Line 4'); expect(output).toContain('Line 5'); + expect(output).toContain('hidden'); expect(output).toMatchSnapshot(); unmount(); }); @@ -391,4 +393,86 @@ describe('ToolResultDisplay', () => { await expect(renderResult).toMatchSvgSnapshot(); unmount(); }); + + it('stays scrolled to the bottom when lines are incrementally added', async () => { + const createAnsiLine = (text: string) => [ + { + text, + fg: '', + bg: '', + bold: false, + italic: false, + underline: false, + dim: false, + inverse: false, + isUninitialized: false, + }, + ]; + + let currentLines: AnsiOutput = []; + + // Start with 3 lines, max lines 5. It should fit without scrolling. + for (let i = 1; i <= 3; i++) { + currentLines.push(createAnsiLine(`Line ${i}`)); + } + + const renderResult = await renderWithProviders( + , + { + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ ui: { useAlternateBuffer: false } }), + uiState: { constrainHeight: true, terminalHeight: 10 }, + }, + ); + + const { waitUntilReady, rerender, lastFrame, unmount } = renderResult; + await waitUntilReady(); + + // Verify initial render has the first 3 lines + expect(lastFrame()).toContain('Line 1'); + expect(lastFrame()).toContain('Line 3'); + + // Incrementally add lines up to 8. Max lines is 5. + // So by the end, it should only show lines 4-8. + for (let i = 4; i <= 8; i++) { + currentLines = [...currentLines, createAnsiLine(`Line ${i}`)]; + rerender( + , + ); + // Wait for the new line to be rendered + await waitFor(() => { + expect(lastFrame()).toContain(`Line ${i}`); + }); + } + + await waitUntilReady(); + const output = lastFrame(); + + // The component should have automatically scrolled to the bottom. + // Lines 1, 2, 3, 4 should be scrolled out of view. + expect(output).not.toContain('Line 1'); + expect(output).not.toContain('Line 2'); + expect(output).not.toContain('Line 3'); + expect(output).not.toContain('Line 4'); + // Lines 5, 6, 7, 8 should be visible along with the truncation indicator. + expect(output).toContain('hidden'); + expect(output).toContain('Line 5'); + expect(output).toContain('Line 8'); + + expect(output).toMatchSnapshot(); + + unmount(); + }); }); diff --git a/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx b/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx index aaa30a74d7..16c6019c98 100644 --- a/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx +++ b/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx @@ -10,6 +10,7 @@ import { DiffRenderer } from './DiffRenderer.js'; import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; import { AnsiOutputText, AnsiLineText } from '../AnsiOutput.js'; import { SlicingMaxSizedBox } from '../shared/SlicingMaxSizedBox.js'; +import { MaxSizedBox } from '../shared/MaxSizedBox.js'; import { theme } from '../../semantic-colors.js'; import { type AnsiOutput, @@ -51,7 +52,7 @@ export const ToolResultDisplay: React.FC = ({ hasFocus = false, overflowDirection = 'top', }) => { - const { renderMarkdown } = useUIState(); + const { renderMarkdown, constrainHeight } = useUIState(); const isAlternateBuffer = useAlternateBuffer(); const availableHeight = calculateToolContentMaxLines({ @@ -209,30 +210,73 @@ export const ToolResultDisplay: React.FC = ({ if (Array.isArray(resultDisplay)) { const limit = maxLines ?? availableHeight ?? ACTIVE_SHELL_MAX_LINES; - const listHeight = Math.min( - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - (resultDisplay as AnsiOutput).length, - limit, - ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const data = resultDisplay as AnsiOutput; - const initialScrollIndex = - overflowDirection === 'bottom' ? 0 : SCROLL_TO_ITEM_END; + // Calculate list height: if not constrained, use full data length. + // If constrained (e.g. alternate buffer), limit to available height + // to ensure virtualization works and fits within the viewport. + const listHeight = !constrainHeight + ? data.length + : Math.min(data.length, limit); - return ( - - 1} - keyExtractor={keyExtractor} - initialScrollIndex={initialScrollIndex} - hasFocus={hasFocus} - fixedItemHeight={true} - /> - - ); + if (isAlternateBuffer) { + const initialScrollIndex = + overflowDirection === 'bottom' ? 0 : SCROLL_TO_ITEM_END; + + return ( + + 1} + fixedItemHeight={true} + keyExtractor={keyExtractor} + initialScrollIndex={initialScrollIndex} + hasFocus={hasFocus} + /> + + ); + } else { + let displayData = data; + let hiddenLines = 0; + + if (constrainHeight && data.length > listHeight) { + hiddenLines = data.length - listHeight; + if (overflowDirection === 'top') { + displayData = data.slice(hiddenLines); + } else { + displayData = data.slice(0, listHeight); + } + } + + return ( + + + {displayData.map((item, index) => { + const actualIndex = + (overflowDirection === 'top' ? hiddenLines : 0) + index; + return ( + + + + ); + })} + + + ); + } } // ASB Mode Handling (Interactive/Fullscreen) diff --git a/packages/cli/src/ui/components/messages/ToolResultDisplayOverflow.test.tsx b/packages/cli/src/ui/components/messages/ToolResultDisplayOverflow.test.tsx index cd06d93616..397f1ba1a7 100644 --- a/packages/cli/src/ui/components/messages/ToolResultDisplayOverflow.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolResultDisplayOverflow.test.tsx @@ -29,11 +29,12 @@ describe('ToolResultDisplay Overflow', () => { await waitUntilReady(); const output = lastFrame(); - expect(output).not.toContain('Line 1'); - expect(output).not.toContain('Line 2'); - expect(output).toContain('Line 3'); - expect(output).toContain('Line 4'); - expect(output).toContain('Line 5'); + expect(output).toContain('Line 1'); + expect(output).toContain('Line 2'); + expect(output).not.toContain('Line 3'); + expect(output).not.toContain('Line 4'); + expect(output).not.toContain('Line 5'); + expect(output).toContain('hidden'); unmount(); }); @@ -57,9 +58,10 @@ describe('ToolResultDisplay Overflow', () => { expect(output).not.toContain('Line 1'); expect(output).not.toContain('Line 2'); - expect(output).toContain('Line 3'); + expect(output).not.toContain('Line 3'); expect(output).toContain('Line 4'); expect(output).toContain('Line 5'); + expect(output).toContain('hidden'); unmount(); }); @@ -95,11 +97,10 @@ describe('ToolResultDisplay Overflow', () => { expect(output).toContain('Line 1'); expect(output).toContain('Line 2'); - expect(output).toContain('Line 3'); + expect(output).not.toContain('Line 3'); expect(output).not.toContain('Line 4'); expect(output).not.toContain('Line 5'); - // ScrollableList uses a scroll thumb rather than writing "hidden" - expect(output).toContain('β–ˆ'); + expect(output).toContain('hidden'); unmount(); }); }); diff --git a/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage-DenseToolMessage-Visual-Regression-matches-SVG-snapshot-for-an-Accepted-file-edit-with-diff-stats.snap.svg b/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage-DenseToolMessage-Visual-Regression-matches-SVG-snapshot-for-an-Accepted-file-edit-with-diff-stats.snap.svg index 39e6604692..7b21bd65a0 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage-DenseToolMessage-Visual-Regression-matches-SVG-snapshot-for-an-Accepted-file-edit-with-diff-stats.snap.svg +++ b/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage-DenseToolMessage-Visual-Regression-matches-SVG-snapshot-for-an-Accepted-file-edit-with-diff-stats.snap.svg @@ -1,18 +1,33 @@ - + - + βœ“ edit test.ts - β†’ - Accepted + β†’ Accepted ( +1 , -1 ) + + 1 + + + - + + + old + + 1 + + + + + + + new \ No newline at end of file diff --git a/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage.test.tsx.snap index 18f5f93a9f..d08b84c1a9 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage.test.tsx.snap @@ -7,12 +7,21 @@ exports[`DenseToolMessage > Toggleable Diff View (Alternate Buffer) > hides diff exports[`DenseToolMessage > Toggleable Diff View (Alternate Buffer) > shows diff content by default when NOT in alternate buffer mode 1`] = ` " βœ“ test-tool test.ts β†’ Accepted + + 1 - old line + 1 + new line " `; exports[`DenseToolMessage > Visual Regression > matches SVG snapshot for a Rejected tool call 1`] = `" - read_file Reading important.txt"`; -exports[`DenseToolMessage > Visual Regression > matches SVG snapshot for an Accepted file edit with diff stats 1`] = `" βœ“ edit test.ts β†’ Accepted (+1, -1)"`; +exports[`DenseToolMessage > Visual Regression > matches SVG snapshot for an Accepted file edit with diff stats 1`] = ` +" βœ“ edit test.ts β†’ Accepted (+1, -1) + + 1 - old + 1 + new +" +`; exports[`DenseToolMessage > does not render result arrow if resultDisplay is missing 1`] = ` " o test-tool Test description @@ -26,11 +35,17 @@ exports[`DenseToolMessage > flattens newlines in string results 1`] = ` exports[`DenseToolMessage > renders correctly for Edit tool using confirmationDetails 1`] = ` " ? Edit styles.scss β†’ Confirming + + 1 - body { color: blue; } + 1 + body { color: red; } " `; exports[`DenseToolMessage > renders correctly for Errored Edit tool 1`] = ` " x Edit styles.scss β†’ Failed (+1, -1) + + 1 - old line + 1 + new line " `; @@ -45,21 +60,33 @@ exports[`DenseToolMessage > renders correctly for ReadManyFiles results 1`] = ` exports[`DenseToolMessage > renders correctly for Rejected Edit tool 1`] = ` " - Edit styles.scss β†’ Rejected (+1, -1) + + 1 - old line + 1 + new line " `; exports[`DenseToolMessage > renders correctly for Rejected Edit tool with confirmationDetails and diffStat 1`] = ` " - Edit styles.scss β†’ Rejected (+1, -1) + + 1 - body { color: blue; } + 1 + body { color: red; } " `; exports[`DenseToolMessage > renders correctly for Rejected WriteFile tool 1`] = ` " - WriteFile config.json β†’ Rejected + + 1 - old content + 1 + new content " `; exports[`DenseToolMessage > renders correctly for WriteFile tool 1`] = ` " βœ“ WriteFile config.json β†’ Accepted (+1, -1) + + 1 - old content + 1 + new content " `; @@ -75,6 +102,9 @@ exports[`DenseToolMessage > renders correctly for error status with string messa exports[`DenseToolMessage > renders correctly for file diff results with stats 1`] = ` " βœ“ test-tool test.ts β†’ Accepted (+15, -6) + + 1 - old line + 1 + diff content " `; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay-ToolResultDisplay-truncates-ANSI-output-when-maxLines-is-provided-even-if-availableTerminalHeight-is-undefined.snap.svg b/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay-ToolResultDisplay-truncates-ANSI-output-when-maxLines-is-provided-even-if-availableTerminalHeight-is-undefined.snap.svg index 2638c4ad3b..619362a3f4 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay-ToolResultDisplay-truncates-ANSI-output-when-maxLines-is-provided-even-if-availableTerminalHeight-is-undefined.snap.svg +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay-ToolResultDisplay-truncates-ANSI-output-when-maxLines-is-provided-even-if-availableTerminalHeight-is-undefined.snap.svg @@ -4,7 +4,7 @@ - Line 26 + ... 26 hidden (Ctrl+O) ... Line 27 Line 28 Line 29 @@ -16,31 +16,18 @@ Line 35 Line 36 Line 37 - Line 38 - β–„ - Line 39 - β–ˆ - Line 40 - β–ˆ - Line 41 - β–ˆ - Line 42 - β–ˆ - Line 43 - β–ˆ - Line 44 - β–ˆ - Line 45 - β–ˆ - Line 46 - β–ˆ - Line 47 - β–ˆ - Line 48 - β–ˆ - Line 49 - β–ˆ - Line 50 - β–ˆ + Line 38 + Line 39 + Line 40 + Line 41 + Line 42 + Line 43 + Line 44 + Line 45 + Line 46 + Line 47 + Line 48 + Line 49 + Line 50 \ No newline at end of file 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 12eff841b8..2175679bfa 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 @@ -33,15 +33,24 @@ exports[`ToolResultDisplay > renders string result as plain text when renderOutp " `; +exports[`ToolResultDisplay > stays scrolled to the bottom when lines are incrementally added 1`] = ` +"... 4 hidden (Ctrl+O) ... +Line 5 +Line 6 +Line 7 +Line 8 +" +`; + exports[`ToolResultDisplay > truncates ANSI output when maxLines is provided 1`] = ` -"Line 3 -Line 4 β–ˆ -Line 5 β–ˆ +"... 3 hidden (Ctrl+O) ... +Line 4 +Line 5 " `; exports[`ToolResultDisplay > truncates ANSI output when maxLines is provided, even if availableTerminalHeight is undefined 1`] = ` -"Line 26 +"... 26 hidden (Ctrl+O) ... Line 27 Line 28 Line 29 @@ -53,34 +62,36 @@ Line 34 Line 35 Line 36 Line 37 -Line 38 β–„ -Line 39 β–ˆ -Line 40 β–ˆ -Line 41 β–ˆ -Line 42 β–ˆ -Line 43 β–ˆ -Line 44 β–ˆ -Line 45 β–ˆ -Line 46 β–ˆ -Line 47 β–ˆ -Line 48 β–ˆ -Line 49 β–ˆ -Line 50 β–ˆ" +Line 38 +Line 39 +Line 40 +Line 41 +Line 42 +Line 43 +Line 44 +Line 45 +Line 46 +Line 47 +Line 48 +Line 49 +Line 50" `; exports[`ToolResultDisplay > truncates very long string results 1`] = ` -"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… β–ˆ +"... 250 hidden (Ctrl+O) ... +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaa " `; diff --git a/packages/cli/src/ui/components/shared/MaxSizedBox.tsx b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx index baadb3b9d8..1f751cc116 100644 --- a/packages/cli/src/ui/components/shared/MaxSizedBox.tsx +++ b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx @@ -115,7 +115,7 @@ export const MaxSizedBox: React.FC = ({ [id, removeOverflowingId], ); - if (effectiveMaxHeight === undefined) { + if (effectiveMaxHeight === undefined && totalHiddenLines === 0) { return ( {children} diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index d4c7c498a5..0edd4af7b0 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -1224,7 +1224,7 @@ export class Config implements McpContext, AgentLoopContext { this.useRipgrep = params.useRipgrep ?? true; this.useBackgroundColor = params.useBackgroundColor ?? true; this.useAlternateBuffer = params.useAlternateBuffer ?? false; - this.useTerminalBuffer = params.useTerminalBuffer ?? true; + this.useTerminalBuffer = params.useTerminalBuffer ?? false; this.useRenderProcess = params.useRenderProcess ?? true; this.enableInteractiveShell = params.enableInteractiveShell ?? false; diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index 5179263596..bb5c9a9d54 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -465,8 +465,8 @@ "terminalBuffer": { "title": "Terminal Buffer", "description": "Use the new terminal buffer architecture for rendering.", - "markdownDescription": "Use the new terminal buffer architecture for rendering.\n\n- Category: `UI`\n- Requires restart: `yes`\n- Default: `true`", - "default": true, + "markdownDescription": "Use the new terminal buffer architecture for rendering.\n\n- Category: `UI`\n- Requires restart: `yes`\n- Default: `false`", + "default": false, "type": "boolean" }, "useBackgroundColor": {