Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
158 changes: 1 addition & 157 deletions packages/cli/src/ui/components/MainContent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@

import { renderWithProviders } from '../../test-utils/render.js';
import { createMockSettings } from '../../test-utils/settings.js';
import {
makeFakeConfig,
CoreToolCallStatus,
UPDATE_TOPIC_TOOL_NAME,
} from '@google/gemini-cli-core';
import { makeFakeConfig, CoreToolCallStatus } from '@google/gemini-cli-core';
import { waitFor } from '../../test-utils/async.js';
import { MainContent } from './MainContent.js';
import { getToolGroupBorderAppearance } from '../utils/borderStyles.js';
Expand Down Expand Up @@ -732,158 +728,6 @@ describe('MainContent', () => {
unmount();
});

describe('Narration Suppression', () => {
const settingsWithNarration = createMockSettings({
merged: {
ui: { inlineThinkingMode: 'expanded' },
experimental: { topicUpdateNarration: true },
},
});

it('suppresses thinking ALWAYS when narration is enabled', async () => {
mockUseSettings.mockReturnValue(settingsWithNarration);
const uiState = {
...defaultMockUiState,
history: [
{ id: 1, type: 'user' as const, text: 'Hello' },
{
id: 2,
type: 'thinking' as const,
thought: {
subject: 'Thinking...',
description: 'Thinking about hello',
},
},
{ id: 3, type: 'gemini' as const, text: 'I am helping.' },
],
};

const { lastFrame, unmount } = await renderWithProviders(
<MainContent />,
{
uiState: uiState as Partial<UIState>,
settings: settingsWithNarration,
},
);

const output = lastFrame();
expect(output).not.toContain('Thinking...');
expect(output).toContain('I am helping.');
unmount();
});

it('suppresses text in intermediate turns (contains non-topic tools)', async () => {
mockUseSettings.mockReturnValue(settingsWithNarration);
const uiState = {
...defaultMockUiState,
history: [
{ id: 100, type: 'user' as const, text: 'Search' },
{
id: 101,
type: 'gemini' as const,
text: 'I will now search the files.',
},
{
id: 102,
type: 'tool_group' as const,
tools: [
{
callId: '1',
name: 'ls',
args: { path: '.' },
status: CoreToolCallStatus.Success,
},
],
},
],
};

const { lastFrame, unmount } = await renderWithProviders(
<MainContent />,
{
uiState: uiState as Partial<UIState>,
settings: settingsWithNarration,
},
);

const output = lastFrame();
expect(output).not.toContain('I will now search the files.');
unmount();
});

it('suppresses text that precedes a topic tool in the same turn', async () => {
mockUseSettings.mockReturnValue(settingsWithNarration);
const uiState = {
...defaultMockUiState,
history: [
{ id: 200, type: 'user' as const, text: 'Hello' },
{ id: 201, type: 'gemini' as const, text: 'I will now help you.' },
{
id: 202,
type: 'tool_group' as const,
tools: [
{
callId: '1',
name: UPDATE_TOPIC_TOOL_NAME,
args: { title: 'Helping', summary: 'Helping the user' },
status: CoreToolCallStatus.Success,
},
],
},
],
};

const { lastFrame, unmount } = await renderWithProviders(
<MainContent />,
{
uiState: uiState as Partial<UIState>,
settings: settingsWithNarration,
},
);

const output = lastFrame();
expect(output).not.toContain('I will now help you.');
expect(output).toContain('Helping');
expect(output).toContain('Helping the user');
unmount();
});

it('shows text in the final turn if it comes AFTER the topic tool', async () => {
mockUseSettings.mockReturnValue(settingsWithNarration);
const uiState = {
...defaultMockUiState,
history: [
{ id: 300, type: 'user' as const, text: 'Hello' },
{
id: 301,
type: 'tool_group' as const,
tools: [
{
callId: '1',
name: UPDATE_TOPIC_TOOL_NAME,
args: { title: 'Final Answer', summary: 'I have finished' },
status: CoreToolCallStatus.Success,
},
],
},
{ id: 302, type: 'gemini' as const, text: 'Here is your answer.' },
],
};

const { lastFrame, unmount } = await renderWithProviders(
<MainContent />,
{
uiState: uiState as Partial<UIState>,
settings: settingsWithNarration,
},
);

const output = lastFrame();
expect(output).toContain('Here is your answer.');
unmount();
});
});

it('renders multiple thinking messages sequentially correctly', async () => {
mockUseSettings.mockReturnValue({
merged: {
Expand Down
43 changes: 8 additions & 35 deletions packages/cli/src/ui/components/MainContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,47 +91,20 @@ export const MainContent = () => {
const flags = new Array<boolean>(combinedHistory.length).fill(false);

if (topicUpdateNarrationEnabled) {
let turnIsIntermediate = false;
let hasTopicToolInTurn = false;

let toolGroupInTurn = false;
for (let i = combinedHistory.length - 1; i >= 0; i--) {
const item = combinedHistory[i];
if (item.type === 'user' || item.type === 'user_shell') {
turnIsIntermediate = false;
hasTopicToolInTurn = false;
toolGroupInTurn = false;
} else if (item.type === 'tool_group') {
const hasTopic = item.tools.some((t) => isTopicTool(t.name));
const hasNonTopic = item.tools.some((t) => !isTopicTool(t.name));
if (hasTopic) {
hasTopicToolInTurn = true;
}
if (hasNonTopic) {
turnIsIntermediate = true;
}
toolGroupInTurn = item.tools.some((t) => isTopicTool(t.name));
} else if (
item.type === 'thinking' ||
item.type === 'gemini' ||
item.type === 'gemini_content'
(item.type === 'thinking' ||
item.type === 'gemini' ||
item.type === 'gemini_content') &&
toolGroupInTurn
) {
// Rule 1: Always suppress thinking when narration is enabled to avoid
// "flashing" as the model starts its response, and because the Topic
// UI provides the necessary high-level intent.
if (item.type === 'thinking') {
flags[i] = true;
continue;
}

// Rule 2: Suppress text in intermediate turns (turns containing non-topic
// tools) to hide mechanical narration.
if (turnIsIntermediate) {
flags[i] = true;
}

// Rule 3: Suppress text that precedes a topic tool in the same turn,
// as the topic tool "replaces" it.
if (hasTopicToolInTurn) {
flags[i] = true;
}
flags[i] = true;
}
}
}
Expand Down
Loading