@@ -25,6 +25,7 @@ export interface LogContentProps {
2525 isLoadingEarlier ?: boolean
2626 loadEarlierError ?: string | null
2727 onLoadMore ?: ( ) => number
28+ onPauseTrimmingChange ?: ( paused : boolean ) => void
2829}
2930
3031export function LogContent ( props : LogContentProps ) {
@@ -34,63 +35,74 @@ export function LogContent(props: LogContentProps) {
3435 const themeCtx = useTheme ( )
3536 const [ scrollRef , setScrollRef ] = createSignal < ScrollBoxRenderable | undefined > ( )
3637 const [ userScrolledAway , setUserScrolledAway ] = createSignal ( false )
37- const [ prevLineCount , setPrevLineCount ] = createSignal ( 0 )
3838
3939 // Reset userScrolledAway when lines reset (indicates agent/log change)
40- createEffect ( ( ) => {
41- const currentCount = props . lines . length
42- const prev = prevLineCount ( )
43- // If lines dropped significantly (more than 50% or to near zero), it's a new log
44- if ( prev > 10 && currentCount < prev * 0.5 ) {
45- debug ( '[LogContent] Lines reset detected (prev=%d, current=%d), resetting scroll state' , prev , currentCount )
46- setUserScrolledAway ( false )
47- }
48- setPrevLineCount ( currentCount )
49- } )
40+ // Use on() to explicitly track only lines.length, preventing unintended effect re-runs
41+ createEffect (
42+ on (
43+ ( ) => props . lines . length ,
44+ ( currentCount , prev ) => {
45+ // If lines dropped significantly (more than 50% or to near zero), it's a new log
46+ if ( prev !== undefined && prev > 10 && currentCount < prev * 0.5 ) {
47+ debug ( '[LogContent] Lines reset detected (prev=%d, current=%d), resetting scroll state' , prev , currentCount )
48+ setUserScrolledAway ( false )
49+ }
50+ }
51+ )
52+ )
5053
5154 // Handle scroll events: load earlier lines + track if user scrolled away from bottom
52- createEffect ( ( ) => {
53- const ref = scrollRef ( )
54- debug ( '[LogContent] Effect running, ref exists: %s' , ! ! ref )
55- if ( ! ref ) return
56-
57- const handleScrollChange = ( ) => {
58- const scrollTop = ref . scrollTop
59- const scrollHeight = ref . scrollHeight
60- const viewportHeight = ref . height
61- const maxScroll = Math . max ( 0 , scrollHeight - viewportHeight )
62- const isAtBottom = scrollTop >= maxScroll - 3
63-
64- debug ( '[LogContent] scroll: top=%d, max=%d, atBottom=%s, hasMore=%s' , scrollTop , maxScroll , isAtBottom , props . hasMoreAbove )
65-
66- // Track if user scrolled away from bottom (to disable stickyScroll)
67- if ( ! isAtBottom && ! userScrolledAway ( ) ) {
68- debug ( '[LogContent] User scrolled away from bottom' )
69- setUserScrolledAway ( true )
70- } else if ( isAtBottom && userScrolledAway ( ) ) {
71- debug ( '[LogContent] User returned to bottom' )
72- setUserScrolledAway ( false )
73- }
55+ // Use on() to only track scrollRef changes, not other props
56+ createEffect (
57+ on ( scrollRef , ( ref ) => {
58+ debug ( '[LogContent] Effect running, ref exists: %s' , ! ! ref )
59+ if ( ! ref ) return
60+
61+ const handleScrollChange = ( ) => {
62+ const scrollTop = ref . scrollTop
63+ const scrollHeight = ref . scrollHeight
64+ const viewportHeight = ref . height
65+ const maxScroll = Math . max ( 0 , scrollHeight - viewportHeight )
66+ const isAtBottom = scrollTop >= maxScroll - 3
67+
68+ debug ( '[LogContent] scroll: top=%d, max=%d, atBottom=%s, hasMore=%s' , scrollTop , maxScroll , isAtBottom , props . hasMoreAbove )
69+
70+ // Track if user scrolled away from bottom (to disable stickyScroll)
71+ if ( ! isAtBottom && ! userScrolledAway ( ) ) {
72+ debug ( '[LogContent] User scrolled away from bottom' )
73+ setUserScrolledAway ( true )
74+ } else if ( isAtBottom && userScrolledAway ( ) ) {
75+ debug ( '[LogContent] User returned to bottom' )
76+ setUserScrolledAway ( false )
77+ }
7478
75- // Trigger load when near the top (within 3 lines) - skip if already loading
76- if ( scrollTop <= 3 && props . hasMoreAbove && props . onLoadMore && ! props . isLoadingEarlier ) {
77- debug ( '[LogContent] Loading earlier lines...' )
78- const linesLoaded = props . onLoadMore ( )
79- debug ( '[LogContent] Lines loaded: %d' , linesLoaded )
80- if ( linesLoaded > 0 ) {
81- ref . scrollTop = linesLoaded // Maintain view position
79+ // Trigger load when near the top (within 3 lines) - skip if already loading
80+ if ( scrollTop <= 3 && props . hasMoreAbove && props . onLoadMore && ! props . isLoadingEarlier ) {
81+ debug ( '[LogContent] Loading earlier lines...' )
82+ const linesLoaded = props . onLoadMore ( )
83+ debug ( '[LogContent] Lines loaded: %d' , linesLoaded )
84+ if ( linesLoaded > 0 ) {
85+ ref . scrollTop = linesLoaded // Maintain view position
86+ }
8287 }
8388 }
84- }
8589
86- debug ( '[LogContent] Setting up scroll listener, verticalScrollBar exists: %s' , ! ! ref . verticalScrollBar )
87- ref . verticalScrollBar ?. on ( "change" , handleScrollChange )
88- onCleanup ( ( ) => ref . verticalScrollBar ?. off ( "change" , handleScrollChange ) )
89- } )
90+ debug ( '[LogContent] Setting up scroll listener, verticalScrollBar exists: %s' , ! ! ref . verticalScrollBar )
91+ ref . verticalScrollBar ?. on ( "change" , handleScrollChange )
92+ onCleanup ( ( ) => ref . verticalScrollBar ?. off ( "change" , handleScrollChange ) )
93+ } )
94+ )
9095
9196 // Compute whether stickyScroll should be active (only when running AND user hasn't scrolled away)
9297 const shouldStickyScroll = ( ) => ( props . isRunning ?? true ) && ! userScrolledAway ( )
9398
99+ // Notify parent when user scrolls away (to pause trimming in log stream)
100+ createEffect (
101+ on ( userScrolledAway , ( scrolledAway ) => {
102+ props . onPauseTrimmingChange ?.( scrolledAway )
103+ } )
104+ )
105+
94106 return (
95107 < box flexGrow = { 1 } flexDirection = "column" paddingLeft = { 1 } paddingRight = { 1 } paddingTop = { 1 } >
96108 < Show when = { props . isLoading } >
0 commit comments