mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-20 10:10:56 -07:00
feat(cli): truncate shell output in UI history and improve active shell display (#17438)
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user