fix(chat): offer the scroll pill when growth leaves the reader behind

With streaming auto-follow off nothing moves the viewport, so a growing
reply slides below the composer without a single scroll event and the
at-end transition that shows the pill never fires. Content growth now
doubles as the signal: once the measured last row extends past the visible
area by the follow re-arm threshold, the end state clears and the pill is
scheduled.
This commit is contained in:
Bohdan Triapitsyn
2026-08-26 16:49:21 +03:00
parent 087c148b2e
commit ba3a604e9e
+25 -2
View File
@@ -9,6 +9,7 @@ import {
getAnchoredTurnMetrics,
getRowBottom,
resolveTimelineIsAtEnd,
TIMELINE_FOLLOW_REARM_THRESHOLD_PX,
type TimelineListMeasurementState,
type TimelineScrollMode,
} from '@/components/chat/lib/scroll/timelineScrollAnchoring';
@@ -617,7 +618,29 @@ export const useChatTimelineScroll = ({
}
}
if (!streamingAutoFollowEnabledRef.current) return;
if (!streamingAutoFollowEnabledRef.current) {
// With auto-follow off nothing moves the viewport, so a growing
// reply slides below the visible area without a single scroll
// event — and the at-end transition that offers the pill never
// fires. Content growth is the signal here: once the real last
// row extends past what the composer leaves visible, the reader
// is factually behind and the pill must say so.
const list = listRef.current;
if (list && isAtEndRef.current) {
const state = list.getState();
const lastIndex = state.data.length - 1;
const lastBottom = lastIndex >= 0 ? getRowBottom(state, lastIndex) : null;
if (lastBottom !== null) {
const visibleBottom = state.scroll + state.scrollLength - composerOverlayHeightRef.current;
if (lastBottom - visibleBottom > TIMELINE_FOLLOW_REARM_THRESHOLD_PX) {
isAtEndRef.current = false;
setIsPinned(false);
scheduleShowScrollButton();
}
}
}
return;
}
if (!isLiveFollowActive()) return;
// Since @legendapp/list 3.3.x, maintainScrollAtEnd follows content
@@ -674,7 +697,7 @@ export const useChatTimelineScroll = ({
});
});
}, [isLiveFollowActive]);
}, [isLiveFollowActive, scheduleShowScrollButton]);
// The streaming tail grows inside one row without changing the entries
// array, so data-change callbacks are silent for the entire stream. The