mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-10 21:30:40 -07:00
Inline thinking bubbles with summary/full modes (#18033)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
@@ -2505,6 +2505,110 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
});
|
||||
describe('Thought Reset', () => {
|
||||
it('should keep full thinking entries in history when mode is full', async () => {
|
||||
const fullThinkingSettings: LoadedSettings = {
|
||||
...mockLoadedSettings,
|
||||
merged: {
|
||||
...mockLoadedSettings.merged,
|
||||
ui: { inlineThinkingMode: 'full' },
|
||||
},
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.Thought,
|
||||
value: {
|
||||
subject: 'Full thought',
|
||||
description: 'Detailed thinking',
|
||||
},
|
||||
};
|
||||
yield {
|
||||
type: ServerGeminiEventType.Content,
|
||||
value: 'Response',
|
||||
};
|
||||
})(),
|
||||
);
|
||||
|
||||
const { result } = renderHookWithProviders(() =>
|
||||
useGeminiStream(
|
||||
new MockedGeminiClientClass(mockConfig),
|
||||
[],
|
||||
mockAddItem,
|
||||
mockConfig,
|
||||
fullThinkingSettings,
|
||||
mockOnDebugMessage,
|
||||
mockHandleSlashCommand,
|
||||
false,
|
||||
() => 'vscode' as EditorType,
|
||||
() => {},
|
||||
() => Promise.resolve(),
|
||||
false,
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
80,
|
||||
24,
|
||||
),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('Test query');
|
||||
});
|
||||
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'thinking',
|
||||
thought: expect.objectContaining({ subject: 'Full thought' }),
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps thought transient and clears it on first non-thought event', async () => {
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.Thought,
|
||||
value: {
|
||||
subject: 'Assessing intent',
|
||||
description: 'Inspecting context',
|
||||
},
|
||||
};
|
||||
yield {
|
||||
type: ServerGeminiEventType.Content,
|
||||
value: 'Model response content',
|
||||
};
|
||||
yield {
|
||||
type: ServerGeminiEventType.Finished,
|
||||
value: { reason: 'STOP', usageMetadata: undefined },
|
||||
};
|
||||
})(),
|
||||
);
|
||||
|
||||
const { result } = renderTestHook();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('Test query');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'gemini',
|
||||
text: 'Model response content',
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.thought).toBeNull();
|
||||
expect(mockAddItem).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'thinking' }),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reset thought to null when starting a new prompt', async () => {
|
||||
// First, simulate a response with a thought
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
|
||||
@@ -50,6 +50,7 @@ import type {
|
||||
import { type Part, type PartListUnion, FinishReason } from '@google/genai';
|
||||
import type {
|
||||
HistoryItem,
|
||||
HistoryItemThinking,
|
||||
HistoryItemWithoutId,
|
||||
HistoryItemToolGroup,
|
||||
IndividualToolCallDisplay,
|
||||
@@ -61,6 +62,7 @@ import { isAtCommand, isSlashCommand } from '../utils/commandUtils.js';
|
||||
import { useShellCommandProcessor } from './shellCommandProcessor.js';
|
||||
import { handleAtCommand } from './atCommandProcessor.js';
|
||||
import { findLastSafeSplitPoint } from '../utils/markdownUtilities.js';
|
||||
import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js';
|
||||
import { useStateAndRef } from './useStateAndRef.js';
|
||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
import { useLogger } from './useLogger.js';
|
||||
@@ -192,9 +194,11 @@ export const useGeminiStream = (
|
||||
const turnCancelledRef = useRef(false);
|
||||
const activeQueryIdRef = useRef<string | null>(null);
|
||||
const [isResponding, setIsResponding] = useState<boolean>(false);
|
||||
const [thought, setThought] = useState<ThoughtSummary | null>(null);
|
||||
const [thought, thoughtRef, setThought] =
|
||||
useStateAndRef<ThoughtSummary | null>(null);
|
||||
const [pendingHistoryItem, pendingHistoryItemRef, setPendingHistoryItem] =
|
||||
useStateAndRef<HistoryItemWithoutId | null>(null);
|
||||
|
||||
const [lastGeminiActivityTime, setLastGeminiActivityTime] =
|
||||
useState<number>(0);
|
||||
const [pushedToolCallIds, pushedToolCallIdsRef, setPushedToolCallIds] =
|
||||
@@ -753,6 +757,7 @@ export const useGeminiStream = (
|
||||
pendingHistoryItemRef.current?.type !== 'gemini' &&
|
||||
pendingHistoryItemRef.current?.type !== 'gemini_content'
|
||||
) {
|
||||
// Flush any pending item before starting gemini content
|
||||
if (pendingHistoryItemRef.current) {
|
||||
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
|
||||
}
|
||||
@@ -798,6 +803,23 @@ export const useGeminiStream = (
|
||||
[addItem, pendingHistoryItemRef, setPendingHistoryItem],
|
||||
);
|
||||
|
||||
const handleThoughtEvent = useCallback(
|
||||
(eventValue: ThoughtSummary, userMessageTimestamp: number) => {
|
||||
setThought(eventValue);
|
||||
|
||||
if (getInlineThinkingMode(settings) === 'full') {
|
||||
addItem(
|
||||
{
|
||||
type: 'thinking',
|
||||
thought: eventValue,
|
||||
} as HistoryItemThinking,
|
||||
userMessageTimestamp,
|
||||
);
|
||||
}
|
||||
},
|
||||
[addItem, settings, setThought],
|
||||
);
|
||||
|
||||
const handleUserCancelledEvent = useCallback(
|
||||
(userMessageTimestamp: number) => {
|
||||
if (turnCancelledRef.current) {
|
||||
@@ -1067,10 +1089,17 @@ export const useGeminiStream = (
|
||||
let geminiMessageBuffer = '';
|
||||
const toolCallRequests: ToolCallRequestInfo[] = [];
|
||||
for await (const event of stream) {
|
||||
if (
|
||||
event.type !== ServerGeminiEventType.Thought &&
|
||||
thoughtRef.current !== null
|
||||
) {
|
||||
setThought(null);
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case ServerGeminiEventType.Thought:
|
||||
setLastGeminiActivityTime(Date.now());
|
||||
setThought(event.value);
|
||||
handleThoughtEvent(event.value, userMessageTimestamp);
|
||||
break;
|
||||
case ServerGeminiEventType.Content:
|
||||
setLastGeminiActivityTime(Date.now());
|
||||
@@ -1157,6 +1186,8 @@ export const useGeminiStream = (
|
||||
},
|
||||
[
|
||||
handleContentEvent,
|
||||
handleThoughtEvent,
|
||||
thoughtRef,
|
||||
handleUserCancelledEvent,
|
||||
handleErrorEvent,
|
||||
scheduleToolCalls,
|
||||
@@ -1171,6 +1202,7 @@ export const useGeminiStream = (
|
||||
addItem,
|
||||
pendingHistoryItemRef,
|
||||
setPendingHistoryItem,
|
||||
setThought,
|
||||
],
|
||||
);
|
||||
const submitQuery = useCallback(
|
||||
@@ -1351,6 +1383,7 @@ export const useGeminiStream = (
|
||||
config,
|
||||
startNewPrompt,
|
||||
getPromptCount,
|
||||
setThought,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user