From 22a2f916b5f2f0e13a6d487fb639f9eb35afcc75 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 12:52:40 +0000 Subject: [PATCH 1/3] Initial plan From f0cac602e1cb511d08759cb4f122d8bfb03d25fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 12:57:50 +0000 Subject: [PATCH 2/3] fix: avoid bottom overscroll jitter during stream updates --- src/renderer/src/pages/ChatPage.vue | 2 +- test/renderer/components/ChatPage.test.ts | 44 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/pages/ChatPage.vue b/src/renderer/src/pages/ChatPage.vue index 5f358a60d..6eef85da4 100644 --- a/src/renderer/src/pages/ChatPage.vue +++ b/src/renderer/src/pages/ChatPage.vue @@ -384,7 +384,7 @@ function scrollToBottom(force = false) { const nextScrollHeight = el.scrollHeight if (shouldForce || nextScrollHeight > lastObservedScrollHeight) { - el.scrollTop = nextScrollHeight + el.scrollTop = Math.max(nextScrollHeight - el.clientHeight, 0) } syncScrollPosition() diff --git a/test/renderer/components/ChatPage.test.ts b/test/renderer/components/ChatPage.test.ts index fc4e70a1e..a953b8f4b 100644 --- a/test/renderer/components/ChatPage.test.ts +++ b/test/renderer/components/ChatPage.test.ts @@ -931,6 +931,50 @@ describe('ChatPage', () => { expect(chatClient.steerActiveTurn).not.toHaveBeenCalled() }) + it('scrolls to bottom using max scrollTop during stream updates near bottom', async () => { + const rafSpy = vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { + cb(0) + return 1 + }) + const cancelRafSpy = vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => {}) + + try { + const { wrapper, messageStore } = await setup() + const chatPage = wrapper.get('[data-testid="chat-page"]').element as HTMLDivElement + + let scrollHeight = 1200 + let scrollTop = 0 + Object.defineProperty(chatPage, 'clientHeight', { + configurable: true, + get: () => 500 + }) + Object.defineProperty(chatPage, 'scrollHeight', { + configurable: true, + get: () => scrollHeight + }) + Object.defineProperty(chatPage, 'scrollTop', { + configurable: true, + get: () => scrollTop, + set: (value: number) => { + scrollTop = value + } + }) + + scrollTop = 700 + await wrapper.get('[data-testid="chat-page"]').trigger('scroll') + await flushPromises() + + scrollHeight = 1250 + messageStore.streamRevision += 1 + await flushPromises() + + expect(scrollTop).toBe(750) + } finally { + rafSpy.mockRestore() + cancelRafSpy.mockRestore() + } + }) + it('opens the inline search with Ctrl+F and closes it with Escape', async () => { const { wrapper } = await setup() From 8bf5cd6fc72e59ee40a4fe0a57921aa520fdd71e Mon Sep 17 00:00:00 2001 From: zerob13 Date: Fri, 29 May 2026 10:13:08 +0800 Subject: [PATCH 3/3] test(chat): fix scroll raf mock --- test/renderer/components/ChatPage.test.ts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/test/renderer/components/ChatPage.test.ts b/test/renderer/components/ChatPage.test.ts index a953b8f4b..3671c3d91 100644 --- a/test/renderer/components/ChatPage.test.ts +++ b/test/renderer/components/ChatPage.test.ts @@ -932,11 +932,23 @@ describe('ChatPage', () => { }) it('scrolls to bottom using max scrollTop during stream updates near bottom', async () => { + let nextFrameId = 1 + const rafCallbacks = new Map() + const flushRaf = async () => { + const callbacks = Array.from(rafCallbacks.values()) + rafCallbacks.clear() + callbacks.forEach((cb) => cb(0)) + await flushPromises() + } const rafSpy = vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { - cb(0) - return 1 + const frameId = nextFrameId + nextFrameId += 1 + rafCallbacks.set(frameId, cb) + return frameId + }) + const cancelRafSpy = vi.spyOn(window, 'cancelAnimationFrame').mockImplementation((frameId) => { + rafCallbacks.delete(frameId) }) - const cancelRafSpy = vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => {}) try { const { wrapper, messageStore } = await setup() @@ -960,13 +972,17 @@ describe('ChatPage', () => { } }) + await flushRaf() + scrollTop = 700 await wrapper.get('[data-testid="chat-page"]').trigger('scroll') await flushPromises() + await flushRaf() scrollHeight = 1250 messageStore.streamRevision += 1 await flushPromises() + await flushRaf() expect(scrollTop).toBe(750) } finally {