From f9e9f308738b6cb5f48d709e5b3b30158dbf008b Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sat, 30 May 2026 02:03:41 +0300 Subject: [PATCH] Fix session history loading (#1468) Fix chat history pagination and scroll preservation Align session history loading with the expected scroll-up pagination UX while keeping OpenChamber-specific initial message limits for constrained runtimes. - Separate initial load sizes from older-history pagination size - Load older messages automatically when scrolling near the top - Continue fetching history until a visible older turn is available - Preserve the current viewport synchronously during prepends - Prevent history loading from fighting pinned-to-bottom follow behavior - Remove delayed scroll-to-bottom correction that caused jumpbacks - Fix the virtualizer fallback path that could render a large blank spacer - Track oldest loaded message per pagination iteration to avoid redundant fetches --- .../ui/src/components/chat/ChatContainer.tsx | 7 +- .../ui/src/components/chat/MessageList.tsx | 44 +--- .../chat/hooks/useChatTimelineController.ts | 195 ++++++++++++++++-- packages/ui/src/sync/use-sync.ts | 23 ++- 4 files changed, 196 insertions(+), 73 deletions(-) diff --git a/packages/ui/src/components/chat/ChatContainer.tsx b/packages/ui/src/components/chat/ChatContainer.tsx index 54ee63d6..63869dc2 100644 --- a/packages/ui/src/components/chat/ChatContainer.tsx +++ b/packages/ui/src/components/chat/ChatContainer.tsx @@ -156,6 +156,7 @@ type ChatViewportProps = { handleMessageContentChange: (reason?: ContentChangeReason) => void; getAnimationHandlers: (messageId: string) => AnimationHandlers; handleLoadOlder: () => void; + handleHistoryScroll: () => void; scrollToBottom: () => void; sessionQuestions: QuestionRequest[]; sessionPermissions: PermissionRequest[]; @@ -181,6 +182,7 @@ const ChatViewport = React.memo(({ handleMessageContentChange, getAnimationHandlers, handleLoadOlder, + handleHistoryScroll, scrollToBottom, sessionQuestions, sessionPermissions, @@ -217,6 +219,7 @@ const ChatViewport = React.memo(({ hideTopShadow={isMobile && stickyUserHeader} tabIndex={0} onClick={focusScrollContainer} + onScroll={handleHistoryScroll} data-scroll-shadow="true" data-scrollbar="chat" > @@ -280,6 +283,7 @@ const ChatViewport = React.memo(({ && prev.handleMessageContentChange === next.handleMessageContentChange && prev.getAnimationHandlers === next.getAnimationHandlers && prev.handleLoadOlder === next.handleLoadOlder + && prev.handleHistoryScroll === next.handleHistoryScroll && prev.scrollToBottom === next.scrollToBottom && prev.sessionQuestions === next.sessionQuestions && prev.sessionPermissions === next.sessionPermissions @@ -655,7 +659,7 @@ export const ChatContainer: React.FC = ({ autoOpenDraft = tr }, [handleMessageContentChange, sessionPermissions, sessionQuestions]); const handleLoadOlder = React.useCallback(() => { - void loadEarlier(); + void loadEarlier({ userInitiated: true }); }, [loadEarlier]); const navigation = useChatTurnNavigation({ @@ -950,6 +954,7 @@ export const ChatContainer: React.FC = ({ autoOpenDraft = tr handleMessageContentChange={handleMessageContentChange} getAnimationHandlers={getAnimationHandlers} handleLoadOlder={handleLoadOlder} + handleHistoryScroll={timelineController.handleHistoryScroll} scrollToBottom={resumeToLatestInstant} sessionQuestions={sessionQuestions} sessionPermissions={sessionPermissions} diff --git a/packages/ui/src/components/chat/MessageList.tsx b/packages/ui/src/components/chat/MessageList.tsx index 95c2674b..121f6910 100644 --- a/packages/ui/src/components/chat/MessageList.tsx +++ b/packages/ui/src/components/chat/MessageList.tsx @@ -1005,15 +1005,9 @@ const StaticHistoryList = React.memo(({ entries, shouldVirtualize, virtualRows, } if (virtualRows.length === 0 && entries.length > 0) { - const fallbackStart = Math.max(0, entries.length - MESSAGE_LIST_OVERSCAN * 2); - const fallbackEntries = entries.slice(fallbackStart); - const fallbackHeight = fallbackEntries.reduce((total, entry) => total + estimateHistoryEntryHeight(entry), 0); - const fallbackPaddingTop = Math.max(0, totalSize - fallbackHeight); - return (
- {fallbackPaddingTop > 0 ?