From 4ed23613874d093bc9f40f44698413e5e18bd608 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Sun, 14 Dec 2025 15:55:09 +0000 Subject: [PATCH] Reduce scroll jitter from virtual items --- packages/ui/src/components/message-section.tsx | 1 + .../ui/src/components/session/session-view.tsx | 3 --- packages/ui/src/components/virtual-item.tsx | 17 ++++++++++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/components/message-section.tsx b/packages/ui/src/components/message-section.tsx index 84cc2a7b..6ee0f933 100644 --- a/packages/ui/src/components/message-section.tsx +++ b/packages/ui/src/components/message-section.tsx @@ -423,6 +423,7 @@ export default function MessageSection(props: MessageSectionProps) { clearQuoteSelection() scheduleScrollPersist() }) + } diff --git a/packages/ui/src/components/session/session-view.tsx b/packages/ui/src/components/session/session-view.tsx index eca9c70a..9e701e55 100644 --- a/packages/ui/src/components/session/session-view.tsx +++ b/packages/ui/src/components/session/session-view.tsx @@ -74,9 +74,6 @@ export const SessionView: Component = (props) => { } async function handleSendMessage(prompt: string, attachments: Attachment[]) { - if (scrollToBottomHandle && import.meta.env?.DEV) { - console.debug("[SessionView] handleSendMessage scroll", props.sessionId) - } scheduleScrollToBottom() await sendMessage(props.instanceId, props.sessionId, prompt, attachments) } diff --git a/packages/ui/src/components/virtual-item.tsx b/packages/ui/src/components/virtual-item.tsx index f7aa4dc6..27c65d47 100644 --- a/packages/ui/src/components/virtual-item.tsx +++ b/packages/ui/src/components/virtual-item.tsx @@ -3,6 +3,7 @@ import { JSX, Accessor, children as resolveChildren, createEffect, createMemo, c const sizeCache = new Map() const DEFAULT_MARGIN_PX = 600 const MIN_PLACEHOLDER_HEIGHT = 32 +const VISIBILITY_BUFFER_PX = 48 type ObserverRoot = Element | Document | null @@ -48,6 +49,19 @@ function createSharedObserver(root: ObserverRoot, margin: number): SharedObserve return { observer, listeners } } +function shouldRenderEntry(entry: IntersectionObserverEntry) { + const rootBounds = entry.rootBounds + if (!rootBounds) { + return entry.isIntersecting + } + const distanceAbove = rootBounds.top - entry.boundingClientRect.bottom + const distanceBelow = entry.boundingClientRect.top - rootBounds.bottom + if (distanceAbove > VISIBILITY_BUFFER_PX || distanceBelow > VISIBILITY_BUFFER_PX) { + return false + } + return true +} + function subscribeToSharedObserver( target: Element, root: ObserverRoot, @@ -224,7 +238,8 @@ export default function VirtualItem(props: VirtualItemProps) { } const margin = props.threshold ?? DEFAULT_MARGIN_PX intersectionCleanup = subscribeToSharedObserver(wrapperRef, targetRoot, margin, (entry) => { - queueVisibility(entry.isIntersecting) + const nextVisible = shouldRenderEntry(entry) + queueVisibility(nextVisible) }) }