@workflow/web-shared 5.0.0-beta.41 → 5.0.0-beta.43

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 (96) hide show
  1. package/dist/components/event-list-view.d.ts +10 -2
  2. package/dist/components/event-list-view.d.ts.map +1 -1
  3. package/dist/components/event-list-view.js +52 -26
  4. package/dist/components/sidebar/attributes-block.d.ts.map +1 -1
  5. package/dist/components/sidebar/attributes-block.js +3 -6
  6. package/dist/components/sidebar/conversation-view.d.ts.map +1 -1
  7. package/dist/components/sidebar/conversation-view.js +19 -36
  8. package/dist/components/sidebar/entity-detail-panel.d.ts +5 -0
  9. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  10. package/dist/components/sidebar/entity-detail-panel.js +2 -2
  11. package/dist/components/sidebar/events-list.d.ts +7 -1
  12. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  13. package/dist/components/sidebar/events-list.js +6 -5
  14. package/dist/components/sidebar/resolve-hook-modal.d.ts.map +1 -1
  15. package/dist/components/sidebar/resolve-hook-modal.js +4 -145
  16. package/dist/components/sidebar/sidebar-data-context.d.ts +6 -0
  17. package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -1
  18. package/dist/components/sidebar/sidebar-data-context.js +1 -1
  19. package/dist/components/stream-viewer-skeleton.js +6 -6
  20. package/dist/components/stream-viewer.js +2 -2
  21. package/dist/components/trace-viewer/components/detail-panel.d.ts.map +1 -1
  22. package/dist/components/trace-viewer/components/detail-panel.js +2 -1
  23. package/dist/components/trace-viewer/components/middle-truncate/measurement-batch.d.ts +9 -0
  24. package/dist/components/trace-viewer/components/middle-truncate/measurement-batch.d.ts.map +1 -0
  25. package/dist/components/trace-viewer/components/middle-truncate/measurement-batch.js +29 -0
  26. package/dist/components/trace-viewer/components/middle-truncate/middle-truncate.d.ts.map +1 -1
  27. package/dist/components/trace-viewer/components/middle-truncate/middle-truncate.js +16 -8
  28. package/dist/components/trace-viewer/components/middle-truncate/use-middle-truncate.d.ts.map +1 -1
  29. package/dist/components/trace-viewer/components/middle-truncate/use-middle-truncate.js +83 -19
  30. package/dist/components/trace-viewer/components/timeline.d.ts.map +1 -1
  31. package/dist/components/trace-viewer/components/timeline.js +13 -22
  32. package/dist/components/trace-viewer/components/trace-viewer-skeleton.d.ts.map +1 -1
  33. package/dist/components/trace-viewer/components/trace-viewer-skeleton.js +58 -18
  34. package/dist/components/trace-viewer/icons.js +5 -5
  35. package/dist/components/trace-viewer/utils.d.ts +6 -10
  36. package/dist/components/trace-viewer/utils.d.ts.map +1 -1
  37. package/dist/components/trace-viewer/utils.js +14 -24
  38. package/dist/components/trace-viewer.d.ts.map +1 -1
  39. package/dist/components/trace-viewer.js +12 -4
  40. package/dist/components/ui/context-card.d.ts.map +1 -1
  41. package/dist/components/ui/context-card.js +9 -21
  42. package/dist/components/ui/data-inspector.d.ts.map +1 -1
  43. package/dist/components/ui/data-inspector.js +8 -35
  44. package/dist/components/ui/duplicate-event-tooltip.d.ts +15 -0
  45. package/dist/components/ui/duplicate-event-tooltip.d.ts.map +1 -0
  46. package/dist/components/ui/duplicate-event-tooltip.js +19 -0
  47. package/dist/components/ui/menu-dropdown.d.ts.map +1 -1
  48. package/dist/components/ui/menu-dropdown.js +3 -19
  49. package/dist/components/ui/skeleton.js +2 -2
  50. package/dist/components/ui/spinner.d.ts.map +1 -1
  51. package/dist/components/ui/spinner.js +5 -11
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +2 -1
  55. package/dist/lib/duplicate-events.d.ts +27 -0
  56. package/dist/lib/duplicate-events.d.ts.map +1 -0
  57. package/dist/lib/duplicate-events.js +166 -0
  58. package/dist/lib/event-materialization.d.ts +15 -2
  59. package/dist/lib/event-materialization.d.ts.map +1 -1
  60. package/dist/lib/event-materialization.js +19 -4
  61. package/dist/lib/trace-builder.d.ts +16 -1
  62. package/dist/lib/trace-builder.d.ts.map +1 -1
  63. package/dist/lib/trace-builder.js +23 -4
  64. package/package.json +4 -4
  65. package/src/components/event-list-view.tsx +70 -32
  66. package/src/components/sidebar/attributes-block.tsx +4 -11
  67. package/src/components/sidebar/conversation-view.tsx +25 -79
  68. package/src/components/sidebar/entity-detail-panel.tsx +6 -0
  69. package/src/components/sidebar/events-list.tsx +21 -3
  70. package/src/components/sidebar/resolve-hook-modal.tsx +23 -186
  71. package/src/components/sidebar/sidebar-data-context.tsx +6 -0
  72. package/src/components/stream-viewer-skeleton.tsx +5 -5
  73. package/src/components/stream-viewer.tsx +1 -1
  74. package/src/components/trace-viewer/components/detail-panel.tsx +1 -0
  75. package/src/components/trace-viewer/components/middle-truncate/measurement-batch.ts +47 -0
  76. package/src/components/trace-viewer/components/middle-truncate/middle-truncate.tsx +15 -7
  77. package/src/components/trace-viewer/components/middle-truncate/use-middle-truncate.ts +132 -38
  78. package/src/components/trace-viewer/components/timeline.tsx +21 -39
  79. package/src/components/trace-viewer/components/trace-viewer-skeleton.tsx +63 -35
  80. package/src/components/trace-viewer/icons.tsx +4 -4
  81. package/src/components/trace-viewer/utils.test.ts +5 -7
  82. package/src/components/trace-viewer/utils.ts +17 -31
  83. package/src/components/trace-viewer.tsx +15 -3
  84. package/src/components/ui/context-card.tsx +16 -26
  85. package/src/components/ui/data-inspector.tsx +20 -65
  86. package/src/components/ui/duplicate-event-tooltip.tsx +44 -0
  87. package/src/components/ui/menu-dropdown.tsx +8 -22
  88. package/src/components/ui/skeleton.tsx +2 -2
  89. package/src/components/ui/spinner.tsx +4 -8
  90. package/src/index.ts +4 -0
  91. package/src/lib/duplicate-events.test.ts +339 -0
  92. package/src/lib/duplicate-events.ts +188 -0
  93. package/src/lib/event-materialization.test.ts +117 -0
  94. package/src/lib/event-materialization.ts +23 -3
  95. package/src/lib/trace-builder.test.ts +74 -0
  96. package/src/lib/trace-builder.ts +30 -3
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { ArrowLeft, ArrowRight } from 'lucide-react';
4
- import type { CSSProperties, ReactNode } from 'react';
4
+ import type { ReactNode } from 'react';
5
5
  import {
6
6
  Fragment,
7
7
  memo,
@@ -28,7 +28,7 @@ import {
28
28
  computeSpanGaps,
29
29
  computeSpanMarkers,
30
30
  computeSpanSegments,
31
- getResourceColor,
31
+ getResourceClassNames,
32
32
  getSpanDurationMs,
33
33
  isSpanErrored,
34
34
  } from '../utils';
@@ -62,11 +62,6 @@ const SEGMENT_CLASSES: Record<SegmentStatus, string> = {
62
62
  received: 'bg-blue-200 border border-blue-500',
63
63
  };
64
64
 
65
- const TIMELINE_INSET_STYLE: CSSProperties = {
66
- left: TIMELINE_PADDING_PX,
67
- right: TIMELINE_PADDING_PX,
68
- };
69
-
70
65
  const STRIPED_SEGMENT_STATUSES: ReadonlySet<SegmentStatus> = new Set([
71
66
  'pending',
72
67
  'running',
@@ -260,18 +255,18 @@ function BoundaryArrow({
260
255
  }
261
256
 
262
257
  function PlainBar({
263
- bg,
264
- border,
258
+ className,
265
259
  label,
266
260
  }: {
267
- bg: string;
268
- border: string;
261
+ className: string;
269
262
  label: string | null;
270
263
  }): ReactNode {
271
264
  return (
272
265
  <div
273
- className="relative h-6 w-full min-w-1 rounded-[0.25rem] border"
274
- style={{ background: bg, borderColor: border }}
266
+ className={cn(
267
+ 'relative h-6 w-full min-w-1 rounded-[0.25rem] border',
268
+ className
269
+ )}
275
270
  >
276
271
  {label ? <DurationLabel label={label} /> : null}
277
272
  </div>
@@ -352,14 +347,13 @@ function SegmentBar({
352
347
  <div
353
348
  key={i}
354
349
  className={cn(
355
- 'absolute h-full overflow-hidden rounded-[0.25rem]',
350
+ 'absolute h-full min-w-px overflow-hidden rounded-[0.25rem]',
356
351
  SEGMENT_CLASSES[seg.status]
357
352
  )}
358
353
  style={{
359
354
  // 1px gap between adjacent segments, distributed equally.
360
355
  left: `calc(${seg.leftPct}% + 0.5px)`,
361
356
  width: `calc(${seg.widthPct}% - 1px)`,
362
- minWidth: 1,
363
357
  }}
364
358
  >
365
359
  {STRIPED_SEGMENT_STATUSES.has(seg.status) ? (
@@ -457,13 +451,10 @@ const TimelineBar = memo(function TimelineBar({
457
451
  markers.length > 0 || offscreen.left !== null || offscreen.right !== null;
458
452
 
459
453
  const isErrored = isSpanErrored(span);
460
- const colors = getResourceColor(span.resource);
461
- const fallbackBg = isErrored
462
- ? (colors.errorBg ?? 'var(--ds-red-200)')
463
- : colors.bg;
464
- const fallbackBorder = isErrored
465
- ? (colors.errorBorder ?? 'var(--ds-red-500)')
466
- : colors.border;
454
+ const colors = getResourceClassNames(span.resource);
455
+ const fallbackClassName = isErrored
456
+ ? (colors.errorClassName ?? 'border-red-500 bg-red-200')
457
+ : colors.className;
467
458
 
468
459
  const totalLabel = formatDurationPrecise(totalDurationMs);
469
460
  const showTotalLabel =
@@ -486,7 +477,7 @@ const TimelineBar = memo(function TimelineBar({
486
477
  )}
487
478
  onClick={handleClick}
488
479
  >
489
- <div className="absolute inset-y-0" style={TIMELINE_INSET_STYLE}>
480
+ <div className="absolute inset-x-4 inset-y-0">
490
481
  <div
491
482
  className="absolute top-1/2 h-6 -translate-y-1/2 overflow-hidden rounded-[0.25rem]"
492
483
  style={getBarPositionStyle(geometry)}
@@ -495,15 +486,13 @@ const TimelineBar = memo(function TimelineBar({
495
486
  <BoundaryArrow direction={geometry.mode.direction} />
496
487
  ) : geometry.mode.kind === 'tiny' ? (
497
488
  <div
498
- className="h-6 rounded-[0.25rem] border"
499
- style={{ background: fallbackBg, borderColor: fallbackBorder }}
489
+ className={cn('h-6 rounded-[0.25rem] border', fallbackClassName)}
500
490
  />
501
491
  ) : segments.length > 0 ? (
502
492
  <SegmentBar segments={segments} showLabels={!hasMarkers} />
503
493
  ) : (
504
494
  <PlainBar
505
- bg={fallbackBg}
506
- border={fallbackBorder}
495
+ className={fallbackClassName}
507
496
  label={showTotalLabel ? totalLabel : null}
508
497
  />
509
498
  )}
@@ -612,10 +601,9 @@ const DeltaMeasureLine = memo(function DeltaMeasureLine({
612
601
  return (
613
602
  <>
614
603
  <div
615
- className="absolute h-px bg-amber-800"
604
+ className="absolute h-px w-1 bg-amber-800"
616
605
  style={{
617
606
  left: Math.min(anchorX, guideX),
618
- width: MEASURE_GUIDE_OUTSET_PX,
619
607
  top: anchorCenterY,
620
608
  }}
621
609
  />
@@ -781,8 +769,7 @@ export function Timeline({
781
769
  >
782
770
  <div
783
771
  aria-hidden
784
- className="absolute inset-y-0 pointer-events-none"
785
- style={TIMELINE_INSET_STYLE}
772
+ className="pointer-events-none absolute inset-x-4 inset-y-0"
786
773
  >
787
774
  {markers.map((marker) =>
788
775
  // Skip the "0s" origin marker since the left edge already implies it.
@@ -796,10 +783,7 @@ export function Timeline({
796
783
  )}
797
784
  </div>
798
785
  {hover != null && (
799
- <div
800
- className="absolute inset-y-0 pointer-events-none z-10"
801
- style={TIMELINE_INSET_STYLE}
802
- >
786
+ <div className="pointer-events-none absolute inset-x-4 inset-y-0 z-10">
803
787
  <div
804
788
  className="absolute top-0 bottom-0 w-px bg-gray-alpha-500"
805
789
  style={{ left: `${hover.fraction * 100}%` }}
@@ -824,8 +808,7 @@ export function Timeline({
824
808
  {altHeld && anchorIndex < 0 && (
825
809
  <div
826
810
  aria-hidden
827
- className="absolute inset-y-0 pointer-events-none"
828
- style={TIMELINE_INSET_STYLE}
811
+ className="pointer-events-none absolute inset-x-4 inset-y-0"
829
812
  >
830
813
  {gapMeasurements.map((gap) => (
831
814
  <DeltaMeasureLine
@@ -839,8 +822,7 @@ export function Timeline({
839
822
  {measurement && (
840
823
  <div
841
824
  aria-hidden
842
- className="absolute inset-y-0 pointer-events-none z-20"
843
- style={TIMELINE_INSET_STYLE}
825
+ className="pointer-events-none absolute inset-x-4 inset-y-0 z-20"
844
826
  >
845
827
  <DeltaMeasureLine {...measurement} timelineWidth={timelineWidth} />
846
828
  </div>
@@ -1,19 +1,62 @@
1
1
  import { Skeleton } from '../../ui/skeleton';
2
- import { DEFAULT_START_PX, paneColTemplate } from './pane-constants';
3
2
 
4
- // Mirrors SplitPane's initial column template so the skeleton lines up with
5
- // the real viewer's first paint.
6
- const COL_TEMPLATE = paneColTemplate(DEFAULT_START_PX);
7
-
8
- const ROWS: { id: string; name: number; off: number; bar: number }[] = [
9
- { id: 'r0', name: 62, off: 0, bar: 72 },
10
- { id: 'r1', name: 78, off: 6, bar: 48 },
11
- { id: 'r2', name: 50, off: 10, bar: 55 },
12
- { id: 'r3', name: 84, off: 18, bar: 30 },
13
- { id: 'r4', name: 45, off: 18, bar: 42 },
14
- { id: 'r5', name: 66, off: 34, bar: 38 },
15
- { id: 'r6', name: 55, off: 41, bar: 25 },
16
- { id: 'r7', name: 40, off: 50, bar: 30 },
3
+ const ROWS = [
4
+ {
5
+ id: 'r0',
6
+ nameClassName: 'w-[62%]',
7
+ offsetClassName: 'left-0',
8
+ barClassName: 'w-[72%]',
9
+ topClassName: 'top-0',
10
+ },
11
+ {
12
+ id: 'r1',
13
+ nameClassName: 'w-[78%]',
14
+ offsetClassName: 'left-[6%]',
15
+ barClassName: 'w-[48%]',
16
+ topClassName: 'top-[3.5px]',
17
+ },
18
+ {
19
+ id: 'r2',
20
+ nameClassName: 'w-1/2',
21
+ offsetClassName: 'left-[10%]',
22
+ barClassName: 'w-[55%]',
23
+ topClassName: 'top-[7px]',
24
+ },
25
+ {
26
+ id: 'r3',
27
+ nameClassName: 'w-[84%]',
28
+ offsetClassName: 'left-[18%]',
29
+ barClassName: 'w-[30%]',
30
+ topClassName: 'top-[10.5px]',
31
+ },
32
+ {
33
+ id: 'r4',
34
+ nameClassName: 'w-[45%]',
35
+ offsetClassName: 'left-[18%]',
36
+ barClassName: 'w-[42%]',
37
+ topClassName: 'top-[14px]',
38
+ },
39
+ {
40
+ id: 'r5',
41
+ nameClassName: 'w-[66%]',
42
+ offsetClassName: 'left-[34%]',
43
+ barClassName: 'w-[38%]',
44
+ topClassName: 'top-[17.5px]',
45
+ },
46
+ {
47
+ id: 'r6',
48
+ nameClassName: 'w-[55%]',
49
+ offsetClassName: 'left-[41%]',
50
+ barClassName: 'w-1/4',
51
+ topClassName: 'top-[21px]',
52
+ },
53
+ {
54
+ id: 'r7',
55
+ nameClassName: 'w-2/5',
56
+ offsetClassName: 'left-1/2',
57
+ barClassName: 'w-[30%]',
58
+ topClassName: 'top-[24.5px]',
59
+ },
17
60
  ];
18
61
 
19
62
  const HEADER_MARKERS = ['m0', 'm1', 'm2', 'm3'];
@@ -36,25 +79,17 @@ export function TraceViewerSkeleton() {
36
79
  {/* Minimap strip: thin density lines tracing the same shape as the bars */}
37
80
  <div className="relative h-10 min-h-10 shrink-0 border-b border-gray-alpha-400">
38
81
  <div className="absolute inset-x-4 top-[6px]">
39
- {ROWS.map((row, index) => (
82
+ {ROWS.map((row) => (
40
83
  <Skeleton
41
84
  key={row.id}
42
- className="absolute h-[3px] rounded-full"
43
- style={{
44
- left: `${row.off}%`,
45
- width: `${row.bar}%`,
46
- top: index * 3.5,
47
- }}
85
+ className={`absolute h-[3px] rounded-full ${row.offsetClassName} ${row.barClassName} ${row.topClassName}`}
48
86
  />
49
87
  ))}
50
88
  </div>
51
89
  </div>
52
90
 
53
91
  {/* Header row: search header | divider | timeline header */}
54
- <div
55
- className="shrink-0 grid"
56
- style={{ gridTemplateColumns: COL_TEMPLATE }}
57
- >
92
+ <div className="grid shrink-0 grid-cols-[340px_1px_minmax(50px,1fr)]">
58
93
  <div className="h-10 min-h-10 flex items-center border-b border-gray-alpha-400 pl-4 pr-2 gap-1.5">
59
94
  <Skeleton className="w-3.5 h-3.5 shrink-0 rounded-sm" />
60
95
  <Skeleton className="h-3.5 w-40" />
@@ -70,10 +105,7 @@ export function TraceViewerSkeleton() {
70
105
  </div>
71
106
 
72
107
  {/* Content row: event list | gutter | timeline */}
73
- <div
74
- className="grid flex-1 min-h-0 overflow-hidden"
75
- style={{ gridTemplateColumns: COL_TEMPLATE }}
76
- >
108
+ <div className="grid min-h-0 flex-1 grid-cols-[340px_1px_minmax(50px,1fr)] overflow-hidden">
77
109
  {/* Sidebar event rows */}
78
110
  <div className="block overflow-visible">
79
111
  <ul className="block divide-y divide-gray-alpha-400 border-b border-gray-alpha-400">
@@ -81,10 +113,7 @@ export function TraceViewerSkeleton() {
81
113
  <li key={row.id} className="h-10 flex items-center pl-4 pr-2">
82
114
  <div className="flex min-w-0 flex-1 items-center gap-2">
83
115
  <Skeleton className="w-4 h-4 shrink-0 rounded-sm" />
84
- <Skeleton
85
- className="h-3.5"
86
- style={{ width: `${row.name}%` }}
87
- />
116
+ <Skeleton className={`h-3.5 ${row.nameClassName}`} />
88
117
  </div>
89
118
  <Skeleton className="ml-2 h-3.5 w-10 shrink-0" />
90
119
  </li>
@@ -101,8 +130,7 @@ export function TraceViewerSkeleton() {
101
130
  <div key={row.id} className="relative h-10">
102
131
  <div className="absolute inset-x-4 inset-y-0">
103
132
  <Skeleton
104
- className="absolute top-1/2 -translate-y-1/2 h-6 rounded-[0.25rem]"
105
- style={{ left: `${row.off}%`, width: `${row.bar}%` }}
133
+ className={`absolute top-1/2 h-6 -translate-y-1/2 rounded-[0.25rem] ${row.offsetClassName} ${row.barClassName}`}
106
134
  />
107
135
  </div>
108
136
  </div>
@@ -1,10 +1,10 @@
1
1
  const WorkflowIcon = () => {
2
2
  return (
3
3
  <svg
4
+ aria-hidden="true"
4
5
  data-testid="geist-icon"
5
6
  height="16"
6
7
  strokeLinejoin="round"
7
- style={{ color: 'currentColor' }}
8
8
  viewBox="0 0 16 16"
9
9
  width="16"
10
10
  >
@@ -19,12 +19,12 @@ const WorkflowIcon = () => {
19
19
  const WebhookIcon = () => {
20
20
  return (
21
21
  <svg
22
+ aria-hidden="true"
22
23
  data-testid="geist-icon"
23
24
  height="16"
24
25
  strokeLinejoin="round"
25
26
  viewBox="0 0 16 16"
26
27
  width="16"
27
- style={{ color: 'currentColor' }}
28
28
  >
29
29
  <path
30
30
  fillRule="evenodd"
@@ -39,12 +39,12 @@ const WebhookIcon = () => {
39
39
  const SleepIcon = () => {
40
40
  return (
41
41
  <svg
42
+ aria-hidden="true"
42
43
  data-testid="geist-icon"
43
44
  height="16"
44
45
  strokeLinejoin="round"
45
46
  viewBox="0 0 16 16"
46
47
  width="16"
47
- style={{ color: 'currentColor' }}
48
48
  >
49
49
  <path
50
50
  fillRule="evenodd"
@@ -59,12 +59,12 @@ const SleepIcon = () => {
59
59
  const StepForwardIcon = () => {
60
60
  return (
61
61
  <svg
62
+ aria-hidden="true"
62
63
  data-testid="geist-icon"
63
64
  height="16"
64
65
  strokeLinejoin="round"
65
66
  viewBox="0 0 16 16"
66
67
  width="16"
67
- style={{ color: 'currentColor' }}
68
68
  >
69
69
  <path
70
70
  fillRule="evenodd"
@@ -7,7 +7,7 @@ import {
7
7
  computeSpanMarkers,
8
8
  computeSpanSegments,
9
9
  computeTimeMarkers,
10
- getResourceColor,
10
+ getResourceClassNames,
11
11
  } from './utils';
12
12
 
13
13
  /** Build a high-res timestamp tuple ([seconds, nanoseconds]) for a given ms. */
@@ -248,13 +248,11 @@ describe('computeTimeMarkers', () => {
248
248
  });
249
249
  });
250
250
 
251
- describe('getResourceColor', () => {
251
+ describe('getResourceClassNames', () => {
252
252
  it('uses gray for hooks (passive spans), not amber', () => {
253
- expect(getResourceColor('hook')).toEqual({
254
- bg: 'var(--ds-gray-200)',
255
- border: 'var(--ds-gray-500)',
256
- errorBg: 'var(--ds-red-200)',
257
- errorBorder: 'var(--ds-red-500)',
253
+ expect(getResourceClassNames('hook')).toEqual({
254
+ className: 'border-gray-500 bg-gray-200',
255
+ errorClassName: 'border-red-500 bg-red-200',
258
256
  });
259
257
  });
260
258
  });
@@ -279,55 +279,41 @@ export function computeSpanDelta(
279
279
  // Resource colors
280
280
  // ---------------------------------------------------------------------------
281
281
 
282
- export const RESOURCE_COLORS: Record<
282
+ export const RESOURCE_CLASS_NAMES: Record<
283
283
  string,
284
284
  {
285
- bg: string;
286
- border: string;
287
- errorBg?: string;
288
- errorBorder?: string;
285
+ className: string;
286
+ errorClassName?: string;
289
287
  }
290
288
  > = {
291
289
  run: {
292
- bg: 'var(--ds-blue-200)',
293
- border: 'var(--ds-blue-500)',
294
- errorBg: 'var(--ds-red-200)',
295
- errorBorder: 'var(--ds-red-500)',
290
+ className: 'border-blue-500 bg-blue-200',
291
+ errorClassName: 'border-red-500 bg-red-200',
296
292
  },
297
293
  step: {
298
- bg: 'var(--ds-green-200)',
299
- border: 'var(--ds-green-500)',
300
- errorBg: 'var(--ds-red-200)',
301
- errorBorder: 'var(--ds-red-500)',
294
+ className: 'border-green-500 bg-green-200',
295
+ errorClassName: 'border-red-500 bg-red-200',
302
296
  },
303
297
  // Passive spans (hooks) stay gray — matches event-list icons and the minimap.
304
298
  hook: {
305
- bg: 'var(--ds-gray-200)',
306
- border: 'var(--ds-gray-500)',
307
- errorBg: 'var(--ds-red-200)',
308
- errorBorder: 'var(--ds-red-500)',
299
+ className: 'border-gray-500 bg-gray-200',
300
+ errorClassName: 'border-red-500 bg-red-200',
309
301
  },
310
302
  sleep: {
311
- bg: 'var(--ds-purple-200)',
312
- border: 'var(--ds-purple-500)',
313
- errorBg: 'var(--ds-red-200)',
314
- errorBorder: 'var(--ds-red-500)',
303
+ className: 'border-purple-500 bg-purple-200',
304
+ errorClassName: 'border-red-500 bg-red-200',
315
305
  },
316
306
  default: {
317
- bg: 'var(--ds-gray-200)',
318
- border: 'var(--ds-gray-500)',
319
- errorBg: 'var(--ds-red-200)',
320
- errorBorder: 'var(--ds-red-500)',
307
+ className: 'border-gray-500 bg-gray-200',
308
+ errorClassName: 'border-red-500 bg-red-200',
321
309
  },
322
310
  };
323
311
 
324
- export function getResourceColor(resource: string): {
325
- bg: string;
326
- border: string;
327
- errorBg?: string;
328
- errorBorder?: string;
312
+ export function getResourceClassNames(resource: string): {
313
+ className: string;
314
+ errorClassName?: string;
329
315
  } {
330
- return RESOURCE_COLORS[resource] ?? RESOURCE_COLORS.default;
316
+ return RESOURCE_CLASS_NAMES[resource] ?? RESOURCE_CLASS_NAMES.default;
331
317
  }
332
318
 
333
319
  // ---------------------------------------------------------------------------
@@ -30,16 +30,28 @@ const TraceViewer = ({
30
30
  if (!run?.runId) {
31
31
  return undefined;
32
32
  }
33
- return buildTrace(run, events, new Date());
33
+ // `hasMore` is the only place that knows whether more of the log is still
34
+ // to be fetched, and a repeat can only be told apart from the event it
35
+ // repeats with the whole log in hand.
36
+ return buildTrace(run, events, new Date(), {
37
+ isCompleteHistory: !hasMore,
38
+ });
34
39
  // eslint-disable-next-line react-hooks/exhaustive-deps -- `new Date()` is intentionally not a dep
35
- }, [run, events]);
40
+ }, [run, events, hasMore]);
41
+
42
+ // The sidebar shows one entity's slice of the log, so it takes the trace's
43
+ // answer rather than recomputing one from the slice.
44
+ const sidebarValue = useMemo(
45
+ () => ({ ...sidebarData, duplicateEventIds: trace?.duplicateEventIds }),
46
+ [sidebarData, trace]
47
+ );
36
48
 
37
49
  if (!trace || (loading && events.length === 0)) {
38
50
  return <TraceViewerSkeleton />;
39
51
  }
40
52
 
41
53
  return (
42
- <SidebarDataProvider value={sidebarData}>
54
+ <SidebarDataProvider value={sidebarValue}>
43
55
  <div className="relative w-full h-full flex">
44
56
  <TraceViewerComponent
45
57
  trace={trace}
@@ -267,17 +267,17 @@ export function ContextCardProvider({
267
267
  ref={ref}
268
268
  >
269
269
  <div
270
- className="min-w-max transition-all duration-150 ease-[easing-function:cubic-bezier(0.3,_0.57,_0.07,_0.95)] will-change-[transform,width,height] motion-reduce:!transition-none"
271
- style={{
272
- opacity: visible ? 1 : 0,
273
- }}
270
+ className={cn(
271
+ 'min-w-max transition-all duration-150 ease-[easing-function:cubic-bezier(0.3,_0.57,_0.07,_0.95)] will-change-[transform,width,height] motion-reduce:!transition-none',
272
+ visible ? 'opacity-100' : 'opacity-0'
273
+ )}
274
274
  >
275
275
  <div
276
276
  className={cn(
277
277
  'absolute bg-background-100 bg-clip-padding rounded-[6px] overflow-visible pointer-events-none z-[1000000] shadow-[var(--ds-shadow-tooltip),0_0_0_1px_var(--ds-background-100)] w-fit [--context-card-tip-stroke:#DBDBDB] dark-theme:[--context-card-tip-stroke:#252525]',
278
278
  skipTransition
279
279
  ? '!transition-none'
280
- : 'transition-[transform,width,height] duration-250 ease-[easing-function:cubic-bezier(0.29,0.31,0.05,0.96)] will-change-[transform,width,height] motion-reduce:!transition-none'
280
+ : 'transition-[transform,width,height] duration-[250ms] ease-[easing-function:cubic-bezier(0.29,0.31,0.05,0.96)] will-change-[transform,width,height] motion-reduce:!transition-none'
281
281
  )}
282
282
  data-skip-transition={skipTransition}
283
283
  style={{
@@ -301,7 +301,7 @@ export function ContextCardProvider({
301
301
  },
302
302
  skipTransition
303
303
  ? '!transition-none'
304
- : 'transition-[transform,width,height] duration-250 ease-[easing-function:cubic-bezier(0.29,0.31,0.05,0.96)] will-change-[transform,width,height] motion-reduce:!transition-none'
304
+ : 'transition-[transform,width,height] duration-[250ms] ease-[easing-function:cubic-bezier(0.29,0.31,0.05,0.96)] will-change-[transform,width,height] motion-reduce:!transition-none'
305
305
  )}
306
306
  style={{
307
307
  ...(activeBounds.side === 'top' ||
@@ -328,12 +328,7 @@ export function ContextCardProvider({
328
328
  <path
329
329
  d="M15 -0.5V0.5H12.9834L12.8184 0.508789C12.4377 0.550822 12.0853 0.738056 11.8359 1.03418L8.53027 4.95996C7.73114 5.90893 6.26886 5.90892 5.46973 4.95996L2.16406 1.03418C1.87905 0.695733 1.45907 0.5 1.0166 0.5H-1V-0.5H15Z"
330
330
  fill="var(--ds-background-100)"
331
- style={{
332
- fill: 'var(--ds-background-100)',
333
- fillOpacity: 1,
334
- stroke: 'var(--context-card-tip-stroke)',
335
- strokeOpacity: 1,
336
- }}
331
+ className="fill-background-100 stroke-[var(--context-card-tip-stroke)]"
337
332
  />
338
333
  </g>
339
334
  <defs>
@@ -342,7 +337,7 @@ export function ContextCardProvider({
342
337
  width="14"
343
338
  height="7"
344
339
  fill="white"
345
- style={{ fill: 'white', fillOpacity: 1 }}
340
+ className="fill-background-100"
346
341
  />
347
342
  </clipPath>
348
343
  </defs>
@@ -748,30 +743,25 @@ export function ContextCardTrigger({
748
743
  ? createPortal(
749
744
  <div
750
745
  className={cn(
751
- 'absolute top-0 p-3 left-0 max-w-max transition-[transform,width,height] duration-250 ease-[easing-function:cubic-bezier(0.29,0.31,0.05,0.96)] will-change-[transform,width,height] motion-reduce:!transition-none',
752
- skipTransition && '!transition-none'
746
+ 'absolute top-0 p-3 left-0 max-w-max transition-[transform,width,height] duration-[250ms] ease-[easing-function:cubic-bezier(0.29,0.31,0.05,0.96)] will-change-[transform,width,height] motion-reduce:!transition-none',
747
+ skipTransition && '!transition-none',
748
+ noPadding && 'p-0',
749
+ rootVisible && visible && !ignoreCardPointerEvents
750
+ ? 'pointer-events-auto'
751
+ : 'pointer-events-none'
753
752
  )}
754
753
  key={id}
755
754
  ref={contentRef}
756
- style={{
757
- ...(noPadding ? { padding: 0 } : {}),
758
- pointerEvents:
759
- rootVisible && visible && !ignoreCardPointerEvents
760
- ? 'all'
761
- : 'none',
762
- }}
763
755
  >
764
756
  <div
765
757
  className={cn(
766
758
  'min-w-max transition-all duration-150 ease-[easing-function:cubic-bezier(0.3,_0.57,_0.07,_0.95)] will-change-[transform,width,height] motion-reduce:!transition-none',
767
759
  skipTransition
768
760
  ? '!transition-none'
769
- : 'transition-[transform,width,height] duration-250 ease-[easing-function:cubic-bezier(0.29,0.31,0.05,0.96)] will-change-[transform,width,height] motion-reduce:!transition-none'
761
+ : 'transition-[transform,width,height] duration-[250ms] ease-[easing-function:cubic-bezier(0.29,0.31,0.05,0.96)] will-change-[transform,width,height] motion-reduce:!transition-none',
762
+ visible ? 'opacity-100' : 'opacity-0'
770
763
  )}
771
764
  ref={contentMeasureRef}
772
- style={{
773
- opacity: Number(visible),
774
- }}
775
765
  >
776
766
  {content}
777
767
  </div>