From ae6873ef1f0fe4063f88cfb404d1da03d0536431 Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Tue, 1 Sep 2026 16:27:20 -0700 Subject: [PATCH] fix(mobile): retain structured transcript during reconnect --- ...se-mobile-structured-agent-session.test.tsx | 18 ++++++++++++++++++ .../use-mobile-structured-agent-state.ts | 10 ++++++++++ src/shared/structured-agent-session-reducer.ts | 3 ++- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/mobile/src/session/use-mobile-structured-agent-session.test.tsx b/mobile/src/session/use-mobile-structured-agent-session.test.tsx index ff27c69abe5..43b3c2a833d 100644 --- a/mobile/src/session/use-mobile-structured-agent-session.test.tsx +++ b/mobile/src/session/use-mobile-structured-agent-session.test.tsx @@ -803,4 +803,22 @@ describe('useMobileStructuredAgentSession', () => { expect(hook?.session.messages).toHaveLength(1) expect(hook?.session.status).toBe('ready') }) + + it('keeps the transcript visible while a reconnect is loading', async () => { + await act(async () => { + renderer = create(createElement(Harness, { connected: true })) + }) + await vi.waitFor(() => expect(listener).toEqual(expect.any(Function))) + act(() => listener?.(snapshotWithMessage())) + expect(hook?.session.messages).toHaveLength(1) + + await act(async () => { + renderer?.update(createElement(Harness, { connected: false })) + }) + await act(async () => { + renderer?.update(createElement(Harness, { connected: true })) + }) + + expect(hook?.session.messages).toHaveLength(1) + }) }) diff --git a/mobile/src/session/use-mobile-structured-agent-state.ts b/mobile/src/session/use-mobile-structured-agent-state.ts index 2b71ed7aac7..a0397949cef 100644 --- a/mobile/src/session/use-mobile-structured-agent-state.ts +++ b/mobile/src/session/use-mobile-structured-agent-state.ts @@ -40,6 +40,7 @@ export function useMobileStructuredAgentState(args: { const [state, setState] = useState(EMPTY_STRUCTURED_AGENT_SESSION) const [loadingOlder, setLoadingOlder] = useState(false) const stateRef = useRef(state) + const sessionIdentityRef = useRef<{ client: RpcClient; sessionId: string } | null>(null) useLayoutEffect(() => { stateRef.current = state }, [state]) @@ -50,6 +51,7 @@ export function useMobileStructuredAgentState(args: { useEffect(() => { if (!client || !sessionId || !enabled) { + sessionIdentityRef.current = null setState(EMPTY_STRUCTURED_AGENT_SESSION) setLoadingOlder(false) return @@ -60,6 +62,14 @@ export function useMobileStructuredAgentState(args: { // nor empty-state copy, so the chat reads as gone rather than offline. return } + const sessionChanged = + sessionIdentityRef.current?.client !== client || + sessionIdentityRef.current?.sessionId !== sessionId + sessionIdentityRef.current = { client, sessionId } + if (sessionChanged) { + // Do not leak the previous tab's transcript while the new session loads. + setState(EMPTY_STRUCTURED_AGENT_SESSION) + } apply({ type: 'loading' }) const holderId = structuredAgentSessionHolderId('mobile-chat') let cancelled = false diff --git a/src/shared/structured-agent-session-reducer.ts b/src/shared/structured-agent-session-reducer.ts index 24b500fc2b3..48029b5910e 100644 --- a/src/shared/structured-agent-session-reducer.ts +++ b/src/shared/structured-agent-session-reducer.ts @@ -95,7 +95,8 @@ export function reduceStructuredAgentSession( action: StructuredAgentSessionAction ): StructuredAgentSessionState { if (action.type === 'loading') { - return { ...EMPTY_STRUCTURED_AGENT_SESSION, status: 'loading' } + // Keep the last transcript visible while a reconnect rehydrates the stream. + return { ...state, status: 'loading', error: undefined } } if (action.type === 'error') { return { ...state, status: 'error', error: action.message }