fix: preserve per-session scroll position on session switch (#1083)

* fix: restore scroll position when switching chat sessions

When switching between chat sessions, scroll position now restores to
where the user left off instead of always jumping to the bottom.

- Save pixel-level scrollPosition (scrollTop/scrollHeight/clientHeight)
  in viewport store on every scroll event
- Add restoreSavedScrollPosition to timeline controller for ratio-based
  restoration (handles content size changes between visits)
- Suppress intermediate scroll events during session transition with an
  explicit flag, cleared deterministically after restore completes
- Cancel in-flight animations/follow-loops on session switch
- Preserve scrollPosition when session-ui-store rebuilds SessionMemoryState

* fix: keep streaming sessions pinned on restore

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
jwcrystal
2026-05-01 12:39:04 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent a23f5e7545
commit 03c9065c90
7 changed files with 164 additions and 44 deletions
+4 -3
View File
@@ -819,10 +819,11 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
if (!memState || !memState.lastUserMessageAt) {
const newMemState = new Map(viewportState.sessionMemoryState)
newMemState.set(currentSessionId, {
viewportAnchor: memState?.viewportAnchor ?? 0,
isStreaming: memState?.isStreaming ?? false,
viewportAnchor: 0,
isStreaming: false,
lastAccessedAt: Date.now(),
backgroundMessageCount: memState?.backgroundMessageCount ?? 0,
backgroundMessageCount: 0,
...memState,
lastUserMessageAt: Date.now(),
})
useViewportStore.setState({ sessionMemoryState: newMemState })
+14 -3
View File
@@ -7,6 +7,12 @@ import { create } from "zustand"
export type SessionMemoryState = {
viewportAnchor: number
/** Last known scrollbar pixel state — saved on every scroll event. */
scrollPosition?: {
scrollTop: number
scrollHeight: number
clientHeight: number
}
isStreaming: boolean
streamStartTime?: number
lastAccessedAt: number
@@ -27,14 +33,14 @@ export type ViewportState = {
sessionMemoryState: Map<string, SessionMemoryState>
isSyncing: boolean
updateViewportAnchor: (sessionId: string, anchor: number) => void
updateViewportAnchor: (sessionId: string, anchor: number, scrollPosition?: SessionMemoryState['scrollPosition']) => void
}
export const useViewportStore = create<ViewportState>()((set) => ({
sessionMemoryState: new Map(),
isSyncing: false,
updateViewportAnchor: (sessionId, anchor) =>
updateViewportAnchor: (sessionId, anchor, scrollPosition) =>
set((s) => {
const map = new Map(s.sessionMemoryState)
const existing = map.get(sessionId) ?? {
@@ -43,7 +49,12 @@ export const useViewportStore = create<ViewportState>()((set) => ({
lastAccessedAt: Date.now(),
backgroundMessageCount: 0,
}
map.set(sessionId, { ...existing, viewportAnchor: anchor, lastAccessedAt: Date.now() })
map.set(sessionId, {
...existing,
viewportAnchor: anchor,
...(scrollPosition ? { scrollPosition } : {}),
lastAccessedAt: Date.now(),
})
return { sessionMemoryState: map }
}),
}))