@@ -190,6 +190,9 @@ public TabInfo(
190190
191191 // Initialize tab header
192192 UpdateTabHeader ( ) ;
193+
194+ // Set up scroll detection for smart auto-scrolling
195+ SetupScrollDetection ( ) ;
193196 }
194197
195198 protected void OnPropertyChanged ( string propertyName )
@@ -319,8 +322,26 @@ public void AppendContent(string text)
319322 if ( _disposed )
320323 return ;
321324
325+ // Check if user was at the bottom before adding content
326+ bool wasAtBottom = IsUserAtBottom ( ) ;
327+ bool isTabFocused = IsTabCurrentlyFocused ( ) ;
328+
322329 ContentTextBox . AppendText ( text ) ;
323- ContentTextBox . ScrollToEnd ( ) ;
330+
331+ // Only auto-scroll if user was already at the bottom or this tab is not currently focused
332+ if ( wasAtBottom || ! isTabFocused )
333+ {
334+ ContentTextBox . ScrollToEnd ( ) ;
335+ }
336+ else
337+ {
338+ // User is scrolled up and tab is focused - indicate new content arrived
339+ // This will make the tab header show the "new content" indicator
340+ if ( ! HasNewContent )
341+ {
342+ SetHasNewContent ( true ) ;
343+ }
344+ }
324345
325346 // Check if we need to optimize memory after adding content
326347 OptimizeMemoryIfNeeded ( ) ;
@@ -451,6 +472,150 @@ public string GetFullContent()
451472 }
452473 }
453474
475+ /// <summary>
476+ /// Checks if the user is currently scrolled to the bottom of the TextBox
477+ /// </summary>
478+ /// <returns>True if at bottom, false if scrolled up</returns>
479+ private bool IsUserAtBottom ( )
480+ {
481+ if ( _disposed || ContentTextBox == null )
482+ return true ; // Default to true to maintain existing behavior
483+
484+ try
485+ {
486+ // Find the ScrollViewer inside the TextBox
487+ var scrollViewer = FindScrollViewer ( ContentTextBox ) ;
488+ if ( scrollViewer != null )
489+ {
490+ var verticalOffset = scrollViewer . VerticalOffset ;
491+ var scrollableHeight = scrollViewer . ScrollableHeight ;
492+
493+ // Consider "at bottom" if within a small tolerance (a few pixels)
494+ const double tolerance = 5.0 ;
495+ return Math . Abs ( verticalOffset - scrollableHeight ) <= tolerance ;
496+ }
497+
498+ // Fallback: check if caret is at the end
499+ return ContentTextBox . CaretIndex == ContentTextBox . Text . Length ;
500+ }
501+ catch
502+ {
503+ return true ; // Default to true if we can't determine scroll position
504+ }
505+ }
506+
507+ /// <summary>
508+ /// Finds the ScrollViewer inside a control using visual tree walking
509+ /// </summary>
510+ private ScrollViewer ? FindScrollViewer ( DependencyObject parent )
511+ {
512+ if ( parent == null )
513+ return null ;
514+
515+ try
516+ {
517+ for ( int i = 0 ; i < VisualTreeHelper . GetChildrenCount ( parent ) ; i ++ )
518+ {
519+ var child = VisualTreeHelper . GetChild ( parent , i ) ;
520+
521+ if ( child is ScrollViewer scrollViewer )
522+ return scrollViewer ;
523+
524+ var result = FindScrollViewer ( child ) ;
525+ if ( result != null )
526+ return result ;
527+ }
528+ }
529+ catch
530+ {
531+ // Ignore visual tree access errors
532+ }
533+
534+ return null ;
535+ }
536+
537+ /// <summary>
538+ /// Checks if this tab is currently focused/selected
539+ /// </summary>
540+ /// <returns>True if this tab is currently active</returns>
541+ private bool IsTabCurrentlyFocused ( )
542+ {
543+ if ( _disposed || TabItem ? . Parent == null )
544+ return false ;
545+
546+ try
547+ {
548+ // Check if this tab is the selected item in its parent TabControl
549+ if ( TabItem . Parent is TabControl tabControl )
550+ {
551+ return tabControl . SelectedItem == TabItem ;
552+ }
553+
554+ // Check if this tab has keyboard focus
555+ return TabItem . IsKeyboardFocused || ContentTextBox ? . IsKeyboardFocused == true ;
556+ }
557+ catch
558+ {
559+ return false ;
560+ }
561+ }
562+
563+ /// <summary>
564+ /// Call this method when the user scrolls back to the bottom to clear the new content indicator
565+ /// </summary>
566+ public void OnUserScrolledToBottom ( )
567+ {
568+ if ( HasNewContent )
569+ {
570+ SetHasNewContent ( false ) ;
571+ }
572+ }
573+
574+ /// <summary>
575+ /// Subscribe to TextBox scroll events to detect when user scrolls back to bottom
576+ /// This should be called from MainWindow to wire up the scroll detection
577+ /// </summary>
578+ public void SetupScrollDetection ( )
579+ {
580+ if ( _disposed || ContentTextBox == null )
581+ return ;
582+
583+ try
584+ {
585+ var scrollViewer = FindScrollViewer ( ContentTextBox ) ;
586+ if ( scrollViewer != null )
587+ {
588+ scrollViewer . ScrollChanged += OnScrollChanged ;
589+ }
590+ }
591+ catch ( Exception ex )
592+ {
593+ System . Diagnostics . Debug . WriteLine ( $ "Error setting up scroll detection for tab '{ TabName } ': { ex . Message } ") ;
594+ }
595+ }
596+
597+ /// <summary>
598+ /// Handle scroll events to detect when user returns to bottom
599+ /// </summary>
600+ private void OnScrollChanged ( object sender , ScrollChangedEventArgs e )
601+ {
602+ if ( _disposed )
603+ return ;
604+
605+ try
606+ {
607+ // If user scrolled to bottom and we have new content indicator, clear it
608+ if ( HasNewContent && IsUserAtBottom ( ) )
609+ {
610+ SetHasNewContent ( false ) ;
611+ }
612+ }
613+ catch ( Exception ex )
614+ {
615+ System . Diagnostics . Debug . WriteLine ( $ "Error in scroll change handler for tab '{ TabName } ': { ex . Message } ") ;
616+ }
617+ }
618+
454619 #region IDisposable Implementation
455620
456621 /// <summary>
@@ -496,6 +661,20 @@ protected virtual void Dispose(bool disposing)
496661 // Clear property change event handlers
497662 PropertyChanged = null ;
498663
664+ // Unsubscribe from scroll events
665+ try
666+ {
667+ var scrollViewer = FindScrollViewer ( ContentTextBox ) ;
668+ if ( scrollViewer != null )
669+ {
670+ scrollViewer . ScrollChanged -= OnScrollChanged ;
671+ }
672+ }
673+ catch
674+ {
675+ // Ignore errors during disposal
676+ }
677+
499678 // Force garbage collection of large content if present
500679 if ( ContentTextBox != null && ContentTextBox . Text . Length > 10_000_000 ) // 10MB threshold
501680 {
0 commit comments