@@ -39,12 +39,6 @@ const POST_STREAM_SETTLE_WINDOW = 300
3939
4040interface UseAutoScrollOptions {
4141 scrollOnMount ?: boolean
42- /**
43- * Consulted at stream teardown; return false to skip the post-stream settle
44- * follow. A user-initiated stop means "freeze" — chasing the stopped-row and
45- * actions mount would visibly nudge the transcript the user just halted.
46- */
47- shouldFollowSettle ?: ( ) => boolean
4842}
4943
5044/**
@@ -56,17 +50,18 @@ interface UseAutoScrollOptions {
5650 * of the bottom to re-engage. Each streaming start re-seeds stickiness from the
5751 * current scroll position, so a user who scrolled up beforehand stays put.
5852 *
59- * Returns `ref` (callback ref for the scroll container) and `scrollToBottom`
60- * for imperative use after layout-changing events like panel expansion.
53+ * Returns `ref` (callback ref for the scroll container), `scrollToBottom` for
54+ * imperative use after layout-changing events like panel expansion, and
55+ * `detach` for programmatic freezes (a user stop) — it parks every chase path
56+ * exactly like a user scroll-away, until the user scrolls back to the bottom
57+ * or the next stream re-seeds stickiness.
6158 */
6259export function useAutoScroll (
6360 isStreaming : boolean ,
64- { scrollOnMount = false , shouldFollowSettle } : UseAutoScrollOptions = { }
61+ { scrollOnMount = false } : UseAutoScrollOptions = { }
6562) {
6663 const containerRef = useRef < HTMLDivElement > ( null )
6764 const stickyRef = useRef ( true )
68- const shouldFollowSettleRef = useRef ( shouldFollowSettle )
69- shouldFollowSettleRef . current = shouldFollowSettle
7065 const userDetachedRef = useRef ( false )
7166 const prevScrollTopRef = useRef ( 0 )
7267 const prevScrollHeightRef = useRef ( 0 )
@@ -91,6 +86,11 @@ export function useAutoScroll(
9186 el . scrollTop = el . scrollHeight
9287 } , [ ] )
9388
89+ const detach = useCallback ( ( ) => {
90+ stickyRef . current = false
91+ userDetachedRef . current = true
92+ } , [ ] )
93+
9494 const callbackRef = useCallback ( ( el : HTMLDivElement | null ) => {
9595 containerRef . current = el
9696 if ( el && scrollOnMountRef . current ) el . scrollTop = el . scrollHeight
@@ -244,12 +244,11 @@ export function useAutoScroll(
244244 lastUserGestureAtRef . current = Number . NEGATIVE_INFINITY
245245 // End-of-turn content mounts just after teardown; follow it briefly. The
246246 // chase's own upward-move interrupt still protects a real user scroll
247- // even with the gesture listeners gone.
248- if ( shouldFollowSettleRef . current ?.( ) !== false ) {
249- chase . kickUntil ( POST_STREAM_SETTLE_WINDOW )
250- }
247+ // even with the gesture listeners gone, and the per-frame sticky check
248+ // makes this a no-op after a detach().
249+ chase . kickUntil ( POST_STREAM_SETTLE_WINDOW )
251250 }
252251 } , [ isStreaming , scrollToBottom ] )
253252
254- return { ref : callbackRef , scrollToBottom }
253+ return { ref : callbackRef , scrollToBottom, detach }
255254}
0 commit comments