diff --git a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx index a6343a9b740..a98d886153e 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx @@ -4035,6 +4035,9 @@ describe('useGeminiStream', () => { await act(async () => { await Promise.resolve(); await Promise.resolve(); + // Flush the macrotask yield (setImmediate) added after addItem() + // so that sendMessageStream is actually invoked. + await vi.advanceTimersByTimeAsync(0); }); expect(mockSendMessageStream).toHaveBeenCalledTimes(1); @@ -4163,6 +4166,8 @@ describe('useGeminiStream', () => { await act(async () => { await Promise.resolve(); await Promise.resolve(); + // Flush the macrotask yield (setImmediate) added after addItem() + await vi.advanceTimersByTimeAsync(0); }); expect(mockSendMessageStream).toHaveBeenCalledTimes(1); @@ -4223,6 +4228,8 @@ describe('useGeminiStream', () => { await act(async () => { await Promise.resolve(); await Promise.resolve(); + // Flush the macrotask yield (setImmediate) added after addItem() + await vi.advanceTimersByTimeAsync(0); }); await act(async () => { @@ -4361,6 +4368,8 @@ describe('useGeminiStream', () => { await act(async () => { await Promise.resolve(); await Promise.resolve(); + // Flush the macrotask yield (setImmediate) added after addItem() + await vi.advanceTimersByTimeAsync(0); }); expect(mockSendMessageStream).toHaveBeenCalledTimes(1); @@ -4711,6 +4720,8 @@ describe('useGeminiStream', () => { await act(async () => { await Promise.resolve(); await Promise.resolve(); + // Flush the macrotask yield (setImmediate) added after addItem() + await vi.advanceTimersByTimeAsync(0); }); // Cancel without advancing the throttle timer; the cancel-time @@ -4828,6 +4839,8 @@ describe('useGeminiStream', () => { await act(async () => { await Promise.resolve(); await Promise.resolve(); + // Flush the macrotask yield (setImmediate) added after addItem() + await vi.advanceTimersByTimeAsync(0); }); // Sanity: the throttle has not fired yet. @@ -6743,6 +6756,8 @@ describe('useGeminiStream', () => { void result.current.submitQuery('think then retry'); await Promise.resolve(); await Promise.resolve(); + // Flush the macrotask yield (setImmediate) added after addItem() + await vi.advanceTimersByTimeAsync(0); }); // Advance past STREAM_UPDATE_THROTTLE_MS (60ms) so the thought @@ -6840,6 +6855,8 @@ describe('useGeminiStream', () => { await act(async () => { await Promise.resolve(); + // Flush the macrotask yield (setImmediate) added after addItem() + await vi.advanceTimersByTimeAsync(0); }); const findErrorItem = () => @@ -6955,6 +6972,12 @@ describe('useGeminiStream', () => { void result.current.submitQuery('Trigger retry after countdown'); }); + await act(async () => { + await Promise.resolve(); + // Flush the macrotask yield (setImmediate) added after addItem() + await vi.advanceTimersByTimeAsync(0); + }); + let errorItem = result.current.pendingHistoryItems.find( (item) => item.type === MessageType.ERROR, ) as { hint?: string } | undefined; diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index ca58fe81063..cd3d35260f7 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -1036,6 +1036,18 @@ export const useGeminiStream = ( id: insertedId, text: trimmedQuery, }; + + // Yield via macrotask to let Ink/React flush the user message + // render before continuing with @-command processing and API + // call. React 19.2.4 (Ink 7.0.3) schedules renders via + // MessageChannel.postMessage (a macrotask), so a microtask yield + // (await Promise.resolve()) does NOT give React a chance to + // render — the continuation runs first. setImmediate fires in + // the check phase after I/O events (where MessageChannel + // delivers its postMessage), guaranteeing React renders first + // without the ~1ms timer overhead of setTimeout(0). + // Only needed for non-Cron submissions since Cron skips addItem(). + await new Promise((r) => setImmediate(r)); } // Handle @-commands (which might involve tool calls)