fix(chatui): respect reasoning panel scroll intent (#10243)

* fix(chatui): respect reasoning panel scroll intent

* Delete dashboard/tests/reasoningSidebarScroll.test.mjs
This commit is contained in:
Soulter
2026-09-26 23:55:32 +08:00
committed by GitHub
parent 69ae35a359
commit c0b9ad60ae
@@ -6,7 +6,17 @@
<v-btn icon="mdi-close" size="small" variant="text" @click="close" />
</div>
<div ref="sidebarBody" class="reasoning-sidebar-body">
<div
ref="sidebarBody"
class="reasoning-sidebar-body"
tabindex="0"
@scroll="handleSidebarScroll"
@wheel.passive="handleSidebarInteraction"
@touchstart.passive="handleSidebarInteraction"
@touchmove.passive="handleSidebarInteraction"
@pointerdown="handleSidebarInteraction"
@keydown="handleSidebarInteraction"
>
<ReasoningTimeline
v-if="parts.length || reasoning"
:parts="parts"
@@ -40,6 +50,10 @@ const props = defineProps<{
}>();
const sidebarBody = ref<HTMLElement | null>(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",
});
</script>
<style scoped>