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:
Tom
2026-06-12 20:31:32 +03:00
committed by GitHub
parent 85d4e2d319
commit db3558f906
2 changed files with 7 additions and 2 deletions
@@ -1372,6 +1372,8 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
scrollEl.scrollTop += prependedHeight;
});
const [virtualVersion, bumpVirtualVersion] = React.useReducer((v: number) => v + 1, 0);
const historyVirtualizer = useVirtualizer({
count: historyEntries.length,
getScrollElement: resolveScrollContainer,
@@ -1381,6 +1383,7 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
useAnimationFrameWithResizeObserver: true,
overscan: MESSAGE_LIST_OVERSCAN,
enabled: shouldVirtualizeHistory,
onChange: bumpVirtualVersion,
});
React.useLayoutEffect(() => {
@@ -1437,7 +1440,7 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
const historyVirtualRows = React.useMemo(
() => (shouldVirtualizeHistory ? historyVirtualizer.getVirtualItems() : EMPTY_VIRTUAL_ROWS),
[historyVirtualizer, shouldVirtualizeHistory],
[historyVirtualizer, shouldVirtualizeHistory, virtualVersion],
);
const allEntries = React.useMemo(() => {
+3 -1
View File
@@ -411,7 +411,9 @@ export const useChatAutoFollow = ({
}, [sessionIsWorking, startFollowLoop]);
// 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 (pendingInitialRestoreRef.current && pendingInitialRestoreRef.current === currentSessionId) {
void restoreSnapshot();