fix(chat): stop double scroll write on prepend that resonates into oscillation
When older history is prepended while the viewport is pinned to the bottom, the
timeline controller wrote the re-pin manually (scrollTop += delta). That write
is not flagged as programmatic, so useChatAutoFollow's scroll handler treated it
as movement and issued its own correcting scroll — a redundant up/down move on
every prepend. On most setups it settles after one move, but with different
virtualizer measurement/timing it never converges, producing the reported
infinite up/down scroll glitch.
When pinned, delegate the prepend re-pin to auto-follow's goToBottom('instant'):
a single authoritative write to the bottom that IS marked programmatic, so
auto-follow ignores it instead of fighting it. The released case (user reading
back through history) is unchanged and still preserves the read position.
This also covers the on-open history auto-load (loadEarlierIfPinnedViewport-
Underfilled), which only runs while pinned, so its prepends now go through the
single writer too.
This commit is contained in:
@@ -350,6 +350,25 @@ export const useChatTimelineController = ({
|
||||
const container = scrollRef.current;
|
||||
if (!container) return;
|
||||
|
||||
// Bottom-pinned: auto-follow is the single owner of the scroll position.
|
||||
// Route the prepend re-pin through goToBottom (a programmatic, authoritative
|
||||
// instant write to the bottom) rather than a manual scrollTop adjustment. A
|
||||
// manual write here is NOT marked programmatic, so auto-follow's scroll
|
||||
// handler treats it as movement and issues its own correcting scroll — a
|
||||
// redundant up/down move on every prepend that, on some setups, resonates
|
||||
// into the reported infinite oscillation. Delegating keeps exactly one
|
||||
// writer and no fight.
|
||||
if (isPinnedRef.current) {
|
||||
prePrependScrollRef.current = null;
|
||||
goToBottom('instant');
|
||||
prependTrackingRef.current = {
|
||||
oldestId: renderedMessages[0]?.info?.id ?? null,
|
||||
newestId: renderedMessages[renderedMessages.length - 1]?.info?.id ?? null,
|
||||
scrollHeight: container.scrollHeight,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const snap = prePrependScrollRef.current;
|
||||
if (snap) {
|
||||
prePrependScrollRef.current = null;
|
||||
@@ -395,7 +414,7 @@ export const useChatTimelineController = ({
|
||||
newestId: renderedMessages[renderedMessages.length - 1]?.info?.id ?? null,
|
||||
scrollHeight: container.scrollHeight,
|
||||
};
|
||||
}, [renderedMessages, scrollRef, restoreViewportAnchor]);
|
||||
}, [renderedMessages, scrollRef, restoreViewportAnchor, goToBottom]);
|
||||
|
||||
const revealBufferedTurns = React.useCallback(async (): Promise<boolean> => false, []);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user