mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-22 11:04:42 -07:00
Add experimental built-in visualization tooling
This commit is contained in:
@@ -106,10 +106,6 @@ export const profiler = {
|
||||
}
|
||||
|
||||
if (idleInPastSecond >= 5) {
|
||||
if (this.openedDebugConsole === false) {
|
||||
this.openedDebugConsole = true;
|
||||
appEvents.emit(AppEvent.OpenDebugConsole);
|
||||
}
|
||||
debugLogger.error(
|
||||
`${idleInPastSecond} frames rendered while the app was ` +
|
||||
`idle in the past second. This likely indicates severe infinite loop ` +
|
||||
|
||||
@@ -8,6 +8,7 @@ import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { ToolResultDisplay } from './ToolResultDisplay.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { AnsiOutput } from '@google/gemini-cli-core';
|
||||
import type { VisualizationResult as VisualizationDisplay } from './VisualizationDisplay.js';
|
||||
|
||||
// Mock UIStateContext partially
|
||||
const mockUseUIState = vi.fn();
|
||||
@@ -190,6 +191,77 @@ describe('ToolResultDisplay', () => {
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders visualization result', () => {
|
||||
const visualization: VisualizationDisplay = {
|
||||
type: 'visualization',
|
||||
kind: 'bar',
|
||||
title: 'Fastest BMW 0-60',
|
||||
unit: 's',
|
||||
data: {
|
||||
series: [
|
||||
{
|
||||
name: 'BMW',
|
||||
points: [
|
||||
{ label: 'M5 CS', value: 2.9 },
|
||||
{ label: 'M8 Competition', value: 3.0 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
meta: {
|
||||
truncated: false,
|
||||
originalItemCount: 2,
|
||||
},
|
||||
};
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay
|
||||
resultDisplay={visualization}
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={20}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('Fastest BMW 0-60');
|
||||
expect(output).toContain('M5 CS');
|
||||
expect(output).toContain('2.90s');
|
||||
});
|
||||
|
||||
it('renders diagram visualization result', () => {
|
||||
const visualization: VisualizationDisplay = {
|
||||
type: 'visualization',
|
||||
kind: 'diagram',
|
||||
title: 'Service Graph',
|
||||
data: {
|
||||
diagramKind: 'architecture',
|
||||
nodes: [
|
||||
{ id: 'ui', label: 'Web UI', type: 'frontend' },
|
||||
{ id: 'api', label: 'API', type: 'service' },
|
||||
],
|
||||
edges: [{ from: 'ui', to: 'api', label: 'HTTPS' }],
|
||||
},
|
||||
meta: {
|
||||
truncated: false,
|
||||
originalItemCount: 2,
|
||||
},
|
||||
};
|
||||
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay
|
||||
resultDisplay={visualization}
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={20}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('Service Graph');
|
||||
expect(output).toContain('Web UI');
|
||||
expect(output).toContain('API');
|
||||
expect(output).toContain('Notes:');
|
||||
expect(output).toContain('Web UI -> API: HTTPS');
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
@@ -19,6 +19,10 @@ import { Scrollable } from '../shared/Scrollable.js';
|
||||
import { ScrollableList } from '../shared/ScrollableList.js';
|
||||
import { SCROLL_TO_ITEM_END } from '../shared/VirtualizedList.js';
|
||||
import { ACTIVE_SHELL_MAX_LINES } from '../../constants.js';
|
||||
import {
|
||||
VisualizationResultDisplay,
|
||||
type VisualizationResult,
|
||||
} from './VisualizationDisplay.js';
|
||||
|
||||
const STATIC_HEIGHT = 1;
|
||||
const RESERVED_LINE_COUNT = 6; // for tool name, status, padding, and 'ShowMoreLines' hint
|
||||
@@ -180,6 +184,19 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
||||
{truncatedResultDisplay}
|
||||
</Text>
|
||||
);
|
||||
} else if (
|
||||
typeof truncatedResultDisplay === 'object' &&
|
||||
'type' in truncatedResultDisplay &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
(truncatedResultDisplay as VisualizationResult).type === 'visualization'
|
||||
) {
|
||||
content = (
|
||||
<VisualizationResultDisplay
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
visualization={truncatedResultDisplay as VisualizationResult}
|
||||
width={childWidth}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
typeof truncatedResultDisplay === 'object' &&
|
||||
'fileDiff' in truncatedResultDisplay
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import {
|
||||
VisualizationResultDisplay,
|
||||
type VisualizationResult as VisualizationDisplay,
|
||||
} from './VisualizationDisplay.js';
|
||||
|
||||
describe('VisualizationResultDisplay', () => {
|
||||
const render = (ui: React.ReactElement) => renderWithProviders(ui);
|
||||
|
||||
it('renders bar visualization', () => {
|
||||
const visualization: VisualizationDisplay = {
|
||||
type: 'visualization',
|
||||
kind: 'bar',
|
||||
title: 'BMW 0-60',
|
||||
unit: 's',
|
||||
data: {
|
||||
series: [
|
||||
{
|
||||
name: 'BMW',
|
||||
points: [
|
||||
{ label: 'M5 CS', value: 2.9 },
|
||||
{ label: 'M8', value: 3.0 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
meta: {
|
||||
truncated: false,
|
||||
originalItemCount: 2,
|
||||
},
|
||||
};
|
||||
|
||||
const { lastFrame } = render(
|
||||
<VisualizationResultDisplay visualization={visualization} width={80} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('BMW 0-60');
|
||||
expect(lastFrame()).toContain('M5 CS');
|
||||
expect(lastFrame()).toContain('2.90s');
|
||||
});
|
||||
|
||||
it('renders line and pie visualizations', () => {
|
||||
const line: VisualizationDisplay = {
|
||||
type: 'visualization',
|
||||
kind: 'line',
|
||||
title: 'BMW models by year',
|
||||
xLabel: 'Year',
|
||||
yLabel: 'Models',
|
||||
data: {
|
||||
series: [
|
||||
{
|
||||
name: 'Total',
|
||||
points: [
|
||||
{ label: '2021', value: 15 },
|
||||
{ label: '2022', value: 16 },
|
||||
{ label: '2023', value: 17 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
meta: {
|
||||
truncated: false,
|
||||
originalItemCount: 3,
|
||||
},
|
||||
};
|
||||
|
||||
const pie: VisualizationDisplay = {
|
||||
type: 'visualization',
|
||||
kind: 'pie',
|
||||
data: {
|
||||
slices: [
|
||||
{ label: 'M', value: 40 },
|
||||
{ label: 'X', value: 60 },
|
||||
],
|
||||
},
|
||||
meta: {
|
||||
truncated: false,
|
||||
originalItemCount: 2,
|
||||
},
|
||||
};
|
||||
|
||||
const lineFrame = render(
|
||||
<VisualizationResultDisplay visualization={line} width={70} />,
|
||||
).lastFrame();
|
||||
const pieFrame = render(
|
||||
<VisualizationResultDisplay visualization={pie} width={70} />,
|
||||
).lastFrame();
|
||||
|
||||
expect(lineFrame).toContain('Total:');
|
||||
expect(lineFrame).toContain('x: Year | y: Models');
|
||||
expect(pieFrame).toContain('Slice');
|
||||
expect(pieFrame).toContain('Share');
|
||||
});
|
||||
|
||||
it('renders rich table visualization with metric bars', () => {
|
||||
const visualization: VisualizationDisplay = {
|
||||
type: 'visualization',
|
||||
kind: 'table',
|
||||
title: 'Risk Table',
|
||||
data: {
|
||||
columns: ['Path', 'Score', 'Lines'],
|
||||
rows: [
|
||||
['src/core.ts', 95, 210],
|
||||
['src/ui.tsx', 45, 80],
|
||||
],
|
||||
metricColumns: [1],
|
||||
},
|
||||
meta: {
|
||||
truncated: true,
|
||||
originalItemCount: 8,
|
||||
},
|
||||
};
|
||||
|
||||
const { lastFrame } = render(
|
||||
<VisualizationResultDisplay visualization={visualization} width={90} />,
|
||||
);
|
||||
|
||||
const frame = lastFrame();
|
||||
expect(frame).toContain('Risk Table');
|
||||
expect(frame).toContain('Path');
|
||||
expect(frame).toContain('Showing truncated data (8 original items)');
|
||||
});
|
||||
|
||||
it('renders diagram visualization with UML-like nodes', () => {
|
||||
const visualization: VisualizationDisplay = {
|
||||
type: 'visualization',
|
||||
kind: 'diagram',
|
||||
title: 'Service Architecture',
|
||||
data: {
|
||||
diagramKind: 'architecture',
|
||||
direction: 'LR',
|
||||
nodes: [
|
||||
{ id: 'ui', label: 'Web UI', type: 'frontend' },
|
||||
{ id: 'api', label: 'API', type: 'service' },
|
||||
],
|
||||
edges: [{ from: 'ui', to: 'api', label: 'HTTPS' }],
|
||||
},
|
||||
meta: {
|
||||
truncated: false,
|
||||
originalItemCount: 2,
|
||||
},
|
||||
};
|
||||
|
||||
const { lastFrame } = render(
|
||||
<VisualizationResultDisplay visualization={visualization} width={90} />,
|
||||
);
|
||||
|
||||
const frame = lastFrame();
|
||||
expect(frame).toContain('Service Architecture');
|
||||
expect(frame).toContain('┌');
|
||||
expect(frame).toContain('>');
|
||||
expect(frame).toContain('Notes:');
|
||||
expect(frame).toContain('Web UI -> API: HTTPS');
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user