diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx
new file mode 100644
index 00000000..8e5961e9
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx
@@ -0,0 +1,344 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { render } from 'ink-testing-library';
+import { describe, it, expect, vi } from 'vitest';
+import { Text } from 'ink';
+import { ToolGroupMessage } from './ToolGroupMessage.js';
+import { IndividualToolCallDisplay, ToolCallStatus } from '../../types.js';
+import { Config, ToolCallConfirmationDetails } from '@google/gemini-cli-core';
+
+// Mock child components to isolate ToolGroupMessage behavior
+vi.mock('./ToolMessage.js', () => ({
+ ToolMessage: function MockToolMessage({
+ callId,
+ name,
+ description,
+ status,
+ emphasis,
+ }: {
+ callId: string;
+ name: string;
+ description: string;
+ status: ToolCallStatus;
+ emphasis: string;
+ }) {
+ const statusSymbol = {
+ [ToolCallStatus.Success]: '✓',
+ [ToolCallStatus.Pending]: 'o',
+ [ToolCallStatus.Executing]: '⊷',
+ [ToolCallStatus.Confirming]: '?',
+ [ToolCallStatus.Canceled]: '-',
+ [ToolCallStatus.Error]: 'x',
+ }[status];
+ return (
+
+ MockTool[{callId}]: {statusSymbol} {name} - {description} ({emphasis})
+
+ );
+ },
+}));
+
+vi.mock('./ToolConfirmationMessage.js', () => ({
+ ToolConfirmationMessage: function MockToolConfirmationMessage({
+ confirmationDetails,
+ }: {
+ confirmationDetails: ToolCallConfirmationDetails;
+ }) {
+ const displayText =
+ confirmationDetails?.type === 'info'
+ ? (confirmationDetails as { prompt: string }).prompt
+ : confirmationDetails?.title || 'confirm';
+ return MockConfirmation: {displayText};
+ },
+}));
+
+describe('', () => {
+ const mockConfig: Config = {} as Config;
+
+ const createToolCall = (
+ overrides: Partial = {},
+ ): IndividualToolCallDisplay => ({
+ callId: 'tool-123',
+ name: 'test-tool',
+ description: 'A tool for testing',
+ resultDisplay: 'Test result',
+ status: ToolCallStatus.Success,
+ confirmationDetails: undefined,
+ renderOutputAsMarkdown: false,
+ ...overrides,
+ });
+
+ const baseProps = {
+ groupId: 1,
+ terminalWidth: 80,
+ config: mockConfig,
+ isFocused: true,
+ };
+
+ describe('Golden Snapshots', () => {
+ it('renders single successful tool call', () => {
+ const toolCalls = [createToolCall()];
+ const { lastFrame } = render(
+ ,
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('renders multiple tool calls with different statuses', () => {
+ const toolCalls = [
+ createToolCall({
+ callId: 'tool-1',
+ name: 'successful-tool',
+ description: 'This tool succeeded',
+ status: ToolCallStatus.Success,
+ }),
+ createToolCall({
+ callId: 'tool-2',
+ name: 'pending-tool',
+ description: 'This tool is pending',
+ status: ToolCallStatus.Pending,
+ }),
+ createToolCall({
+ callId: 'tool-3',
+ name: 'error-tool',
+ description: 'This tool failed',
+ status: ToolCallStatus.Error,
+ }),
+ ];
+ const { lastFrame } = render(
+ ,
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('renders tool call awaiting confirmation', () => {
+ const toolCalls = [
+ createToolCall({
+ callId: 'tool-confirm',
+ name: 'confirmation-tool',
+ description: 'This tool needs confirmation',
+ status: ToolCallStatus.Confirming,
+ confirmationDetails: {
+ type: 'info',
+ title: 'Confirm Tool Execution',
+ prompt: 'Are you sure you want to proceed?',
+ onConfirm: vi.fn(),
+ },
+ }),
+ ];
+ const { lastFrame } = render(
+ ,
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('renders shell command with yellow border', () => {
+ const toolCalls = [
+ createToolCall({
+ callId: 'shell-1',
+ name: 'run_shell_command',
+ description: 'Execute shell command',
+ status: ToolCallStatus.Success,
+ }),
+ ];
+ const { lastFrame } = render(
+ ,
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('renders mixed tool calls including shell command', () => {
+ const toolCalls = [
+ createToolCall({
+ callId: 'tool-1',
+ name: 'read_file',
+ description: 'Read a file',
+ status: ToolCallStatus.Success,
+ }),
+ createToolCall({
+ callId: 'tool-2',
+ name: 'run_shell_command',
+ description: 'Run command',
+ status: ToolCallStatus.Executing,
+ }),
+ createToolCall({
+ callId: 'tool-3',
+ name: 'write_file',
+ description: 'Write to file',
+ status: ToolCallStatus.Pending,
+ }),
+ ];
+ const { lastFrame } = render(
+ ,
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('renders with limited terminal height', () => {
+ const toolCalls = [
+ createToolCall({
+ callId: 'tool-1',
+ name: 'tool-with-result',
+ description: 'Tool with output',
+ resultDisplay:
+ 'This is a long result that might need height constraints',
+ }),
+ createToolCall({
+ callId: 'tool-2',
+ name: 'another-tool',
+ description: 'Another tool',
+ resultDisplay: 'More output here',
+ }),
+ ];
+ const { lastFrame } = render(
+ ,
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('renders when not focused', () => {
+ const toolCalls = [createToolCall()];
+ const { lastFrame } = render(
+ ,
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('renders with narrow terminal width', () => {
+ const toolCalls = [
+ createToolCall({
+ name: 'very-long-tool-name-that-might-wrap',
+ description:
+ 'This is a very long description that might cause wrapping issues',
+ }),
+ ];
+ const { lastFrame } = render(
+ ,
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('renders empty tool calls array', () => {
+ const { lastFrame } = render(
+ ,
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ });
+ });
+
+ describe('Border Color Logic', () => {
+ it('uses yellow border when tools are pending', () => {
+ const toolCalls = [createToolCall({ status: ToolCallStatus.Pending })];
+ const { lastFrame } = render(
+ ,
+ );
+ // The snapshot will capture the visual appearance including border color
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('uses yellow border for shell commands even when successful', () => {
+ const toolCalls = [
+ createToolCall({
+ name: 'run_shell_command',
+ status: ToolCallStatus.Success,
+ }),
+ ];
+ const { lastFrame } = render(
+ ,
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('uses gray border when all tools are successful and no shell commands', () => {
+ const toolCalls = [
+ createToolCall({ status: ToolCallStatus.Success }),
+ createToolCall({
+ callId: 'tool-2',
+ name: 'another-tool',
+ status: ToolCallStatus.Success,
+ }),
+ ];
+ const { lastFrame } = render(
+ ,
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ });
+ });
+
+ describe('Height Calculation', () => {
+ it('calculates available height correctly with multiple tools with results', () => {
+ const toolCalls = [
+ createToolCall({
+ callId: 'tool-1',
+ resultDisplay: 'Result 1',
+ }),
+ createToolCall({
+ callId: 'tool-2',
+ resultDisplay: 'Result 2',
+ }),
+ createToolCall({
+ callId: 'tool-3',
+ resultDisplay: '', // No result
+ }),
+ ];
+ const { lastFrame } = render(
+ ,
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ });
+ });
+
+ describe('Confirmation Handling', () => {
+ it('shows confirmation dialog for first confirming tool only', () => {
+ const toolCalls = [
+ createToolCall({
+ callId: 'tool-1',
+ name: 'first-confirm',
+ status: ToolCallStatus.Confirming,
+ confirmationDetails: {
+ type: 'info',
+ title: 'Confirm First Tool',
+ prompt: 'Confirm first tool',
+ onConfirm: vi.fn(),
+ },
+ }),
+ createToolCall({
+ callId: 'tool-2',
+ name: 'second-confirm',
+ status: ToolCallStatus.Confirming,
+ confirmationDetails: {
+ type: 'info',
+ title: 'Confirm Second Tool',
+ prompt: 'Confirm second tool',
+ onConfirm: vi.fn(),
+ },
+ }),
+ ];
+ const { lastFrame } = render(
+ ,
+ );
+ // Should only show confirmation for the first tool
+ expect(lastFrame()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx
index 843ebf03..fab8aeba 100644
--- a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx
+++ b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx
@@ -71,19 +71,19 @@ describe('', () => {
StreamingState.Idle,
);
const output = lastFrame();
- expect(output).toContain('√'); // Success indicator
+ expect(output).toContain('✓'); // Success indicator
expect(output).toContain('test-tool');
expect(output).toContain('A tool for testing');
expect(output).toContain('MockMarkdown:Test result');
});
describe('ToolStatusIndicator rendering', () => {
- it('shows √ for Success status', () => {
+ it('shows ✓ for Success status', () => {
const { lastFrame } = renderWithContext(
,
StreamingState.Idle,
);
- expect(lastFrame()).toContain('√');
+ expect(lastFrame()).toContain('✓');
});
it('shows o for Pending status', () => {
@@ -125,7 +125,7 @@ describe('', () => {
);
expect(lastFrame()).toContain('⊷');
expect(lastFrame()).not.toContain('MockRespondingSpinner');
- expect(lastFrame()).not.toContain('√');
+ expect(lastFrame()).not.toContain('✓');
});
it('shows paused spinner for Executing status when streamingState is WaitingForConfirmation', () => {
@@ -135,7 +135,7 @@ describe('', () => {
);
expect(lastFrame()).toContain('⊷');
expect(lastFrame()).not.toContain('MockRespondingSpinner');
- expect(lastFrame()).not.toContain('√');
+ expect(lastFrame()).not.toContain('✓');
});
it('shows MockRespondingSpinner for Executing status when streamingState is Responding', () => {
@@ -144,7 +144,7 @@ describe('', () => {
StreamingState.Responding, // Simulate app still responding
);
expect(lastFrame()).toContain('MockRespondingSpinner');
- expect(lastFrame()).not.toContain('√');
+ expect(lastFrame()).not.toContain('✓');
});
});
diff --git a/packages/cli/src/ui/components/messages/ToolMessage.tsx b/packages/cli/src/ui/components/messages/ToolMessage.tsx
index a4d24529..d6ddb4f1 100644
--- a/packages/cli/src/ui/components/messages/ToolMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolMessage.tsx
@@ -127,7 +127,7 @@ const ToolStatusIndicator: React.FC = ({
/>
)}
{status === ToolCallStatus.Success && (
- √
+ ✓
)}
{status === ToolCallStatus.Confirming && (
?
diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap
new file mode 100644
index 00000000..ee798b67
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap
@@ -0,0 +1,105 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[` > Border Color Logic > uses gray border when all tools are successful and no shell commands 1`] = `
+" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
+ │MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) │
+ │ │
+ │MockTool[tool-2]: ✓ another-tool - A tool for testing (medium) │
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
+`;
+
+exports[` > Border Color Logic > uses yellow border for shell commands even when successful 1`] = `
+" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
+ │MockTool[tool-123]: ✓ run_shell_command - A tool for testing (medium) │
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
+`;
+
+exports[` > Border Color Logic > uses yellow border when tools are pending 1`] = `
+" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
+ │MockTool[tool-123]: o test-tool - A tool for testing (medium) │
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
+`;
+
+exports[` > Confirmation Handling > shows confirmation dialog for first confirming tool only 1`] = `
+" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
+ │MockTool[tool-1]: ? first-confirm - A tool for testing (high) │
+ │MockConfirmation: Confirm first tool │
+ │ │
+ │MockTool[tool-2]: ? second-confirm - A tool for testing (low) │
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
+`;
+
+exports[` > Golden Snapshots > renders empty tool calls array 1`] = `
+" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
+`;
+
+exports[` > Golden Snapshots > renders mixed tool calls including shell command 1`] = `
+" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
+ │MockTool[tool-1]: ✓ read_file - Read a file (medium) │
+ │ │
+ │MockTool[tool-2]: ⊷ run_shell_command - Run command (medium) │
+ │ │
+ │MockTool[tool-3]: o write_file - Write to file (medium) │
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
+`;
+
+exports[` > Golden Snapshots > renders multiple tool calls with different statuses 1`] = `
+" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
+ │MockTool[tool-1]: ✓ successful-tool - This tool succeeded (medium) │
+ │ │
+ │MockTool[tool-2]: o pending-tool - This tool is pending (medium) │
+ │ │
+ │MockTool[tool-3]: x error-tool - This tool failed (medium) │
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
+`;
+
+exports[` > Golden Snapshots > renders shell command with yellow border 1`] = `
+" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
+ │MockTool[shell-1]: ✓ run_shell_command - Execute shell command (medium) │
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
+`;
+
+exports[` > Golden Snapshots > renders single successful tool call 1`] = `
+" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
+ │MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) │
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
+`;
+
+exports[` > Golden Snapshots > renders tool call awaiting confirmation 1`] = `
+" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
+ │MockTool[tool-confirm]: ? confirmation-tool - This tool needs confirmation (high) │
+ │MockConfirmation: Are you sure you want to proceed? │
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
+`;
+
+exports[` > Golden Snapshots > renders when not focused 1`] = `
+" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
+ │MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) │
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
+`;
+
+exports[` > Golden Snapshots > renders with limited terminal height 1`] = `
+" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
+ │MockTool[tool-1]: ✓ tool-with-result - Tool with output (medium) │
+ │ │
+ │MockTool[tool-2]: ✓ another-tool - Another tool (medium) │
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
+`;
+
+exports[` > Golden Snapshots > renders with narrow terminal width 1`] = `
+" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
+ │MockTool[tool-123]: ✓ very-long-tool-name-that-might-wrap - This is a very long description that │
+ │might cause wrapping issues (medium) │
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
+`;
+
+exports[` > Height Calculation > calculates available height correctly with multiple tools with results 1`] = `
+" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
+ │MockTool[tool-1]: ✓ test-tool - A tool for testing (medium) │
+ │ │
+ │MockTool[tool-2]: ✓ test-tool - A tool for testing (medium) │
+ │ │
+ │MockTool[tool-3]: ✓ test-tool - A tool for testing (medium) │
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
+`;