refactor: streamline chat scroll manager

- Removed unused activeTurnAnchorId and activeTurnSpacerHeight from MemoryDebugPanel.
- Simplified useChatScrollManager by eliminating unnecessary state and functions related to active turn anchoring.
- Updated loadMessages function signatures to accept an optional limit parameter across message and session stores.
- Enhanced scroll behavior in useScrollEngine to support dynamic bottom tracking during animations.
- Improved event stream handling to allow for resyncing messages with a specified limit.
- Cleaned up session memory state to remove active turn properties, focusing on viewport anchoring.
This commit is contained in:
Bohdan Triapitsyn
2026-01-20 03:23:39 +02:00
parent 8fa16baf3b
commit 6544b12e78
15 changed files with 318 additions and 937 deletions
@@ -25,8 +25,6 @@ export const ChatContainer: React.FC = () => {
loadMessages,
loadMoreMessages,
updateViewportAnchor,
updateActiveTurnAnchor,
getActiveTurnAnchor,
sessionMemoryState,
openNewSessionDraft,
isSyncing,
@@ -75,26 +73,22 @@ export const ChatContainer: React.FC = () => {
const {
scrollRef,
handleMessageContentChange,
getAnimationHandlers,
showScrollButton,
scrollToBottom,
spacerHeight,
pendingAnchorId,
hasActiveAnchor,
getAnimationHandlers,
showScrollButton,
scrollToBottom,
scrollToPosition,
isPinned,
} = useChatScrollManager({
currentSessionId,
sessionMessages,
streamingMessageId,
sessionMemoryState,
updateViewportAnchor,
updateActiveTurnAnchor,
getActiveTurnAnchor,
isSyncing,
isMobile,
messageStreamStates,
sessionPermissions: sessionBlockingCards,
trimToViewportWindow,
sessionActivityPhase,
});
const memoryState = React.useMemo(() => {
@@ -123,12 +117,12 @@ export const ChatContainer: React.FC = () => {
await loadMoreMessages(currentSessionId, 'up');
if (container && prevHeight !== null && prevTop !== null) {
const heightDiff = container.scrollHeight - prevHeight;
container.scrollTop = prevTop + heightDiff;
scrollToPosition(prevTop + heightDiff, { instant: true });
}
} finally {
setIsLoadingOlder(false);
}
}, [currentSessionId, isLoadingOlder, loadMoreMessages, scrollRef]);
}, [currentSessionId, isLoadingOlder, loadMoreMessages, scrollRef, scrollToPosition]);
// Scroll to a specific message by ID (for timeline dialog)
const scrollToMessage = React.useCallback((messageId: string) => {
@@ -169,7 +163,8 @@ export const ChatContainer: React.FC = () => {
} finally {
const currentPhase = sessionActivityPhase?.get(currentSessionId) ?? 'idle';
const isActivePhase = currentPhase === 'busy' || currentPhase === 'cooldown';
const shouldSkipScroll = isActivePhase && hasActiveAnchor;
// When pinned and active, scroll is already maintained automatically
const shouldSkipScroll = isActivePhase && isPinned;
if (!shouldSkipScroll) {
if (typeof window === 'undefined') {
@@ -184,7 +179,7 @@ export const ChatContainer: React.FC = () => {
};
void load();
}, [currentSessionId, hasActiveAnchor, loadMessages, messages, scrollToBottom, sessionActivityPhase]);
}, [currentSessionId, isPinned, loadMessages, messages, scrollToBottom, sessionActivityPhase]);
if (!currentSessionId && !draftOpen) {
return (
@@ -277,7 +272,6 @@ export const ChatContainer: React.FC = () => {
}}
data-scroll-shadow="true"
data-scrollbar="chat"
hideBottomShadow={!!pendingAnchorId}
>
<div className="relative z-0 min-h-full">
<MessageList
@@ -290,16 +284,7 @@ export const ChatContainer: React.FC = () => {
isLoadingOlder={isLoadingOlder}
onLoadOlder={handleLoadOlder}
scrollToBottom={scrollToBottom}
pendingAnchorId={pendingAnchorId}
/>
{}
{spacerHeight > 0 && hasActiveAnchor && (
<div
data-role="active-turn-spacer"
style={{ height: spacerHeight }}
aria-hidden="true"
/>
)}
</div>
</ScrollShadow>
<OverlayScrollbar containerRef={scrollRef} />