From 91836cd6bc946a6915b5c453951eac63e32400af Mon Sep 17 00:00:00 2001 From: paisley <8197966+su8su@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:30:24 +0800 Subject: [PATCH] fix chat follow-up auto-scroll (#1177) --- src/hooks/use-stick-to-bottom-instant.ts | 4 ++-- src/pages/Chat/index.tsx | 6 +++++- tests/e2e/chat-scroll-pin-bottom.spec.ts | 8 ++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/hooks/use-stick-to-bottom-instant.ts b/src/hooks/use-stick-to-bottom-instant.ts index 62e85a25..92030238 100644 --- a/src/hooks/use-stick-to-bottom-instant.ts +++ b/src/hooks/use-stick-to-bottom-instant.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from "react"; +import { useCallback, useEffect, useLayoutEffect, useRef } from "react"; import { useStickToBottom } from "use-stick-to-bottom"; const ESCAPE_FROM_LOCK_OFFSET_PX = 70; @@ -31,7 +31,7 @@ export function useStickToBottomInstant(resetKey?: string, active = false) { // callback without re-creating the observer on every render. const activeRef = useRef(active); const escapedRef = useRef(escapedFromLock); - useEffect(() => { + useLayoutEffect(() => { activeRef.current = active; escapedRef.current = escapedFromLock; }, [active, escapedFromLock]); diff --git a/src/pages/Chat/index.tsx b/src/pages/Chat/index.tsx index 48fbaabf..a018f4ca 100644 --- a/src/pages/Chat/index.tsx +++ b/src/pages/Chat/index.tsx @@ -427,12 +427,16 @@ export function Chat() { } if (!loaded) return; } - await sendAcpPrompt({ + const sendPromise = sendAcpPrompt({ sessionKey, cwd: promptCwd, message: text, media, }); + requestAnimationFrame(() => { + void scrollToBottom({ animation: 'instant', ignoreEscapes: true }); + }); + await sendPromise; })(); }} onStop={() => void cancelAcp()} diff --git a/tests/e2e/chat-scroll-pin-bottom.spec.ts b/tests/e2e/chat-scroll-pin-bottom.spec.ts index 17f4419b..d9d5fa7d 100644 --- a/tests/e2e/chat-scroll-pin-bottom.spec.ts +++ b/tests/e2e/chat-scroll-pin-bottom.spec.ts @@ -168,9 +168,17 @@ test.describe('ClawX chat scroll pin-to-bottom during runs', () => { // Start a run so pinning becomes active (sending === true). await expect(page.getByTestId('chat-composer-input')).toBeEnabled({ timeout: 30_000 }); + await scrollContainer.evaluate((el) => { + const element = el as HTMLElement; + element.scrollTop = 0; + element.dispatchEvent(new Event('scroll', { bubbles: true })); + }); + await expect(page.getByTestId('chat-scroll-to-latest')).toBeVisible(); await page.getByTestId('chat-composer-input').fill('do a multi-tool task'); await page.getByTestId('chat-composer-send').click(); await expect(page.getByTestId('chat-composer-send')).toHaveAttribute('title', 'Stop'); + await expect(page.getByText('do a multi-tool task')).toBeInViewport(); + await expectPinnedToBottom(); // Growing text stream -> height keeps increasing; bar must stay at bottom. await emitDelta({ sessionUpdate: 'agent_message', messageId: 'streaming-assistant', content: [{ type: 'text', text: streamingText(3) }] });