feat(cli): truncate shell output in UI history and improve active shell display (#17438)

This commit is contained in:
Jarrod Whelan
2026-02-08 00:09:48 -08:00
committed by GitHub
parent 31522045cd
commit 4a48d7cf93
34 changed files with 1553 additions and 579 deletions
@@ -4,34 +4,21 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../../test-utils/render.js';
import { renderWithProviders } from '../../../test-utils/render.js';
import { ToolResultDisplay } from './ToolResultDisplay.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Box, Text } from 'ink';
import type { AnsiOutput } from '@google/gemini-cli-core';
// Mock child components to simplify testing
vi.mock('./DiffRenderer.js', () => ({
DiffRenderer: ({
diffContent,
filename,
}: {
diffContent: string;
filename: string;
}) => (
<Box>
<Text>
DiffRenderer: {filename} - {diffContent}
</Text>
</Box>
),
}));
// Mock UIStateContext
// Mock UIStateContext partially
const mockUseUIState = vi.fn();
vi.mock('../../contexts/UIStateContext.js', () => ({
useUIState: () => mockUseUIState(),
}));
vi.mock('../../contexts/UIStateContext.js', async (importOriginal) => {
const actual =
await importOriginal<typeof import('../../contexts/UIStateContext.js')>();
return {
...actual,
useUIState: () => mockUseUIState(),
};
});
// Mock useAlternateBuffer
const mockUseAlternateBuffer = vi.fn();
@@ -39,28 +26,6 @@ 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(),
}),
useOverflowState: () => ({
overflowingIds: new Set(),
}),
}));
describe('ToolResultDisplay', () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -68,6 +33,66 @@ describe('ToolResultDisplay', () => {
mockUseAlternateBuffer.mockReturnValue(false);
});
// Helper to use renderWithProviders
const render = (ui: React.ReactElement) => renderWithProviders(ui);
it('uses ScrollableList for ANSI output in alternate buffer mode', () => {
mockUseAlternateBuffer.mockReturnValue(true);
const content = 'ansi content';
const ansiResult: AnsiOutput = [
[
{
text: content,
fg: 'red',
bg: 'black',
bold: false,
italic: false,
underline: false,
dim: false,
inverse: false,
},
],
];
const { lastFrame } = render(
<ToolResultDisplay
resultDisplay={ansiResult}
terminalWidth={80}
maxLines={10}
/>,
);
const output = lastFrame();
expect(output).toContain(content);
});
it('uses Scrollable for non-ANSI output in alternate buffer mode', () => {
mockUseAlternateBuffer.mockReturnValue(true);
const { lastFrame } = render(
<ToolResultDisplay
resultDisplay="**Markdown content**"
terminalWidth={80}
maxLines={10}
/>,
);
const output = lastFrame();
// With real components, we check for the content itself
expect(output).toContain('Markdown content');
});
it('passes hasFocus prop to scrollable components', () => {
mockUseAlternateBuffer.mockReturnValue(true);
const { lastFrame } = render(
<ToolResultDisplay
resultDisplay="Some result"
terminalWidth={80}
hasFocus={true}
/>,
);
expect(lastFrame()).toContain('Some result');
});
it('renders string result as markdown by default', () => {
const { lastFrame } = render(
<ToolResultDisplay resultDisplay="**Some result**" terminalWidth={80} />,
@@ -194,4 +219,86 @@ describe('ToolResultDisplay', () => {
expect(output).toMatchSnapshot();
});
it('truncates ANSI output when maxLines is provided', () => {
const ansiResult: AnsiOutput = [
[
{
text: 'Line 1',
fg: '',
bg: '',
bold: false,
italic: false,
underline: false,
dim: false,
inverse: false,
},
],
[
{
text: 'Line 2',
fg: '',
bg: '',
bold: false,
italic: false,
underline: false,
dim: false,
inverse: false,
},
],
[
{
text: 'Line 3',
fg: '',
bg: '',
bold: false,
italic: false,
underline: false,
dim: false,
inverse: false,
},
],
];
const { lastFrame } = render(
<ToolResultDisplay
resultDisplay={ansiResult}
terminalWidth={80}
availableTerminalHeight={20}
maxLines={2}
/>,
);
const output = lastFrame();
expect(output).not.toContain('Line 1');
expect(output).toContain('Line 2');
expect(output).toContain('Line 3');
});
it('truncates ANSI output when maxLines is provided, even if availableTerminalHeight is undefined', () => {
const ansiResult: AnsiOutput = Array.from({ length: 50 }, (_, i) => [
{
text: `Line ${i + 1}`,
fg: '',
bg: '',
bold: false,
italic: false,
underline: false,
dim: false,
inverse: false,
},
]);
const { lastFrame } = render(
<ToolResultDisplay
resultDisplay={ansiResult}
terminalWidth={80}
maxLines={25}
availableTerminalHeight={undefined}
/>,
);
const output = lastFrame();
// It SHOULD truncate to 25 lines because maxLines is provided
expect(output).not.toContain('Line 1');
expect(output).toContain('Line 50');
});
});