From 7567c25c00c4c946932b1878239d9cbebd409fe7 Mon Sep 17 00:00:00 2001 From: archipelago Date: Sat, 8 Aug 2026 07:08:28 -0400 Subject: [PATCH] fix(aiui): stop fighting the virtualizer over scrollTop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "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) --- .../app/src/components/chat/ChatWindow.vue | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/aiui/packages/app/src/components/chat/ChatWindow.vue b/aiui/packages/app/src/components/chat/ChatWindow.vue index e8f969ca..0e921682 100644 --- a/aiui/packages/app/src/components/chat/ChatWindow.vue +++ b/aiui/packages/app/src/components/chat/ChatWindow.vue @@ -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 }) }