fix: enable auto-scroll during assistant response streaming

Fix auto-scroll behavior when AI assistant streams responses in the web UI.
Previously, the viewport would remain at the sent message position and users
had to manually click a badge to see streaming responses.

Fixes #14959

Changes:
- Reset chat scroll state before sending message to ensure viewport readiness
- Force scroll to bottom after message send to position viewport correctly
- Detect streaming start (chatStream: null -> string) and trigger auto-scroll
- Ensure smooth scroll-following during entire streaming response

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaewon Hwang 2026-02-13 09:50:42 +09:00 committed by Val Alexander
parent eeb140b4f0
commit 04985dab23
2 changed files with 12 additions and 3 deletions

View File

@ -1,5 +1,5 @@
import { parseAgentSessionKey } from "../../../src/sessions/session-key-utils.js";
import { scheduleChatScroll } from "./app-scroll.ts";
import { scheduleChatScroll, resetChatScroll } from "./app-scroll.ts";
import { setLastActiveSessionKey } from "./app-settings.ts";
import { resetToolStream } from "./app-tool-stream.ts";
import type { OpenClawApp } from "./app.ts";
@ -121,6 +121,8 @@ async function sendChatMessageNow(
},
) {
resetToolStream(host as unknown as Parameters<typeof resetToolStream>[0]);
// Reset scroll state before sending to ensure auto-scroll works for the response
resetChatScroll(host as unknown as Parameters<typeof resetChatScroll>[0]);
const runId = await sendChatMessage(host as unknown as OpenClawApp, message, opts?.attachments);
const ok = Boolean(runId);
if (!ok && opts?.previousDraft != null) {
@ -141,7 +143,8 @@ async function sendChatMessageNow(
if (ok && opts?.restoreAttachments && opts.previousAttachments?.length) {
host.chatAttachments = opts.previousAttachments;
}
scheduleChatScroll(host as unknown as Parameters<typeof scheduleChatScroll>[0]);
// Force scroll after sending to ensure viewport is at bottom for incoming stream
scheduleChatScroll(host as unknown as Parameters<typeof scheduleChatScroll>[0], true);
if (ok && !host.chatRunId) {
void flushChatQueue(host);
}

View File

@ -99,9 +99,15 @@ export function handleUpdated(host: LifecycleHost, changed: Map<PropertyKey, unk
const forcedByTab = changed.has("tab");
const forcedByLoad =
changed.has("chatLoading") && changed.get("chatLoading") === true && !host.chatLoading;
// Detect streaming start: chatStream changed from null/undefined to a string value
const previousStream = changed.get("chatStream") as string | null | undefined;
const streamJustStarted =
changed.has("chatStream") &&
(previousStream === null || previousStream === undefined) &&
typeof host.chatStream === "string";
scheduleChatScroll(
host as unknown as Parameters<typeof scheduleChatScroll>[0],
forcedByTab || forcedByLoad || !host.chatHasAutoScrolled,
forcedByTab || forcedByLoad || streamJustStarted || !host.chatHasAutoScrolled,
);
}
if (