Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -176,19 +176,23 @@ function BoundedViewport({ children, isStreaming }: BoundedViewportProps) {
const ref = useRef<HTMLDivElement>(null)
const rafRef = useRef<number | null>(null)
const stickToBottomRef = useRef(true)
const prevScrollTopRef = useRef(0)
const [hasOverflow, setHasOverflow] = useState(false)

useEffect(() => {
const el = ref.current
if (!el) return
// Any upward user input detaches auto-stick. A subsequent scroll-to-bottom
// (wheel back down or dragging scrollbar) re-attaches it.
// Upward user input detaches auto-stick; a downward scroll reaching the
// bottom re-attaches it (a small upward flick can't re-stick itself).
const handleWheel = (e: WheelEvent) => {
if (e.deltaY < 0) stickToBottomRef.current = false
}
const handleScroll = () => {
const distance = el.scrollHeight - el.scrollTop - el.clientHeight
if (distance < BOTTOM_STICK_THRESHOLD_PX) stickToBottomRef.current = true
if (distance < BOTTOM_STICK_THRESHOLD_PX && el.scrollTop > prevScrollTopRef.current) {
stickToBottomRef.current = true
Comment thread
TheodoreSpeaks marked this conversation as resolved.
}
Comment thread
TheodoreSpeaks marked this conversation as resolved.
prevScrollTopRef.current = el.scrollTop
}
el.addEventListener('wheel', handleWheel, { passive: true })
el.addEventListener('scroll', handleScroll, { passive: true })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,16 @@ export function MothershipChat({
rangeExtractor,
})

/**
* Instance property — silently ignored if passed as a `useVirtualizer`
* option. Skips scroll compensation for the streaming last row: it starts
* above the viewport but grows at its bottom edge, so the default dragged
* the viewport down in lockstep with growth even after the user scrolled
* away. Other rows keep the library default.
*/
virtualizer.shouldAdjustScrollPositionOnItemSizeChange = (item, _delta, instance) =>
item.index !== lastIndex && item.start < (instance.scrollElement?.scrollTop ?? 0)
Comment thread
TheodoreSpeaks marked this conversation as resolved.

const scrolledChatRef = useRef<string | undefined | typeof UNSCROLLED>(UNSCROLLED)
const userInputRef = useRef<UserInputHandle>(null)
const messageQueueRef = useRef(messageQueue)
Expand Down
11 changes: 8 additions & 3 deletions apps/sim/hooks/use-auto-scroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ interface UseAutoScrollOptions {
*
* Stays pinned to the bottom while content streams in. Detaches immediately
* on any upward user gesture (wheel, touch, scrollbar drag, keyboard). Once
* detached, the user must scroll back to within {@link REATTACH_THRESHOLD} of
* the bottom to re-engage. Each streaming start re-seeds stickiness from the
* detached, the user must scroll back down to within {@link REATTACH_THRESHOLD}
* of the bottom to re-engage. Each streaming start re-seeds stickiness from the
* current scroll position, so a user who scrolled up beforehand stays put.
*
* Returns `ref` (callback ref for the scroll container) and `scrollToBottom`
Expand Down Expand Up @@ -164,6 +164,10 @@ export function useAutoScroll(
* (pointer held) or a recent keyboard scroll. A programmatic upward scroll, e.g.
* a virtualizer re-pinning content on a row-size shrink, has neither and must not
* be mistaken for the user scrolling away.
*
* Re-attach also requires a downward move once detached — a small upward
* flick still lands within {@link REATTACH_THRESHOLD}, and would otherwise
* re-stick on its own scroll event.
*/
const onScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = el
Expand All @@ -172,8 +176,9 @@ export function useAutoScroll(
const userDriven =
pointerDownRef.current ||
performance.now() - lastUserGestureAtRef.current < USER_GESTURE_WINDOW
const movedDown = scrollTop > prevScrollTopRef.current

if (distanceFromBottom <= threshold) {
if (distanceFromBottom <= threshold && (!userDetachedRef.current || movedDown)) {
Comment thread
TheodoreSpeaks marked this conversation as resolved.
stickyRef.current = true
Comment thread
TheodoreSpeaks marked this conversation as resolved.
userDetachedRef.current = false
} else if (
Expand Down
Loading