From 3f0867c83da10a5eb7fbc294558b2b4613386910 Mon Sep 17 00:00:00 2001 From: LaZzyMan Date: Mon, 18 May 2026 12:29:32 +0800 Subject: [PATCH] fix(cli): commit pending retry error to history on new turn (#4169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guard at the top of `submitQuery` cleared `pendingRetryErrorItem` via `clearRetryCountdown()` without first persisting it to history, so any new user turn — including informational slash commands like `/status`, `/about`, or `/help` — would silently discard a visible API error. The user could not refer back to the failure they were investigating. The inline comment already described the intended behavior ("Commit any pending retry error to history (without hint) since the user is starting a new conversation turn") but the commit step was missing. Add the `addItem` call before clearing, stripping the now-stale "Press Ctrl+Y to retry" hint so the persisted history entry reads cleanly. Fixes #4169 --- .../cli/src/ui/hooks/useGeminiStream.test.tsx | 68 +++++++++++++++++++ packages/cli/src/ui/hooks/useGeminiStream.ts | 5 ++ 2 files changed, 73 insertions(+) diff --git a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx index 8517598e315..fcc03cd7111 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx @@ -4026,6 +4026,74 @@ describe('useGeminiStream', () => { expect(errorItem).toBeUndefined(); }); }); + + // Regression for #4169: when a pending retry error is cleared as the user + // starts a new turn, the error must be committed to the persistent + // history first — otherwise running /status (or any new turn) silently + // discards the failure the user was investigating. + it('commits pending retry error to history (without hint) when a new query starts', async () => { + mockSendMessageStream.mockReturnValueOnce( + (async function* () { + yield { + type: ServerGeminiEventType.Error, + value: { error: { message: 'First error' } }, + }; + })(), + ); + + const { result } = renderTestHook(); + + await act(async () => { + await result.current.submitQuery('First query'); + }); + + await waitFor(() => { + const errorItem = result.current.pendingHistoryItems.find( + (item) => item.type === 'error', + ); + expect(errorItem).toBeDefined(); + }); + + // Sanity check: the error has NOT yet been committed to history while + // it lives as a pending retry item. + expect(mockAddItem).not.toHaveBeenCalledWith( + expect.objectContaining({ type: 'error' }), + expect.any(Number), + ); + + mockSendMessageStream.mockReturnValueOnce( + (async function* () { + yield { + type: ServerGeminiEventType.Content, + value: 'Second response', + }; + })(), + ); + + await act(async () => { + await result.current.submitQuery('Second query'); + }); + + // The pending error is now committed to history… + await waitFor(() => { + expect(mockAddItem).toHaveBeenCalledWith( + expect.objectContaining({ type: 'error' }), + expect.any(Number), + ); + }); + + // …and the retry hint is stripped, since it is no longer actionable. + const errorCommit = mockAddItem.mock.calls.find( + ([item]) => item && typeof item === 'object' && item.type === 'error', + ); + expect(errorCommit?.[0]).not.toHaveProperty('hint'); + + // The pending region is cleared, as before. + const errorItem = result.current.pendingHistoryItems.find( + (item) => item.type === 'error', + ); + expect(errorItem).toBeUndefined(); + }); }); describe('Concurrent Execution Prevention', () => { diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 0bd888d44e6..75e874ebd85 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -1688,6 +1688,11 @@ export const useGeminiStream = ( pendingRetryCountdownItemRef.current || pendingRetryErrorItemRef.current ) { + const pendingError = pendingRetryErrorItemRef.current; + if (pendingError && pendingError.type === 'error') { + const { hint: _hint, ...errorWithoutHint } = pendingError; + addItem(errorWithoutHint, userMessageTimestamp); + } clearRetryCountdown(); } }