mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-13 04:48:09 -07:00
Queue up final response and show at the end.
This commit is contained in:
@@ -66,6 +66,7 @@ import { MessageType, StreamingState } from '../types.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { findLastSafeSplitPoint } from '../utils/markdownUtilities.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { createMockSettings } from '../../test-utils/mockConfig.js';
|
||||
|
||||
// --- MOCKS ---
|
||||
const mockSendMessageStream = vi
|
||||
@@ -4240,4 +4241,100 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
expect(spanMetadata.input).toBe('telemetry test query');
|
||||
});
|
||||
|
||||
describe('topicUpdateNarration blocking', () => {
|
||||
it('should block text updates while streaming and show them at the end if no tools are called', async () => {
|
||||
const settings = createMockSettings({
|
||||
merged: {
|
||||
experimental: { topicUpdateNarration: true },
|
||||
ui: { compactToolOutput: true },
|
||||
},
|
||||
});
|
||||
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield { type: ServerGeminiEventType.Content, value: 'Hello ' };
|
||||
yield { type: ServerGeminiEventType.Content, value: 'world!' };
|
||||
})(),
|
||||
);
|
||||
|
||||
const { result } = await renderTestHook([], undefined, settings);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('Hi');
|
||||
});
|
||||
|
||||
// During streaming, addItem should NOT have been called with 'gemini' type.
|
||||
// However, it IS called at the end of the turn because there are no tool calls.
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'gemini', text: 'Hello world!' }),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('should discard text updates if tool calls are present in the turn', async () => {
|
||||
const settings = createMockSettings({
|
||||
merged: {
|
||||
experimental: { topicUpdateNarration: true },
|
||||
ui: { compactToolOutput: true },
|
||||
},
|
||||
});
|
||||
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.Content,
|
||||
value: 'I will call a tool.',
|
||||
};
|
||||
yield {
|
||||
type: ServerGeminiEventType.ToolCallRequest,
|
||||
value: { callId: '1', name: 'some_tool', args: {} },
|
||||
};
|
||||
})(),
|
||||
);
|
||||
|
||||
const { result } = await renderTestHook([], undefined, settings);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('Hi');
|
||||
});
|
||||
|
||||
// addItem should NOT have been called with 'gemini' type because there was a tool call.
|
||||
expect(mockAddItem).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'gemini' }),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('should block thinking history items when narration is enabled', async () => {
|
||||
const settings = createMockSettings({
|
||||
merged: {
|
||||
experimental: { topicUpdateNarration: true },
|
||||
ui: { inlineThinkingMode: 'full' },
|
||||
},
|
||||
});
|
||||
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.Thought,
|
||||
value: { thought: 'I am thinking...' },
|
||||
};
|
||||
yield { type: ServerGeminiEventType.Content, value: 'Final answer' };
|
||||
})(),
|
||||
);
|
||||
|
||||
const { result } = await renderTestHook([], undefined, settings);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('Hi');
|
||||
});
|
||||
|
||||
// addItem should NOT have been called with 'thinking' type.
|
||||
expect(mockAddItem).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'thinking' }),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -238,6 +238,8 @@ export const useGeminiStream = (
|
||||
null,
|
||||
);
|
||||
const isLowErrorVerbosity = settings.merged.ui?.errorVerbosity !== 'full';
|
||||
const topicUpdateNarrationEnabled =
|
||||
settings.merged.experimental?.topicUpdateNarration === true;
|
||||
const suppressedToolErrorCountRef = useRef(0);
|
||||
const suppressedToolErrorNoteShownRef = useRef(false);
|
||||
const lowVerbosityFailureNoteShownRef = useRef(false);
|
||||
@@ -1082,9 +1084,24 @@ export const useGeminiStream = (
|
||||
if (pendingHistoryItemRef.current) {
|
||||
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
|
||||
}
|
||||
|
||||
// When narration is enabled, we block all text updates during the turn.
|
||||
// The text is accumulated and only shown at the end of the turn if
|
||||
// no tool calls were made.
|
||||
if (topicUpdateNarrationEnabled) {
|
||||
setPendingHistoryItem(null);
|
||||
return newGeminiMessageBuffer;
|
||||
}
|
||||
|
||||
setPendingHistoryItem({ type: 'gemini', text: '' });
|
||||
newGeminiMessageBuffer = eventValue;
|
||||
}
|
||||
|
||||
// When narration is enabled, skip updating the UI with incremental text.
|
||||
if (topicUpdateNarrationEnabled) {
|
||||
return newGeminiMessageBuffer;
|
||||
}
|
||||
|
||||
// Split large messages for better rendering performance. Ideally,
|
||||
// we should maximize the amount of output sent to <Static />.
|
||||
const splitPoint = findLastSafeSplitPoint(newGeminiMessageBuffer);
|
||||
@@ -1123,21 +1140,31 @@ export const useGeminiStream = (
|
||||
}
|
||||
return newGeminiMessageBuffer;
|
||||
},
|
||||
[addItem, pendingHistoryItemRef, setPendingHistoryItem],
|
||||
[
|
||||
addItem,
|
||||
pendingHistoryItemRef,
|
||||
setPendingHistoryItem,
|
||||
topicUpdateNarrationEnabled,
|
||||
],
|
||||
);
|
||||
|
||||
const handleThoughtEvent = useCallback(
|
||||
(eventValue: ThoughtSummary, _userMessageTimestamp: number) => {
|
||||
setThought(eventValue);
|
||||
|
||||
if (getInlineThinkingMode(settings) === 'full') {
|
||||
// Block thinking history items when narration is enabled to avoid
|
||||
// UI flickering and provide a cleaner experience.
|
||||
if (
|
||||
!topicUpdateNarrationEnabled &&
|
||||
getInlineThinkingMode(settings) === 'full'
|
||||
) {
|
||||
addItem({
|
||||
type: 'thinking',
|
||||
thought: eventValue,
|
||||
} as HistoryItemThinking);
|
||||
}
|
||||
},
|
||||
[addItem, settings, setThought],
|
||||
[addItem, settings, setThought, topicUpdateNarrationEnabled],
|
||||
);
|
||||
|
||||
const handleUserCancelledEvent = useCallback(
|
||||
@@ -1545,6 +1572,14 @@ export const useGeminiStream = (
|
||||
setPendingHistoryItem(null);
|
||||
}
|
||||
await scheduleToolCalls(toolCallRequests, signal);
|
||||
} else if (
|
||||
topicUpdateNarrationEnabled &&
|
||||
geminiMessageBuffer.length > 0
|
||||
) {
|
||||
// When narration is enabled, we only show the final text response
|
||||
// if no tools were called in the current turn. This hides intermediate
|
||||
// narration during multi-turn orchestration.
|
||||
setPendingHistoryItem({ type: 'gemini', text: geminiMessageBuffer });
|
||||
}
|
||||
return StreamProcessingStatus.Completed;
|
||||
},
|
||||
@@ -1567,6 +1602,7 @@ export const useGeminiStream = (
|
||||
pendingHistoryItemRef,
|
||||
setPendingHistoryItem,
|
||||
setThought,
|
||||
topicUpdateNarrationEnabled,
|
||||
],
|
||||
);
|
||||
const submitQuery = useCallback(
|
||||
|
||||
Reference in New Issue
Block a user