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 }) }