fix: prevent blank chat viewport when switching sessions (#1553)
When switching to a session with long context (especially in Electron desktop when changing servers), the chat viewport could render blank until the user scrolled. Two interacting issues caused this: 1. historyVirtualRows memo never recomputed after the first render because historyVirtualizer (from useVirtualizer's useState) is a stable reference. Frozen range meant items rendered at the top while paddingBottom filled the visible viewport after scrolling. 2. pendingInitialRestoreRef replay ran in useEffect (after paint), showing a frame at scrollTop:0 with the stale virtualizer range. Fixed by: adding a virtualVersion counter driven by useVirtualizer's onChange to bust the memo; switching the replay to useLayoutEffect so scroll position is set before the browser paints.
This commit is contained in:
@@ -1372,6 +1372,8 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
|
|||||||
scrollEl.scrollTop += prependedHeight;
|
scrollEl.scrollTop += prependedHeight;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [virtualVersion, bumpVirtualVersion] = React.useReducer((v: number) => v + 1, 0);
|
||||||
|
|
||||||
const historyVirtualizer = useVirtualizer({
|
const historyVirtualizer = useVirtualizer({
|
||||||
count: historyEntries.length,
|
count: historyEntries.length,
|
||||||
getScrollElement: resolveScrollContainer,
|
getScrollElement: resolveScrollContainer,
|
||||||
@@ -1381,6 +1383,7 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
|
|||||||
useAnimationFrameWithResizeObserver: true,
|
useAnimationFrameWithResizeObserver: true,
|
||||||
overscan: MESSAGE_LIST_OVERSCAN,
|
overscan: MESSAGE_LIST_OVERSCAN,
|
||||||
enabled: shouldVirtualizeHistory,
|
enabled: shouldVirtualizeHistory,
|
||||||
|
onChange: bumpVirtualVersion,
|
||||||
});
|
});
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
@@ -1437,7 +1440,7 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
|
|||||||
|
|
||||||
const historyVirtualRows = React.useMemo(
|
const historyVirtualRows = React.useMemo(
|
||||||
() => (shouldVirtualizeHistory ? historyVirtualizer.getVirtualItems() : EMPTY_VIRTUAL_ROWS),
|
() => (shouldVirtualizeHistory ? historyVirtualizer.getVirtualItems() : EMPTY_VIRTUAL_ROWS),
|
||||||
[historyVirtualizer, shouldVirtualizeHistory],
|
[historyVirtualizer, shouldVirtualizeHistory, virtualVersion],
|
||||||
);
|
);
|
||||||
|
|
||||||
const allEntries = React.useMemo(() => {
|
const allEntries = React.useMemo(() => {
|
||||||
|
|||||||
@@ -411,7 +411,9 @@ export const useChatAutoFollow = ({
|
|||||||
}, [sessionIsWorking, startFollowLoop]);
|
}, [sessionIsWorking, startFollowLoop]);
|
||||||
|
|
||||||
// Replay a deferred restoreSnapshot once ChatViewport mounts.
|
// Replay a deferred restoreSnapshot once ChatViewport mounts.
|
||||||
React.useEffect(() => {
|
// useLayoutEffect ensures scroll position is set before the browser paints,
|
||||||
|
// preventing a visible flash of content at the wrong scroll position.
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
if (!containerEl) return;
|
if (!containerEl) return;
|
||||||
if (pendingInitialRestoreRef.current && pendingInitialRestoreRef.current === currentSessionId) {
|
if (pendingInitialRestoreRef.current && pendingInitialRestoreRef.current === currentSessionId) {
|
||||||
void restoreSnapshot();
|
void restoreSnapshot();
|
||||||
|
|||||||
Reference in New Issue
Block a user