fix session bottom scroll alignment (#1100)

This commit is contained in:
ekko
2026-05-28 21:12:01 +08:00
committed by GitHub
parent 932c913d63
commit e89c192488
2 changed files with 47 additions and 6 deletions
@@ -14,6 +14,7 @@ const { t } = useI18n();
const { isDark } = useTheme();
const { toolTraceVisible } = useToolTraceVisibility();
const listRef = ref<InstanceType<typeof VirtualMessageList> | null>(null);
const pendingBottomSessionId = ref<string | null>(null);
function formatTokens(n: number): string {
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M'
@@ -115,6 +116,7 @@ watch(
() => chatStore.activeSessionId,
(id) => {
if (!id) return;
pendingBottomSessionId.value = id;
if (chatStore.focusMessageId) {
scrollToMessage(chatStore.focusMessageId);
return;
@@ -124,6 +126,20 @@ watch(
{ immediate: true },
);
watch(
() => [chatStore.activeSessionId, chatStore.messages.length] as const,
([id, length]) => {
if (!id || pendingBottomSessionId.value !== id || length === 0) return;
pendingBottomSessionId.value = null;
if (chatStore.focusMessageId) {
scrollToMessage(chatStore.focusMessageId);
return;
}
scrollToBottom();
},
{ flush: "post" },
);
watch(
() => chatStore.focusMessageId,
(messageId) => {
@@ -39,6 +39,8 @@ const heightVersion = ref(0);
const measuredHeights = new Map<string, number>();
const observedElements = new Map<string, HTMLElement>();
const observers = new Map<string, ResizeObserver>();
let keepBottomUntil = 0;
let bottomFrame: number | null = null;
const messageKeys = computed(() => props.messages.map(messageKey));
@@ -58,9 +60,10 @@ function setMeasuredHeight(key: string, height: number) {
const oldHeight = itemHeight(key);
if (oldHeight === height) return;
const el = scrollerRef.value;
const shouldKeepBottom = !!el && (Date.now() < keepBottomUntil || isNearBottom(64));
const index = messageKeys.value.indexOf(key);
if (index >= 0) {
const el = scrollerRef.value;
if (index >= 0 && !shouldKeepBottom) {
const rowTop = layout.value.offsets[index] || 0;
const delta = height - oldHeight;
if (el && rowTop < scrollTop.value && delta !== 0) {
@@ -71,6 +74,7 @@ function setMeasuredHeight(key: string, height: number) {
measuredHeights.set(key, height);
heightVersion.value += 1;
if (shouldKeepBottom) scheduleScrollToBottom(2);
}
const layout = computed(() => {
@@ -167,14 +171,34 @@ function isNearBottom(threshold = 200): boolean {
}
function scrollToBottom() {
keepBottomUntil = Date.now() + 700;
nextTick(() => {
const el = scrollerRef.value;
if (!el) return;
el.scrollTop = el.scrollHeight;
syncViewport();
scheduleScrollToBottom(3);
});
}
function setScrollToBottomNow() {
const el = scrollerRef.value;
if (!el) return;
el.scrollTop = Math.max(0, el.scrollHeight - el.clientHeight);
syncViewport();
}
function scheduleScrollToBottom(frames = 1) {
if (bottomFrame != null) cancelAnimationFrame(bottomFrame);
const step = (remaining: number) => {
setScrollToBottomNow();
if (remaining <= 1) {
bottomFrame = null;
return;
}
bottomFrame = requestAnimationFrame(() => step(remaining - 1));
};
bottomFrame = requestAnimationFrame(() => step(frames));
}
function scrollToMessage(messageId: string) {
const index = props.messages.findIndex(message => String(message.id) === messageId);
if (index < 0) return;
@@ -237,6 +261,7 @@ onMounted(() => {
});
onBeforeUnmount(() => {
if (bottomFrame != null) cancelAnimationFrame(bottomFrame);
resizeObserver?.disconnect();
for (const observer of observers.values()) observer.disconnect();
observers.clear();