66} from "@heroicons/react/20/solid" ;
77import { formatDurationMilliseconds } from "@trigger.dev/core/v3" ;
88import {
9+ createContext ,
910 useCallback ,
11+ useContext ,
1012 useEffect ,
1113 useLayoutEffect ,
1214 useMemo ,
@@ -108,6 +110,7 @@ import {
108110 RUNS_FILL_MAX ,
109111 RUNS_PAGE_SIZE ,
110112 type LoadedRuns ,
113+ type NewestPageReason ,
111114 useSessionRunPages ,
112115} from "./useSessionRunPages" ;
113116import { useSessionTraceSelection } from "./useSessionTraceSelection" ;
@@ -120,13 +123,19 @@ function isScrolledToBottom(element: HTMLElement) {
120123// Like the run page: arrowing through rows doesn't write the URL on every step.
121124const SELECTION_URL_DEBOUNCE_MS = 250 ;
122125
126+ // Rows' clocks stop while the tab is hidden.
127+ const TraceTabActiveContext = createContext ( true ) ;
128+
123129/** The session's runs as one tree, newest page first, with earlier pages on request. */
124130export function SessionTraceTab ( {
125131 runsPath,
126132 treeSnapshot,
133+ active,
127134} : {
128135 runsPath : string ;
129136 treeSnapshot : ResizableSnapshot | undefined ;
137+ /** False while the Conversation tab is shown: the tab stays mounted but makes no requests. */
138+ active : boolean ;
130139} ) {
131140 // A run in the URL when the tab opens is a deep link: its page is read with the newest one.
132141 const { selection, setSelection } = useSessionTraceSelection ( ) ;
@@ -149,24 +158,33 @@ export function SessionTraceTab({
149158 // New runs: re-read the newest page while the tab is visible (focus also refreshes it). It
150159 // lives here, not in the tree, so a session with no runs yet still picks up its first one.
151160 const [ checkedAtMs , setCheckedAtMs ] = useState ( ( ) => Date . now ( ) ) ;
161+ const { refreshNewest } = pages ;
162+ const checkNewest = useCallback (
163+ ( reason : NewestPageReason ) => {
164+ setCheckedAtMs ( Date . now ( ) ) ;
165+ refreshNewest ( reason ) ;
166+ } ,
167+ [ refreshNewest ]
168+ ) ;
152169 useInterval ( {
153170 interval : newRunsPollIntervalMs ( pages . runs ?? [ ] , pages . totalCount , checkedAtMs ) ,
154- disabled : pages . state . status !== "ready" ,
171+ disabled : pages . state . status !== "ready" || ! active ,
155172 onLoad : false ,
156173 onFocus : false ,
157- callback : ( ) => {
158- setCheckedAtMs ( Date . now ( ) ) ;
159- pages . refreshNewest ( "poll" ) ;
160- } ,
174+ callback : ( ) => checkNewest ( "poll" ) ,
161175 } ) ;
162176 useInterval ( {
163- disabled : pages . state . status !== "ready" ,
177+ disabled : pages . state . status !== "ready" || ! active ,
164178 onLoad : false ,
165- callback : ( ) => {
166- setCheckedAtMs ( Date . now ( ) ) ;
167- pages . refreshNewest ( "focus" ) ;
168- } ,
179+ callback : ( ) => checkNewest ( "focus" ) ,
169180 } ) ;
181+ // Shown again after Conversation: check at once instead of waiting for the next poll.
182+ const wasActive = useRef ( active ) ;
183+ useEffect ( ( ) => {
184+ const shown = active && ! wasActive . current ;
185+ wasActive . current = active ;
186+ if ( shown && pages . state . status === "ready" ) checkNewest ( "focus" ) ;
187+ } , [ active , pages . state . status , checkNewest ] ) ;
170188
171189 if ( pages . state . status === "loading" ) {
172190 return (
@@ -199,23 +217,26 @@ export function SessionTraceTab({
199217 }
200218
201219 return (
202- < SessionTraceTree
203- runs = { pages . runs }
204- view = { pages . view }
205- totalCount = { pages . totalCount }
206- canLoadEarlier = { pages . canLoadEarlier }
207- loadingEarlier = { pages . loadingEarlier }
208- earlierError = { pages . earlierError }
209- onLoadEarlier = { pages . loadEarlier }
210- olderLoadBusy = { pages . olderLoadBusy }
211- fillingRunFriendlyId = { pages . fillingRunFriendlyId }
212- fillFailedRunFriendlyId = { pages . fillFailedRunFriendlyId }
213- onFillHole = { pages . fillHole }
214- onRunDataStale = { pages . refreshNewest }
215- checkedAtMs = { checkedAtMs }
216- treeSnapshot = { treeSnapshot }
217- notice = { showRunNotFound ? "That run isn't in this session." : undefined }
218- />
220+ < TraceTabActiveContext . Provider value = { active } >
221+ < SessionTraceTree
222+ runs = { pages . runs }
223+ view = { pages . view }
224+ totalCount = { pages . totalCount }
225+ canLoadEarlier = { pages . canLoadEarlier }
226+ loadingEarlier = { pages . loadingEarlier }
227+ earlierError = { pages . earlierError }
228+ onLoadEarlier = { pages . loadEarlier }
229+ olderLoadBusy = { pages . olderLoadBusy }
230+ fillingRunFriendlyId = { pages . fillingRunFriendlyId }
231+ fillFailedRunFriendlyId = { pages . fillFailedRunFriendlyId }
232+ onFillHole = { pages . fillHole }
233+ onRunDataStale = { pages . refreshNewest }
234+ checkedAtMs = { checkedAtMs }
235+ treeSnapshot = { treeSnapshot }
236+ notice = { showRunNotFound ? "That run isn't in this session." : undefined }
237+ active = { active }
238+ />
239+ </ TraceTabActiveContext . Provider >
219240 ) ;
220241}
221242
@@ -238,6 +259,7 @@ function SessionTraceTree({
238259 checkedAtMs,
239260 treeSnapshot,
240261 notice,
262+ active,
241263} : {
242264 runs : SessionTraceRun [ ] ;
243265 view : LoadedRuns [ "view" ] ;
@@ -255,6 +277,7 @@ function SessionTraceTree({
255277 checkedAtMs : number ;
256278 treeSnapshot : ResizableSnapshot | undefined ;
257279 notice : string | undefined ;
280+ active : boolean ;
258281} ) {
259282 const containerRef = useRef < HTMLDivElement > ( null ) ;
260283 const parentRef = useRef < HTMLDivElement > ( null ) ;
@@ -335,6 +358,9 @@ function SessionTraceTree({
335358 loadsRef . current = sessionRunLoadsReducer ( loadsRef . current , action ) ;
336359 reactDispatchLoads ( action ) ;
337360 } , [ ] ) ;
361+ useEffect ( ( ) => {
362+ dispatchLoads ( { type : "paused" , paused : ! active } ) ;
363+ } , [ active , dispatchLoads ] ) ;
338364 const onSettle = useCallback (
339365 ( runFriendlyId : string , loadId : number , outcome : SessionRunLoadOutcome ) =>
340366 dispatchLoads ( { type : "settle" , runFriendlyId, loadId, outcome } ) ,
@@ -411,7 +437,11 @@ function SessionTraceTree({
411437 // Only the live run's scale moves with the clock; the rest are fixed at page load.
412438 const [ openedAtMs ] = useState ( ( ) => Date . now ( ) ) ;
413439 const [ nowMs , setNowMs ] = useState ( openedAtMs ) ;
414- useInterval ( { interval : 1_000 , disabled : ! liveRunId , callback : ( ) => setNowMs ( Date . now ( ) ) } ) ;
440+ useInterval ( {
441+ interval : 1_000 ,
442+ disabled : ! liveRunId || ! active ,
443+ callback : ( ) => setNowMs ( Date . now ( ) ) ,
444+ } ) ;
415445 const settledScales = useMemo ( ( ) => {
416446 const scales = new Map < string , TraceTimelineScale > ( ) ;
417447 for ( const run of runs ) {
@@ -869,6 +899,7 @@ function SessionTraceTree({
869899 run = { { ...run , status : run . status } }
870900 load = { load }
871901 live = { run . friendlyId === newestRunId }
902+ paused = { ! active }
872903 errorsOnly = { filter . errorsOnly }
873904 selectedSpanId = {
874905 deepLink ?. kind === "span" && deepLink . runFriendlyId === run . friendlyId
@@ -1018,8 +1049,9 @@ function SessionTraceTree({
10181049 </ ResizablePanelGroup >
10191050 < div className = "flex items-center justify-between gap-2 border-t border-grid-dimmed px-4" >
10201051 < div className = "flex items-center gap-4 overflow-hidden" >
1052+ { /* Shortcuts listen on the document, so `inert` on the hidden tab doesn't stop them. */ }
10211053 < KeyboardShortcuts
1022- enabled
1054+ enabled = { active }
10231055 onExpandAll = { expandAll }
10241056 onCollapseAll = { collapseAll }
10251057 onToggleLevel = { toggleLevel }
@@ -1450,8 +1482,13 @@ function RunLoadStatus({
14501482
14511483// Only the live run counts up; an older unfinished run shows when it started instead.
14521484function RunDurationLabel ( { run, isLive } : { run : SessionTraceRun ; isLive : boolean } ) {
1485+ const active = useContext ( TraceTabActiveContext ) ;
14531486 const [ nowMs , setNowMs ] = useState ( ( ) => Date . now ( ) ) ;
1454- useInterval ( { interval : 1_000 , disabled : ! isLive , callback : ( ) => setNowMs ( Date . now ( ) ) } ) ;
1487+ useInterval ( {
1488+ interval : 1_000 ,
1489+ disabled : ! isLive || ! active ,
1490+ callback : ( ) => setNowMs ( Date . now ( ) ) ,
1491+ } ) ;
14551492
14561493 const duration = runDuration ( run , nowMs ) ;
14571494 if ( ! duration ) return run . status && ! isFinalRunStatus ( run . status ) ? < > Queued</ > : null ;
0 commit comments