From e3cd4151971c34f45b605d7a8176cf519d9a1649 Mon Sep 17 00:00:00 2001 From: Mihir-Mavalankar Date: Fri, 27 Mar 2026 10:01:41 -0700 Subject: [PATCH] fix(seer-explorer): Prevent optimistic state clearing on rethink with same message When a user rethinks and re-sends the same message, the clearing effect was matching stale pre-truncation server blocks, causing deleted blocks to briefly reappear and the thinking indicator to vanish. Store the session updated_at as a baseline when setting optimistic state and skip the clearing effect until the server has actually processed the request. Co-Authored-By: Claude Sonnet 4 --- .../hooks/useSeerExplorer.spec.tsx | 50 +++++++++++++++++++ .../seerExplorer/hooks/useSeerExplorer.tsx | 8 ++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/static/app/views/seerExplorer/hooks/useSeerExplorer.spec.tsx b/static/app/views/seerExplorer/hooks/useSeerExplorer.spec.tsx index b48377fb76a0a0..41582af913dfec 100644 --- a/static/app/views/seerExplorer/hooks/useSeerExplorer.spec.tsx +++ b/static/app/views/seerExplorer/hooks/useSeerExplorer.spec.tsx @@ -222,5 +222,55 @@ describe('useSeerExplorer', () => { const blocks = result.current.sessionData?.blocks ?? []; expect(blocks.some(b => b.message.role === 'assistant' && b.loading)).toBe(true); }); + + it('keeps optimistic state when rethinking with the same message', async () => { + const chatUrl = `/organizations/${organization.slug}/seer/explorer-chat/`; + const ts = '2024-01-01T00:00:00Z'; + + MockApiClient.addMockResponse({url: chatUrl, method: 'GET', body: {session: null}}); + MockApiClient.addMockResponse({ + url: `${chatUrl}789/`, + method: 'GET', + body: { + session: { + blocks: [ + { + id: 'u0', + message: {role: 'user', content: 'hello'}, + timestamp: ts, + loading: false, + }, + { + id: 'a1', + message: {role: 'assistant', content: 'Hi!'}, + timestamp: ts, + loading: false, + }, + ], + run_id: 789, + status: 'completed', + updated_at: ts, + }, + }, + }); + MockApiClient.addMockResponse({ + url: `${chatUrl}789/`, + method: 'POST', + body: {run_id: 789}, + }); + + const {result} = renderHookWithProviders(() => useSeerExplorer(), {organization}); + + act(() => result.current.switchToRun(789)); + await waitFor(() => result.current.sessionData?.blocks?.length === 2); + + act(() => result.current.deleteFromIndex(0)); + await act(async () => { + await result.current.sendMessage('hello'); + }); + + expect(result.current.sessionData?.blocks?.some(b => b.loading)).toBe(true); + expect(result.current.deletedFromIndex).toBe(0); + }); }); }); diff --git a/static/app/views/seerExplorer/hooks/useSeerExplorer.tsx b/static/app/views/seerExplorer/hooks/useSeerExplorer.tsx index bf4200a326c6fa..f72bca47046201 100644 --- a/static/app/views/seerExplorer/hooks/useSeerExplorer.tsx +++ b/static/app/views/seerExplorer/hooks/useSeerExplorer.tsx @@ -148,6 +148,7 @@ export const useSeerExplorer = () => { const [optimistic, setOptimistic] = useState<{ assistantBlockId: string; assistantContent: string; + baselineUpdatedAt: string | undefined; insertIndex: number; userBlockId: string; userQuery: string; @@ -231,6 +232,7 @@ export const useSeerExplorer = () => { calculatedInsertIndex + 1 ), assistantContent: assistantContent || 'Thinking...', + baselineUpdatedAt: apiData?.session?.updated_at, }); try { @@ -450,6 +452,10 @@ export const useSeerExplorer = () => { return undefined; } + if (apiData?.session?.updated_at === optimistic.baselineUpdatedAt) { + return undefined; + } + const serverBlocks = apiData?.session?.blocks || []; const blockAtInsert = serverBlocks[optimistic.insertIndex]; @@ -471,7 +477,7 @@ export const useSeerExplorer = () => { } return undefined; - }, [apiData?.session?.blocks, optimistic]); + }, [apiData?.session?.blocks, apiData?.session?.updated_at, optimistic]); // Detect PR creation errors and show error messages useEffect(() => {