1- import { useCallback , useEffect , useRef } from 'react'
1+ import { useCallback , useEffect , useMemo , useRef } from 'react'
22
33import type { ScrollBoxRenderable } from '@opentui/core'
4+ import { isZedIDE } from '../utils/detect-ide'
5+ import { ZedScrollAccel } from '../utils/zed-scroll-accel'
46
5- export const useScrollManagement = (
7+ const easeOutCubic = ( t : number ) : number => {
8+ return 1 - Math . pow ( 1 - t , 3 )
9+ }
10+
11+ export const useChatScrollbox = (
612 scrollRef : React . RefObject < ScrollBoxRenderable | null > ,
713 messages : any [ ] ,
814 agentRefsMap : React . MutableRefObject < Map < string , any > > ,
915) => {
16+ const isZed = isZedIDE ( )
17+ const scrollAcceleration = useMemo (
18+ ( ) => ( isZed ? new ZedScrollAccel ( ) : undefined ) ,
19+ [ isZed ] ,
20+ )
1021 const autoScrollEnabledRef = useRef < boolean > ( true )
1122 const programmaticScrollRef = useRef < boolean > ( false )
23+ const animationFrameRef = useRef < number | null > ( null )
24+
25+ const cancelAnimation = useCallback ( ( ) => {
26+ if ( animationFrameRef . current !== null ) {
27+ clearTimeout ( animationFrameRef . current )
28+ animationFrameRef . current = null
29+ }
30+ } , [ ] )
31+
32+ const animateScrollTo = useCallback (
33+ ( targetScroll : number , duration : number = isZed ? 400 : 200 ) => {
34+ const scrollbox = scrollRef . current
35+ if ( ! scrollbox ) return
36+
37+ cancelAnimation ( )
38+
39+ const startScroll = scrollbox . scrollTop
40+ const distance = targetScroll - startScroll
41+ const startTime = Date . now ( )
42+ const frameInterval = isZed ? 40 : 16
43+
44+ const animate = ( ) => {
45+ const elapsed = Date . now ( ) - startTime
46+ const progress = Math . min ( elapsed / duration , 1 )
47+ const easedProgress = easeOutCubic ( progress )
48+ const newScroll = startScroll + distance * easedProgress
49+
50+ programmaticScrollRef . current = true
51+ scrollbox . scrollTop = newScroll
52+
53+ if ( progress < 1 ) {
54+ animationFrameRef . current = setTimeout ( animate , frameInterval ) as any
55+ } else {
56+ animationFrameRef . current = null
57+ }
58+ }
59+
60+ animate ( )
61+ } ,
62+ [ scrollRef , isZed , cancelAnimation ] ,
63+ )
1264
1365 const scrollToLatest = useCallback ( ( ) : void => {
1466 const scrollbox = scrollRef . current
@@ -18,9 +70,8 @@ export const useScrollManagement = (
1870 0 ,
1971 scrollbox . scrollHeight - scrollbox . viewport . height ,
2072 )
21- programmaticScrollRef . current = true
22- scrollbox . verticalScrollBar . scrollPosition = maxScroll
23- } , [ scrollRef ] )
73+ animateScrollTo ( maxScroll )
74+ } , [ scrollRef , animateScrollTo ] )
2475
2576 const scrollToAgent = useCallback (
2677 ( agentId : string , retries = 5 ) => {
@@ -64,11 +115,10 @@ export const useScrollManagement = (
64115 )
65116 }
66117
67- programmaticScrollRef . current = true
68- scrollbox . scrollTo ( targetScroll )
118+ animateScrollTo ( targetScroll )
69119 } , 100 )
70120 } ,
71- [ scrollRef , agentRefsMap ] ,
121+ [ scrollRef , agentRefsMap , animateScrollTo ] ,
72122 )
73123
74124 useEffect ( ( ) => {
@@ -89,6 +139,7 @@ export const useScrollManagement = (
89139 return
90140 }
91141
142+ cancelAnimation ( )
92143 autoScrollEnabledRef . current = isNearBottom
93144 }
94145
@@ -97,7 +148,7 @@ export const useScrollManagement = (
97148 return ( ) => {
98149 scrollbox . verticalScrollBar . off ( 'change' , handleScrollChange )
99150 }
100- } , [ scrollRef ] )
151+ } , [ scrollRef , cancelAnimation ] )
101152
102153 useEffect ( ( ) => {
103154 const scrollbox = scrollRef . current
@@ -120,8 +171,17 @@ export const useScrollManagement = (
120171 return undefined
121172 } , [ messages , scrollToLatest , scrollRef ] )
122173
174+ useEffect ( ( ) => {
175+ return ( ) => {
176+ cancelAnimation ( )
177+ }
178+ } , [ cancelAnimation ] )
179+
123180 return {
124181 scrollToLatest,
125182 scrollToAgent,
183+ scrollboxProps : {
184+ scrollAcceleration,
185+ } ,
126186 }
127187}
0 commit comments