@workflow/web-shared 5.0.0-beta.2 → 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 (161) hide show
  1. package/dist/components/index.d.ts +2 -0
  2. package/dist/components/index.d.ts.map +1 -1
  3. package/dist/components/index.js +2 -0
  4. package/dist/components/index.js.map +1 -1
  5. package/dist/components/new-trace-viewer/components/copy-button.d.ts +9 -0
  6. package/dist/components/new-trace-viewer/components/copy-button.d.ts.map +1 -0
  7. package/dist/components/new-trace-viewer/components/copy-button.js +27 -0
  8. package/dist/components/new-trace-viewer/components/copy-button.js.map +1 -0
  9. package/dist/components/new-trace-viewer/components/event-list.d.ts +8 -0
  10. package/dist/components/new-trace-viewer/components/event-list.d.ts.map +1 -0
  11. package/dist/components/new-trace-viewer/components/event-list.js +38 -0
  12. package/dist/components/new-trace-viewer/components/event-list.js.map +1 -0
  13. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts +18 -0
  14. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts.map +1 -0
  15. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js +54 -0
  16. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js.map +1 -0
  17. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts +20 -0
  18. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts.map +1 -0
  19. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js +81 -0
  20. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js.map +1 -0
  21. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts +25 -0
  22. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts.map +1 -0
  23. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js +101 -0
  24. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js.map +1 -0
  25. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts +20 -0
  26. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts.map +1 -0
  27. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.js +114 -0
  28. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.js.map +1 -0
  29. package/dist/components/new-trace-viewer/components/split-pane.d.ts +14 -0
  30. package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -0
  31. package/dist/components/new-trace-viewer/components/split-pane.js +99 -0
  32. package/dist/components/new-trace-viewer/components/split-pane.js.map +1 -0
  33. package/dist/components/new-trace-viewer/components/timeline.d.ts +31 -0
  34. package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -0
  35. package/dist/components/new-trace-viewer/components/timeline.js +138 -0
  36. package/dist/components/new-trace-viewer/components/timeline.js.map +1 -0
  37. package/dist/components/new-trace-viewer/context.d.ts +15 -0
  38. package/dist/components/new-trace-viewer/context.d.ts.map +1 -0
  39. package/dist/components/new-trace-viewer/context.js +47 -0
  40. package/dist/components/new-trace-viewer/context.js.map +1 -0
  41. package/dist/components/new-trace-viewer/detail-panel.d.ts +10 -0
  42. package/dist/components/new-trace-viewer/detail-panel.d.ts.map +1 -0
  43. package/dist/components/new-trace-viewer/detail-panel.js +12 -0
  44. package/dist/components/new-trace-viewer/detail-panel.js.map +1 -0
  45. package/dist/components/new-trace-viewer/icons.d.ts +6 -0
  46. package/dist/components/new-trace-viewer/icons.d.ts.map +1 -0
  47. package/dist/components/new-trace-viewer/icons.js +15 -0
  48. package/dist/components/new-trace-viewer/icons.js.map +1 -0
  49. package/dist/components/new-trace-viewer/trace-viewer.d.ts +8 -0
  50. package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -0
  51. package/dist/components/new-trace-viewer/trace-viewer.js +349 -0
  52. package/dist/components/new-trace-viewer/trace-viewer.js.map +1 -0
  53. package/dist/components/new-trace-viewer/types.d.ts +1 -0
  54. package/dist/components/new-trace-viewer/types.d.ts.map +1 -0
  55. package/dist/components/new-trace-viewer/types.js +2 -0
  56. package/dist/components/new-trace-viewer/types.js.map +1 -0
  57. package/dist/components/new-trace-viewer/util/constants.d.ts +1 -0
  58. package/dist/components/new-trace-viewer/util/constants.d.ts.map +1 -0
  59. package/dist/components/new-trace-viewer/util/constants.js +2 -0
  60. package/dist/components/new-trace-viewer/util/constants.js.map +1 -0
  61. package/dist/components/new-trace-viewer/utils.d.ts +42 -0
  62. package/dist/components/new-trace-viewer/utils.d.ts.map +1 -0
  63. package/dist/components/new-trace-viewer/utils.js +305 -0
  64. package/dist/components/new-trace-viewer/utils.js.map +1 -0
  65. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  66. package/dist/components/sidebar/attribute-panel.js +42 -23
  67. package/dist/components/sidebar/attribute-panel.js.map +1 -1
  68. package/dist/components/sidebar/copyable-data-block.js +1 -1
  69. package/dist/components/sidebar/copyable-data-block.js.map +1 -1
  70. package/dist/components/sidebar/detail-card.d.ts.map +1 -1
  71. package/dist/components/sidebar/detail-card.js +1 -1
  72. package/dist/components/sidebar/detail-card.js.map +1 -1
  73. package/dist/components/sidebar/entity-detail-panel.d.ts +1 -3
  74. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  75. package/dist/components/sidebar/entity-detail-panel.js +3 -25
  76. package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
  77. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  78. package/dist/components/sidebar/events-list.js +2 -2
  79. package/dist/components/sidebar/events-list.js.map +1 -1
  80. package/dist/components/sidebar/sidebar-data-context.d.ts +29 -0
  81. package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -0
  82. package/dist/components/sidebar/sidebar-data-context.js +19 -0
  83. package/dist/components/sidebar/sidebar-data-context.js.map +1 -0
  84. package/dist/components/stream-viewer.d.ts +1 -1
  85. package/dist/components/stream-viewer.d.ts.map +1 -1
  86. package/dist/components/stream-viewer.js +2 -26
  87. package/dist/components/stream-viewer.js.map +1 -1
  88. package/dist/components/trace-viewer/util/timing.d.ts +1 -1
  89. package/dist/components/trace-viewer/util/timing.d.ts.map +1 -1
  90. package/dist/components/trace-viewer/util/timing.js +5 -8
  91. package/dist/components/trace-viewer/util/timing.js.map +1 -1
  92. package/dist/components/trace-viewer-new.d.ts +10 -0
  93. package/dist/components/trace-viewer-new.d.ts.map +1 -0
  94. package/dist/components/trace-viewer-new.js +23 -0
  95. package/dist/components/trace-viewer-new.js.map +1 -0
  96. package/dist/components/ui/data-inspector.d.ts +21 -0
  97. package/dist/components/ui/data-inspector.d.ts.map +1 -1
  98. package/dist/components/ui/data-inspector.js +99 -4
  99. package/dist/components/ui/data-inspector.js.map +1 -1
  100. package/dist/components/ui/inspector-theme.d.ts +9 -10
  101. package/dist/components/ui/inspector-theme.d.ts.map +1 -1
  102. package/dist/components/ui/inspector-theme.js +38 -71
  103. package/dist/components/ui/inspector-theme.js.map +1 -1
  104. package/dist/components/workflow-trace-view.d.ts +1 -3
  105. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  106. package/dist/components/workflow-trace-view.js +4 -4
  107. package/dist/components/workflow-trace-view.js.map +1 -1
  108. package/dist/components/workflow-traces/trace-span-construction.d.ts +2 -2
  109. package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
  110. package/dist/components/workflow-traces/trace-span-construction.js +16 -3
  111. package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
  112. package/dist/index.d.ts +1 -0
  113. package/dist/index.d.ts.map +1 -1
  114. package/dist/index.js.map +1 -1
  115. package/dist/lib/stream-display.d.ts +18 -0
  116. package/dist/lib/stream-display.d.ts.map +1 -0
  117. package/dist/lib/stream-display.js +30 -0
  118. package/dist/lib/stream-display.js.map +1 -0
  119. package/dist/lib/trace-builder.d.ts.map +1 -1
  120. package/dist/lib/trace-builder.js +2 -1
  121. package/dist/lib/trace-builder.js.map +1 -1
  122. package/dist/lib/utils.d.ts +9 -9
  123. package/dist/lib/utils.d.ts.map +1 -1
  124. package/dist/lib/utils.js +26 -31
  125. package/dist/lib/utils.js.map +1 -1
  126. package/dist/styles.css +470 -1
  127. package/package.json +3 -2
  128. package/src/components/index.ts +5 -0
  129. package/src/components/new-trace-viewer/components/copy-button.tsx +68 -0
  130. package/src/components/new-trace-viewer/components/event-list.tsx +116 -0
  131. package/src/components/new-trace-viewer/components/middle-truncate/copy-selection.ts +114 -0
  132. package/src/components/new-trace-viewer/components/middle-truncate/middle-truncate.tsx +157 -0
  133. package/src/components/new-trace-viewer/components/middle-truncate/truncate.ts +132 -0
  134. package/src/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.ts +167 -0
  135. package/src/components/new-trace-viewer/components/split-pane.tsx +185 -0
  136. package/src/components/new-trace-viewer/components/timeline.tsx +382 -0
  137. package/src/components/new-trace-viewer/context.tsx +92 -0
  138. package/src/components/new-trace-viewer/detail-panel.tsx +58 -0
  139. package/src/components/new-trace-viewer/icons.tsx +79 -0
  140. package/src/components/new-trace-viewer/trace-viewer.tsx +645 -0
  141. package/src/components/new-trace-viewer/types.ts +0 -0
  142. package/src/components/new-trace-viewer/util/constants.ts +0 -0
  143. package/src/components/new-trace-viewer/utils.ts +450 -0
  144. package/src/components/sidebar/attribute-panel.tsx +43 -32
  145. package/src/components/sidebar/copyable-data-block.tsx +1 -1
  146. package/src/components/sidebar/detail-card.tsx +1 -19
  147. package/src/components/sidebar/entity-detail-panel.tsx +0 -71
  148. package/src/components/sidebar/events-list.tsx +9 -6
  149. package/src/components/sidebar/sidebar-data-context.tsx +62 -0
  150. package/src/components/stream-viewer.tsx +18 -40
  151. package/src/components/trace-viewer/util/timing.ts +5 -9
  152. package/src/components/trace-viewer-new.tsx +49 -0
  153. package/src/components/ui/data-inspector.tsx +235 -4
  154. package/src/components/ui/inspector-theme.ts +39 -81
  155. package/src/components/workflow-trace-view.tsx +2 -6
  156. package/src/components/workflow-traces/trace-span-construction.ts +22 -4
  157. package/src/index.ts +1 -0
  158. package/src/lib/stream-display.ts +48 -0
  159. package/src/lib/trace-builder.ts +2 -1
  160. package/src/lib/utils.ts +27 -32
  161. package/src/styles.css +470 -1
@@ -0,0 +1,645 @@
1
+ 'use client';
2
+
3
+ import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name';
4
+ import { RotateCcw, Search, X, ZoomIn, ZoomOut } from 'lucide-react';
5
+ import {
6
+ type ReactNode,
7
+ useCallback,
8
+ useEffect,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ } from 'react';
13
+ import { ErrorBoundary } from '../error-boundary';
14
+ import {
15
+ EntityDetailPanel,
16
+ type SelectedSpanInfo,
17
+ } from '../sidebar/entity-detail-panel';
18
+ import { useSidebarDataOptional } from '../sidebar/sidebar-data-context';
19
+ import type { Trace } from '../trace-viewer/types';
20
+ import { formatDuration, getHighResInMs } from '../trace-viewer/util/timing';
21
+ import { CopyButton } from './components/copy-button';
22
+ import EventList from './components/event-list';
23
+ import { SplitPane } from './components/split-pane';
24
+ import {
25
+ Timeline,
26
+ TimelineHeader,
27
+ TIMELINE_PADDING_PX,
28
+ } from './components/timeline';
29
+ import { ActiveSpanProvider, useActiveSpan } from './context';
30
+ import { DetailPanel } from './detail-panel';
31
+ import { computeRootBounds, computeTimeMarkers } from './utils';
32
+
33
+ interface NewTraceViewerProps {
34
+ trace: Trace;
35
+ }
36
+
37
+ const MIN_VIEWPORT_MS = 0.001;
38
+
39
+ interface Viewport {
40
+ start: number;
41
+ end: number;
42
+ }
43
+
44
+ function useAnimatedViewport(initial: Viewport) {
45
+ const [viewport, setViewportState] = useState<Viewport>(initial);
46
+ const animRef = useRef<{
47
+ raf: number;
48
+ from: Viewport;
49
+ to: Viewport;
50
+ start: number;
51
+ } | null>(null);
52
+ const currentRef = useRef(initial);
53
+ currentRef.current = viewport;
54
+
55
+ const cancel = useCallback(() => {
56
+ if (animRef.current) {
57
+ cancelAnimationFrame(animRef.current.raf);
58
+ animRef.current = null;
59
+ }
60
+ }, []);
61
+
62
+ const animateTo = useCallback(
63
+ (target: Viewport) => {
64
+ cancel();
65
+ const from = currentRef.current;
66
+ const anim = { raf: 0, from, to: target, start: performance.now() };
67
+
68
+ const tick = () => {
69
+ const t = Math.min((performance.now() - anim.start) / 150, 1);
70
+ const e = 1 - (1 - t) * (1 - t);
71
+ setViewportState({
72
+ start: anim.from.start + (anim.to.start - anim.from.start) * e,
73
+ end: anim.from.end + (anim.to.end - anim.from.end) * e,
74
+ });
75
+ if (t < 1) anim.raf = requestAnimationFrame(tick);
76
+ else animRef.current = null;
77
+ };
78
+
79
+ animRef.current = anim;
80
+ anim.raf = requestAnimationFrame(tick);
81
+ },
82
+ [cancel]
83
+ );
84
+
85
+ const setViewport = useCallback(
86
+ (update: Viewport | ((prev: Viewport) => Viewport)) => {
87
+ cancel();
88
+ if (typeof update === 'function') {
89
+ setViewportState((prev) => {
90
+ const next = update(prev);
91
+ currentRef.current = next;
92
+ return next;
93
+ });
94
+ } else {
95
+ currentRef.current = update;
96
+ setViewportState(update);
97
+ }
98
+ },
99
+ [cancel]
100
+ );
101
+
102
+ useEffect(() => cancel, [cancel]);
103
+
104
+ return { viewport, setViewport, animateTo };
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Hook: bridge ActiveSpanContext + SidebarDataContext → SelectedSpanInfo
109
+ // ---------------------------------------------------------------------------
110
+
111
+ function useSelectedSpanInfo(): SelectedSpanInfo | null {
112
+ const { activeSpan } = useActiveSpan();
113
+ const sidebar = useSidebarDataOptional();
114
+
115
+ return useMemo(() => {
116
+ if (!activeSpan || !sidebar) return null;
117
+
118
+ const correlationId = activeSpan.spanId;
119
+ const rawEvents = correlationId
120
+ ? sidebar.events.filter((e) => e.correlationId === correlationId)
121
+ : [];
122
+
123
+ return {
124
+ data: activeSpan.attributes?.data,
125
+ resource: activeSpan.attributes?.resource as string | undefined,
126
+ spanId: activeSpan.spanId,
127
+ rawEvents,
128
+ };
129
+ }, [activeSpan, sidebar?.events]);
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Root component
134
+ // ---------------------------------------------------------------------------
135
+
136
+ export function NewTraceViewer({ trace }: NewTraceViewerProps): ReactNode {
137
+ return (
138
+ <ActiveSpanProvider spans={trace.spans}>
139
+ <NewTraceViewerContent trace={trace} />
140
+ </ActiveSpanProvider>
141
+ );
142
+ }
143
+
144
+ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
145
+ const { activeSpan, activeSpanId, setActiveSpan, clearActiveSpan } =
146
+ useActiveSpan();
147
+
148
+ const sidebar = useSidebarDataOptional();
149
+ const selectedSpan = useSelectedSpanInfo();
150
+
151
+ const [searchQuery, setSearchQuery] = useState('');
152
+
153
+ const filteredSpans = useMemo(() => {
154
+ const q = searchQuery.trim().toLowerCase();
155
+ if (!q) return trace.spans;
156
+ return trace.spans.filter(
157
+ (s) =>
158
+ s.name.toLowerCase().includes(q) || s.resource.toLowerCase().includes(q)
159
+ );
160
+ }, [trace.spans, searchQuery]);
161
+
162
+ const root = useMemo(() => computeRootBounds(trace.spans), [trace.spans]);
163
+
164
+ const { viewport, setViewport, animateTo } = useAnimatedViewport({
165
+ start: root.startTime,
166
+ end: root.startTime + root.duration,
167
+ });
168
+
169
+ const prevRootRef = useRef<Viewport>({
170
+ start: root.startTime,
171
+ end: root.startTime + root.duration,
172
+ });
173
+
174
+ useEffect(() => {
175
+ const prevRoot = prevRootRef.current;
176
+ const newStart = root.startTime;
177
+ const newEnd = root.startTime + root.duration;
178
+
179
+ setViewport((prev) => {
180
+ const wasAtFullExtent =
181
+ Math.abs(prev.start - prevRoot.start) < 0.01 &&
182
+ Math.abs(prev.end - prevRoot.end) < 0.01;
183
+
184
+ if (wasAtFullExtent) {
185
+ return { start: newStart, end: newEnd };
186
+ }
187
+ return prev;
188
+ });
189
+
190
+ prevRootRef.current = { start: newStart, end: newEnd };
191
+ }, [root.startTime, root.duration]);
192
+
193
+ const viewDuration = viewport.end - viewport.start;
194
+
195
+ const timeMarkers = useMemo(
196
+ () => computeTimeMarkers(viewDuration, viewport.start - root.startTime),
197
+ [viewDuration, viewport.start, root.startTime]
198
+ );
199
+
200
+ const resetZoom = useCallback(() => {
201
+ animateTo({ start: root.startTime, end: root.startTime + root.duration });
202
+ }, [animateTo, root.startTime, root.duration]);
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
+
240
+ const handleSelectSpan = useCallback(
241
+ (spanId: string) => {
242
+ if (spanId === activeSpanId) {
243
+ clearActiveSpan();
244
+ return;
245
+ }
246
+ setActiveSpan(spanId);
247
+
248
+ const span = trace.spans.find((s) => s.spanId === spanId);
249
+ if (!span) return;
250
+
251
+ const spanStart = getHighResInMs(span.startTime);
252
+ const spanEnd = getHighResInMs(span.endTime);
253
+ const spanDuration = spanEnd - spanStart;
254
+
255
+ const rootS = root.startTime;
256
+ const rootE = root.startTime + root.duration;
257
+ const rootD = root.duration;
258
+
259
+ if (spanDuration > rootD * 0.8) {
260
+ animateTo({ start: rootS, end: rootE });
261
+ return;
262
+ }
263
+
264
+ const padding = Math.max(spanDuration * 0.2, MIN_VIEWPORT_MS / 2);
265
+ let newStart = spanStart - padding;
266
+ let newEnd = spanEnd + padding;
267
+
268
+ if (newEnd - newStart < MIN_VIEWPORT_MS) {
269
+ const center = (spanStart + spanEnd) / 2;
270
+ newStart = center - MIN_VIEWPORT_MS / 2;
271
+ newEnd = center + MIN_VIEWPORT_MS / 2;
272
+ }
273
+
274
+ if (newStart < rootS) {
275
+ const duration = newEnd - newStart;
276
+ newStart = rootS;
277
+ newEnd = Math.min(rootE, rootS + duration);
278
+ }
279
+ if (newEnd > rootE) {
280
+ const duration = newEnd - newStart;
281
+ newEnd = rootE;
282
+ newStart = Math.max(rootS, rootE - duration);
283
+ }
284
+
285
+ animateTo({ start: newStart, end: newEnd });
286
+ },
287
+ [
288
+ animateTo,
289
+ setActiveSpan,
290
+ clearActiveSpan,
291
+ activeSpanId,
292
+ trace.spans,
293
+ root.startTime,
294
+ root.duration,
295
+ ]
296
+ );
297
+
298
+ const [altHeld, setAltHeld] = useState(false);
299
+
300
+ useEffect(() => {
301
+ const onKeyDown = (e: KeyboardEvent): void => {
302
+ if (e.key === 'Escape') {
303
+ clearActiveSpan();
304
+ }
305
+ if (e.key === 'Alt') {
306
+ e.preventDefault();
307
+ setAltHeld(true);
308
+ }
309
+ };
310
+ const onKeyUp = (e: KeyboardEvent): void => {
311
+ if (e.key === 'Alt') setAltHeld(false);
312
+ };
313
+ const onBlur = (): void => setAltHeld(false);
314
+
315
+ window.addEventListener('keydown', onKeyDown);
316
+ window.addEventListener('keyup', onKeyUp);
317
+ window.addEventListener('blur', onBlur);
318
+ return () => {
319
+ window.removeEventListener('keydown', onKeyDown);
320
+ window.removeEventListener('keyup', onKeyUp);
321
+ window.removeEventListener('blur', onBlur);
322
+ };
323
+ }, [clearActiveSpan]);
324
+
325
+ const timelineRef = useRef<HTMLDivElement>(null);
326
+ const [hoverFraction, setHoverFraction] = useState<number | null>(null);
327
+
328
+ const hoverInfo = useMemo(() => {
329
+ if (hoverFraction == null) return null;
330
+ const absTime = viewport.start + hoverFraction * viewDuration;
331
+ const offset = absTime - root.startTime;
332
+ return { fraction: hoverFraction, label: formatDuration(offset, true) };
333
+ }, [hoverFraction, viewport.start, viewDuration, root.startTime]);
334
+
335
+ const handleTimelineMouseMove = useCallback(
336
+ (e: React.MouseEvent<HTMLDivElement>) => {
337
+ const el = timelineRef.current;
338
+ if (!el) return;
339
+ const rect = el.getBoundingClientRect();
340
+ const contentWidth = rect.width - TIMELINE_PADDING_PX * 2;
341
+ if (contentWidth <= 0) return;
342
+ const fraction = Math.max(
343
+ 0,
344
+ Math.min(
345
+ 1,
346
+ (e.clientX - rect.left - TIMELINE_PADDING_PX) / contentWidth
347
+ )
348
+ );
349
+ setHoverFraction(fraction);
350
+ },
351
+ []
352
+ );
353
+
354
+ const handleTimelineMouseLeave = useCallback(() => {
355
+ setHoverFraction(null);
356
+ }, []);
357
+
358
+ useEffect(() => {
359
+ const el = timelineRef.current;
360
+ if (!el) return;
361
+
362
+ const rootS = root.startTime;
363
+ const rootE = root.startTime + root.duration;
364
+ const rootD = root.duration;
365
+ if (rootD <= 0) return;
366
+
367
+ const onWheel = (e: WheelEvent): void => {
368
+ const isZoomGesture = e.ctrlKey || e.metaKey;
369
+ const hasDeltaX = Math.abs(e.deltaX) > Math.abs(e.deltaY);
370
+
371
+ if (!isZoomGesture && !hasDeltaX) return;
372
+ e.preventDefault();
373
+
374
+ const rect = el.getBoundingClientRect();
375
+ const contentWidth = rect.width - TIMELINE_PADDING_PX * 2;
376
+ if (contentWidth <= 0) return;
377
+
378
+ if (isZoomGesture) {
379
+ let dy = e.deltaY;
380
+ if (e.deltaMode === 1) dy *= 16;
381
+
382
+ const cursorFraction = Math.max(
383
+ 0,
384
+ Math.min(
385
+ 1,
386
+ (e.clientX - rect.left - TIMELINE_PADDING_PX) / contentWidth
387
+ )
388
+ );
389
+ const scaleFactor = Math.pow(2, dy / 200);
390
+
391
+ setViewport((prev) => {
392
+ const prevDuration = prev.end - prev.start;
393
+ const cursorTime = prev.start + cursorFraction * prevDuration;
394
+ const newDuration = Math.max(
395
+ MIN_VIEWPORT_MS,
396
+ Math.min(rootD, prevDuration * scaleFactor)
397
+ );
398
+
399
+ let newStart = cursorTime - cursorFraction * newDuration;
400
+ let newEnd = newStart + newDuration;
401
+
402
+ if (newStart < rootS) {
403
+ newStart = rootS;
404
+ newEnd = rootS + newDuration;
405
+ }
406
+ if (newEnd > rootE) {
407
+ newEnd = rootE;
408
+ newStart = Math.max(rootS, rootE - newDuration);
409
+ }
410
+
411
+ return { start: newStart, end: newEnd };
412
+ });
413
+ } else {
414
+ let dx = e.deltaX;
415
+ if (e.deltaMode === 1) dx *= 16;
416
+
417
+ setViewport((prev) => {
418
+ const prevDuration = prev.end - prev.start;
419
+ const panAmount = (dx / contentWidth) * prevDuration;
420
+
421
+ let newStart = prev.start + panAmount;
422
+ let newEnd = prev.end + panAmount;
423
+
424
+ if (newStart < rootS) {
425
+ newStart = rootS;
426
+ newEnd = rootS + prevDuration;
427
+ }
428
+ if (newEnd > rootE) {
429
+ newEnd = rootE;
430
+ newStart = Math.max(rootS, rootE - prevDuration);
431
+ }
432
+
433
+ return { start: newStart, end: newEnd };
434
+ });
435
+ }
436
+ };
437
+
438
+ el.addEventListener('wheel', onWheel, { passive: false });
439
+ return () => el.removeEventListener('wheel', onWheel);
440
+ }, [root.startTime, root.duration]);
441
+
442
+ // Derive the selected span name and metadata for the panel header
443
+ const selectedSpanName = useMemo(() => {
444
+ if (!selectedSpan?.data) return 'Details';
445
+ const data = selectedSpan.data as Record<string, unknown>;
446
+ const stepName = data.stepName as string | undefined;
447
+ const workflowName = data.workflowName as string | undefined;
448
+ return (
449
+ (stepName ? parseStepName(stepName)?.shortName : undefined) ??
450
+ (workflowName ? parseWorkflowName(workflowName)?.shortName : undefined) ??
451
+ stepName ??
452
+ workflowName ??
453
+ (data.hookId as string) ??
454
+ 'Details'
455
+ );
456
+ }, [selectedSpan?.data]);
457
+
458
+ const selectedResource = selectedSpan?.resource as string | undefined;
459
+ const selectedResourceId = useMemo(() => {
460
+ if (!selectedSpan?.data) return undefined;
461
+ const data = selectedSpan.data as Record<string, unknown>;
462
+ return (
463
+ (data.stepId as string) ??
464
+ (data.runId as string) ??
465
+ (data.hookId as string) ??
466
+ selectedSpan.spanId
467
+ );
468
+ }, [selectedSpan?.data, selectedSpan?.spanId]);
469
+
470
+ return (
471
+ <div
472
+ data-pane='pane-root'
473
+ data-has-detail={activeSpan ? '' : undefined}
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%)]'
475
+ >
476
+ <div
477
+ id='trace-parent'
478
+ className='grid grid-rows-[1fr] h-full min-h-0 overflow-hidden relative bg-background-100'
479
+ >
480
+ <SplitPane
481
+ startHeader={
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' />
484
+ <input
485
+ id='trace-viewer-search'
486
+ name='trace-viewer-search'
487
+ type='text'
488
+ value={searchQuery}
489
+ onChange={(e) => setSearchQuery(e.target.value)}
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'
493
+ />
494
+ {searchQuery && (
495
+ <button
496
+ type='button'
497
+ onClick={() => setSearchQuery('')}
498
+ className='shrink-0 p-0.5 rounded-sm text-gray-800 hover:text-gray-1000 hover:bg-gray-200 transition-colors'
499
+ >
500
+ <X className='w-3 h-3' />
501
+ </button>
502
+ )}
503
+ </div>
504
+ }
505
+ endHeader={
506
+ <TimelineHeader markers={timeMarkers} hoverInfo={hoverInfo} />
507
+ }
508
+ >
509
+ <div className='block overflow-visible'>
510
+ <EventList
511
+ spans={filteredSpans}
512
+ activeSpanId={activeSpanId}
513
+ onSelectSpan={handleSelectSpan}
514
+ />
515
+ </div>
516
+ <div
517
+ ref={timelineRef}
518
+ className='block min-h-0 overflow-visible relative'
519
+ onDoubleClick={resetZoom}
520
+ onMouseMove={handleTimelineMouseMove}
521
+ onMouseLeave={handleTimelineMouseLeave}
522
+ >
523
+ <Timeline
524
+ spans={filteredSpans}
525
+ viewStart={viewport.start}
526
+ viewEnd={viewport.end}
527
+ markers={timeMarkers}
528
+ selectedId={activeSpanId}
529
+ onSelect={handleSelectSpan}
530
+ hoverFraction={hoverFraction}
531
+ altHeld={altHeld}
532
+ />
533
+ </div>
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>
561
+ </div>
562
+
563
+ {/* Detail panel */}
564
+ {activeSpan && sidebar ? (
565
+ <aside className='flex flex-col h-full max-h-full bg-background-100 border-l border-gray-alpha-400 overflow-auto'>
566
+ {/* Panel header */}
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'>
571
+ {selectedSpanName}
572
+ </span>
573
+ {selectedResourceId && (
574
+ <div className='mt-1 flex items-center gap-2'>
575
+ {selectedResource && (
576
+ <span
577
+ className={`inline-flex items-center rounded-md px-1.5 py-0.5 text-[11px] font-medium leading-none shrink-0 ${
578
+ selectedResource === 'step'
579
+ ? 'bg-green-200 text-green-900'
580
+ : selectedResource === 'run'
581
+ ? 'bg-blue-200 text-blue-900'
582
+ : selectedResource === 'hook'
583
+ ? 'bg-yellow-200 text-yellow-900'
584
+ : 'bg-gray-200 text-gray-900'
585
+ }`}
586
+ >
587
+ {selectedResource.charAt(0).toUpperCase() +
588
+ selectedResource.slice(1)}
589
+ </span>
590
+ )}
591
+ <div
592
+ className='flex items-center gap-1 text-[13px] font-mono text-gray-700 min-w-0'
593
+ title={selectedResourceId}
594
+ >
595
+ <span className='truncate'>{selectedResourceId}</span>
596
+ <CopyButton
597
+ copyText={selectedResourceId}
598
+ ariaLabel='Copy ID'
599
+ className='shrink-0'
600
+ />
601
+ </div>
602
+ </div>
603
+ )}
604
+ </div>
605
+ <button
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'
608
+ onClick={clearActiveSpan}
609
+ >
610
+ <X className='w-4 h-4' />
611
+ </button>
612
+ </div>
613
+ </div>
614
+ {/* Panel body */}
615
+ <div className='flex-1 overflow-y-auto'>
616
+ <ErrorBoundary>
617
+ <EntityDetailPanel
618
+ run={sidebar.run}
619
+ onStreamClick={sidebar.onStreamClick}
620
+ onRunClick={sidebar.onRunClick}
621
+ spanDetailData={sidebar.spanDetailData}
622
+ spanDetailError={sidebar.spanDetailError}
623
+ spanDetailLoading={sidebar.spanDetailLoading}
624
+ onSpanSelect={sidebar.onSpanSelect}
625
+ onWakeUpSleep={sidebar.onWakeUpSleep}
626
+ onLoadEventData={sidebar.onLoadEventData}
627
+ onResolveHook={sidebar.onResolveHook}
628
+ encryptionKey={sidebar.encryptionKey}
629
+ onDecrypt={sidebar.onDecrypt}
630
+ isDecrypting={sidebar.isDecrypting}
631
+ selectedSpan={selectedSpan}
632
+ />
633
+ </ErrorBoundary>
634
+ </div>
635
+ </aside>
636
+ ) : activeSpan ? (
637
+ <DetailPanel
638
+ span={activeSpan}
639
+ rootStart={root.startTime}
640
+ onClose={clearActiveSpan}
641
+ />
642
+ ) : null}
643
+ </div>
644
+ );
645
+ }
File without changes