@@ -81,6 +81,10 @@ import {
8181import { useSessionTraceSelection } from "./useSessionTraceSelection" ;
8282
8383const ROW_HEIGHT_PX = 32 ;
84+
85+ function isScrolledToBottom ( element : HTMLElement ) {
86+ return element . scrollTop + element . clientHeight >= element . scrollHeight - ROW_HEIGHT_PX ;
87+ }
8488// Like the run page: arrowing through rows doesn't write the URL on every step.
8589const SELECTION_URL_DEBOUNCE_MS = 250 ;
8690
@@ -406,6 +410,11 @@ function SessionTraceTree({
406410 [ tree ]
407411 ) ;
408412 const revealedDeepLink = useRef ( deepLink === undefined ) ;
413+ // After a deep link, following new spans waits for the user's own scroll, click or key: the
414+ // reveal can leave a short live trace at the bottom, and following from there would push the
415+ // linked row away. Until armed, scrolls don't update `atBottom`.
416+ const followArmed = useRef ( deepLink === undefined ) ;
417+ const atBottom = useRef ( deepLink === undefined ) ;
409418 const deepLinkStillIntended = deepLink !== undefined && isSameSelection ( intended , deepLink ) ;
410419 useLayoutEffect ( ( ) => {
411420 if ( ! deepLink || revealedDeepLink . current ) return ;
@@ -424,10 +433,14 @@ function SessionTraceTree({
424433 // Scrolling, a click or a key in the tab means the user has moved on: stop revealing.
425434 useEffect ( ( ) => {
426435 const container = containerRef . current ;
427- if ( ! container || revealedDeepLink . current ) return ;
436+ // Until following is armed: a run link's reveal is already done by the time this runs.
437+ if ( ! container || followArmed . current ) return ;
428438 const stopRevealing = ( ) => {
429439 revealedDeepLink . current = true ;
430440 pendingScroll . current = undefined ;
441+ followArmed . current = true ;
442+ const scrollElement = parentRef . current ;
443+ atBottom . current = scrollElement ? isScrolledToBottom ( scrollElement ) : false ;
431444 } ;
432445 const events = [ "wheel" , "touchstart" , "pointerdown" , "keydown" ] as const ;
433446 for ( const event of events ) {
@@ -485,8 +498,8 @@ function SessionTraceTree({
485498 } ;
486499
487500 // Follow the newest run's new spans, but only if the view was at the bottom when they arrived.
501+ // `atBottom` is tracked on scroll, so rows added without a scroll (a new run) keep it true.
488502 // The tree takes the new rows a render later, so scroll once its node state has changed.
489- const atBottom = useRef ( deepLink === undefined ) ;
490503 const newestSpanCount = newestRunId ? ( snapshots [ newestRunId ] ?. events . length ?? 0 ) : 0 ;
491504 const pendingFollow = useRef < { nodes : typeof nodes } | null > ( null ) ;
492505 const latestNodes = useRef ( nodes ) ;
@@ -669,10 +682,7 @@ function SessionTraceTree({
669682 }
670683 onScroll = { ( scrollTop ) => {
671684 const element = treeScrollRef . current ;
672- if ( element ) {
673- atBottom . current =
674- scrollTop + element . clientHeight >= element . scrollHeight - ROW_HEIGHT_PX ;
675- }
685+ if ( element && followArmed . current ) atBottom . current = isScrolledToBottom ( element ) ;
676686 const timeline = timelineScrollRef . current ;
677687 if ( timeline && Math . abs ( timeline . scrollTop - scrollTop ) >= 0.5 ) {
678688 timeline . scrollTop = scrollTop ;
0 commit comments