Skip to content

Commit dfc904a

Browse files
MariefayTrigger.dev RepoOps
authored andcommitted
fix(webapp): keep a deep-linked span in view while its live run streams
Behind a feature flag, a shared link to a span in a run that is still streaming now keeps that span in view in the session Trace tab, instead of scrolling it away as new spans arrive. Mono-RevId: d4b90bc82d2acfe4987a6487e1e91dabc773d68a
1 parent 9ed0076 commit dfc904a

1 file changed

Lines changed: 16 additions & 6 deletions

File tree

‎apps/webapp/app/components/sessions/v1/trace/SessionTraceTab.tsx‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ import {
8181
import { useSessionTraceSelection } from "./useSessionTraceSelection";
8282

8383
const 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.
8589
const 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

Comments
 (0)