Skip to content

Commit c08a22c

Browse files
committed
fix(chat): address review — reset pointer ref on teardown, stop wheel/touch opening detach window
- Reset pointerDownRef in effect cleanup so a pointer held through teardown (e.g. dragging the scrollbar as a stream finishes) can't leak a stuck-true ref into the next session and detach on the first programmatic re-pin. - Wheel-up and touch-drag already detach directly, so the onScroll delta heuristic only needs to authorize scrollbar drag (pointerDownRef) and keyboard. Stop stamping the gesture window on wheel/touch, which otherwise let a harmless downward wheel open a 250ms window where a virtualizer shrink could falsely detach.
1 parent db5566d commit c08a22c

1 file changed

Lines changed: 21 additions & 19 deletions

File tree

apps/sim/hooks/use-auto-scroll.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ const STICK_THRESHOLD = 30
55
/** User must scroll back to within this distance to re-engage auto-scroll. */
66
const REATTACH_THRESHOLD = 5
77
/**
8-
* A scrollbar-drag detach is only honored if a real user gesture occurred within
9-
* this window. Virtualizers (react-virtual) programmatically move `scrollTop` to
10-
* keep content stable when a measured row's size changes — including
11-
* the transient height *shrinks* a streaming markdown renderer emits as it re-parses
12-
* each token. Without this guard, that upward programmatic scroll is misread as the
13-
* user scrolling away and auto-scroll detaches mid-stream.
8+
* A keyboard-driven scroll (PageUp, arrows) only emits `scroll` events, so its
9+
* detach is honored when it lands within this window of a `keydown`. Wheel and
10+
* touch detach directly via their own handlers, and scrollbar drags are tracked
11+
* through {@link pointerDownRef}, so neither feeds this window.
12+
*
13+
* The guard exists because virtualizers (react-virtual) programmatically move
14+
* `scrollTop` to keep content stable when a measured row's size changes —
15+
* including the transient height *shrinks* a streaming markdown renderer emits as
16+
* it re-parses each token. Without it, that upward programmatic scroll is misread
17+
* as the user scrolling away and auto-scroll detaches mid-stream.
1418
*/
1519
const USER_GESTURE_WINDOW = 250
1620

@@ -74,43 +78,37 @@ export function useAutoScroll(
7478
userDetachedRef.current = true
7579
}
7680

77-
const markGesture = () => {
78-
lastUserGestureAtRef.current = performance.now()
79-
}
80-
8181
const onWheel = (e: WheelEvent) => {
82-
markGesture()
8382
if (e.deltaY < 0) detach()
8483
}
8584

8685
const onTouchStart = (e: TouchEvent) => {
87-
markGesture()
8886
touchStartYRef.current = e.touches[0].clientY
8987
}
9088

9189
const onTouchMove = (e: TouchEvent) => {
92-
markGesture()
9390
if (e.touches[0].clientY > touchStartYRef.current) detach()
9491
}
9592

9693
const onPointerDown = () => {
9794
pointerDownRef.current = true
98-
markGesture()
9995
}
10096
const onPointerUp = () => {
10197
pointerDownRef.current = false
10298
}
103-
const onKeyDown = markGesture
99+
const onKeyDown = () => {
100+
lastUserGestureAtRef.current = performance.now()
101+
}
104102

105103
const onScroll = () => {
106104
const { scrollTop, scrollHeight, clientHeight } = el
107105
const distanceFromBottom = scrollHeight - scrollTop - clientHeight
108106
const threshold = userDetachedRef.current ? REATTACH_THRESHOLD : STICK_THRESHOLD
109107

110-
// Only a genuine, recent user gesture (scrollbar drag, keyboard) may detach via
111-
// a scroll-position delta. A programmatic upward scroll — e.g. a virtualizer
112-
// re-pinning content on a row-size shrink — has no preceding gesture and must
113-
// not be mistaken for the user scrolling away.
108+
// Only a genuine user scroll may detach via a scroll-position delta: an active
109+
// scrollbar drag (pointer held) or a recent keyboard scroll. A programmatic
110+
// upward scroll — e.g. a virtualizer re-pinning content on a row-size shrink —
111+
// has neither and must not be mistaken for the user scrolling away.
114112
const userDriven =
115113
pointerDownRef.current ||
116114
performance.now() - lastUserGestureAtRef.current < USER_GESTURE_WINDOW
@@ -182,6 +180,10 @@ export function useAutoScroll(
182180
window.removeEventListener('pointercancel', onPointerUp)
183181
observer.disconnect()
184182
cancelAnimationFrame(rafIdRef.current)
183+
// A pointer held through teardown (e.g. dragging the scrollbar as the stream
184+
// finishes) would never see its window `pointerup`, leaving the ref stuck true
185+
// into the next session and detaching on the first programmatic re-pin.
186+
pointerDownRef.current = false
185187
if (stickyRef.current) scrollToBottom()
186188
}
187189
}, [isStreaming, scrollToBottom])

0 commit comments

Comments
 (0)