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",
+});