diff --git a/ui/src/ui/app-chat.ts b/ui/src/ui/app-chat.ts index 45227c3fd192..35936886ece4 100644 --- a/ui/src/ui/app-chat.ts +++ b/ui/src/ui/app-chat.ts @@ -443,7 +443,9 @@ function enqueuePendingSendMessage( host.chatQueue = [...host.chatQueue, pending]; recordChatSendTiming(host, pending, "pending-visible", submittedAtMs); schedulePendingSendPaintTiming(host, pending, submittedAtMs); - scheduleChatScroll(host as unknown as Parameters[0], true); + scheduleChatScroll(host as unknown as Parameters[0], true, false, { + source: "manual", + }); return pending; } diff --git a/ui/src/ui/e2e/chat-flow.e2e.test.ts b/ui/src/ui/e2e/chat-flow.e2e.test.ts index ad40f035a359..5e1b30dcbb7c 100644 --- a/ui/src/ui/e2e/chat-flow.e2e.test.ts +++ b/ui/src/ui/e2e/chat-flow.e2e.test.ts @@ -56,6 +56,38 @@ async function chatThreadDistanceFromBottom(page: Page): Promise { }); } +async function waitForChatScrollIdle(page: Page): Promise { + await expect + .poll( + () => + page.evaluate(() => { + const app = document.querySelector("openclaw-app") as + | (Element & { + chatIsProgrammaticScroll?: boolean; + chatScrollFrame?: number | null; + chatScrollTimeout?: number | null; + }) + | null; + return Boolean( + app && + app.chatScrollFrame == null && + app.chatScrollTimeout == null && + !app.chatIsProgrammaticScroll, + ); + }), + { timeout: 10_000 }, + ) + .toBe(true); +} + +async function scrollChatThreadToTop(page: Page): Promise { + await page.locator(".chat-thread").evaluate((element) => { + const thread = element as HTMLElement; + thread.scrollTop = 0; + thread.dispatchEvent(new Event("scroll", { bubbles: true })); + }); +} + async function controlUiEventPayloads( page: Page, event: string, @@ -485,9 +517,8 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => { .poll(() => chatThreadDistanceFromBottom(page), { timeout: 10_000 }) .toBeLessThanOrEqual(4); - await page.locator(".chat-thread").evaluate((element) => { - (element as HTMLElement).scrollTop = 0; - }); + await waitForChatScrollIdle(page); + await scrollChatThreadToTop(page); await expect .poll(() => chatThreadDistanceFromBottom(page), { timeout: 10_000 }) .toBeGreaterThan(200);