"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>