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:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
a23f5e7545
commit
03c9065c90
@@ -551,6 +551,7 @@ export const ChatContainer: React.FC = () => {
|
||||
isPinned,
|
||||
isOverflowing,
|
||||
isProgrammaticFollowActive,
|
||||
clearRestoreInProgress,
|
||||
} = useChatScrollManager({
|
||||
currentSessionId,
|
||||
sessionMessageCount,
|
||||
@@ -578,15 +579,32 @@ export const ChatContainer: React.FC = () => {
|
||||
isPinned,
|
||||
isOverflowing,
|
||||
});
|
||||
const { loadEarlier, resumeToBottomInstant } = timelineController;
|
||||
const { loadEarlier, resumeToBottomInstant, restoreSavedScrollPosition } = timelineController;
|
||||
|
||||
const runLatestInstantResume = React.useCallback(async () => {
|
||||
if (!currentSessionId) {
|
||||
scrollToBottom({ instant: true, force: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this session has a saved non-bottom scroll position.
|
||||
const savedMemState = sessionMemoryStateMap.get(currentSessionId);
|
||||
const savedPos = savedMemState?.scrollPosition;
|
||||
if (savedPos && !sessionIsWorking) {
|
||||
const savedMaxScroll = Math.max(0, savedPos.scrollHeight - savedPos.clientHeight);
|
||||
// Use the same pixel threshold as the pin logic: 10% of clientHeight, clamped.
|
||||
const threshold = Math.max(24, Math.min(200, savedPos.clientHeight * 0.10));
|
||||
const distanceFromSavedBottom = savedMaxScroll - savedPos.scrollTop;
|
||||
if (savedMaxScroll > 0 && distanceFromSavedBottom > threshold) {
|
||||
await restoreSavedScrollPosition(savedPos);
|
||||
clearRestoreInProgress(currentSessionId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await resumeToBottomInstant();
|
||||
}, [currentSessionId, resumeToBottomInstant, scrollToBottom]);
|
||||
clearRestoreInProgress(currentSessionId);
|
||||
}, [clearRestoreInProgress, currentSessionId, restoreSavedScrollPosition, resumeToBottomInstant, scrollToBottom, sessionIsWorking, sessionMemoryStateMap]);
|
||||
|
||||
const resumeToLatestInstant = React.useCallback(() => {
|
||||
void runLatestInstantResume();
|
||||
@@ -729,6 +747,7 @@ export const ChatContainer: React.FC = () => {
|
||||
const hasHashTarget = typeof window !== 'undefined' && window.location.hash.length > 0;
|
||||
if (hasHashTarget) {
|
||||
lastScrolledSessionRef.current = currentSessionId;
|
||||
clearRestoreInProgress(currentSessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -742,7 +761,7 @@ export const ChatContainer: React.FC = () => {
|
||||
window.requestAnimationFrame(() => {
|
||||
resumeToLatestInstant();
|
||||
});
|
||||
}, [currentSessionId, resumeToLatestInstant]);
|
||||
}, [clearRestoreInProgress, currentSessionId, resumeToLatestInstant]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentSessionId) return;
|
||||
|
||||
Reference in New Issue
Block a user