@workflow/web-shared 4.1.0-beta.52 → 4.1.0-beta.53

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 (145) hide show
  1. package/dist/components/error-boundary.d.ts +15 -20
  2. package/dist/components/error-boundary.d.ts.map +1 -1
  3. package/dist/components/error-boundary.js +17 -31
  4. package/dist/components/error-boundary.js.map +1 -1
  5. package/dist/components/event-list-view.d.ts +7 -6
  6. package/dist/components/event-list-view.d.ts.map +1 -1
  7. package/dist/components/event-list-view.js +492 -109
  8. package/dist/components/event-list-view.js.map +1 -1
  9. package/dist/components/index.d.ts +1 -0
  10. package/dist/components/index.d.ts.map +1 -1
  11. package/dist/components/index.js +1 -0
  12. package/dist/components/index.js.map +1 -1
  13. package/dist/components/run-trace-view.d.ts +2 -1
  14. package/dist/components/run-trace-view.d.ts.map +1 -1
  15. package/dist/components/run-trace-view.js +2 -2
  16. package/dist/components/run-trace-view.js.map +1 -1
  17. package/dist/components/sidebar/attribute-panel.d.ts +2 -1
  18. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  19. package/dist/components/sidebar/attribute-panel.js +53 -142
  20. package/dist/components/sidebar/attribute-panel.js.map +1 -1
  21. package/dist/components/sidebar/conversation-view.d.ts.map +1 -1
  22. package/dist/components/sidebar/conversation-view.js +3 -17
  23. package/dist/components/sidebar/conversation-view.js.map +1 -1
  24. package/dist/components/sidebar/entity-detail-panel.d.ts +3 -1
  25. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  26. package/dist/components/sidebar/entity-detail-panel.js +63 -10
  27. package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
  28. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  29. package/dist/components/sidebar/events-list.js +4 -8
  30. package/dist/components/sidebar/events-list.js.map +1 -1
  31. package/dist/components/sidebar/resolve-hook-modal.d.ts +3 -0
  32. package/dist/components/sidebar/resolve-hook-modal.d.ts.map +1 -1
  33. package/dist/components/sidebar/resolve-hook-modal.js +152 -3
  34. package/dist/components/sidebar/resolve-hook-modal.js.map +1 -1
  35. package/dist/components/stream-viewer.d.ts +7 -5
  36. package/dist/components/stream-viewer.d.ts.map +1 -1
  37. package/dist/components/stream-viewer.js +54 -22
  38. package/dist/components/stream-viewer.js.map +1 -1
  39. package/dist/components/trace-viewer/components/markers.d.ts +2 -1
  40. package/dist/components/trace-viewer/components/markers.d.ts.map +1 -1
  41. package/dist/components/trace-viewer/components/markers.js +59 -20
  42. package/dist/components/trace-viewer/components/markers.js.map +1 -1
  43. package/dist/components/trace-viewer/components/node.d.ts +5 -1
  44. package/dist/components/trace-viewer/components/node.d.ts.map +1 -1
  45. package/dist/components/trace-viewer/components/node.js +250 -68
  46. package/dist/components/trace-viewer/components/node.js.map +1 -1
  47. package/dist/components/trace-viewer/components/span-content.d.ts +19 -0
  48. package/dist/components/trace-viewer/components/span-content.d.ts.map +1 -0
  49. package/dist/components/trace-viewer/components/span-content.js +137 -0
  50. package/dist/components/trace-viewer/components/span-content.js.map +1 -0
  51. package/dist/components/trace-viewer/components/span-detail-panel.d.ts.map +1 -1
  52. package/dist/components/trace-viewer/components/span-detail-panel.js +3 -2
  53. package/dist/components/trace-viewer/components/span-detail-panel.js.map +1 -1
  54. package/dist/components/trace-viewer/components/span-segments.d.ts +50 -0
  55. package/dist/components/trace-viewer/components/span-segments.d.ts.map +1 -0
  56. package/dist/components/trace-viewer/components/span-segments.js +392 -0
  57. package/dist/components/trace-viewer/components/span-segments.js.map +1 -0
  58. package/dist/components/trace-viewer/components/span-strategies.d.ts +46 -0
  59. package/dist/components/trace-viewer/components/span-strategies.d.ts.map +1 -0
  60. package/dist/components/trace-viewer/components/span-strategies.js +108 -0
  61. package/dist/components/trace-viewer/components/span-strategies.js.map +1 -0
  62. package/dist/components/trace-viewer/context.d.ts +7 -6
  63. package/dist/components/trace-viewer/context.d.ts.map +1 -1
  64. package/dist/components/trace-viewer/context.js +47 -18
  65. package/dist/components/trace-viewer/context.js.map +1 -1
  66. package/dist/components/trace-viewer/trace-viewer.d.ts +5 -1
  67. package/dist/components/trace-viewer/trace-viewer.d.ts.map +1 -1
  68. package/dist/components/trace-viewer/trace-viewer.js +87 -11
  69. package/dist/components/trace-viewer/trace-viewer.js.map +1 -1
  70. package/dist/components/trace-viewer/trace-viewer.module.css +179 -6
  71. package/dist/components/trace-viewer/util/timing.d.ts +5 -0
  72. package/dist/components/trace-viewer/util/timing.d.ts.map +1 -1
  73. package/dist/components/trace-viewer/util/timing.js +12 -0
  74. package/dist/components/trace-viewer/util/timing.js.map +1 -1
  75. package/dist/components/trace-viewer/util/use-streaming-spans.d.ts +1 -1
  76. package/dist/components/trace-viewer/util/use-streaming-spans.d.ts.map +1 -1
  77. package/dist/components/trace-viewer/util/use-streaming-spans.js +29 -17
  78. package/dist/components/trace-viewer/util/use-streaming-spans.js.map +1 -1
  79. package/dist/components/trace-viewer/worker.js +3 -1
  80. package/dist/components/trace-viewer/worker.js.map +1 -1
  81. package/dist/components/ui/alert.js +3 -3
  82. package/dist/components/ui/alert.js.map +1 -1
  83. package/dist/components/ui/card.d.ts.map +1 -1
  84. package/dist/components/ui/card.js +2 -2
  85. package/dist/components/ui/card.js.map +1 -1
  86. package/dist/components/ui/data-inspector.d.ts +17 -0
  87. package/dist/components/ui/data-inspector.d.ts.map +1 -0
  88. package/dist/components/ui/data-inspector.js +184 -0
  89. package/dist/components/ui/data-inspector.js.map +1 -0
  90. package/dist/components/ui/error-card.d.ts.map +1 -1
  91. package/dist/components/ui/error-card.js +4 -1
  92. package/dist/components/ui/error-card.js.map +1 -1
  93. package/dist/components/ui/inspector-theme.d.ts +39 -24
  94. package/dist/components/ui/inspector-theme.d.ts.map +1 -1
  95. package/dist/components/ui/inspector-theme.js +90 -38
  96. package/dist/components/ui/inspector-theme.js.map +1 -1
  97. package/dist/components/ui/skeleton.d.ts +1 -1
  98. package/dist/components/ui/skeleton.d.ts.map +1 -1
  99. package/dist/components/ui/skeleton.js +2 -2
  100. package/dist/components/ui/skeleton.js.map +1 -1
  101. package/dist/components/workflow-trace-view.d.ts +3 -1
  102. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  103. package/dist/components/workflow-trace-view.js +435 -21
  104. package/dist/components/workflow-trace-view.js.map +1 -1
  105. package/dist/components/workflow-traces/trace-span-construction.d.ts +1 -1
  106. package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
  107. package/dist/components/workflow-traces/trace-span-construction.js +2 -2
  108. package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
  109. package/dist/lib/hydration.d.ts.map +1 -1
  110. package/dist/lib/hydration.js +17 -3
  111. package/dist/lib/hydration.js.map +1 -1
  112. package/dist/styles.css +186 -0
  113. package/package.json +8 -7
  114. package/src/components/error-boundary.tsx +29 -40
  115. package/src/components/event-list-view.tsx +1000 -287
  116. package/src/components/index.ts +1 -0
  117. package/src/components/run-trace-view.tsx +3 -0
  118. package/src/components/sidebar/attribute-panel.tsx +58 -258
  119. package/src/components/sidebar/conversation-view.tsx +30 -27
  120. package/src/components/sidebar/entity-detail-panel.tsx +86 -20
  121. package/src/components/sidebar/events-list.tsx +4 -11
  122. package/src/components/sidebar/resolve-hook-modal.tsx +206 -47
  123. package/src/components/stream-viewer.tsx +138 -61
  124. package/src/components/trace-viewer/components/markers.tsx +69 -21
  125. package/src/components/trace-viewer/components/node.tsx +346 -100
  126. package/src/components/trace-viewer/components/span-content.tsx +247 -0
  127. package/src/components/trace-viewer/components/span-detail-panel.tsx +7 -2
  128. package/src/components/trace-viewer/components/span-segments.ts +516 -0
  129. package/src/components/trace-viewer/components/span-strategies.ts +205 -0
  130. package/src/components/trace-viewer/context.tsx +92 -40
  131. package/src/components/trace-viewer/trace-viewer.module.css +179 -6
  132. package/src/components/trace-viewer/trace-viewer.tsx +115 -11
  133. package/src/components/trace-viewer/util/timing.ts +13 -0
  134. package/src/components/trace-viewer/util/use-streaming-spans.ts +28 -17
  135. package/src/components/trace-viewer/worker.ts +4 -1
  136. package/src/components/ui/alert.tsx +3 -3
  137. package/src/components/ui/card.tsx +3 -5
  138. package/src/components/ui/data-inspector.tsx +318 -0
  139. package/src/components/ui/error-card.tsx +17 -6
  140. package/src/components/ui/inspector-theme.ts +127 -39
  141. package/src/components/ui/skeleton.tsx +3 -1
  142. package/src/components/workflow-trace-view.tsx +625 -26
  143. package/src/components/workflow-traces/trace-span-construction.ts +3 -2
  144. package/src/lib/hydration.ts +17 -8
  145. package/src/styles.css +186 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { clsx } from 'clsx';
4
4
  import type { CSSProperties, MutableRefObject, ReactNode } from 'react';
5
- import { memo, useRef } from 'react';
5
+ import { memo, useEffect, useRef, useState } from 'react';
6
6
  import styles from '../trace-viewer.module.css';
7
7
  import type {
8
8
  MemoCache,
@@ -14,46 +14,76 @@ import type {
14
14
  VisibleSpanEvent,
15
15
  } from '../types';
16
16
  import { MARKER_HEIGHT, ROW_HEIGHT, ROW_PADDING } from '../util/constants';
17
- import { formatDuration } from '../util/timing';
18
-
19
- const isSpanSmall = (node: VisibleSpan, scale: number): boolean =>
20
- node.duration * scale < 64;
21
-
22
- const isSpanHuge = (node: VisibleSpan, scale: number): boolean =>
23
- node.duration * scale >= 500;
17
+ import { formatDuration, getHighResInMs } from '../util/timing';
18
+ import { SpanContent } from './span-content';
19
+ import { computeSegments } from './span-segments';
20
+ import {
21
+ type ResourceType,
22
+ type SpanLayout,
23
+ getResourceType,
24
+ getSpanLayout,
25
+ } from './span-strategies';
24
26
 
25
27
  export const getSpanColorClassName = (node: SpanNode): string => {
26
28
  if (node.isVercel) return String(styles.colorVercel);
27
29
  return String(styles[`color${node.resourceIndex % 5}` as 'color0']);
28
30
  };
29
31
 
30
- export const getSpanClassName = (node: VisibleSpan, scale: number): string => {
31
- const isHuge = isSpanHuge(node, scale);
32
- const isHovered = node.isHovered && !isHuge && node.isHighlighted !== false;
33
-
34
- return clsx(
35
- styles.spanNode,
36
- isHuge && styles.huge,
37
- isSpanSmall(node, scale) && styles.small,
38
- node.isSelected && styles.selected,
39
- isHovered && styles.xHover,
40
- node.isHighlighted ? styles.colorHighlight : getSpanColorClassName(node),
41
- node.isHighlighted === false && styles.unlit
42
- );
43
- };
32
+ function getTerminalTimestamp(
33
+ resourceType: ResourceType,
34
+ node: VisibleSpan
35
+ ): number | undefined {
36
+ const events = node.events;
37
+ if (!events?.length) return undefined;
44
38
 
45
- const getDuration = (node: SpanNode): string => {
46
- if (node.isInstrumentationHint) {
47
- return 'Get Started';
39
+ switch (resourceType) {
40
+ case 'hook':
41
+ for (let i = events.length; i--; ) {
42
+ if (events[i].event.name === 'hook_disposed') {
43
+ return events[i].timestamp;
44
+ }
45
+ }
46
+ return undefined;
47
+ case 'sleep':
48
+ for (let i = events.length; i--; ) {
49
+ if (events[i].event.name === 'wait_completed') {
50
+ return events[i].timestamp;
51
+ }
52
+ }
53
+ return undefined;
54
+ case 'run':
55
+ for (let i = events.length; i--; ) {
56
+ const name = events[i].event.name;
57
+ if (
58
+ name === 'run_completed' ||
59
+ name === 'run_failed' ||
60
+ name === 'run_cancelled'
61
+ ) {
62
+ return events[i].timestamp;
63
+ }
64
+ }
65
+ return undefined;
66
+ case 'step':
67
+ for (let i = events.length; i--; ) {
68
+ const name = events[i].event.name;
69
+ if (name === 'step_completed' || name === 'step_failed') {
70
+ return events[i].timestamp;
71
+ }
72
+ if (name === 'step_started' || name === 'step_retrying') {
73
+ return undefined;
74
+ }
75
+ }
76
+ return undefined;
77
+ default:
78
+ return undefined;
48
79
  }
49
-
50
- return formatDuration(node.duration);
51
- };
80
+ }
52
81
 
53
82
  export const SpanNodes = memo(function SpanNodes({
54
83
  root,
55
84
  scale,
56
85
  spans,
86
+ isLive = false,
57
87
  scrollSnapshotRef,
58
88
  cache,
59
89
  customSpanClassNameFunc,
@@ -62,7 +92,9 @@ export const SpanNodes = memo(function SpanNodes({
62
92
  root: RootNode;
63
93
  scale: number;
64
94
  spans: VisibleSpan[];
95
+ isLive?: boolean;
65
96
  scrollSnapshotRef: MutableRefObject<ScrollSnapshot | undefined>;
97
+ /** Not used in the body — exists solely to bust React.memo when the global memo cache changes. */
66
98
  cacheKey?: MemoCacheKey;
67
99
  cache: MemoCache;
68
100
  customSpanClassNameFunc?: (span: SpanNode) => string;
@@ -77,15 +109,54 @@ export const SpanNodes = memo(function SpanNodes({
77
109
  node={x}
78
110
  root={root}
79
111
  scale={scale}
112
+ traceIsLive={isLive}
80
113
  scrollSnapshotRef={scrollSnapshotRef}
81
114
  />
82
115
  ));
83
116
  });
84
117
 
118
+ // ──────────────────────────────────────────────────────────────────────────
119
+ // Compute inline styles from a SpanLayout
120
+ // ──────────────────────────────────────────────────────────────────────────
121
+
122
+ function getSpanStyle(
123
+ layout: SpanLayout,
124
+ node: VisibleSpan,
125
+ root: RootNode,
126
+ scale: number
127
+ ): CSSProperties {
128
+ // Use isExpanded (hovered or selected) for width expansion so that
129
+ // selected small spans stay expanded and show their name.
130
+ const expanded = layout.isExpanded;
131
+ return {
132
+ // Use actualWidth for CSS variable so hover expansion is accurate
133
+ '--span-width': `${Math.max(layout.actualWidth, 1)}px`,
134
+ minWidth: expanded ? layout.width : undefined,
135
+ width: expanded ? undefined : layout.width,
136
+ height: layout.height,
137
+ maxWidth:
138
+ expanded && !layout.isNearRightSide
139
+ ? (root.endTime - node.startTime) * scale
140
+ : undefined,
141
+ containIntrinsicWidth: expanded ? undefined : layout.width,
142
+ containIntrinsicHeight: layout.height,
143
+ left: layout.isNearRightSide ? undefined : layout.left,
144
+ right: layout.isNearRightSide
145
+ ? (root.endTime - node.endTime) * scale
146
+ : undefined,
147
+ top: layout.top,
148
+ } as CSSProperties;
149
+ }
150
+
151
+ // ──────────────────────────────────────────────────────────────────────────
152
+ // SpanComponent
153
+ // ──────────────────────────────────────────────────────────────────────────
154
+
85
155
  export const SpanComponent = memo(function SpanComponent({
86
156
  node,
87
157
  root,
88
158
  scale,
159
+ traceIsLive = false,
89
160
  scrollSnapshotRef,
90
161
  customSpanClassNameFunc,
91
162
  customSpanEventClassNameFunc,
@@ -93,99 +164,179 @@ export const SpanComponent = memo(function SpanComponent({
93
164
  node: VisibleSpan;
94
165
  root: RootNode;
95
166
  scale: number;
167
+ traceIsLive?: boolean;
96
168
  scrollSnapshotRef: MutableRefObject<ScrollSnapshot | undefined>;
169
+ /** Not used in the body — exists solely to bust React.memo for this span. */
97
170
  cacheKey?: MemoCacheKey;
98
171
  customSpanClassNameFunc?: (span: SpanNode) => string;
99
172
  customSpanEventClassNameFunc?: (event: VisibleSpanEvent) => string;
100
173
  }): ReactNode {
101
174
  const ref = useRef<HTMLButtonElement>(null);
102
- node.ref = ref;
103
175
 
104
- const { span } = node;
105
- const duration = getDuration(node);
106
-
107
- const left = (node.startTime - root.startTime) * scale;
108
- let top = MARKER_HEIGHT + (ROW_HEIGHT + ROW_PADDING) * node.row;
109
- const actualWidth = node.duration * scale;
110
- // Enforce minimum width so very short spans are always visible
111
- const MIN_SPAN_WIDTH = 4;
112
- const width = Math.max(actualWidth, MIN_SPAN_WIDTH);
113
- let height = ROW_HEIGHT;
114
- const isHuge = isSpanHuge(node, scale);
115
- // Check if span is small based on actual width, not minimum width
116
- const isSmall = actualWidth < 64;
117
- const isHovered = node.isHovered && !isHuge && node.isHighlighted !== false;
118
- if (isSmall && !isHovered) {
119
- height *= 0.4;
120
- top += (ROW_HEIGHT - height) * 0.5;
121
- }
176
+ useEffect(() => {
177
+ node.ref = ref;
178
+ return () => {
179
+ node.ref = undefined;
180
+ };
181
+ }, [node]);
122
182
 
123
- let isNearRightSide = false;
124
- if (isHovered) {
125
- let { duration: visibleDuration, endTime: visibleEndTime } = root;
126
- const snapshot = scrollSnapshotRef.current;
127
- if (snapshot) {
128
- visibleDuration = snapshot.endTime - snapshot.startTime;
129
- visibleEndTime = snapshot.endTime;
130
- }
131
- isNearRightSide = visibleEndTime - node.startTime < 0.25 * visibleDuration;
132
- }
183
+ const { span } = node;
184
+ const resourceType = getResourceType(node);
185
+ const [liveNow, setLiveNow] = useState(0);
133
186
 
134
187
  // Get custom class name from callback if provided
135
188
  const customClassName = customSpanClassNameFunc
136
189
  ? customSpanClassNameFunc(node)
137
190
  : '';
138
191
 
192
+ // Workflow span types use colored segments + boundary line markers
193
+ // Generic OTEL spans use diamond event markers
194
+ const isWorkflowSpan = resourceType !== 'default';
195
+
196
+ // Determine if this span is still active (live). We require the parent run
197
+ // to be explicitly live to avoid completed/cancelled traces continuing to
198
+ // grow due to timing heuristics.
199
+ const data = span.attributes?.data as Record<string, unknown> | undefined;
200
+ const terminalTimestamp = getTerminalTimestamp(resourceType, node);
201
+ const hasTerminalEvent = terminalTimestamp != null;
202
+ const isCompletedByData =
203
+ resourceType === 'hook'
204
+ ? Boolean(data?.disposedAt)
205
+ : Boolean(data?.completedAt);
206
+ const isCompleted = isCompletedByData || hasTerminalEvent;
207
+ const isLive = traceIsLive && isWorkflowSpan && !isCompleted;
208
+
209
+ useEffect(() => {
210
+ if (!isLive) return;
211
+ setLiveNow(Date.now());
212
+ const interval = setInterval(() => {
213
+ setLiveNow(Date.now());
214
+ }, 250);
215
+ return () => clearInterval(interval);
216
+ }, [isLive]);
217
+
218
+ const canonicalStartTime = getHighResInMs(span.startTime);
219
+ const canonicalEndTime = getHighResInMs(span.endTime);
220
+ const canonicalDuration = getHighResInMs(span.duration);
221
+ const canonicalActiveStartTime = span.activeStartTime
222
+ ? getHighResInMs(span.activeStartTime)
223
+ : undefined;
224
+
225
+ const durationMs =
226
+ terminalTimestamp != null
227
+ ? Math.max(0, terminalTimestamp - canonicalStartTime)
228
+ : isLive
229
+ ? Math.max(0, (liveNow || Date.now()) - canonicalStartTime)
230
+ : canonicalDuration;
231
+ const duration = node.isInstrumentationHint
232
+ ? 'Get Started'
233
+ : formatDuration(durationMs);
234
+ const baseNode = {
235
+ ...node,
236
+ startTime: canonicalStartTime,
237
+ endTime: canonicalEndTime,
238
+ duration: canonicalDuration,
239
+ activeStartTime: canonicalActiveStartTime,
240
+ } as VisibleSpan;
241
+ const segmentNode = isLive
242
+ ? ({
243
+ ...baseNode,
244
+ duration: durationMs,
245
+ endTime: canonicalStartTime + durationMs,
246
+ } as VisibleSpan)
247
+ : baseNode;
248
+ const hasSegments =
249
+ isWorkflowSpan &&
250
+ computeSegments(resourceType, segmentNode).segments.length > 0;
251
+ const layout = getSpanLayout(
252
+ resourceType,
253
+ segmentNode,
254
+ root,
255
+ scale,
256
+ scrollSnapshotRef
257
+ );
258
+
259
+ // For live spans the data-level `node.duration` can be stale (from the last
260
+ // fetch) while the visual width is growing via rAF. Recompute from real
261
+ // elapsed time so that:
262
+ // - height/isSmall classification stays correct (not shrunken to 40%)
263
+ // - React-rendered width matches the rAF-driven width, preventing a brief
264
+ // shrink→expand flash on re-render that breaks cursor hit-testing
265
+ if (isLive) {
266
+ const elapsed = durationMs;
267
+ const liveActualWidth = elapsed * scale;
268
+ const liveWidth = Math.max(liveActualWidth, 4);
269
+ layout.actualWidth = liveActualWidth;
270
+ layout.width = liveWidth;
271
+ layout.isSmall = liveActualWidth < 64;
272
+ layout.isHuge = liveActualWidth >= 500;
273
+ if (!layout.isSmall) {
274
+ layout.height = ROW_HEIGHT;
275
+ layout.top = MARKER_HEIGHT + (ROW_HEIGHT + ROW_PADDING) * segmentNode.row;
276
+ }
277
+ }
278
+
279
+ // Smoothly grow active span width at 60fps using wall clock time
280
+ useEffect(() => {
281
+ if (!isLive || !ref.current) return;
282
+
283
+ let rafId = 0;
284
+ const tick = (): void => {
285
+ const $el = ref.current;
286
+ if (!$el) return;
287
+ const elapsed = Date.now() - canonicalStartTime;
288
+ const w = Math.max(elapsed * scale, 2);
289
+ $el.style.width = `${w}px`;
290
+ $el.style.setProperty('--span-width', `${w}px`);
291
+ rafId = requestAnimationFrame(tick);
292
+ };
293
+ rafId = requestAnimationFrame(tick);
294
+ return () => cancelAnimationFrame(rafId);
295
+ }, [isLive, canonicalStartTime, scale]);
296
+
297
+ const renderNode = segmentNode;
298
+
139
299
  return (
140
300
  <>
141
301
  <button
142
302
  aria-label={`${span.name} - ${duration}`}
143
- className={clsx(getSpanClassName(node, scale), customClassName)}
303
+ className={clsx(
304
+ styles.spanNode,
305
+ layout.isHuge && styles.huge,
306
+ layout.isSmall && styles.small,
307
+ layout.isExpanded && styles.xHover,
308
+ node.isHighlighted
309
+ ? styles.colorHighlight
310
+ : getSpanColorClassName(node),
311
+ node.isHighlighted === false && styles.unlit,
312
+ customClassName,
313
+ hasSegments && styles.hasSegments
314
+ )}
144
315
  data-span-id={span.spanId}
145
- data-start-time={node.startTime - root.startTime}
146
- data-right-side={isNearRightSide}
316
+ data-start-time={segmentNode.startTime - root.startTime}
317
+ data-right-side={layout.isNearRightSide}
318
+ data-selected={node.isSelected ? '' : undefined}
147
319
  ref={ref}
148
- style={
149
- {
150
- // Use actualWidth for CSS variable so hover expansion is accurate
151
- '--span-width': `${Math.max(actualWidth, 1)}px`,
152
- minWidth: isHovered ? width : undefined,
153
- width: isHovered ? undefined : width,
154
- height,
155
- maxWidth:
156
- isHovered && !isNearRightSide
157
- ? (root.endTime - node.startTime) * scale
158
- : undefined,
159
- containIntrinsicWidth: isHovered ? undefined : width,
160
- containIntrinsicHeight: height,
161
- left: isNearRightSide ? undefined : left,
162
- right: isNearRightSide
163
- ? (root.endTime - node.endTime) * scale
164
- : undefined,
165
- top,
166
- } as CSSProperties
167
- }
320
+ style={getSpanStyle(layout, segmentNode, root, scale)}
168
321
  type="button"
169
322
  >
170
- {isSmall && !isHovered ? null : (
171
- <>
172
- <span className={styles.spanName}>{node.label || span.name}</span>
173
- {isHuge ? <span className={styles.spanSpacer} /> : null}
174
- {isHovered || width > 128 ? (
175
- <span className={styles.spanDuration}>{duration}</span>
176
- ) : null}
177
- </>
178
- )}
323
+ <SpanContent
324
+ durationMs={durationMs}
325
+ resourceType={resourceType}
326
+ node={renderNode}
327
+ layout={layout}
328
+ />
179
329
  </button>
180
- {node.events && !isSmall
181
- ? node.events.map((x) => (
330
+ {segmentNode.events && !layout.isSmall
331
+ ? segmentNode.events.map((x) => (
182
332
  <SpanEventComponent
183
333
  customSpanEventClassNameFunc={customSpanEventClassNameFunc}
184
334
  event={x}
185
335
  key={x.key}
186
- node={node}
336
+ node={segmentNode}
187
337
  root={root}
188
338
  scale={scale}
339
+ asBoundary={isWorkflowSpan}
189
340
  />
190
341
  ))
191
342
  : null}
@@ -193,29 +344,102 @@ export const SpanComponent = memo(function SpanComponent({
193
344
  );
194
345
  });
195
346
 
347
+ // ──────────────────────────────────────────────────────────────────────────
348
+ // SpanEventComponent
349
+ // ──────────────────────────────────────────────────────────────────────────
350
+
351
+ /** Human-readable labels for workflow event types */
352
+ const BOUNDARY_LABELS: Record<string, string> = {
353
+ step_started: 'Started',
354
+ step_retrying: 'Retrying',
355
+ step_failed: 'Failed',
356
+ hook_created: 'Created',
357
+ hook_received: 'Received',
358
+ hook_disposed: 'Resolved',
359
+ wait_created: 'Sleep started',
360
+ wait_completed: 'Sleep completed',
361
+ run_started: 'Started',
362
+ run_completed: 'Completed',
363
+ run_failed: 'Run failed',
364
+ step_completed: 'Completed',
365
+ };
366
+
196
367
  export const SpanEventComponent = memo(function SpanEventComponent({
197
368
  event,
198
369
  node,
199
370
  root,
200
371
  scale,
201
372
  customSpanEventClassNameFunc,
373
+ asBoundary = false,
202
374
  }: {
203
375
  event: VisibleSpanEvent;
204
376
  node: VisibleSpan;
205
377
  root: RootNode;
206
378
  scale: number;
207
379
  customSpanEventClassNameFunc?: (event: VisibleSpanEvent) => string;
380
+ asBoundary?: boolean;
208
381
  }): ReactNode {
209
382
  const ref = useRef<HTMLDivElement>(null);
210
- event.ref = ref;
383
+
384
+ useEffect(() => {
385
+ event.ref = ref;
386
+ return () => {
387
+ event.ref = undefined;
388
+ };
389
+ }, [event]);
211
390
 
212
391
  const {
213
392
  event: { name },
214
393
  } = event;
215
394
  const timestamp = formatDuration(event.timestamp - root.startTime);
395
+ const displayLabel = asBoundary ? (BOUNDARY_LABELS[name] ?? name) : name;
396
+
397
+ // For boundary events, compute the duration of the phase.
398
+ // "Forward" events (started, retrying) measure until the next event.
399
+ // "Terminal" events (completed) measure from the previous started event.
400
+ const isForwardEvent =
401
+ asBoundary &&
402
+ (name === 'step_started' ||
403
+ name === 'run_started' ||
404
+ name === 'step_retrying');
405
+ const isTerminalEvent =
406
+ asBoundary && (name === 'step_completed' || name === 'run_completed');
407
+
408
+ let phaseDuration: string | null = null;
409
+ let phaseLabel: string | null = null;
410
+ if ((isForwardEvent || isTerminalEvent) && node.events) {
411
+ const sortedNodeEvents = [...node.events].sort(
412
+ (a, b) => a.timestamp - b.timestamp
413
+ );
414
+ const currentIdx = sortedNodeEvents.findIndex((e) => e.key === event.key);
415
+
416
+ if (isTerminalEvent) {
417
+ // Look backward to find the last step_started
418
+ let prevStartTime: number | null = null;
419
+ for (let i = currentIdx - 1; i >= 0; i--) {
420
+ if (sortedNodeEvents[i].event.name === 'step_started') {
421
+ prevStartTime = sortedNodeEvents[i].timestamp;
422
+ break;
423
+ }
424
+ }
425
+ if (prevStartTime !== null) {
426
+ phaseDuration = formatDuration(event.timestamp - prevStartTime);
427
+ phaseLabel = 'Executed';
428
+ }
429
+ } else {
430
+ // Look forward to the next event
431
+ const nextEvent = sortedNodeEvents[currentIdx + 1];
432
+ const endTime = nextEvent ? nextEvent.timestamp : node.endTime;
433
+ phaseDuration = formatDuration(endTime - event.timestamp);
434
+ phaseLabel = name === 'step_retrying' ? 'Waited' : 'Executed';
435
+ }
436
+ }
216
437
 
217
438
  const left = (event.timestamp - root.startTime) * scale;
218
439
  const top = MARKER_HEIGHT + (ROW_HEIGHT + ROW_PADDING) * node.row;
440
+ const isLeftAligned =
441
+ node.duration <= 0 ||
442
+ (event.timestamp - node.startTime) / node.duration < 0.5;
219
443
 
220
444
  // Get custom class name from callback if provided
221
445
  const customClassName = customSpanEventClassNameFunc
@@ -224,10 +448,15 @@ export const SpanEventComponent = memo(function SpanEventComponent({
224
448
 
225
449
  return (
226
450
  <div
227
- title={`${name} at ${timestamp}`}
451
+ title={
452
+ phaseDuration
453
+ ? `${displayLabel} ${timestamp}\n${phaseLabel} ${phaseDuration}`
454
+ : `${displayLabel} ${timestamp}`
455
+ }
228
456
  className={clsx(
229
457
  styles.spanNodeEvent,
230
458
  customClassName,
459
+ asBoundary && styles.boundaryMarker,
231
460
  node.isHighlighted
232
461
  ? styles.colorHighlight
233
462
  : getSpanColorClassName(node),
@@ -245,15 +474,32 @@ export const SpanEventComponent = memo(function SpanEventComponent({
245
474
  <div
246
475
  className={clsx(
247
476
  styles.hoverInfo,
248
- (event.timestamp - node.startTime) / node.duration < 0.5
249
- ? styles.alignStart
250
- : styles.alignEnd
477
+ isLeftAligned ? styles.alignStart : styles.alignEnd
251
478
  )}
252
479
  >
253
- <span className={styles.eventName}>{name}</span>
254
- <span className={styles.eventTimestamp}>{timestamp}</span>
480
+ {asBoundary ? (
481
+ <>
482
+ <span className={styles.eventName}>{displayLabel}</span>
483
+ <span className={styles.eventTimestamp}>{timestamp}</span>
484
+ {phaseDuration ? (
485
+ <>
486
+ <span className={styles.eventName}>{phaseLabel}</span>
487
+ <span className={styles.eventTimestamp}>{phaseDuration}</span>
488
+ </>
489
+ ) : null}
490
+ </>
491
+ ) : (
492
+ <>
493
+ <span className={styles.eventName}>{name}</span>
494
+ <span className={styles.eventTimestamp}>{timestamp}</span>
495
+ </>
496
+ )}
255
497
  </div>
256
- <div className={styles.eventDiamond} />
498
+ {asBoundary ? (
499
+ <div className={styles.boundaryLine} />
500
+ ) : (
501
+ <div className={styles.eventDiamond} />
502
+ )}
257
503
  </div>
258
504
  );
259
505
  });