perf: freeze detached chat history updates

Stop historical chat view from waking on live tail deltas by suspending detached part updates, narrowing scroll manager dependencies, and hardening sync-context HMR behavior during dev.
This commit is contained in:
Bohdan Triapitsyn
2026-04-05 15:58:06 +03:00
parent 4663abd6f3
commit e56f2f99cf
4 changed files with 50 additions and 20 deletions
+7 -14
View File
@@ -1,6 +1,4 @@
import React from 'react';
import type { Part } from '@opencode-ai/sdk/v2';
import { MessageFreshnessDetector } from '@/lib/messageFreshness';
import { createScrollSpy } from '@/components/chat/lib/scroll/scrollSpy';
import {
@@ -13,11 +11,6 @@ import { useScrollEngine } from './useScrollEngine';
export type ContentChangeReason = 'text' | 'structural' | 'permission';
interface ChatMessageRecord {
info: Record<string, unknown>;
parts: Part[];
}
interface SessionMemoryState {
viewportAnchor: number;
isStreaming: boolean;
@@ -31,7 +24,7 @@ interface SessionMemoryState {
interface UseChatScrollManagerOptions {
currentSessionId: string | null;
sessionMessages: ChatMessageRecord[];
sessionMessageCount: number;
sessionPermissions: unknown[];
streamingMessageId: string | null;
sessionMemoryState: Map<string, SessionMemoryState>;
@@ -73,7 +66,7 @@ const VIEWPORT_ANCHOR_MIN_UPDATE_MS = 150;
export const useChatScrollManager = ({
currentSessionId,
sessionMessages,
sessionMessageCount,
streamingMessageId,
updateViewportAnchor,
isSyncing,
@@ -330,7 +323,7 @@ export const useChatScrollManager = ({
const { scrollTop, scrollHeight, clientHeight } = container;
const position = (scrollTop + clientHeight / 2) / Math.max(scrollHeight, 1);
const estimatedIndex = Math.floor(position * sessionMessages.length);
const estimatedIndex = Math.floor(position * sessionMessageCount);
queueViewportAnchor(currentSessionId, estimatedIndex);
}, [
currentSessionId,
@@ -339,7 +332,7 @@ export const useChatScrollManager = ({
queueViewportAnchor,
schedulePinnedStateAndIndicators,
scrollEngine,
sessionMessages.length,
sessionMessageCount,
updatePinnedState,
]);
@@ -469,7 +462,7 @@ export const useChatScrollManager = ({
return;
}
schedulePinnedStateAndIndicators();
}, [isSyncing, schedulePinnedStateAndIndicators, sessionMessages.length, shouldSkipLiveContentSync]);
}, [isSyncing, schedulePinnedStateAndIndicators, sessionMessageCount, shouldSkipLiveContentSync]);
// Use ResizeObserver to detect content changes and maintain pin
React.useEffect(() => {
@@ -551,7 +544,7 @@ export const useChatScrollManager = ({
return () => {
window.cancelAnimationFrame(rafId);
};
}, [currentSessionId, schedulePinnedStateAndIndicators, sessionMessages.length, shouldSkipLiveContentSync]);
}, [currentSessionId, schedulePinnedStateAndIndicators, sessionMessageCount, shouldSkipLiveContentSync]);
const animationHandlersRef = React.useRef<Map<string, AnimationHandlers>>(new Map());
@@ -714,7 +707,7 @@ export const useChatScrollManager = ({
mutationObserver.disconnect();
spy.destroy();
};
}, [currentSessionId, onActiveTurnChange, scrollRef, sessionMessages.length]);
}, [currentSessionId, onActiveTurnChange, scrollRef, sessionMessageCount]);
return {
scrollRef,