From a26d74bd3c71dbd13b9e562b3956f325f9a313c1 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 25 Sep 2026 11:14:32 +0800 Subject: [PATCH] fix(chatui): preserve scroll position while loading history --- dashboard/src/components/chat/Chat.vue | 7 ++++++- dashboard/tests/chatHistoryLoading.test.mjs | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/dashboard/src/components/chat/Chat.vue b/dashboard/src/components/chat/Chat.vue index 7f4ea64ec..fadea595e 100644 --- a/dashboard/src/components/chat/Chat.vue +++ b/dashboard/src/components/chat/Chat.vue @@ -1631,6 +1631,7 @@ async function loadEarlierWithAnchor() { const container = messagesContainer.value; const firstMessage = activeMessages.value[0]; const firstId = firstMessage?.id == null ? "" : String(firstMessage.id); + const beforeScrollTop = Math.max(0, container?.scrollTop || 0); const beforeTop = firstId ? container ?.querySelector( @@ -1648,7 +1649,11 @@ async function loadEarlierWithAnchor() { `[data-message-id="${CSS.escape(firstId)}"]`, ); if (row) { - container.scrollTop += row.getBoundingClientRect().top - beforeTop; + const currentScrollTop = Math.max(0, container.scrollTop); + const userScrollDelta = currentScrollTop - beforeScrollTop; + container.scrollTop += + row.getBoundingClientRect().top - beforeTop + userScrollDelta; + lastMessagesScrollTop = Math.max(0, container.scrollTop); } } finally { suppressAutoScroll.value = false; diff --git a/dashboard/tests/chatHistoryLoading.test.mjs b/dashboard/tests/chatHistoryLoading.test.mjs index 748f4ad31..afd37300a 100644 --- a/dashboard/tests/chatHistoryLoading.test.mjs +++ b/dashboard/tests/chatHistoryLoading.test.mjs @@ -47,11 +47,14 @@ test("failed history loading pauses scroll requests until an explicit retry", as }; const current = { id: 51, content: { message: [] } }; const records = { session: [current] }; + let anchorTop = 100; const container = { scrollHeight: 1000, clientHeight: 400, scrollTop: 0, - querySelector: () => null, + querySelector: () => ({ + getBoundingClientRect: () => ({ top: anchorTop }), + }), }; const requests = []; const context = vm.createContext({ @@ -61,6 +64,7 @@ test("failed history loading pauses scroll requests until an explicit retry", as messagesContainer: { value: container }, suppressAutoScroll: { value: false }, LOAD_EARLIER_SCROLL_THRESHOLD: 120, + lastMessagesScrollTop: 0, CSS: { escape: (value) => value }, nextTick: async () => {}, paginationBySession: { session: state }, @@ -96,6 +100,7 @@ test("failed history loading pauses scroll requests until an explicit retry", as "scroll events must not retry a failed page", ); + container.scrollTop = 80; const retry = context.retryCurrentSessionLoad(); assert.equal(requests.length, 2); assert.equal( @@ -104,6 +109,8 @@ test("failed history loading pauses scroll requests until an explicit retry", as "retry must request the failed page again", ); assert.equal(state.error, undefined); + container.scrollTop = 20; + anchorTop = 360; context.maybeLoadEarlierOnScroll(container); assert.equal(requests.length, 2); requests[1].resolve({ @@ -118,9 +125,15 @@ test("failed history loading pauses scroll requests until an explicit retry", as }, }); await retry; + assert.equal( + container.scrollTop, + 220, + "prepending history must preserve scrolling that continues during loading", + ); assert.equal(state.page, 2); assert.equal(state.error, undefined); assert.equal(records.session.length, 2); + container.scrollTop = 0; context.maybeLoadEarlierOnScroll(container); assert.equal( requests.length,