@@ -3,7 +3,7 @@ import { useData } from "../context"
33import { useDiffComponent } from "../context/diff"
44import { getDirectory , getFilename } from "@opencode-ai/util/path"
55import { checksum } from "@opencode-ai/util/encode"
6- import { batch , createEffect , createMemo , For , Match , onCleanup , ParentProps , Show , Switch } from "solid-js"
6+ import { createEffect , createMemo , For , Match , onCleanup , ParentProps , Show , Switch } from "solid-js"
77import { createResizeObserver } from "@solid-primitives/resize-observer"
88import { DiffChanges } from "./diff-changes"
99import { Typewriter } from "./typewriter"
@@ -19,6 +19,7 @@ import { Button } from "./button"
1919import { Spinner } from "./spinner"
2020import { createStore } from "solid-js/store"
2121import { DateTime , DurationUnit , Interval } from "luxon"
22+ import { createAutoScroll } from "../hooks"
2223
2324function computeStatusFromPart ( part : PartType | undefined ) : string | undefined {
2425 if ( ! part ) return undefined
@@ -233,17 +234,14 @@ export function SessionTurn(
233234 } )
234235 }
235236
236- let scrollRef : HTMLDivElement | undefined
237+ const autoScroll = createAutoScroll ( {
238+ working,
239+ onUserInteracted : props . onUserInteracted ,
240+ } )
241+
237242 const [ store , setStore ] = createStore ( {
238- contentRef : undefined as HTMLDivElement | undefined ,
239243 stickyTitleRef : undefined as HTMLDivElement | undefined ,
240244 stickyTriggerRef : undefined as HTMLDivElement | undefined ,
241- lastScrollTop : 0 ,
242- lastScrollHeight : 0 ,
243- lastContainerWidth : 0 ,
244- autoScrolled : false ,
245- userScrolled : false ,
246- reflowing : false ,
247245 stickyHeaderHeight : 0 ,
248246 retrySeconds : 0 ,
249247 status : rawStatus ( ) ,
@@ -265,104 +263,6 @@ export function SessionTurn(
265263 onCleanup ( ( ) => clearInterval ( timer ) )
266264 } )
267265
268- function handleScroll ( ) {
269- if ( ! scrollRef || store . autoScrolled ) return
270-
271- const scrollTop = scrollRef . scrollTop
272- const scrollHeight = scrollRef . scrollHeight
273-
274- if ( store . reflowing ) {
275- batch ( ( ) => {
276- setStore ( "lastScrollTop" , scrollTop )
277- setStore ( "lastScrollHeight" , scrollHeight )
278- } )
279- return
280- }
281-
282- const scrollHeightChanged = Math . abs ( scrollHeight - store . lastScrollHeight ) > 10
283- const scrollTopDelta = scrollTop - store . lastScrollTop
284-
285- if ( scrollHeightChanged && scrollTopDelta < 0 ) {
286- const heightRatio = store . lastScrollHeight > 0 ? scrollHeight / store . lastScrollHeight : 1
287- const expectedScrollTop = store . lastScrollTop * heightRatio
288- if ( Math . abs ( scrollTop - expectedScrollTop ) < 100 ) {
289- batch ( ( ) => {
290- setStore ( "lastScrollTop" , scrollTop )
291- setStore ( "lastScrollHeight" , scrollHeight )
292- } )
293- return
294- }
295- }
296-
297- const reset = scrollTop <= 0 && store . lastScrollTop > 0 && working ( ) && ! store . userScrolled
298- if ( reset ) {
299- batch ( ( ) => {
300- setStore ( "lastScrollTop" , scrollTop )
301- setStore ( "lastScrollHeight" , scrollHeight )
302- } )
303- requestAnimationFrame ( scrollToBottom )
304- return
305- }
306-
307- const scrolledUp = scrollTop < store . lastScrollTop - 50 && ! scrollHeightChanged
308- if ( scrolledUp && working ( ) ) {
309- setStore ( "userScrolled" , true )
310- props . onUserInteracted ?.( )
311- }
312-
313- batch ( ( ) => {
314- setStore ( "lastScrollTop" , scrollTop )
315- setStore ( "lastScrollHeight" , scrollHeight )
316- } )
317- }
318-
319- function handleInteraction ( ) {
320- if ( working ( ) ) {
321- setStore ( "userScrolled" , true )
322- props . onUserInteracted ?.( )
323- }
324- }
325-
326- function scrollToBottom ( ) {
327- if ( ! scrollRef || store . userScrolled || ! working ( ) ) return
328- setStore ( "autoScrolled" , true )
329- requestAnimationFrame ( ( ) => {
330- scrollRef ?. scrollTo ( { top : scrollRef . scrollHeight , behavior : "smooth" } )
331- requestAnimationFrame ( ( ) => {
332- batch ( ( ) => {
333- setStore ( "lastScrollTop" , scrollRef ?. scrollTop ?? 0 )
334- setStore ( "lastScrollHeight" , scrollRef ?. scrollHeight ?? 0 )
335- setStore ( "autoScrolled" , false )
336- } )
337- } )
338- } )
339- }
340-
341- createResizeObserver (
342- ( ) => store . contentRef ,
343- ( { width } ) => {
344- const widthChanged = Math . abs ( width - store . lastContainerWidth ) > 5
345- if ( widthChanged && store . lastContainerWidth > 0 ) {
346- setStore ( "reflowing" , true )
347- requestAnimationFrame ( ( ) => {
348- requestAnimationFrame ( ( ) => {
349- setStore ( "reflowing" , false )
350- if ( working ( ) && ! store . userScrolled ) {
351- scrollToBottom ( )
352- }
353- } )
354- } )
355- } else if ( ! store . reflowing ) {
356- scrollToBottom ( )
357- }
358- setStore ( "lastContainerWidth" , width )
359- } ,
360- )
361-
362- createEffect ( ( ) => {
363- if ( ! working ( ) ) setStore ( "userScrolled" , false )
364- } )
365-
366266 createResizeObserver (
367267 ( ) => store . stickyTitleRef ,
368268 ( { height } ) => {
@@ -412,12 +312,17 @@ export function SessionTurn(
412312
413313 return (
414314 < div data-component = "session-turn" class = { props . classes ?. root } >
415- < div ref = { scrollRef } onScroll = { handleScroll } data-slot = "session-turn-content" class = { props . classes ?. content } >
416- < div onClick = { handleInteraction } >
315+ < div
316+ ref = { autoScroll . scrollRef }
317+ onScroll = { autoScroll . handleScroll }
318+ data-slot = "session-turn-content"
319+ class = { props . classes ?. content }
320+ >
321+ < div onClick = { autoScroll . handleInteraction } >
417322 < Show when = { message ( ) } >
418323 { ( msg ) => (
419324 < div
420- ref = { ( el ) => setStore ( " contentRef" , el ) }
325+ ref = { autoScroll . contentRef }
421326 data-message = { msg ( ) . id }
422327 data-slot = "session-turn-message-container"
423328 class = { props . classes ?. container }
0 commit comments