Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.8k
fix(mothership): keep scroll position when user scrolls up during streaming#5639
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
TheodoreSpeaks marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| prevScrollTopRef.current = el.scrollTop | ||
| } | ||
| el.addEventListener('wheel', handleWheel, { passive: true }) | ||
| el.addEventListener('scroll', handleScroll, { passive: true }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
TheodoreSpeaks marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| const scrolledChatRef = useRef<string | undefined | typeof UNSCROLLED>(UNSCROLLED) | ||
| const userInputRef = useRef<UserInputHandle>(null) | ||
| const messageQueueRef = useRef(messageQueue) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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` | ||
| @@ -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 | ||
| @@ -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)) { | ||
TheodoreSpeaks marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| stickyRef.current = true | ||
TheodoreSpeaks marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| userDetachedRef.current = false | ||
| } else if ( | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.