From c0b9ad60aecd2c4b46819ece0f8aaccfd28a9a0d Mon Sep 17 00:00:00 2001 From: Soulter <37870767+Soulter@users.noreply.github.com> Date: Sat, 26 Sep 2026 23:55:32 +0800 Subject: [PATCH] fix(chatui): respect reasoning panel scroll intent (#10243) * fix(chatui): respect reasoning panel scroll intent * Delete dashboard/tests/reasoningSidebarScroll.test.mjs --- .../src/components/chat/ReasoningSidebar.vue | 97 +++++++++++++++++-- 1 file changed, 91 insertions(+), 6 deletions(-) diff --git a/dashboard/src/components/chat/ReasoningSidebar.vue b/dashboard/src/components/chat/ReasoningSidebar.vue index 4797d0baf..d2a2541ba 100644 --- a/dashboard/src/components/chat/ReasoningSidebar.vue +++ b/dashboard/src/components/chat/ReasoningSidebar.vue @@ -6,7 +6,17 @@ -
+
(); const sidebarBody = ref(null); +const shouldStickToBottom = ref(true); +let lastSidebarScrollTop = 0; +let touchScrollY = 0; +let scrollIntent = 0; const emit = defineEmits<{ "update:modelValue": [value: boolean]; @@ -60,19 +74,90 @@ function close() { } function scrollToLatestActivity() { - if (!props.modelValue) return; + if (!props.modelValue || !shouldStickToBottom.value) return; void nextTick(() => { const body = sidebarBody.value; - if (!body) return; + if (!body || !shouldStickToBottom.value) return; body.scrollTop = body.scrollHeight; + lastSidebarScrollTop = Math.max(0, body.scrollTop); }); } +function handleSidebarInteraction( + event: WheelEvent | TouchEvent | PointerEvent | KeyboardEvent, +) { + if (event instanceof WheelEvent) { + if (event.ctrlKey || event.deltaY === 0) return; + scrollIntent = Math.sign(event.deltaY); + } else if (event.type === "touchstart" || event.type === "touchmove") { + const touch = (event as TouchEvent).touches[0]; + if (!touch) return; + if (event.type === "touchstart") { + touchScrollY = touch.clientY; + return; + } + scrollIntent = Math.sign(touchScrollY - touch.clientY); + touchScrollY = touch.clientY; + } else if (event instanceof KeyboardEvent) { + const target = event.target as HTMLElement; + if ( + target.closest("input, textarea, select, [contenteditable], button, a") + ) { + return; + } + if ( + ["ArrowUp", "PageUp", "Home"].includes(event.key) || + (event.key === " " && event.shiftKey) + ) { + scrollIntent = -1; + } else if (["ArrowDown", "PageDown", "End", " "].includes(event.key)) { + scrollIntent = 1; + } else { + return; + } + } else { + if (event.target !== sidebarBody.value) return; + scrollIntent = 0; + shouldStickToBottom.value = false; + } + if (scrollIntent < 0) shouldStickToBottom.value = false; +} + +function handleSidebarScroll() { + const body = sidebarBody.value; + if (!body) return; + const maxScrollTop = Math.max(0, body.scrollHeight - body.clientHeight); + const scrollTop = Math.max(0, body.scrollTop); + const previousTop = Math.min(lastSidebarScrollTop, maxScrollTop); + const isAwayFromBottom = maxScrollTop - scrollTop > 2; + if (scrollTop < previousTop) { + shouldStickToBottom.value = false; + } else if ( + scrollTop > previousTop && + !isAwayFromBottom && + scrollIntent >= 0 + ) { + shouldStickToBottom.value = true; + } + lastSidebarScrollTop = scrollTop; +} + watch( - () => [props.modelValue, props.reasoning, props.parts], - scrollToLatestActivity, - { deep: true, flush: "post", immediate: true }, + () => props.modelValue, + (open) => { + if (!open) return; + shouldStickToBottom.value = true; + scrollIntent = 0; + lastSidebarScrollTop = 0; + scrollToLatestActivity(); + }, + { flush: "post", immediate: true }, ); + +watch(() => [props.reasoning, props.parts], scrollToLatestActivity, { + deep: true, + flush: "post", +});