@@ -70,6 +70,8 @@ export function OutputWindow(props: OutputWindowProps) {
7070 const themeCtx = useTheme ( )
7171 const [ scrollRef , setScrollRef ] = createSignal < ScrollBoxRenderable | undefined > ( )
7272 const [ userScrolledAway , setUserScrolledAway ] = createSignal ( false )
73+ // Scroll lock prevents ALL scroll handling during content prepend operations
74+ const [ scrollLock , setScrollLock ] = createSignal ( false )
7375
7476 // Reset userScrolledAway when agent changes
7577 // Use on() to explicitly track only the agent name, not other props
@@ -90,7 +92,17 @@ export function OutputWindow(props: OutputWindowProps) {
9092 debug ( '[OutputWindow] Scroll effect running, ref exists: %s' , ! ! ref )
9193 if ( ! ref ) return
9294
95+ // PRELOAD_THRESHOLD: Load earlier for smoother experience
96+ // When user is 15 lines from top, start loading
97+ const PRELOAD_THRESHOLD = 15
98+
9399 const handleScrollChange = ( ) => {
100+ // Absolute lock during content prepend operations - prevents ALL interference
101+ if ( scrollLock ( ) ) {
102+ debug ( '[OutputWindow] Scroll event ignored - scroll lock active' )
103+ return
104+ }
105+
94106 const scrollTop = ref . scrollTop
95107 const scrollHeight = ref . scrollHeight
96108 const viewportHeight = ref . height
@@ -108,13 +120,40 @@ export function OutputWindow(props: OutputWindowProps) {
108120 setUserScrolledAway ( false )
109121 }
110122
111- // Trigger load when near the top (within 3 lines) - skip if already loading
112- if ( scrollTop <= 3 && props . hasMoreAbove && props . onLoadMore && ! props . isLoadingEarlier ) {
113- debug ( '[OutputWindow] Loading earlier lines...' )
123+ // Preload when near the top
124+ if ( scrollTop <= PRELOAD_THRESHOLD && props . hasMoreAbove && props . onLoadMore && ! props . isLoadingEarlier ) {
125+ debug ( '[OutputWindow] Preloading earlier lines (scrollTop=%d, threshold=%d)...' , scrollTop , PRELOAD_THRESHOLD )
126+
127+ // 1. Lock and detach listener to prevent any scroll events during operation
128+ setScrollLock ( true )
129+ ref . verticalScrollBar ?. off ( "change" , handleScrollChange )
130+
131+ // 2. Capture current scroll position BEFORE loading
132+ const originalScrollTop = scrollTop
133+
134+ // 3. Load content (this updates state and triggers re-render)
114135 const linesLoaded = props . onLoadMore ( )
115136 debug ( '[OutputWindow] Lines loaded: %d' , linesLoaded )
137+
116138 if ( linesLoaded > 0 ) {
117- ref . scrollTop = linesLoaded // Maintain view position
139+ // 4. SYNCHRONOUS scroll adjustment - no async delay
140+ // Set scroll position immediately in the same execution frame
141+ // This prevents the flash by adjusting before the next paint
142+ const newScrollTop = originalScrollTop + linesLoaded
143+ ref . scrollTop = newScrollTop
144+ debug ( '[OutputWindow] Scroll restored (sync): original=%d, added=%d, new=%d' ,
145+ originalScrollTop , linesLoaded , ref . scrollTop )
146+
147+ // 5. Use setImmediate only for re-attaching listener (after scroll is stable)
148+ setImmediate ( ( ) => {
149+ ref . verticalScrollBar ?. on ( "change" , handleScrollChange )
150+ setScrollLock ( false )
151+ debug ( '[OutputWindow] Scroll lock released, listener re-attached' )
152+ } )
153+ } else {
154+ // No lines loaded, re-attach and unlock immediately
155+ ref . verticalScrollBar ?. on ( "change" , handleScrollChange )
156+ setScrollLock ( false )
118157 }
119158 }
120159 }
@@ -126,7 +165,8 @@ export function OutputWindow(props: OutputWindowProps) {
126165 )
127166
128167 // Compute whether stickyScroll should be active
129- const shouldStickyScroll = ( ) => ! userScrolledAway ( )
168+ // Disable during scroll lock to prevent interference with scroll position restoration
169+ const shouldStickyScroll = ( ) => ! userScrolledAway ( ) && ! scrollLock ( )
130170
131171 // Notify parent when user scrolls away (to pause trimming in log stream)
132172 createEffect (
0 commit comments