@@ -136,6 +136,7 @@ export function RecordingViewerApp({ authStatus }: RecordingViewerAppProps) {
136136 setIsVideoPlaying ( false ) ;
137137 videoTimeRef . current = 0 ;
138138 setZoomedXDomain ( null ) ;
139+ setAutoFollowLive ( true ) ;
139140 const firstSessionStart = events . find ( e => e . type === 'sessionStart' ) as SessionStartEvent | undefined ;
140141 const schemaVersion = resolveSchemaVersion ( metadata , firstSessionStart ) ;
141142 setSession ( { metadata, events, fileName : sessionId , schemaVersion, replayEq, annotations : loadedAnnotations } ) ;
@@ -203,6 +204,7 @@ export function RecordingViewerApp({ authStatus }: RecordingViewerAppProps) {
203204 setViewMode ( 'timeline' ) ;
204205 setScrollToTimestamp ( null ) ;
205206 setZoomedXDomain ( null ) ;
207+ setAutoFollowLive ( true ) ;
206208 setStickyLive ( false ) ;
207209 setEndedLiveSessionId ( null ) ;
208210 setSession ( loaded ) ;
@@ -218,6 +220,7 @@ export function RecordingViewerApp({ authStatus }: RecordingViewerAppProps) {
218220 setViewMode ( 'timeline' ) ;
219221 setScrollToTimestamp ( null ) ;
220222 setZoomedXDomain ( null ) ;
223+ setAutoFollowLive ( true ) ;
221224 setSession ( null ) ;
222225 } , [ ] ) ;
223226
@@ -281,6 +284,7 @@ export function RecordingViewerApp({ authStatus }: RecordingViewerAppProps) {
281284 const [ viewMode , setViewMode ] = useState < 'timeline' | 'list' > ( 'timeline' ) ;
282285 const [ scrollToTimestamp , setScrollToTimestamp ] = useState < number | null > ( null ) ;
283286 const [ zoomedXDomain , setZoomedXDomain ] = useState < [ number , number ] | null > ( null ) ;
287+ const [ autoFollowLive , setAutoFollowLive ] = useState ( true ) ;
284288
285289 const handleViewInList = useCallback ( ( timestamp : number ) => {
286290 setScrollToTimestamp ( timestamp ) ;
@@ -328,8 +332,29 @@ export function RecordingViewerApp({ authStatus }: RecordingViewerAppProps) {
328332
329333 const effectiveXDomain = zoomedXDomain ?? xDomain ;
330334
335+ // Slide the zoomed window right when new live events arrive, preserving width.
336+ // Only active when isLiveSession + autoFollowLive + currently zoomed in.
337+ useEffect ( ( ) => {
338+ if ( ! autoFollowLive || ! isLiveSession || ! zoomedXDomain || ! xDomain ) return ;
339+ if ( xDomain [ 1 ] <= zoomedXDomain [ 1 ] ) return ;
340+ const range = zoomedXDomain [ 1 ] - zoomedXDomain [ 0 ] ;
341+ setZoomedXDomain ( [ xDomain [ 1 ] - range , xDomain [ 1 ] ] ) ;
342+ } , [ autoFollowLive , isLiveSession , xDomain , zoomedXDomain ] ) ;
343+
344+ const handleToggleAutoFollow = useCallback ( ( ) => {
345+ setAutoFollowLive ( ( prev ) => {
346+ const next = ! prev ;
347+ if ( next && zoomedXDomain && xDomain ) {
348+ const range = zoomedXDomain [ 1 ] - zoomedXDomain [ 0 ] ;
349+ setZoomedXDomain ( [ xDomain [ 1 ] - range , xDomain [ 1 ] ] ) ;
350+ }
351+ return next ;
352+ } ) ;
353+ } , [ xDomain , zoomedXDomain ] ) ;
354+
331355 const handleZoomChange = useCallback ( ( domain : [ number , number ] | null ) => {
332356 setZoomedXDomain ( domain ) ;
357+ setAutoFollowLive ( false ) ;
333358 } , [ ] ) ;
334359
335360 const handleZoomIn = useCallback ( ( ) => {
@@ -341,6 +366,7 @@ export function RecordingViewerApp({ authStatus }: RecordingViewerAppProps) {
341366 if ( newRange < 2000 ) return ;
342367 const center = ( min + max ) / 2 ;
343368 setZoomedXDomain ( [ center - newRange / 2 , center + newRange / 2 ] ) ;
369+ setAutoFollowLive ( false ) ;
344370 } , [ xDomain , zoomedXDomain ] ) ;
345371
346372 const handleZoomOut = useCallback ( ( ) => {
@@ -354,6 +380,7 @@ export function RecordingViewerApp({ authStatus }: RecordingViewerAppProps) {
354380 setZoomedXDomain ( null ) ;
355381 return ;
356382 }
383+ setAutoFollowLive ( false ) ;
357384 let newMin = ( min + max ) / 2 - newRange / 2 ;
358385 let newMax = ( min + max ) / 2 + newRange / 2 ;
359386 if ( newMin < xDomain [ 0 ] ) { newMin = xDomain [ 0 ] ; newMax = newMin + newRange ; }
@@ -480,6 +507,15 @@ export function RecordingViewerApp({ authStatus }: RecordingViewerAppProps) {
480507 { zoomedXDomain && (
481508 < button className = "zoom-btn reset" onClick = { ( ) => setZoomedXDomain ( null ) } title = "Reset zoom" > Reset</ button >
482509 ) }
510+ { isLiveSession && (
511+ < button
512+ className = { `zoom-btn follow ${ autoFollowLive ? 'active' : '' } ` }
513+ onClick = { handleToggleAutoFollow }
514+ title = { autoFollowLive ? 'Auto-follow latest events (on)' : 'Auto-follow latest events (off)' }
515+ >
516+ Follow
517+ </ button >
518+ ) }
483519 </ div >
484520 ) }
485521 { ! writesDisabled && (
0 commit comments