fix(aiui): stop fighting the virtualizer over scrollTop

"Failed to scroll to index N after 10 attempts" appeared in the console on
every send. It was not a real failure — it was two scroll controllers
arguing.

scrollToBottom() called virtualizer.scrollToIndex(last) AND then assigned
el.scrollTop on the next tick. scrollToIndex runs a retry loop that nudges
scrollTop toward the target row's measured offset and re-checks, up to ten
times, because dynamically-measured rows move the target as they settle.
The manual assignment overwrote each nudge, so the loop never observed
itself converge and always exhausted its attempts.

For "go to the end" the index-settling machinery buys nothing: scrollHeight
already is the bottom, the virtualizer renders whatever window that offset
implies, and it keeps working while a response streams and the last row
grows — the case the manual fallback was added for in the first place.
scrollToMessageIndex still uses scrollToIndex, which is the right tool for
jumping to an arbitrary row.

Console-only change; needs a device check that the chat still pins to the
bottom while streaming.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-08 07:08:28 -04:00
co-authored by Claude Opus 5
parent 3ac59a73b3
commit 7567c25c00
@@ -466,15 +466,26 @@ function handlePromptSelect(_userMsg: Message, assistantMsg: Message | null) {
}
function scrollToBottom() {
// Container scroll ONLY — deliberately not virtualizer.scrollToIndex.
//
// Doing both is what produced "Failed to scroll to index N after 10
// attempts" in the console on every send. scrollToIndex runs a retry loop
// that nudges scrollTop toward the measured offset of the target row and
// re-checks, up to 10 times, because dynamically-measured rows move the
// target as they settle. Assigning scrollTop ourselves on the next tick
// overwrote each of those nudges, so the loop never saw itself converge and
// always exhausted its attempts — the warning was the two of us fighting
// over the same scrollTop, not a real failure.
//
// For "go to the end" the virtualizer's index-settling machinery buys
// nothing: scrollHeight is already the bottom, and the virtualizer renders
// whatever window that offset implies. It also keeps working while a
// response streams and the last row keeps growing, which is exactly the
// case the old fallback was added for. scrollToMessageIndex still uses
// scrollToIndex, which is the right tool for jumping to an arbitrary row.
nextTick(() => {
if (messages.value.length > 0) {
virtualizer.value.scrollToIndex(messages.value.length - 1, { align: 'end' })
}
// Fallback: also scroll the container directly for streaming updates
nextTick(() => {
const el = messageListRef.value
if (el) el.scrollTop = el.scrollHeight
})
const el = messageListRef.value
if (el) el.scrollTop = el.scrollHeight
})
}