@workflow/web-shared 5.0.0-beta.40 → 5.0.0-beta.42

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 (86) 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/timeline.d.ts.map +1 -1
  24. package/dist/components/trace-viewer/components/timeline.js +13 -22
  25. package/dist/components/trace-viewer/components/trace-viewer-skeleton.d.ts.map +1 -1
  26. package/dist/components/trace-viewer/components/trace-viewer-skeleton.js +58 -18
  27. package/dist/components/trace-viewer/icons.js +5 -5
  28. package/dist/components/trace-viewer/utils.d.ts +6 -10
  29. package/dist/components/trace-viewer/utils.d.ts.map +1 -1
  30. package/dist/components/trace-viewer/utils.js +14 -24
  31. package/dist/components/trace-viewer.d.ts.map +1 -1
  32. package/dist/components/trace-viewer.js +12 -4
  33. package/dist/components/ui/context-card.d.ts.map +1 -1
  34. package/dist/components/ui/context-card.js +9 -21
  35. package/dist/components/ui/data-inspector.d.ts.map +1 -1
  36. package/dist/components/ui/data-inspector.js +8 -35
  37. package/dist/components/ui/duplicate-event-tooltip.d.ts +15 -0
  38. package/dist/components/ui/duplicate-event-tooltip.d.ts.map +1 -0
  39. package/dist/components/ui/duplicate-event-tooltip.js +19 -0
  40. package/dist/components/ui/menu-dropdown.d.ts.map +1 -1
  41. package/dist/components/ui/menu-dropdown.js +3 -19
  42. package/dist/components/ui/skeleton.js +2 -2
  43. package/dist/components/ui/spinner.d.ts.map +1 -1
  44. package/dist/components/ui/spinner.js +5 -11
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +2 -1
  48. package/dist/lib/duplicate-events.d.ts +27 -0
  49. package/dist/lib/duplicate-events.d.ts.map +1 -0
  50. package/dist/lib/duplicate-events.js +166 -0
  51. package/dist/lib/event-materialization.d.ts +15 -2
  52. package/dist/lib/event-materialization.d.ts.map +1 -1
  53. package/dist/lib/event-materialization.js +19 -4
  54. package/dist/lib/trace-builder.d.ts +16 -1
  55. package/dist/lib/trace-builder.d.ts.map +1 -1
  56. package/dist/lib/trace-builder.js +23 -4
  57. package/package.json +4 -4
  58. package/src/components/event-list-view.tsx +70 -32
  59. package/src/components/sidebar/attributes-block.tsx +4 -11
  60. package/src/components/sidebar/conversation-view.tsx +25 -79
  61. package/src/components/sidebar/entity-detail-panel.tsx +6 -0
  62. package/src/components/sidebar/events-list.tsx +21 -3
  63. package/src/components/sidebar/resolve-hook-modal.tsx +23 -186
  64. package/src/components/sidebar/sidebar-data-context.tsx +6 -0
  65. package/src/components/stream-viewer-skeleton.tsx +5 -5
  66. package/src/components/stream-viewer.tsx +1 -1
  67. package/src/components/trace-viewer/components/detail-panel.tsx +1 -0
  68. package/src/components/trace-viewer/components/timeline.tsx +21 -39
  69. package/src/components/trace-viewer/components/trace-viewer-skeleton.tsx +63 -35
  70. package/src/components/trace-viewer/icons.tsx +4 -4
  71. package/src/components/trace-viewer/utils.test.ts +5 -7
  72. package/src/components/trace-viewer/utils.ts +17 -31
  73. package/src/components/trace-viewer.tsx +15 -3
  74. package/src/components/ui/context-card.tsx +16 -26
  75. package/src/components/ui/data-inspector.tsx +20 -65
  76. package/src/components/ui/duplicate-event-tooltip.tsx +44 -0
  77. package/src/components/ui/menu-dropdown.tsx +8 -22
  78. package/src/components/ui/skeleton.tsx +2 -2
  79. package/src/components/ui/spinner.tsx +4 -8
  80. package/src/index.ts +4 -0
  81. package/src/lib/duplicate-events.test.ts +339 -0
  82. package/src/lib/duplicate-events.ts +188 -0
  83. package/src/lib/event-materialization.test.ts +117 -0
  84. package/src/lib/event-materialization.ts +23 -3
  85. package/src/lib/trace-builder.test.ts +74 -0
  86. package/src/lib/trace-builder.ts +30 -3
@@ -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>
@@ -25,6 +25,7 @@ import {
25
25
  useRef,
26
26
  useState,
27
27
  } from 'react';
28
+ import { cn } from '../../lib/cn';
28
29
  import { ENCRYPTED_DISPLAY_NAME } from '../../lib/hydration';
29
30
  import {
30
31
  type DecodedStreamChunkSource,
@@ -182,16 +183,8 @@ function EncryptedInlineLabel() {
182
183
  );
183
184
  }
184
185
  return (
185
- <span style={{ color: 'var(--ds-gray-600)', fontStyle: 'italic' }}>
186
- <Lock
187
- className="h-3 w-3"
188
- style={{
189
- display: 'inline',
190
- verticalAlign: 'middle',
191
- marginRight: '3px',
192
- marginTop: '-1px',
193
- }}
194
- />
186
+ <span className="text-gray-600 italic">
187
+ <Lock className="-mt-px mr-[3px] inline h-3 w-3 align-middle" />
195
188
  Encrypted
196
189
  </span>
197
190
  );
@@ -199,18 +192,10 @@ function EncryptedInlineLabel() {
199
192
 
200
193
  function StreamRefInline({ streamRef }: { streamRef: StreamRef }) {
201
194
  const onStreamClick = useContext(StreamClickContext);
202
- const [hovered, setHovered] = useState(false);
203
195
  return (
204
196
  <button
205
197
  type="button"
206
- className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-mono cursor-pointer underline decoration-transparent transition-colors"
207
- style={{
208
- backgroundColor: hovered ? 'var(--ds-blue-200)' : 'var(--ds-blue-100)',
209
- color: 'var(--ds-blue-900)',
210
- border: '1px solid var(--ds-blue-300)',
211
- }}
212
- onMouseEnter={() => setHovered(true)}
213
- onMouseLeave={() => setHovered(false)}
198
+ className="inline-flex cursor-pointer items-center gap-1 rounded border border-blue-300 bg-blue-100 px-1.5 py-0.5 font-mono text-[10px] text-blue-900 underline decoration-transparent transition-colors hover:bg-blue-200"
214
199
  onClick={(e) => {
215
200
  e.stopPropagation();
216
201
  onStreamClick?.(streamRef.streamId);
@@ -225,20 +210,10 @@ function StreamRefInline({ streamRef }: { streamRef: StreamRef }) {
225
210
 
226
211
  function RunRefInline({ runRef }: { runRef: RunRef }) {
227
212
  const onRunClick = useContext(RunClickContext);
228
- const [hovered, setHovered] = useState(false);
229
213
  return (
230
214
  <button
231
215
  type="button"
232
- className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-mono cursor-pointer underline decoration-transparent transition-colors"
233
- style={{
234
- backgroundColor: hovered
235
- ? 'var(--ds-purple-200)'
236
- : 'var(--ds-purple-100)',
237
- color: 'var(--ds-purple-900)',
238
- border: '1px solid var(--ds-purple-300)',
239
- }}
240
- onMouseEnter={() => setHovered(true)}
241
- onMouseLeave={() => setHovered(false)}
216
+ className="inline-flex cursor-pointer items-center gap-1 rounded border border-purple-300 bg-purple-100 px-1.5 py-0.5 font-mono text-[10px] text-purple-900 underline decoration-transparent transition-colors hover:bg-purple-200"
242
217
  onClick={(e) => {
243
218
  e.stopPropagation();
244
219
  onRunClick?.(runRef.runId);
@@ -267,10 +242,7 @@ function DecodedBytesChunk({
267
242
  {selectedView === 'decoded' ? (
268
243
  <div className="min-w-0">
269
244
  {typeof parsed === 'string' ? (
270
- <span
271
- className="whitespace-pre-wrap break-words"
272
- style={{ color: 'var(--ds-gray-1000)' }}
273
- >
245
+ <span className="whitespace-pre-wrap break-words text-gray-1000">
274
246
  {deserializeChunkText(parsed)}
275
247
  </span>
276
248
  ) : (
@@ -282,20 +254,15 @@ function DecodedBytesChunk({
282
254
  )}
283
255
  <div className="mt-2 flex">
284
256
  <div
285
- className="inline-flex overflow-hidden rounded border"
286
- style={{ borderColor: 'var(--ds-gray-400)' }}
257
+ className="inline-flex overflow-hidden rounded border border-gray-400"
287
258
  title={`${source.type} decoded as ${source.encoding.toUpperCase()} text. Switch to Bytes to inspect the summarized raw value.`}
288
259
  >
289
260
  <button
290
261
  type="button"
291
- className="h-5 px-1.5 text-[10px] font-medium"
292
- style={{
293
- backgroundColor:
294
- selectedView === 'decoded'
295
- ? 'var(--ds-gray-200)'
296
- : 'var(--ds-gray-100)',
297
- color: 'var(--ds-gray-900)',
298
- }}
262
+ className={cn(
263
+ 'h-5 px-1.5 font-medium text-[10px] text-gray-900',
264
+ selectedView === 'decoded' ? 'bg-gray-200' : 'bg-gray-100'
265
+ )}
299
266
  onClick={() => setSelectedView('decoded')}
300
267
  aria-pressed={selectedView === 'decoded'}
301
268
  aria-label="Show decoded text"
@@ -304,15 +271,10 @@ function DecodedBytesChunk({
304
271
  </button>
305
272
  <button
306
273
  type="button"
307
- className="h-5 border-l px-1.5 text-[10px] font-medium"
308
- style={{
309
- borderColor: 'var(--ds-gray-400)',
310
- backgroundColor:
311
- selectedView === 'bytes'
312
- ? 'var(--ds-gray-200)'
313
- : 'var(--ds-gray-100)',
314
- color: 'var(--ds-gray-900)',
315
- }}
274
+ className={cn(
275
+ 'h-5 border-gray-400 border-l px-1.5 font-medium text-[10px] text-gray-900',
276
+ selectedView === 'bytes' ? 'bg-gray-200' : 'bg-gray-100'
277
+ )}
316
278
  onClick={() => setSelectedView('bytes')}
317
279
  aria-pressed={selectedView === 'bytes'}
318
280
  aria-label="Show raw bytes summary"
@@ -338,23 +300,19 @@ function DecodedBytesInspector({
338
300
  <div className="font-mono">
339
301
  <button
340
302
  type="button"
341
- className="flex max-w-full items-start gap-1 text-left"
342
- style={{ color: 'var(--ds-gray-1000)' }}
303
+ className="flex max-w-full items-start gap-1 text-left text-gray-1000"
343
304
  onClick={() => setExpanded((value) => !value)}
344
305
  title={`${source.type} decoded as ${source.encoding.toUpperCase()} text`}
345
306
  >
346
- <span className="select-none" style={{ color: 'var(--ds-gray-700)' }}>
307
+ <span className="select-none text-gray-700">
347
308
  {expanded ? '▼' : '▶'}
348
309
  </span>
349
310
  <span className="min-w-0 break-words">{source.rawSummary}</span>
350
311
  </button>
351
312
  {expanded && (
352
313
  <div className="mt-1 pl-5">
353
- <span style={{ color: 'var(--ds-gray-700)' }}>decoded: </span>
354
- <span
355
- className="whitespace-pre-wrap break-words"
356
- style={{ color: 'var(--ds-green-900)' }}
357
- >
314
+ <span className="text-gray-700">decoded: </span>
315
+ <span className="whitespace-pre-wrap break-words text-green-900">
358
316
  {JSON.stringify(decodedText)}
359
317
  </span>
360
318
  </div>
@@ -373,10 +331,7 @@ function BytesDisplayValue({ display }: { display: BytesDisplay }) {
373
331
  );
374
332
  }
375
333
  return (
376
- <span
377
- className="whitespace-pre-wrap break-words"
378
- style={{ color: 'var(--ds-gray-1000)' }}
379
- >
334
+ <span className="whitespace-pre-wrap break-words text-gray-1000">
380
335
  {display.text}
381
336
  </span>
382
337
  );
@@ -0,0 +1,44 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import { DUPLICATE_EVENT_MESSAGE } from '../../lib/duplicate-events';
5
+ import {
6
+ Tooltip,
7
+ TooltipContent,
8
+ TooltipProvider,
9
+ TooltipTrigger,
10
+ } from './tooltip';
11
+
12
+ /**
13
+ * Explains why an event is shown greyed out: it repeats a class the log
14
+ * already records for the same entity, after that entity finished.
15
+ *
16
+ * Renders `children` untouched when `isDuplicate` is false, so a call site can
17
+ * wrap an event label unconditionally. Mounts its own {@link TooltipProvider}
18
+ * so it works in the sidebar and the events table alike; nesting one inside an
19
+ * existing provider is harmless.
20
+ */
21
+ export function DuplicateEventTooltip({
22
+ isDuplicate = false,
23
+ children,
24
+ }: {
25
+ isDuplicate?: boolean;
26
+ children: ReactNode;
27
+ }): ReactNode {
28
+ if (!isDuplicate) return children;
29
+
30
+ return (
31
+ <TooltipProvider delayDuration={300}>
32
+ <Tooltip>
33
+ <TooltipTrigger asChild>{children}</TooltipTrigger>
34
+ <TooltipContent
35
+ className="max-w-[264px] leading-snug"
36
+ collisionPadding={8}
37
+ side="top"
38
+ >
39
+ {DUPLICATE_EVENT_MESSAGE}
40
+ </TooltipContent>
41
+ </Tooltip>
42
+ </TooltipProvider>
43
+ );
44
+ }
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useEffect, useRef, useState } from 'react';
4
+ import { cn } from '../../lib/cn';
4
5
 
5
6
  const STYLES = `.wf-menu-btn{appearance:none;-webkit-appearance:none;border:none;display:inline-flex;align-items:center;justify-content:center;height:40px;padding:0 12px;border-radius:6px;font-size:14px;font-weight:500;line-height:20px;color:var(--ds-gray-1000);background:var(--ds-background-100);box-shadow:0 0 0 1px var(--ds-gray-400);cursor:pointer;white-space:nowrap;transition:background 150ms}.wf-menu-btn:hover{background:var(--ds-gray-alpha-200)}.wf-menu-item{appearance:none;-webkit-appearance:none;border:none;display:flex;align-items:center;width:100%;height:40px;padding:0 8px;border-radius:6px;font-size:14px;color:var(--ds-gray-1000);background:transparent;cursor:pointer;transition:background 150ms}.wf-menu-item:hover{background:var(--ds-gray-alpha-100)}`;
6
7
 
@@ -41,7 +42,7 @@ export function MenuDropdown<T extends string = string>({
41
42
  }, [open]);
42
43
 
43
44
  return (
44
- <div ref={ref} style={{ position: 'relative', flexShrink: 0 }}>
45
+ <div ref={ref} className="relative shrink-0">
45
46
  <style dangerouslySetInnerHTML={{ __html: STYLES }} />
46
47
 
47
48
  <button
@@ -55,11 +56,7 @@ export function MenuDropdown<T extends string = string>({
55
56
  height={16}
56
57
  viewBox="0 0 16 16"
57
58
  fill="none"
58
- style={{
59
- marginLeft: 16,
60
- marginRight: -4,
61
- color: 'var(--ds-gray-900)',
62
- }}
59
+ className="-mr-1 ml-4 text-gray-900"
63
60
  >
64
61
  <path
65
62
  d="M4.5 6L8 9.5L11.5 6"
@@ -73,18 +70,7 @@ export function MenuDropdown<T extends string = string>({
73
70
 
74
71
  {open && (
75
72
  <div
76
- style={{
77
- position: 'absolute',
78
- right: 0,
79
- top: '100%',
80
- marginTop: 4,
81
- minWidth: 140,
82
- padding: 4,
83
- borderRadius: 12,
84
- background: 'var(--ds-background-100)',
85
- boxShadow: 'var(--ds-shadow-menu, var(--ds-shadow-medium))',
86
- zIndex: 2001,
87
- }}
73
+ className="absolute top-full right-0 z-[2001] mt-1 min-w-[140px] bg-background-100 p-1 material-menu"
88
74
  role="menu"
89
75
  >
90
76
  {options.map((option) => (
@@ -92,10 +78,10 @@ export function MenuDropdown<T extends string = string>({
92
78
  key={option.value}
93
79
  type="button"
94
80
  role="menuitem"
95
- className="wf-menu-item"
96
- style={{
97
- fontWeight: option.value === value ? 500 : 400,
98
- }}
81
+ className={cn(
82
+ 'wf-menu-item',
83
+ option.value === value ? 'font-medium' : 'font-normal'
84
+ )}
99
85
  onClick={() => {
100
86
  onChange(option.value);
101
87
  setOpen(false);
@@ -8,8 +8,8 @@ function Skeleton({
8
8
  return (
9
9
  <div
10
10
  {...props}
11
- className={cn('rounded-md', className)}
12
- style={{ backgroundColor: 'var(--ds-gray-200)', ...style }}
11
+ className={cn('rounded-md bg-gray-200', className)}
12
+ style={style}
13
13
  />
14
14
  );
15
15
  }
@@ -44,9 +44,8 @@ export function Spinner({
44
44
 
45
45
  return (
46
46
  <span
47
+ className="relative inline-flex"
47
48
  style={{
48
- display: 'inline-flex',
49
- position: 'relative',
50
49
  width: size,
51
50
  height: size,
52
51
  }}
@@ -55,18 +54,15 @@ export function Spinner({
55
54
  {config.delays.map((delay, i) => (
56
55
  <span
57
56
  key={delay}
57
+ className="absolute top-1/2 left-1/2 rounded-[1px] bg-gray-700 [animation-iteration-count:infinite] [animation-name:wf-spinner-fade] [animation-timing-function:linear]"
58
58
  style={{
59
- position: 'absolute',
60
- left: '50%',
61
- top: '50%',
62
59
  width: config.lineW,
63
60
  height: config.lineH,
64
61
  marginLeft: -config.lineW / 2,
65
62
  marginTop: -config.lineH / 2,
66
- borderRadius: 1,
67
- backgroundColor: color ?? 'var(--ds-gray-700)',
63
+ backgroundColor: color,
68
64
  transform: `rotate(${i * config.angle}deg) translate(${size * 0.36}px)`,
69
- animation: `wf-spinner-fade ${config.duration}ms linear infinite`,
65
+ animationDuration: `${config.duration}ms`,
70
66
  animationDelay: `${delay}ms`,
71
67
  }}
72
68
  />
package/src/index.ts CHANGED
@@ -10,6 +10,10 @@ export {
10
10
  stepEventsToStepEntity,
11
11
  waitEventsToWaitEntity,
12
12
  } from './components/workflow-traces/trace-span-construction';
13
+ export {
14
+ DUPLICATE_EVENT_MESSAGE,
15
+ findDuplicateEventIds,
16
+ } from './lib/duplicate-events';
13
17
  export type { EventAnalysis } from './lib/event-analysis';
14
18
  export {
15
19
  analyzeEvents,