Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7f7e69e
feat(agent): implement tool-controlled display protocol (Steps 2-3)
mbleigh Apr 10, 2026
fae963f
fix(core,cli): handle structured tool display properly and prevent me…
mbleigh Apr 10, 2026
cbff793
Merge branch 'main' into mb/atui/00-display-content
mbleigh Apr 10, 2026
da2fb08
Merge branch 'main' into mb/atui/00-display-content
mbleigh Apr 10, 2026
fbc8767
refactor(cli): consume simplified ToolDisplay property
mbleigh Apr 10, 2026
43f93c3
fix(ui): resolve rebase conflicts and type errors for ToolDisplay
mbleigh Apr 11, 2026
88bebef
fix(ui): hide summary in header when displayed in box
mbleigh Apr 11, 2026
383cb7d
wip: HistoryItemToolGroupDisplay
mbleigh Apr 12, 2026
410e675
revert: remove invasive ToolDisplay logic from legacy UI components
mbleigh Apr 12, 2026
45eabab
feat(cli): refactor tool rendering to declarative ToolDisplay system
mbleigh Apr 12, 2026
9e03476
feat(cli): support 'notice' format and refine declarative tool rendering
mbleigh Apr 12, 2026
8548c66
test(cli): add unit tests for ToolGroupDisplay and implement tool hiding
mbleigh Apr 12, 2026
46377d2
fix(core): restore ReadFolder declarative display and add missing tes…
mbleigh Apr 12, 2026
e2b2621
revert(core): restore intentional ReadFolder display behavior (result…
mbleigh Apr 12, 2026
9802cc7
Merge branch 'main' into mb/atui/00-display-content
mbleigh Apr 13, 2026
ebae075
Merge branch 'mb/atui/00-display-content' into mb/atui/01-ui-rendering
mbleigh Apr 13, 2026
de9a98c
fix(ui): flatten multiline summaries in compact ToolGroupDisplay and …
mbleigh Apr 13, 2026
af5dfc4
feat(cli): refine tool display aesthetics for legacy UI parity
mbleigh Apr 14, 2026
f5e2cf5
merge: catch up mb/atui/01-ui-rendering with main and resolve conflicts
mbleigh May 5, 2026
f50f8aa
fix(ui): address PR feedback for tool display and shell name
mbleigh May 6, 2026
14dd7d1
Merge branch 'main' into mb/atui/01-ui-rendering
mbleigh May 6, 2026
c309ad6
Merge branch 'main' into mb/atui/01-ui-rendering
mbleigh May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/cli/src/ui/components/HistoryItemDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { GeminiMessage } from './messages/GeminiMessage.js';
import { InfoMessage } from './messages/InfoMessage.js';
import { ErrorMessage } from './messages/ErrorMessage.js';
import { ToolGroupMessage } from './messages/ToolGroupMessage.js';
import { ToolGroupDisplay } from './messages/ToolGroupDisplay.js';
import { GeminiMessageContent } from './messages/GeminiMessageContent.js';
import { CompressionMessage } from './messages/CompressionMessage.js';
import { WarningMessage } from './messages/WarningMessage.js';
Expand Down Expand Up @@ -195,6 +196,12 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
isExpandable={isExpandable}
/>
)}
{itemForDisplay.type === 'tool_display_group' && (
<ToolGroupDisplay
item={itemForDisplay}
isToolGroupBoundary={isToolGroupBoundary}
/>
)}
{itemForDisplay.type === 'subagent' && (
<SubagentHistoryMessage
item={itemForDisplay}
Expand Down
304 changes: 304 additions & 0 deletions packages/cli/src/ui/components/messages/ToolGroupDisplay.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, it, expect, vi, afterEach } from 'vitest';
import { renderWithProviders } from '../../../test-utils/render.js';
import { createMockSettings } from '../../../test-utils/settings.js';
import { ToolGroupDisplay } from './ToolGroupDisplay.js';
import {
CoreToolCallStatus,
UPDATE_TOPIC_DISPLAY_NAME,
} from '@google/gemini-cli-core';
import type {
HistoryItemToolDisplayGroup,
ToolDisplayItem,
} from '../../types.js';

describe('<ToolGroupDisplay />', () => {
afterEach(() => {
vi.restoreAllMocks();
});

const createToolItem = (
overrides: Partial<ToolDisplayItem> = {},
): ToolDisplayItem => ({
status: CoreToolCallStatus.Success,
name: 'test-tool',
description: 'Test description',
...overrides,
});

const createHistoryItem = (
tools: ToolDisplayItem[],
overrides: Partial<HistoryItemToolDisplayGroup> = {},
): HistoryItemToolDisplayGroup => ({
type: 'tool_display_group',
tools,
borderColor: 'gray',
borderDimColor: true,
borderTop: true,
borderBottom: true,
...overrides,
});

const fullVerbositySettings = createMockSettings({
ui: { errorVerbosity: 'full', compactToolOutput: false },
});
const compactSettings = createMockSettings({
ui: { compactToolOutput: true },
});

describe('Golden Snapshots', () => {
it('renders notices at the top (hoisting)', async () => {
const tools = [
createToolItem({ name: 'Tool A', format: 'box' }),
createToolItem({
name: UPDATE_TOPIC_DISPLAY_NAME,
description: 'New Topic',
format: 'notice',
}),
];
const item = createHistoryItem(tools);

const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);

const output = lastFrame();
// Notice should be before Tool A
expect(output.indexOf(UPDATE_TOPIC_DISPLAY_NAME)).toBeLessThan(
output.indexOf('Tool A'),
);
expect(output).toMatchSnapshot();
});

it('renders in compact mode (no box borders)', async () => {
const tools = [
createToolItem({ name: 'Tool A' }),
createToolItem({ name: 'Tool B' }),
];
const item = createHistoryItem(tools);

const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: compactSettings },
);

const output = lastFrame();
// Should not contain box drawing characters for the outer box
expect(output).not.toContain('╭');
expect(output).not.toContain('╰');
expect(output).toMatchSnapshot();
});

it('renders in boxed mode (full verbosity)', async () => {
const tools = [createToolItem({ name: 'Tool A' })];
const item = createHistoryItem(tools);

const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);

const output = lastFrame();
expect(output).toContain('╭');
expect(output).toContain('╰');
expect(output).toMatchSnapshot();
});

it('renders standalone notices without a box', async () => {
const tools = [
createToolItem({
name: 'Notice Only',
format: 'notice',
}),
];
const item = createHistoryItem(tools);

const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);

const output = lastFrame();
expect(output).not.toContain('╭');
expect(output).toMatchSnapshot();
});

it('renders error message when display info is missing', async () => {
// Create an item that effectively has no display properties
const tools = [
{
status: CoreToolCallStatus.Executing,
originalRequestName: 'missing-tool',
} as ToolDisplayItem,
];
const item = createHistoryItem(tools);

const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
);

const output = lastFrame();
expect(output).toContain('Error: Tool display missing');
expect(output).toMatchSnapshot();
});

it('hides tools awaiting approval (confirming)', async () => {
const tools = [
createToolItem({
name: 'Confirming Tool',
status: CoreToolCallStatus.AwaitingApproval,
}),
];
const item = createHistoryItem(tools);

const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
);

// Should render nothing (null)
expect(lastFrame({ allowEmpty: true })).toBe('');
});
});

describe('Result Formatting', () => {
it('renders text results with summary below', async () => {
const tools = [
createToolItem({
result: { type: 'text', text: 'Detailed output' },
resultSummary: 'Short summary',
format: 'box',
}),
];
const item = createHistoryItem(tools);

const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);

const output = lastFrame();
expect(output).toContain('Detailed output');
expect(output).toContain('Short summary');
// Summary should be below detailed output
expect(output.indexOf('Detailed output')).toBeLessThan(
output.indexOf('Short summary'),
);
expect(output).toMatchSnapshot();
});

it('renders compact tools with summary on same line', async () => {
const tools = [
createToolItem({
resultSummary: 'Success summary',
format: 'compact',
}),
];
const item = createHistoryItem(tools);

const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
);

const output = lastFrame();
expect(output).toContain('→ Success summary');
expect(output).toMatchSnapshot();
});

it('renders placeholder for diff results', async () => {
const tools = [
createToolItem({
result: {
type: 'diff',
beforeText: 'old',
afterText: 'new',
path: 'file.ts',
},
}),
];
const item = createHistoryItem(tools);

const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);

const output = lastFrame();
expect(output).toContain('[Diff Display: 3 -> 3 chars]');
expect(output).toMatchSnapshot();
});

it('renders placeholder for terminal results', async () => {
const tools = [
createToolItem({
result: { type: 'terminal' },
}),
];
const item = createHistoryItem(tools);

const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);

expect(lastFrame()).toContain('[Terminal Output]');
});

it('renders placeholder for agent results', async () => {
const tools = [
createToolItem({
result: { type: 'agent', threadId: 'thread-123' },
}),
];
const item = createHistoryItem(tools);

const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);

expect(lastFrame()).toContain('[Subagent: thread-123]');
});
});

describe('Border & Margin Logic', () => {
it('forces top border on box when it follows a notice', async () => {
const tools = [
createToolItem({ name: 'Notice', format: 'notice' }),
createToolItem({ name: 'Tool in Box', format: 'box' }),
];
// Even if item.borderTop is false (continuing a group),
// the box should have a top border because it follows a notice.
const item = createHistoryItem(tools, { borderTop: false });

const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);

const output = lastFrame();
expect(output).toContain('Notice');
expect(output).toContain('╭'); // Top border for the box
expect(output).toMatchSnapshot();
});

it('applies bottom margin in compact mode when group is at boundary', async () => {
const tools = [createToolItem({ name: 'Compact Tool' })];
const item = createHistoryItem(tools, { borderBottom: true });

const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: compactSettings },
);

// This is hard to assert via string check, but ensure match snapshot
// captures the vertical spacing.
expect(lastFrame()).toMatchSnapshot();
});
});
});
Loading
Loading