diff --git a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx index 0616b97ed..fc2418889 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx @@ -2734,6 +2734,51 @@ describe('useGeminiStream', () => { }); }); + it('should render descriptions from subject-bearing thought chunks', async () => { + mockSendMessageStream.mockReturnValue( + (async function* () { + yield { + type: ServerGeminiEventType.Thought, + value: { + subject: 'Evaluating installation approach', + description: 'The', + }, + }; + yield { + type: ServerGeminiEventType.Thought, + value: { + subject: '', + description: ' user mentioned globally installed qwen,', + }, + }; + yield { + type: ServerGeminiEventType.Finished, + value: { reason: 'STOP', usageMetadata: undefined }, + }; + })(), + ); + + const { result } = renderTestHook(); + + await act(async () => { + await result.current.submitQuery('Streamed thought'); + }); + + await waitFor(() => { + expect(mockAddItem).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'gemini_thought', + text: 'The user mentioned globally installed qwen,', + }), + expect.any(Number), + ); + }); + expect(result.current.thought).toEqual({ + subject: 'Evaluating installation approach', + description: 'The user mentioned globally installed qwen,', + }); + }); + it('should show a retry countdown and update pending history over time', async () => { vi.useFakeTimers(); try { diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index e59112f3d..8195b91e0 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -1285,9 +1285,12 @@ export const useGeminiStream = ( dualOutput?.processEvent(event); switch (event.type) { case ServerGeminiEventType.Thought: - // If the thought has a subject, it's a discrete status update rather than - // a streamed textual thought, so we update the thought state directly. - if (event.value.subject) { + // Subject-only chunks are discrete status updates for the + // loading indicator and render immediately. Anything carrying + // streamed text (with or without a subject) goes through the + // throttled buffer so it batches with adjacent reasoning + // chunks; the flush merger preserves the subject. + if (event.value.subject && !event.value.description) { flushBufferedStreamEvents(); setThought(event.value); } else {