mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-08 04:10:35 -07:00
fix(ui): improve narration suppression and reduce flicker (#24635)
This commit is contained in:
committed by
GitHub
parent
e116aa34f4
commit
7872d6d7fe
@@ -6,7 +6,11 @@
|
||||
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { createMockSettings } from '../../test-utils/settings.js';
|
||||
import { makeFakeConfig, CoreToolCallStatus } from '@google/gemini-cli-core';
|
||||
import {
|
||||
makeFakeConfig,
|
||||
CoreToolCallStatus,
|
||||
UPDATE_TOPIC_TOOL_NAME,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { MainContent } from './MainContent.js';
|
||||
import { getToolGroupBorderAppearance } from '../utils/borderStyles.js';
|
||||
@@ -728,6 +732,158 @@ 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: {
|
||||
|
||||
@@ -91,20 +91,47 @@ export const MainContent = () => {
|
||||
const flags = new Array<boolean>(combinedHistory.length).fill(false);
|
||||
|
||||
if (topicUpdateNarrationEnabled) {
|
||||
let toolGroupInTurn = false;
|
||||
let turnIsIntermediate = false;
|
||||
let hasTopicToolInTurn = false;
|
||||
|
||||
for (let i = combinedHistory.length - 1; i >= 0; i--) {
|
||||
const item = combinedHistory[i];
|
||||
if (item.type === 'user' || item.type === 'user_shell') {
|
||||
toolGroupInTurn = false;
|
||||
turnIsIntermediate = false;
|
||||
hasTopicToolInTurn = false;
|
||||
} else if (item.type === 'tool_group') {
|
||||
toolGroupInTurn = item.tools.some((t) => isTopicTool(t.name));
|
||||
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;
|
||||
}
|
||||
} else if (
|
||||
(item.type === 'thinking' ||
|
||||
item.type === 'gemini' ||
|
||||
item.type === 'gemini_content') &&
|
||||
toolGroupInTurn
|
||||
item.type === 'thinking' ||
|
||||
item.type === 'gemini' ||
|
||||
item.type === 'gemini_content'
|
||||
) {
|
||||
flags[i] = true;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user