@workflow/web-shared 5.0.0-beta.3 → 5.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/components/new-trace-viewer/components/event-list.d.ts.map +1 -1
  2. package/dist/components/new-trace-viewer/components/event-list.js +7 -4
  3. package/dist/components/new-trace-viewer/components/event-list.js.map +1 -1
  4. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts +18 -0
  5. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts.map +1 -0
  6. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js +54 -0
  7. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js.map +1 -0
  8. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts +20 -0
  9. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts.map +1 -0
  10. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js +81 -0
  11. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js.map +1 -0
  12. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts +25 -0
  13. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts.map +1 -0
  14. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js +101 -0
  15. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js.map +1 -0
  16. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts +20 -0
  17. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts.map +1 -0
  18. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.js +114 -0
  19. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.js.map +1 -0
  20. package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -1
  21. package/dist/components/new-trace-viewer/components/split-pane.js +2 -3
  22. package/dist/components/new-trace-viewer/components/split-pane.js.map +1 -1
  23. package/dist/components/new-trace-viewer/components/timeline.d.ts +10 -11
  24. package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -1
  25. package/dist/components/new-trace-viewer/components/timeline.js +57 -53
  26. package/dist/components/new-trace-viewer/components/timeline.js.map +1 -1
  27. package/dist/components/new-trace-viewer/context.d.ts.map +1 -1
  28. package/dist/components/new-trace-viewer/context.js +5 -7
  29. package/dist/components/new-trace-viewer/context.js.map +1 -1
  30. package/dist/components/new-trace-viewer/detail-panel.d.ts.map +1 -1
  31. package/dist/components/new-trace-viewer/detail-panel.js +2 -1
  32. package/dist/components/new-trace-viewer/detail-panel.js.map +1 -1
  33. package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -1
  34. package/dist/components/new-trace-viewer/trace-viewer.js +37 -17
  35. package/dist/components/new-trace-viewer/trace-viewer.js.map +1 -1
  36. package/dist/components/new-trace-viewer/utils.d.ts +3 -8
  37. package/dist/components/new-trace-viewer/utils.d.ts.map +1 -1
  38. package/dist/components/new-trace-viewer/utils.js +12 -65
  39. package/dist/components/new-trace-viewer/utils.js.map +1 -1
  40. package/dist/components/trace-viewer/util/timing.d.ts +1 -1
  41. package/dist/components/trace-viewer/util/timing.d.ts.map +1 -1
  42. package/dist/components/trace-viewer/util/timing.js +5 -8
  43. package/dist/components/trace-viewer/util/timing.js.map +1 -1
  44. package/dist/components/workflow-traces/trace-span-construction.d.ts +2 -2
  45. package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
  46. package/dist/components/workflow-traces/trace-span-construction.js +16 -3
  47. package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
  48. package/dist/lib/trace-builder.d.ts.map +1 -1
  49. package/dist/lib/trace-builder.js +2 -1
  50. package/dist/lib/trace-builder.js.map +1 -1
  51. package/dist/lib/utils.d.ts +9 -7
  52. package/dist/lib/utils.d.ts.map +1 -1
  53. package/dist/lib/utils.js +27 -27
  54. package/dist/lib/utils.js.map +1 -1
  55. package/package.json +2 -2
  56. package/src/components/new-trace-viewer/components/event-list.tsx +27 -20
  57. package/src/components/new-trace-viewer/components/middle-truncate/copy-selection.ts +114 -0
  58. package/src/components/new-trace-viewer/components/middle-truncate/middle-truncate.tsx +157 -0
  59. package/src/components/new-trace-viewer/components/middle-truncate/truncate.ts +132 -0
  60. package/src/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.ts +167 -0
  61. package/src/components/new-trace-viewer/components/split-pane.tsx +2 -3
  62. package/src/components/new-trace-viewer/components/timeline.tsx +119 -115
  63. package/src/components/new-trace-viewer/context.tsx +7 -9
  64. package/src/components/new-trace-viewer/detail-panel.tsx +2 -1
  65. package/src/components/new-trace-viewer/trace-viewer.tsx +120 -60
  66. package/src/components/new-trace-viewer/utils.ts +19 -93
  67. package/src/components/trace-viewer/util/timing.ts +5 -9
  68. package/src/components/workflow-traces/trace-span-construction.ts +22 -4
  69. package/src/lib/trace-builder.ts +2 -1
  70. package/src/lib/utils.ts +29 -29
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name';
4
- import { Search, X } from 'lucide-react';
4
+ import { RotateCcw, Search, X, ZoomIn, ZoomOut } from 'lucide-react';
5
5
  import {
6
6
  type ReactNode,
7
7
  useCallback,
@@ -19,19 +19,22 @@ import { useSidebarDataOptional } from '../sidebar/sidebar-data-context';
19
19
  import type { Trace } from '../trace-viewer/types';
20
20
  import { formatDuration, getHighResInMs } from '../trace-viewer/util/timing';
21
21
  import { CopyButton } from './components/copy-button';
22
- import { SplitPane } from './components/split-pane';
23
22
  import EventList from './components/event-list';
24
- import { Timeline, TimelineHeader } from './components/timeline';
23
+ import { SplitPane } from './components/split-pane';
24
+ import {
25
+ Timeline,
26
+ TimelineHeader,
27
+ TIMELINE_PADDING_PX,
28
+ } from './components/timeline';
25
29
  import { ActiveSpanProvider, useActiveSpan } from './context';
26
30
  import { DetailPanel } from './detail-panel';
27
- import { buildTimeCompression, computeRootBounds } from './utils';
31
+ import { computeRootBounds, computeTimeMarkers } from './utils';
28
32
 
29
33
  interface NewTraceViewerProps {
30
34
  trace: Trace;
31
35
  }
32
36
 
33
37
  const MIN_VIEWPORT_MS = 0.001;
34
- const TIMELINE_PADDING = 16;
35
38
 
36
39
  interface Viewport {
37
40
  start: number;
@@ -189,19 +192,51 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
189
192
 
190
193
  const viewDuration = viewport.end - viewport.start;
191
194
 
192
- const compression = useMemo(
193
- () => buildTimeCompression(trace.spans, viewport.start, viewport.end),
194
- [trace.spans, viewport.start, viewport.end]
195
+ const timeMarkers = useMemo(
196
+ () => computeTimeMarkers(viewDuration, viewport.start - root.startTime),
197
+ [viewDuration, viewport.start, root.startTime]
195
198
  );
196
199
 
197
- const isZoomed =
198
- viewport.start > root.startTime + 0.01 ||
199
- viewport.end < root.startTime + root.duration - 0.01;
200
-
201
200
  const resetZoom = useCallback(() => {
202
201
  animateTo({ start: root.startTime, end: root.startTime + root.duration });
203
202
  }, [animateTo, root.startTime, root.duration]);
204
203
 
204
+ const ZOOM_FACTOR = 0.5;
205
+
206
+ const zoomBy = useCallback(
207
+ (factor: number) => {
208
+ const rootS = root.startTime;
209
+ const rootE = root.startTime + root.duration;
210
+ const rootD = root.duration;
211
+
212
+ setViewport((prev) => {
213
+ const prevDuration = prev.end - prev.start;
214
+ const center = (prev.start + prev.end) / 2;
215
+ const newDuration = Math.max(
216
+ MIN_VIEWPORT_MS,
217
+ Math.min(rootD, prevDuration * factor)
218
+ );
219
+ let newStart = center - newDuration / 2;
220
+ let newEnd = center + newDuration / 2;
221
+
222
+ if (newStart < rootS) {
223
+ newStart = rootS;
224
+ newEnd = rootS + newDuration;
225
+ }
226
+ if (newEnd > rootE) {
227
+ newEnd = rootE;
228
+ newStart = Math.max(rootS, rootE - newDuration);
229
+ }
230
+
231
+ return { start: newStart, end: newEnd };
232
+ });
233
+ },
234
+ [setViewport, root.startTime, root.duration]
235
+ );
236
+
237
+ const zoomIn = useCallback(() => zoomBy(ZOOM_FACTOR), [zoomBy]);
238
+ const zoomOut = useCallback(() => zoomBy(1 / ZOOM_FACTOR), [zoomBy]);
239
+
205
240
  const handleSelectSpan = useCallback(
206
241
  (spanId: string) => {
207
242
  if (spanId === activeSpanId) {
@@ -292,22 +327,24 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
292
327
 
293
328
  const hoverInfo = useMemo(() => {
294
329
  if (hoverFraction == null) return null;
295
- const absTime = compression.fromVisual(hoverFraction);
330
+ const absTime = viewport.start + hoverFraction * viewDuration;
296
331
  const offset = absTime - root.startTime;
297
332
  return { fraction: hoverFraction, label: formatDuration(offset, true) };
298
- }, [hoverFraction, compression, root.startTime]);
333
+ }, [hoverFraction, viewport.start, viewDuration, root.startTime]);
299
334
 
300
335
  const handleTimelineMouseMove = useCallback(
301
336
  (e: React.MouseEvent<HTMLDivElement>) => {
302
337
  const el = timelineRef.current;
303
338
  if (!el) return;
304
339
  const rect = el.getBoundingClientRect();
305
- const padding = 8;
306
- const contentWidth = rect.width - padding * 2;
340
+ const contentWidth = rect.width - TIMELINE_PADDING_PX * 2;
307
341
  if (contentWidth <= 0) return;
308
342
  const fraction = Math.max(
309
343
  0,
310
- Math.min(1, (e.clientX - rect.left - padding) / contentWidth)
344
+ Math.min(
345
+ 1,
346
+ (e.clientX - rect.left - TIMELINE_PADDING_PX) / contentWidth
347
+ )
311
348
  );
312
349
  setHoverFraction(fraction);
313
350
  },
@@ -335,7 +372,7 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
335
372
  e.preventDefault();
336
373
 
337
374
  const rect = el.getBoundingClientRect();
338
- const contentWidth = rect.width - TIMELINE_PADDING * 2;
375
+ const contentWidth = rect.width - TIMELINE_PADDING_PX * 2;
339
376
  if (contentWidth <= 0) return;
340
377
 
341
378
  if (isZoomGesture) {
@@ -344,7 +381,10 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
344
381
 
345
382
  const cursorFraction = Math.max(
346
383
  0,
347
- Math.min(1, (e.clientX - rect.left - TIMELINE_PADDING) / contentWidth)
384
+ Math.min(
385
+ 1,
386
+ (e.clientX - rect.left - TIMELINE_PADDING_PX) / contentWidth
387
+ )
348
388
  );
349
389
  const scaleFactor = Math.pow(2, dy / 200);
350
390
 
@@ -429,52 +469,44 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
429
469
 
430
470
  return (
431
471
  <div
432
- data-pane="pane-root"
472
+ data-pane='pane-root'
433
473
  data-has-detail={activeSpan ? '' : undefined}
434
- className="grid w-full h-full max-h-full grid-cols-[minmax(100px,1fr)] data-[has-detail]:grid-cols-[minmax(100px,1fr)_clamp(280px,420px,100%)]"
474
+ className='grid w-full h-full max-h-full grid-cols-[minmax(100px,1fr)] data-[has-detail]:grid-cols-[minmax(100px,1fr)_clamp(280px,420px,100%)]'
435
475
  >
436
476
  <div
437
- id="trace-parent"
438
- className="grid grid-rows-[1fr] h-full min-h-0 overflow-hidden relative bg-background-100"
477
+ id='trace-parent'
478
+ className='grid grid-rows-[1fr] h-full min-h-0 overflow-hidden relative bg-background-100'
439
479
  >
440
480
  <SplitPane
441
481
  startHeader={
442
- <div className="bg-background-100 border-b border-gray-alpha-400 h-10 min-h-10 flex items-center px-2 gap-1.5">
443
- <Search className="w-3.5 h-3.5 shrink-0 text-gray-800" />
482
+ <div className='bg-background-100 border-b border-gray-alpha-400 h-10 min-h-10 flex items-center px-2 gap-1.5'>
483
+ <Search className='w-3.5 h-3.5 shrink-0 text-gray-800' />
444
484
  <input
445
- id="trace-viewer-search"
446
- name="trace-viewer-search"
447
- type="text"
485
+ id='trace-viewer-search'
486
+ name='trace-viewer-search'
487
+ type='text'
448
488
  value={searchQuery}
449
489
  onChange={(e) => setSearchQuery(e.target.value)}
450
- placeholder="Search spans..."
451
- aria-label="Search spans"
452
- className="flex-1 min-w-0 bg-transparent text-sm text-gray-1000 placeholder:text-gray-800 outline-none"
490
+ placeholder='Search spans...'
491
+ aria-label='Search spans'
492
+ className='flex-1 min-w-0 bg-transparent text-sm text-gray-1000 placeholder:text-gray-800 outline-none'
453
493
  />
454
494
  {searchQuery && (
455
495
  <button
456
- type="button"
496
+ type='button'
457
497
  onClick={() => setSearchQuery('')}
458
- className="shrink-0 p-0.5 rounded-sm text-gray-800 hover:text-gray-1000 hover:bg-gray-200 transition-colors"
498
+ className='shrink-0 p-0.5 rounded-sm text-gray-800 hover:text-gray-1000 hover:bg-gray-200 transition-colors'
459
499
  >
460
- <X className="w-3 h-3" />
500
+ <X className='w-3 h-3' />
461
501
  </button>
462
502
  )}
463
503
  </div>
464
504
  }
465
505
  endHeader={
466
- <TimelineHeader
467
- viewStart={viewport.start}
468
- viewDuration={viewDuration}
469
- rootStart={root.startTime}
470
- compression={compression}
471
- isZoomed={isZoomed}
472
- onResetZoom={resetZoom}
473
- hoverInfo={hoverInfo}
474
- />
506
+ <TimelineHeader markers={timeMarkers} hoverInfo={hoverInfo} />
475
507
  }
476
508
  >
477
- <div className="block overflow-visible">
509
+ <div className='block overflow-visible'>
478
510
  <EventList
479
511
  spans={filteredSpans}
480
512
  activeSpanId={activeSpanId}
@@ -483,14 +515,16 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
483
515
  </div>
484
516
  <div
485
517
  ref={timelineRef}
486
- className="block min-h-0 overflow-visible relative px-2"
518
+ className='block min-h-0 overflow-visible relative'
487
519
  onDoubleClick={resetZoom}
488
520
  onMouseMove={handleTimelineMouseMove}
489
521
  onMouseLeave={handleTimelineMouseLeave}
490
522
  >
491
523
  <Timeline
492
524
  spans={filteredSpans}
493
- compression={compression}
525
+ viewStart={viewport.start}
526
+ viewEnd={viewport.end}
527
+ markers={timeMarkers}
494
528
  selectedId={activeSpanId}
495
529
  onSelect={handleSelectSpan}
496
530
  hoverFraction={hoverFraction}
@@ -498,20 +532,46 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
498
532
  />
499
533
  </div>
500
534
  </SplitPane>
535
+ <div className='absolute right-3 bottom-3 z-[5] flex items-center border border-gray-alpha-400 rounded-lg bg-background-100 shadow-sm overflow-hidden divide-x divide-gray-alpha-400'>
536
+ <button
537
+ type='button'
538
+ className='flex items-center justify-center w-8 h-8 text-gray-900 cursor-pointer transition-colors duration-[120ms] ease-in-out hover:text-gray-1000 hover:bg-gray-alpha-100'
539
+ onClick={zoomOut}
540
+ aria-label='Zoom out'
541
+ >
542
+ <ZoomOut className='w-4 h-4' />
543
+ </button>
544
+ <button
545
+ type='button'
546
+ className='flex items-center justify-center w-8 h-8 text-gray-900 cursor-pointer transition-colors duration-[120ms] ease-in-out hover:text-gray-1000 hover:bg-gray-alpha-100'
547
+ onClick={resetZoom}
548
+ aria-label='Reset zoom'
549
+ >
550
+ <RotateCcw className='w-3.5 h-3.5' />
551
+ </button>
552
+ <button
553
+ type='button'
554
+ className='flex items-center justify-center w-8 h-8 text-gray-900 cursor-pointer transition-colors duration-[120ms] ease-in-out hover:text-gray-1000 hover:bg-gray-alpha-100'
555
+ onClick={zoomIn}
556
+ aria-label='Zoom in'
557
+ >
558
+ <ZoomIn className='w-4 h-4' />
559
+ </button>
560
+ </div>
501
561
  </div>
502
562
 
503
563
  {/* Detail panel */}
504
564
  {activeSpan && sidebar ? (
505
- <aside className="flex flex-col h-full max-h-full bg-background-100 border-l border-gray-alpha-400 overflow-auto">
565
+ <aside className='flex flex-col h-full max-h-full bg-background-100 border-l border-gray-alpha-400 overflow-auto'>
506
566
  {/* Panel header */}
507
- <div className="flex-shrink-0 px-4 pt-4 pb-3">
508
- <div className="flex items-start justify-between gap-2">
509
- <div className="min-w-0 flex-1">
510
- <span className="text-[15px] font-semibold text-gray-1000 truncate block">
567
+ <div className='flex-shrink-0 px-4 pt-4 pb-3'>
568
+ <div className='flex items-start justify-between gap-2'>
569
+ <div className='min-w-0 flex-1'>
570
+ <span className='text-[15px] font-semibold text-gray-1000 truncate block'>
511
571
  {selectedSpanName}
512
572
  </span>
513
573
  {selectedResourceId && (
514
- <div className="mt-1 flex items-center gap-2">
574
+ <div className='mt-1 flex items-center gap-2'>
515
575
  {selectedResource && (
516
576
  <span
517
577
  className={`inline-flex items-center rounded-md px-1.5 py-0.5 text-[11px] font-medium leading-none shrink-0 ${
@@ -529,30 +589,30 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
529
589
  </span>
530
590
  )}
531
591
  <div
532
- className="flex items-center gap-1 text-[13px] font-mono text-gray-700 min-w-0"
592
+ className='flex items-center gap-1 text-[13px] font-mono text-gray-700 min-w-0'
533
593
  title={selectedResourceId}
534
594
  >
535
- <span className="truncate">{selectedResourceId}</span>
595
+ <span className='truncate'>{selectedResourceId}</span>
536
596
  <CopyButton
537
597
  copyText={selectedResourceId}
538
- ariaLabel="Copy ID"
539
- className="shrink-0"
598
+ ariaLabel='Copy ID'
599
+ className='shrink-0'
540
600
  />
541
601
  </div>
542
602
  </div>
543
603
  )}
544
604
  </div>
545
605
  <button
546
- type="button"
547
- className="p-1 rounded-md text-gray-900 hover:text-gray-1000 hover:bg-gray-alpha-200 transition-colors shrink-0"
606
+ type='button'
607
+ className='p-1 rounded-md text-gray-900 hover:text-gray-1000 hover:bg-gray-alpha-200 transition-colors shrink-0'
548
608
  onClick={clearActiveSpan}
549
609
  >
550
- <X className="w-4 h-4" />
610
+ <X className='w-4 h-4' />
551
611
  </button>
552
612
  </div>
553
613
  </div>
554
614
  {/* Panel body */}
555
- <div className="flex-1 overflow-y-auto">
615
+ <div className='flex-1 overflow-y-auto'>
556
616
  <ErrorBoundary>
557
617
  <EntityDetailPanel
558
618
  run={sidebar.run}
@@ -30,33 +30,11 @@ export function computeRootBounds(spans: Span[]): RootBounds {
30
30
  return { startTime: minStart, endTime: maxEnd, duration };
31
31
  }
32
32
 
33
- // ---------------------------------------------------------------------------
34
- // Time compression
35
- // ---------------------------------------------------------------------------
36
-
37
- export interface TimeCompression {
38
- toVisual(time: number): number;
39
- fromVisual(fraction: number): number;
40
- isCompressed: boolean;
41
- }
42
-
43
- export function buildTimeCompression(
44
- _spans: Span[],
45
- viewStart: number,
46
- viewEnd: number
47
- ): TimeCompression {
48
- const range = viewEnd - viewStart;
49
-
50
- return {
51
- isCompressed: false,
52
- toVisual(time: number): number {
53
- if (range <= 0) return 0;
54
- return Math.min(Math.max((time - viewStart) / range, 0), 1);
55
- },
56
- fromVisual(fraction: number): number {
57
- return viewStart + fraction * range;
58
- },
59
- };
33
+ export function getSpanDurationMs(span: Span): number {
34
+ return Math.max(
35
+ 0,
36
+ getHighResInMs(span.endTime) - getHighResInMs(span.startTime)
37
+ );
60
38
  }
61
39
 
62
40
  // ---------------------------------------------------------------------------
@@ -66,6 +44,7 @@ export function buildTimeCompression(
66
44
  export interface TimeMarker {
67
45
  position: number;
68
46
  label: string;
47
+ value: number;
69
48
  }
70
49
 
71
50
  const NICE_INTERVALS = [
@@ -102,36 +81,7 @@ export function computeTimeMarkers(
102
81
  markers.push({
103
82
  position: Math.min(Math.max(position, 0), 1),
104
83
  label: formatDuration(Math.abs(t), true),
105
- });
106
- if (markers.length >= MAX_MARKERS) break;
107
- }
108
-
109
- return markers;
110
- }
111
-
112
- export function computeCompressedTimeMarkers(
113
- compression: TimeCompression,
114
- viewStart: number,
115
- viewEnd: number,
116
- rootStart: number
117
- ): TimeMarker[] {
118
- const viewDuration = viewEnd - viewStart;
119
- if (viewDuration <= 0) return [];
120
-
121
- const maxTicks = 6;
122
- const interval = pickInterval(viewDuration, maxTicks);
123
- const offset = viewStart - rootStart;
124
-
125
- const firstTick = Math.ceil(offset / interval) * interval;
126
- const markers: TimeMarker[] = [];
127
-
128
- for (let t = firstTick; t <= offset + viewDuration; t += interval) {
129
- const absTime = rootStart + t;
130
- const position = compression.toVisual(absTime);
131
- if (position < -0.01 || position > 1.01) continue;
132
- markers.push({
133
- position: Math.min(Math.max(position, 0), 1),
134
- label: formatDuration(Math.abs(t), true),
84
+ value: t,
135
85
  });
136
86
  if (markers.length >= MAX_MARKERS) break;
137
87
  }
@@ -152,8 +102,12 @@ export interface SpanGap {
152
102
 
153
103
  export function computeSpanGaps(
154
104
  spans: Span[],
155
- compression: TimeCompression
105
+ viewStart: number,
106
+ viewEnd: number
156
107
  ): SpanGap[] {
108
+ const range = viewEnd - viewStart;
109
+ if (range <= 0) return [];
110
+
157
111
  const gaps: SpanGap[] = [];
158
112
  for (let i = 0; i < spans.length - 1; i++) {
159
113
  const endTime = getHighResInMs(spans[i].endTime);
@@ -161,8 +115,8 @@ export function computeSpanGaps(
161
115
  const gapMs = startTime - endTime;
162
116
  if (gapMs <= 0) continue;
163
117
 
164
- const leftFrac = compression.toVisual(endTime);
165
- const rightFrac = compression.toVisual(startTime);
118
+ const leftFrac = Math.min(Math.max((endTime - viewStart) / range, 0), 1);
119
+ const rightFrac = Math.min(Math.max((startTime - viewStart) / range, 0), 1);
166
120
  if (rightFrac - leftFrac < 0.001) continue;
167
121
 
168
122
  gaps.push({ gapMs, leftFrac, rightFrac, rowIndex: i });
@@ -379,39 +333,12 @@ function computeHookSegmentsFromSpan(
379
333
  }
380
334
 
381
335
  function computeSleepSegmentsFromSpan(
382
- startMs: number,
336
+ _startMs: number,
383
337
  duration: number,
384
- events: SpanEvent[]
338
+ _events: SpanEvent[]
385
339
  ): Segment[] {
386
- const segments: Segment[] = [];
387
- if (duration <= 0) return segments;
388
-
389
- const completedTime = events
390
- .filter((e) => e.name === 'wait_completed')
391
- .map((e) => getHighResInMs(e.timestamp))
392
- .sort((a, b) => a - b)[0];
393
-
394
- if (completedTime === undefined) {
395
- segments.push({ startFraction: 0, endFraction: 1, status: 'sleeping' });
396
- return segments;
397
- }
398
-
399
- const completedFrac = timeToFraction(completedTime, startMs, duration);
400
- if (completedFrac > 0.001) {
401
- segments.push({
402
- startFraction: 0,
403
- endFraction: completedFrac,
404
- status: 'sleeping',
405
- });
406
- }
407
-
408
- segments.push({
409
- startFraction: completedFrac,
410
- endFraction: 1,
411
- status: 'succeeded',
412
- });
413
-
414
- return segments;
340
+ if (duration <= 0) return [];
341
+ return [{ startFraction: 0, endFraction: 1, status: 'sleeping' }];
415
342
  }
416
343
 
417
344
  function computeRunSegmentsFromSpan(
@@ -497,8 +424,7 @@ function computeV1RunSegments(
497
424
  */
498
425
  export function computeSpanSegments(span: Span): Segment[] {
499
426
  const startMs = getHighResInMs(span.startTime);
500
- const endMs = getHighResInMs(span.endTime);
501
- const duration = endMs - startMs;
427
+ const duration = getSpanDurationMs(span);
502
428
  const activeStartMs = span.activeStartTime
503
429
  ? getHighResInMs(span.activeStartTime)
504
430
  : undefined;
@@ -14,22 +14,18 @@ export function getMsInHighRes(ms: number): [number, number] {
14
14
  }
15
15
 
16
16
  /**
17
- * Formats a duration for timeline display (compact single-unit format).
17
+ * Formats a duration for timeline display (compact format).
18
18
  * @deprecated Use formatDuration(ms, true) instead.
19
19
  */
20
20
  export const formatDurationForTimeline = (ms: number): string =>
21
21
  formatDuration(ms, true);
22
22
 
23
- const timeSelectionFormatter = new Intl.NumberFormat(undefined, {
24
- minimumFractionDigits: 2,
25
- maximumFractionDigits: 2,
26
- });
27
-
28
23
  export function formatTimeSelection(ms: number): string {
29
- if (ms >= 1000) {
30
- return `${timeSelectionFormatter.format(ms / 1000)}s`;
24
+ const roundedMs = Math.round(ms);
25
+ if (roundedMs < 1000) {
26
+ return `${roundedMs}ms`;
31
27
  }
32
- return `${timeSelectionFormatter.format(ms)}ms`;
28
+ return `${Math.round(ms / 1000)}s`;
33
29
  }
34
30
 
35
31
  /**
@@ -59,7 +59,7 @@ export const waitEventsToWaitEntity = (
59
59
  waitId: string;
60
60
  runId: string;
61
61
  createdAt: Date;
62
- resumeAt: Date;
62
+ resumeAt?: Date;
63
63
  completedAt?: Date;
64
64
  } | null => {
65
65
  const startEvent = events.find((event) => event.eventType === 'wait_created');
@@ -73,7 +73,9 @@ export const waitEventsToWaitEntity = (
73
73
  waitId: startEvent.correlationId,
74
74
  runId: startEvent.runId,
75
75
  createdAt: startEvent.createdAt,
76
- resumeAt: startEvent.eventData?.resumeAt,
76
+ resumeAt: startEvent.eventData?.resumeAt
77
+ ? new Date(startEvent.eventData.resumeAt)
78
+ : undefined,
77
79
  completedAt: completedEvent?.createdAt,
78
80
  };
79
81
  };
@@ -81,13 +83,29 @@ export const waitEventsToWaitEntity = (
81
83
  /**
82
84
  * Converts a workflow Wait to an OpenTelemetry Span
83
85
  */
84
- export function waitToSpan(events: Event[], maxEndTime: Date): Span | null {
86
+ export function waitToSpan(
87
+ events: Event[],
88
+ maxEndTime: Date,
89
+ fallbackEndTime = maxEndTime
90
+ ): Span | null {
85
91
  const wait = waitEventsToWaitEntity(events);
86
92
  if (!wait) {
87
93
  return null;
88
94
  }
89
95
  const startTime = wait.createdAt;
90
- const endTime = wait.completedAt ?? maxEndTime;
96
+ const startMs = startTime.getTime();
97
+ let endTime = wait.completedAt;
98
+ if (!endTime) {
99
+ const fallbackCap =
100
+ wait.resumeAt && wait.resumeAt.getTime() < fallbackEndTime.getTime()
101
+ ? wait.resumeAt
102
+ : fallbackEndTime;
103
+ endTime =
104
+ maxEndTime.getTime() > startMs &&
105
+ maxEndTime.getTime() < fallbackCap.getTime()
106
+ ? maxEndTime
107
+ : fallbackCap;
108
+ }
91
109
  const start = dateToOtelTime(startTime);
92
110
  const end = dateToOtelTime(endTime);
93
111
  const duration = calculateDuration(startTime, endTime);
@@ -117,6 +117,7 @@ function buildSpans(
117
117
  // unknown territory. Even when the run is completed, we may not have loaded
118
118
  // all events yet, so always use the latest event we've actually seen.
119
119
  const childMaxEnd = latestKnownTime;
120
+ const runMaxEnd = run.completedAt ?? now;
120
121
 
121
122
  const stepSpans = Array.from(groupedEvents.eventsByStepId.values())
122
123
  .map((events) => stepToSpan(events, childMaxEnd))
@@ -127,7 +128,7 @@ function buildSpans(
127
128
  .filter((span): span is Span => span !== null);
128
129
 
129
130
  const waitSpans = Array.from(groupedEvents.timerEvents.values())
130
- .map((events) => waitToSpan(events, childMaxEnd))
131
+ .map((events) => waitToSpan(events, childMaxEnd, runMaxEnd))
131
132
  .filter((span): span is Span => span !== null);
132
133
 
133
134
  return {