From f0378ed816b7d548ea2289e793adc13c04756f32 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 22 Oct 2026 10:48:00 +0000 Subject: [PATCH] Clear Ask when you leave while it is still answering. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Leaving the page aborted the request but left the loading flag set, so coming back showed Typing forever and the composer stayed disabled. The partial answer is now kept and the composer is usable again. Co-authored-by: Gáborjáni-Szabó Ádám --- src/pages/AskPage.tsx | 33 ++++++++++++++++++++++-- src/test/pages/AskAccountSwitch.test.tsx | 18 +++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/pages/AskPage.tsx b/src/pages/AskPage.tsx index 52c2d31a9..39c233e79 100644 --- a/src/pages/AskPage.tsx +++ b/src/pages/AskPage.tsx @@ -84,9 +84,38 @@ export default function AskPage() { setAskLoading(false); }; + // Leaving the page aborts the request. The in-flight handler then sees a new + // request id and will not clear the flags, so do it here or Ask comes back + // stuck on Typing with the composer disabled. A partial answer is kept. + const leaveCleanupRef = useRef({ + abortActiveRequest, + addAskMessage, + setAskLoading, + setAskStreamingAssistant, + stoppedNote: t('ask.stopped', 'You stopped this answer before it finished.'), + }); + leaveCleanupRef.current = { + abortActiveRequest, + addAskMessage, + setAskLoading, + setAskStreamingAssistant, + stoppedNote: t('ask.stopped', 'You stopped this answer before it finished.'), + }; useEffect(() => () => { - abortActiveRequest(); - }, [abortActiveRequest]); + const cleanup = leaveCleanupRef.current; + const partial = useSessionStore.getState().askStreamingAssistant; + cleanup.abortActiveRequest(); + cleanup.setAskLoading(false); + cleanup.setAskStreamingAssistant(null); + const kept = partial?.content.trim(); + if (!kept) return; + cleanup.addAskMessage({ + id: partial?.id, + role: 'assistant', + content: kept, + output: { response: kept, followUpQuestions: [], uncertainties: [cleanup.stoppedNote] }, + }); + }, []); // A reply still in flight belongs to the scope it was asked in; a new debate // or one-off mode starts a fresh conversation, so drop the late answer. diff --git a/src/test/pages/AskAccountSwitch.test.tsx b/src/test/pages/AskAccountSwitch.test.tsx index 92035fd5a..0656610e6 100644 --- a/src/test/pages/AskAccountSwitch.test.tsx +++ b/src/test/pages/AskAccountSwitch.test.tsx @@ -61,4 +61,22 @@ describe('Ask across an account change', () => { expect(screen.queryByText(/Account A private answer/)).not.toBeInTheDocument(); expect(useSessionStore.getState().askStreamingAssistant).toBeNull(); }); + + it('does not stay on Typing after you leave mid-answer', async () => { + mocks.userId = 'user-a'; + enableOneOffMode(); + useSessionStore.getState().clearAskMessages(); + const view = render(); + fireEvent.change(screen.getByPlaceholderText('Ask your debate coach...'), { target: { value: 'How do I weigh cost?' } }); + fireEvent.click(screen.getByRole('button', { name: 'Send message' })); + await waitFor(() => expect(mocks.live).toBeDefined()); + act(() => mocks.live!.onDraft({ response: 'A partial answer' })); + expect(useSessionStore.getState().askLoading).toBe(true); + + view.unmount(); + + expect(useSessionStore.getState().askLoading).toBe(false); + expect(useSessionStore.getState().askStreamingAssistant).toBeNull(); + expect(useSessionStore.getState().askMessages.map((message) => message.content)).toContain('A partial answer'); + }); });