From 83d4bc7b59c015e672eac63c2e0f3a145cb461c1 Mon Sep 17 00:00:00 2001 From: herjarsa Date: Thu, 9 Jul 2026 21:55:57 +0200 Subject: [PATCH] fix(chat): save previous-session anchor in microtask with bail check When switching sessions, the previous session's viewport anchor save was deferred via setTimeout(..., 0). This races with the new session's restoreSnapshot effect: the timer can fire after React has flushed the new session's render and before the restore effect runs, leaving the saved anchor and the restored scroll position fighting over the same viewport store entry. The save reads messages (can be expensive) on the same tick as the new session's skeleton render. Replace setTimeout(..., 0) with queueMicrotask() so the save runs immediately after the current synchronous call stack and before the next macrotask / paint. This guarantees the save completes before the new session's restoreSnapshot effect fires. Add a bail check: if the user switched sessions again between the microtask scheduling and execution (rapid switching), the save is now stale. Comparing the captured newId to the current currentSessionId at microtask runtime avoids clobbering the in-flight session's anchor with data from a session that is no longer "previous". This is the queueMicrotask + bail change acknowledged as 'great' in the review of #1675, extracted as a focused single-file PR. --- packages/ui/src/sync/session-ui-store.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/sync/session-ui-store.ts b/packages/ui/src/sync/session-ui-store.ts index 397fb484..154ae984 100644 --- a/packages/ui/src/sync/session-ui-store.ts +++ b/packages/ui/src/sync/session-ui-store.ts @@ -617,7 +617,16 @@ export const useSessionUIStore = create()((set, get) => ({ // skeleton to render and reads messages which can be expensive. if (previousSessionId && previousSessionId !== id) { const prevId = previousSessionId - setTimeout(() => { + const newId = id + // queueMicrotask runs after the current synchronous call stack (and + // before the next macrotask / setTimeout(0) / paint), so the previous + // session's anchor is saved before the new session's restoreSnapshot + // effect fires. This eliminates the race where save and restore + // interleave against the same viewport store entry. + queueMicrotask(() => { + // Bail if the user already switched again — save is now stale. + const current = get().currentSessionId + if (current !== newId) return const memState = getViewportSessionMemory(prevId) if (!memState?.isStreaming) { const prevMessages = getSyncMessages(prevId) @@ -625,7 +634,7 @@ export const useSessionUIStore = create()((set, get) => ({ useViewportStore.getState().updateViewportAnchor(prevId, prevMessages.length - 1) } } - }, 0) + }); } // Mark session viewed in notification store + update active session ref