@workflow/web-shared 5.0.0-beta.0 → 5.0.0-beta.10

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 (275) hide show
  1. package/README.md +1 -1
  2. package/dist/components/error-boundary.js +1 -1
  3. package/dist/components/event-list-view.d.ts +18 -1
  4. package/dist/components/event-list-view.d.ts.map +1 -1
  5. package/dist/components/event-list-view.js +242 -142
  6. package/dist/components/hook-actions.js +1 -1
  7. package/dist/components/index.d.ts +4 -1
  8. package/dist/components/index.d.ts.map +1 -1
  9. package/dist/components/index.js +5 -2
  10. package/dist/components/new-trace-viewer/components/copy-button.d.ts +9 -0
  11. package/dist/components/new-trace-viewer/components/copy-button.d.ts.map +1 -0
  12. package/dist/components/new-trace-viewer/components/copy-button.js +27 -0
  13. package/dist/components/new-trace-viewer/components/event-list.d.ts +10 -0
  14. package/dist/components/new-trace-viewer/components/event-list.d.ts.map +1 -0
  15. package/dist/components/new-trace-viewer/components/event-list.js +39 -0
  16. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts +18 -0
  17. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts.map +1 -0
  18. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js +54 -0
  19. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts +20 -0
  20. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts.map +1 -0
  21. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js +81 -0
  22. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts +25 -0
  23. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts.map +1 -0
  24. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js +101 -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/split-pane.d.ts +14 -0
  29. package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -0
  30. package/dist/components/new-trace-viewer/components/split-pane.js +99 -0
  31. package/dist/components/new-trace-viewer/components/timeline.d.ts +34 -0
  32. package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -0
  33. package/dist/components/new-trace-viewer/components/timeline.js +223 -0
  34. package/dist/components/new-trace-viewer/context.d.ts +15 -0
  35. package/dist/components/new-trace-viewer/context.d.ts.map +1 -0
  36. package/dist/components/new-trace-viewer/context.js +47 -0
  37. package/dist/components/new-trace-viewer/detail-panel.d.ts +10 -0
  38. package/dist/components/new-trace-viewer/detail-panel.d.ts.map +1 -0
  39. package/dist/components/new-trace-viewer/detail-panel.js +13 -0
  40. package/dist/components/new-trace-viewer/icons.d.ts +6 -0
  41. package/dist/components/new-trace-viewer/icons.d.ts.map +1 -0
  42. package/dist/components/new-trace-viewer/icons.js +15 -0
  43. package/dist/components/new-trace-viewer/search.d.ts +19 -0
  44. package/dist/components/new-trace-viewer/search.d.ts.map +1 -0
  45. package/dist/components/new-trace-viewer/search.js +134 -0
  46. package/dist/components/new-trace-viewer/trace-viewer.d.ts +8 -0
  47. package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -0
  48. package/dist/components/new-trace-viewer/trace-viewer.js +387 -0
  49. package/dist/components/new-trace-viewer/types.d.ts +1 -0
  50. package/dist/components/new-trace-viewer/types.d.ts.map +1 -0
  51. package/dist/components/new-trace-viewer/types.js +2 -0
  52. package/dist/components/new-trace-viewer/util/constants.d.ts +1 -0
  53. package/dist/components/new-trace-viewer/util/constants.d.ts.map +1 -0
  54. package/dist/components/new-trace-viewer/util/constants.js +2 -0
  55. package/dist/components/new-trace-viewer/utils.d.ts +46 -0
  56. package/dist/components/new-trace-viewer/utils.d.ts.map +1 -0
  57. package/dist/components/new-trace-viewer/utils.js +330 -0
  58. package/dist/components/run-trace-view.d.ts +2 -1
  59. package/dist/components/run-trace-view.d.ts.map +1 -1
  60. package/dist/components/run-trace-view.js +3 -3
  61. package/dist/components/sidebar/attribute-panel.d.ts +7 -1
  62. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  63. package/dist/components/sidebar/attribute-panel.js +131 -70
  64. package/dist/components/sidebar/conversation-view.js +1 -1
  65. package/dist/components/sidebar/copyable-data-block.d.ts +1 -0
  66. package/dist/components/sidebar/copyable-data-block.d.ts.map +1 -1
  67. package/dist/components/sidebar/copyable-data-block.js +16 -15
  68. package/dist/components/sidebar/detail-card.d.ts +5 -6
  69. package/dist/components/sidebar/detail-card.d.ts.map +1 -1
  70. package/dist/components/sidebar/detail-card.js +32 -16
  71. package/dist/components/sidebar/entity-detail-panel.d.ts +3 -1
  72. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  73. package/dist/components/sidebar/entity-detail-panel.js +20 -37
  74. package/dist/components/sidebar/events-list.d.ts +1 -1
  75. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  76. package/dist/components/sidebar/events-list.js +46 -32
  77. package/dist/components/sidebar/resolve-hook-modal.js +1 -1
  78. package/dist/components/sidebar/sidebar-data-context.d.ts +29 -0
  79. package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -0
  80. package/dist/components/sidebar/sidebar-data-context.js +19 -0
  81. package/dist/components/stream-viewer.d.ts +1 -1
  82. package/dist/components/stream-viewer.d.ts.map +1 -1
  83. package/dist/components/stream-viewer.js +3 -27
  84. package/dist/components/trace-viewer/components/map.js +1 -1
  85. package/dist/components/trace-viewer/components/markers.js +1 -1
  86. package/dist/components/trace-viewer/components/node.js +1 -1
  87. package/dist/components/trace-viewer/components/search-input.js +1 -1
  88. package/dist/components/trace-viewer/components/search.js +1 -1
  89. package/dist/components/trace-viewer/components/span-content.js +1 -1
  90. package/dist/components/trace-viewer/components/span-detail-panel.js +1 -1
  91. package/dist/components/trace-viewer/components/span-segments.js +1 -1
  92. package/dist/components/trace-viewer/components/span-strategies.js +1 -1
  93. package/dist/components/trace-viewer/components/ui.js +1 -1
  94. package/dist/components/trace-viewer/components/zoom-button.js +1 -1
  95. package/dist/components/trace-viewer/components/zoom-icons.js +1 -1
  96. package/dist/components/trace-viewer/context.js +1 -1
  97. package/dist/components/trace-viewer/index.js +1 -1
  98. package/dist/components/trace-viewer/trace-viewer.js +1 -1
  99. package/dist/components/trace-viewer/types.js +1 -1
  100. package/dist/components/trace-viewer/util/constants.js +1 -1
  101. package/dist/components/trace-viewer/util/scrollbar-width.js +1 -1
  102. package/dist/components/trace-viewer/util/timing.d.ts +1 -1
  103. package/dist/components/trace-viewer/util/timing.d.ts.map +1 -1
  104. package/dist/components/trace-viewer/util/timing.js +6 -9
  105. package/dist/components/trace-viewer/util/tree.js +1 -1
  106. package/dist/components/trace-viewer/util/use-immediate-style.js +1 -1
  107. package/dist/components/trace-viewer/util/use-streaming-spans.js +1 -1
  108. package/dist/components/trace-viewer/util/use-trackpad-zoom.js +1 -1
  109. package/dist/components/trace-viewer/worker.js +1 -1
  110. package/dist/components/trace-viewer-new.d.ts +10 -0
  111. package/dist/components/trace-viewer-new.d.ts.map +1 -0
  112. package/dist/components/trace-viewer-new.js +23 -0
  113. package/dist/components/ui/alert.js +1 -1
  114. package/dist/components/ui/button.d.ts +10 -0
  115. package/dist/components/ui/button.d.ts.map +1 -0
  116. package/dist/components/ui/button.js +46 -0
  117. package/dist/components/ui/card.js +1 -1
  118. package/dist/components/ui/data-inspector.d.ts +39 -1
  119. package/dist/components/ui/data-inspector.d.ts.map +1 -1
  120. package/dist/components/ui/data-inspector.js +207 -22
  121. package/dist/components/ui/decrypt-button.js +1 -1
  122. package/dist/components/ui/error-card.js +1 -1
  123. package/dist/components/ui/error-stack-block.d.ts.map +1 -1
  124. package/dist/components/ui/error-stack-block.js +27 -19
  125. package/dist/components/ui/icon-button.d.ts +13 -0
  126. package/dist/components/ui/icon-button.d.ts.map +1 -0
  127. package/dist/components/ui/icon-button.js +29 -0
  128. package/dist/components/ui/inspector-theme.d.ts +9 -10
  129. package/dist/components/ui/inspector-theme.d.ts.map +1 -1
  130. package/dist/components/ui/inspector-theme.js +39 -72
  131. package/dist/components/ui/load-more-button.js +1 -1
  132. package/dist/components/ui/menu-dropdown.js +1 -1
  133. package/dist/components/ui/skeleton.js +1 -1
  134. package/dist/components/ui/spinner.js +1 -1
  135. package/dist/components/ui/timestamp-tooltip.js +1 -1
  136. package/dist/components/workflow-trace-view.d.ts +3 -1
  137. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  138. package/dist/components/workflow-trace-view.js +10 -5
  139. package/dist/components/workflow-traces/event-colors.js +1 -1
  140. package/dist/components/workflow-traces/trace-colors.js +1 -1
  141. package/dist/components/workflow-traces/trace-span-construction.d.ts +2 -2
  142. package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
  143. package/dist/components/workflow-traces/trace-span-construction.js +19 -6
  144. package/dist/components/workflow-traces/trace-time-utils.js +1 -1
  145. package/dist/hooks/use-dark-mode.js +1 -1
  146. package/dist/hooks/use-reduced-motion.d.ts +10 -0
  147. package/dist/hooks/use-reduced-motion.d.ts.map +1 -0
  148. package/dist/hooks/use-reduced-motion.js +29 -0
  149. package/dist/index.d.ts +3 -1
  150. package/dist/index.d.ts.map +1 -1
  151. package/dist/index.js +3 -2
  152. package/dist/lib/event-analysis.js +1 -1
  153. package/dist/lib/event-materialization.js +1 -1
  154. package/dist/lib/exact-event-search-id.d.ts +25 -0
  155. package/dist/lib/exact-event-search-id.d.ts.map +1 -0
  156. package/dist/lib/exact-event-search-id.js +39 -0
  157. package/dist/lib/hydration.d.ts +15 -2
  158. package/dist/lib/hydration.d.ts.map +1 -1
  159. package/dist/lib/hydration.js +168 -16
  160. package/dist/lib/stream-display.d.ts +18 -0
  161. package/dist/lib/stream-display.d.ts.map +1 -0
  162. package/dist/lib/stream-display.js +30 -0
  163. package/dist/lib/toast.js +1 -1
  164. package/dist/lib/trace-builder.d.ts.map +1 -1
  165. package/dist/lib/trace-builder.js +3 -2
  166. package/dist/lib/utils.d.ts +9 -9
  167. package/dist/lib/utils.d.ts.map +1 -1
  168. package/dist/lib/utils.js +27 -32
  169. package/dist/styles.css +475 -1
  170. package/package.json +6 -4
  171. package/src/components/event-list-view.tsx +447 -282
  172. package/src/components/index.ts +12 -1
  173. package/src/components/new-trace-viewer/components/copy-button.tsx +68 -0
  174. package/src/components/new-trace-viewer/components/event-list.tsx +127 -0
  175. package/src/components/new-trace-viewer/components/middle-truncate/copy-selection.ts +114 -0
  176. package/src/components/new-trace-viewer/components/middle-truncate/middle-truncate.tsx +157 -0
  177. package/src/components/new-trace-viewer/components/middle-truncate/truncate.ts +132 -0
  178. package/src/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.ts +167 -0
  179. package/src/components/new-trace-viewer/components/split-pane.tsx +185 -0
  180. package/src/components/new-trace-viewer/components/timeline.tsx +552 -0
  181. package/src/components/new-trace-viewer/context.tsx +92 -0
  182. package/src/components/new-trace-viewer/detail-panel.tsx +55 -0
  183. package/src/components/new-trace-viewer/icons.tsx +79 -0
  184. package/src/components/new-trace-viewer/search.ts +178 -0
  185. package/src/components/new-trace-viewer/trace-viewer.tsx +694 -0
  186. package/src/components/new-trace-viewer/types.ts +0 -0
  187. package/src/components/new-trace-viewer/util/constants.ts +0 -0
  188. package/src/components/new-trace-viewer/utils.ts +482 -0
  189. package/src/components/run-trace-view.tsx +3 -0
  190. package/src/components/sidebar/attribute-panel.tsx +240 -196
  191. package/src/components/sidebar/copyable-data-block.tsx +49 -27
  192. package/src/components/sidebar/detail-card.tsx +119 -59
  193. package/src/components/sidebar/entity-detail-panel.tsx +92 -148
  194. package/src/components/sidebar/events-list.tsx +93 -108
  195. package/src/components/sidebar/sidebar-data-context.tsx +62 -0
  196. package/src/components/stream-viewer.tsx +18 -40
  197. package/src/components/trace-viewer/util/timing.ts +5 -9
  198. package/src/components/trace-viewer-new.tsx +49 -0
  199. package/src/components/ui/button.tsx +68 -0
  200. package/src/components/ui/data-inspector.tsx +427 -31
  201. package/src/components/ui/error-stack-block.tsx +39 -30
  202. package/src/components/ui/icon-button.tsx +57 -0
  203. package/src/components/ui/inspector-theme.ts +39 -81
  204. package/src/components/workflow-trace-view.tsx +12 -2
  205. package/src/components/workflow-traces/trace-span-construction.ts +24 -6
  206. package/src/hooks/use-reduced-motion.ts +34 -0
  207. package/src/index.ts +9 -0
  208. package/src/lib/exact-event-search-id.ts +67 -0
  209. package/src/lib/hydration.ts +182 -14
  210. package/src/lib/stream-display.ts +48 -0
  211. package/src/lib/trace-builder.ts +2 -1
  212. package/src/lib/utils.ts +27 -32
  213. package/src/styles.css +475 -1
  214. package/dist/components/error-boundary.js.map +0 -1
  215. package/dist/components/event-list-view.js.map +0 -1
  216. package/dist/components/hook-actions.js.map +0 -1
  217. package/dist/components/index.js.map +0 -1
  218. package/dist/components/run-trace-view.js.map +0 -1
  219. package/dist/components/sidebar/attribute-panel.js.map +0 -1
  220. package/dist/components/sidebar/conversation-view.js.map +0 -1
  221. package/dist/components/sidebar/copyable-data-block.js.map +0 -1
  222. package/dist/components/sidebar/detail-card.js.map +0 -1
  223. package/dist/components/sidebar/entity-detail-panel.js.map +0 -1
  224. package/dist/components/sidebar/events-list.js.map +0 -1
  225. package/dist/components/sidebar/resolve-hook-modal.js.map +0 -1
  226. package/dist/components/stream-viewer.js.map +0 -1
  227. package/dist/components/trace-viewer/components/map.js.map +0 -1
  228. package/dist/components/trace-viewer/components/markers.js.map +0 -1
  229. package/dist/components/trace-viewer/components/node.js.map +0 -1
  230. package/dist/components/trace-viewer/components/search-input.js.map +0 -1
  231. package/dist/components/trace-viewer/components/search.js.map +0 -1
  232. package/dist/components/trace-viewer/components/span-content.js.map +0 -1
  233. package/dist/components/trace-viewer/components/span-detail-panel.js.map +0 -1
  234. package/dist/components/trace-viewer/components/span-segments.js.map +0 -1
  235. package/dist/components/trace-viewer/components/span-strategies.js.map +0 -1
  236. package/dist/components/trace-viewer/components/ui.js.map +0 -1
  237. package/dist/components/trace-viewer/components/zoom-button.js.map +0 -1
  238. package/dist/components/trace-viewer/components/zoom-icons.js.map +0 -1
  239. package/dist/components/trace-viewer/context.js.map +0 -1
  240. package/dist/components/trace-viewer/index.js.map +0 -1
  241. package/dist/components/trace-viewer/trace-viewer.js.map +0 -1
  242. package/dist/components/trace-viewer/types.js.map +0 -1
  243. package/dist/components/trace-viewer/util/constants.js.map +0 -1
  244. package/dist/components/trace-viewer/util/scrollbar-width.js.map +0 -1
  245. package/dist/components/trace-viewer/util/timing.js.map +0 -1
  246. package/dist/components/trace-viewer/util/tree.js.map +0 -1
  247. package/dist/components/trace-viewer/util/use-immediate-style.js.map +0 -1
  248. package/dist/components/trace-viewer/util/use-streaming-spans.js.map +0 -1
  249. package/dist/components/trace-viewer/util/use-trackpad-zoom.js.map +0 -1
  250. package/dist/components/trace-viewer/worker.js.map +0 -1
  251. package/dist/components/ui/alert.js.map +0 -1
  252. package/dist/components/ui/card.js.map +0 -1
  253. package/dist/components/ui/data-inspector.js.map +0 -1
  254. package/dist/components/ui/decrypt-button.js.map +0 -1
  255. package/dist/components/ui/error-card.js.map +0 -1
  256. package/dist/components/ui/error-stack-block.js.map +0 -1
  257. package/dist/components/ui/inspector-theme.js.map +0 -1
  258. package/dist/components/ui/load-more-button.js.map +0 -1
  259. package/dist/components/ui/menu-dropdown.js.map +0 -1
  260. package/dist/components/ui/skeleton.js.map +0 -1
  261. package/dist/components/ui/spinner.js.map +0 -1
  262. package/dist/components/ui/timestamp-tooltip.js.map +0 -1
  263. package/dist/components/workflow-trace-view.js.map +0 -1
  264. package/dist/components/workflow-traces/event-colors.js.map +0 -1
  265. package/dist/components/workflow-traces/trace-colors.js.map +0 -1
  266. package/dist/components/workflow-traces/trace-span-construction.js.map +0 -1
  267. package/dist/components/workflow-traces/trace-time-utils.js.map +0 -1
  268. package/dist/hooks/use-dark-mode.js.map +0 -1
  269. package/dist/index.js.map +0 -1
  270. package/dist/lib/event-analysis.js.map +0 -1
  271. package/dist/lib/event-materialization.js.map +0 -1
  272. package/dist/lib/hydration.js.map +0 -1
  273. package/dist/lib/toast.js.map +0 -1
  274. package/dist/lib/trace-builder.js.map +0 -1
  275. package/dist/lib/utils.js.map +0 -1
@@ -0,0 +1,552 @@
1
+ 'use client';
2
+
3
+ import { ArrowLeft, ArrowRight } from 'lucide-react';
4
+ import type { CSSProperties, ReactNode } from 'react';
5
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
+ import { cn } from '../../../lib/utils';
7
+ import type { Span } from '../../trace-viewer/types';
8
+ import { formatDuration, getHighResInMs } from '../../trace-viewer/util/timing';
9
+ import type { Segment, SegmentStatus, TimeMarker } from '../utils';
10
+ import { isSpanDimmedBySearch, type SpanSearchResult } from '../search';
11
+ import {
12
+ computeSpanGaps,
13
+ computeSpanSegments,
14
+ getResourceColor,
15
+ getSpanDurationMs,
16
+ } from '../utils';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Constants
20
+ // ---------------------------------------------------------------------------
21
+
22
+ const TINY_BAR_BOX_SIZE_PX = 24;
23
+ const TINY_BAR_WIDTH_PX = 4;
24
+ export const TIMELINE_PADDING_PX = 16;
25
+
26
+ const SEGMENT_CLASSES: Record<SegmentStatus, string> = {
27
+ queued: 'bg-gray-400 border border-gray-500',
28
+ retrying: 'bg-gray-400 border border-gray-500',
29
+ waiting: 'bg-gray-400 border border-gray-500',
30
+ running: 'bg-blue-200 border border-blue-500',
31
+ failed: 'bg-red-200 border border-red-500',
32
+ succeeded: 'bg-green-200 border border-green-500',
33
+ sleeping: 'bg-gray-400 border border-gray-500',
34
+ received: 'bg-blue-200 border border-blue-500',
35
+ };
36
+
37
+ const TIMELINE_INSET_STYLE: CSSProperties = {
38
+ left: TIMELINE_PADDING_PX,
39
+ right: TIMELINE_PADDING_PX,
40
+ };
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Bar geometry
44
+ // ---------------------------------------------------------------------------
45
+
46
+ type BarMode =
47
+ | { kind: 'arrow'; direction: 'left' | 'right' }
48
+ | { kind: 'tiny' }
49
+ | { kind: 'full' };
50
+
51
+ interface BarGeometry {
52
+ mode: BarMode;
53
+ leftPct: number;
54
+ widthPct: number;
55
+ visibleStartMs: number;
56
+ visibleEndMs: number;
57
+ visiblePixelWidth: number;
58
+ }
59
+
60
+ /**
61
+ * Compute the bar's geometry inside the timeline viewport. Percentages are
62
+ * always in [0, 100] so we never emit CSS values that exceed browser layout
63
+ * limits at extreme zoom.
64
+ */
65
+ function computeBarGeometry(
66
+ startMs: number,
67
+ endMs: number,
68
+ viewStart: number,
69
+ viewEnd: number,
70
+ containerWidth: number
71
+ ): BarGeometry {
72
+ const viewDuration = viewEnd - viewStart;
73
+ const visibleStartMs = Math.max(startMs, viewStart);
74
+ const visibleEndMs = Math.min(endMs, viewEnd);
75
+ const visibleDurationMs = Math.max(0, visibleEndMs - visibleStartMs);
76
+ const widthFrac = viewDuration > 0 ? visibleDurationMs / viewDuration : 0;
77
+ const visiblePixelWidth = widthFrac * containerWidth;
78
+
79
+ const isTiny = containerWidth > 0 && visiblePixelWidth < TINY_BAR_BOX_SIZE_PX;
80
+ const extendsOffLeft = startMs < viewStart;
81
+ const extendsOffRight = endMs > viewEnd;
82
+
83
+ const mode: BarMode =
84
+ isTiny && (extendsOffLeft || extendsOffRight)
85
+ ? { kind: 'arrow', direction: extendsOffRight ? 'right' : 'left' }
86
+ : isTiny
87
+ ? { kind: 'tiny' }
88
+ : { kind: 'full' };
89
+
90
+ return {
91
+ mode,
92
+ leftPct:
93
+ viewDuration > 0
94
+ ? ((visibleStartMs - viewStart) / viewDuration) * 100
95
+ : 0,
96
+ widthPct: widthFrac * 100,
97
+ visibleStartMs,
98
+ visibleEndMs,
99
+ visiblePixelWidth,
100
+ };
101
+ }
102
+
103
+ function getBarPositionStyle(geometry: BarGeometry): {
104
+ left: string;
105
+ width: string;
106
+ } {
107
+ switch (geometry.mode.kind) {
108
+ case 'arrow':
109
+ return {
110
+ left:
111
+ geometry.mode.direction === 'right'
112
+ ? `calc(100% - ${TINY_BAR_BOX_SIZE_PX}px)`
113
+ : '0px',
114
+ width: `${TINY_BAR_BOX_SIZE_PX}px`,
115
+ };
116
+ case 'tiny':
117
+ return {
118
+ left: `min(${geometry.leftPct}%, calc(100% - ${TINY_BAR_WIDTH_PX}px))`,
119
+ width: `${TINY_BAR_WIDTH_PX}px`,
120
+ };
121
+ case 'full':
122
+ return {
123
+ left: `${geometry.leftPct}%`,
124
+ width: `max(${geometry.widthPct}%, 4px)`,
125
+ };
126
+ }
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Segment projection
131
+ // ---------------------------------------------------------------------------
132
+
133
+ interface VisibleSegment {
134
+ status: SegmentStatus;
135
+ leftPct: number;
136
+ widthPct: number;
137
+ pixelWidth: number;
138
+ fullDurationMs: number;
139
+ }
140
+
141
+ /**
142
+ * Project status segments onto the visible portion of a bar. Segments fully
143
+ * outside the visible window are dropped; segments crossing the edge are
144
+ * clipped to [0%, 100%] of the visible bar.
145
+ */
146
+ function projectSegments(
147
+ segments: Segment[],
148
+ spanStartMs: number,
149
+ spanDurationMs: number,
150
+ geometry: BarGeometry
151
+ ): VisibleSegment[] {
152
+ const visibleDurationMs = geometry.visibleEndMs - geometry.visibleStartMs;
153
+ if (visibleDurationMs <= 0) return [];
154
+
155
+ return segments.flatMap((seg) => {
156
+ const segStartMs = spanStartMs + seg.startFraction * spanDurationMs;
157
+ const segEndMs = spanStartMs + seg.endFraction * spanDurationMs;
158
+ const startMs = Math.max(segStartMs, geometry.visibleStartMs);
159
+ const endMs = Math.min(segEndMs, geometry.visibleEndMs);
160
+ if (endMs <= startMs) return [];
161
+
162
+ const widthFrac = (endMs - startMs) / visibleDurationMs;
163
+ return [
164
+ {
165
+ status: seg.status,
166
+ leftPct:
167
+ ((startMs - geometry.visibleStartMs) / visibleDurationMs) * 100,
168
+ widthPct: widthFrac * 100,
169
+ pixelWidth: widthFrac * geometry.visiblePixelWidth,
170
+ fullDurationMs: (seg.endFraction - seg.startFraction) * spanDurationMs,
171
+ },
172
+ ];
173
+ });
174
+ }
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // Small render helpers
178
+ // ---------------------------------------------------------------------------
179
+
180
+ function DurationLabel({ label }: { label: string }): ReactNode {
181
+ return (
182
+ <span className="pointer-events-none absolute inset-0 flex items-center justify-start overflow-hidden px-1 text-[10px] font-mono font-medium leading-none whitespace-nowrap text-left text-gray-1000 tabular-nums opacity-0 group-hover/timeline-row:opacity-100">
183
+ {label}
184
+ </span>
185
+ );
186
+ }
187
+
188
+ function BoundaryArrow({
189
+ direction,
190
+ }: {
191
+ direction: 'left' | 'right';
192
+ }): ReactNode {
193
+ const Icon = direction === 'right' ? ArrowRight : ArrowLeft;
194
+ return (
195
+ <div className="flex h-6 w-6 items-center justify-center rounded-[0.25rem]">
196
+ <Icon className="size-3 text-gray-900" />
197
+ </div>
198
+ );
199
+ }
200
+
201
+ function PlainBar({
202
+ bg,
203
+ border,
204
+ label,
205
+ }: {
206
+ bg: string;
207
+ border: string;
208
+ label: string | null;
209
+ }): ReactNode {
210
+ return (
211
+ <div
212
+ className="relative h-6 w-full min-w-1 rounded-[0.25rem] border"
213
+ style={{ background: bg, borderColor: border }}
214
+ >
215
+ {label ? <DurationLabel label={label} /> : null}
216
+ </div>
217
+ );
218
+ }
219
+
220
+ function SegmentBar({ segments }: { segments: VisibleSegment[] }): ReactNode {
221
+ return (
222
+ <div className="relative h-6 w-full">
223
+ {segments.map((seg, i) => {
224
+ const label = formatDuration(seg.fullDurationMs);
225
+ // Only render the label when there's enough room for it without clipping.
226
+ const showLabel = seg.pixelWidth >= Math.max(40, label.length * 6 + 12);
227
+ // Beef up the queued segment when it's too narrow to read.
228
+ const isNarrowQueued = seg.status === 'queued' && seg.pixelWidth < 20;
229
+ const overrideBg = isNarrowQueued ? 'var(--ds-gray-400)' : undefined;
230
+ const overrideBorder = isNarrowQueued
231
+ ? 'var(--ds-gray-500)'
232
+ : undefined;
233
+
234
+ return (
235
+ <div
236
+ key={`${seg.status}-${i}`}
237
+ className={cn(
238
+ 'absolute h-full rounded-[0.25rem]',
239
+ SEGMENT_CLASSES[seg.status]
240
+ )}
241
+ style={{
242
+ // 1px gap between adjacent segments, distributed equally.
243
+ left: `calc(${seg.leftPct}% + 0.5px)`,
244
+ width: `calc(${seg.widthPct}% - 1px)`,
245
+ minWidth: 1,
246
+ background: overrideBg,
247
+ borderColor: overrideBorder,
248
+ }}
249
+ >
250
+ {showLabel ? <DurationLabel label={label} /> : null}
251
+ </div>
252
+ );
253
+ })}
254
+ </div>
255
+ );
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // TimelineBar
260
+ // ---------------------------------------------------------------------------
261
+
262
+ const TimelineBar = memo(function TimelineBar({
263
+ span,
264
+ viewStart,
265
+ viewDuration,
266
+ containerWidth,
267
+ isSelected,
268
+ isDimmed,
269
+ onSelect,
270
+ }: {
271
+ span: Span;
272
+ viewStart: number;
273
+ viewDuration: number;
274
+ containerWidth: number;
275
+ isSelected: boolean;
276
+ isDimmed?: boolean;
277
+ onSelect: (spanId: string) => void;
278
+ }): ReactNode {
279
+ const startMs = getHighResInMs(span.startTime);
280
+ const endMs = getHighResInMs(span.endTime);
281
+ const totalDurationMs = getSpanDurationMs(span);
282
+
283
+ const geometry = useMemo(
284
+ () =>
285
+ computeBarGeometry(
286
+ startMs,
287
+ endMs,
288
+ viewStart,
289
+ viewStart + viewDuration,
290
+ containerWidth
291
+ ),
292
+ [startMs, endMs, viewStart, viewDuration, containerWidth]
293
+ );
294
+
295
+ const baseSegments = useMemo(() => computeSpanSegments(span), [span]);
296
+ const segments = useMemo(
297
+ () =>
298
+ geometry.mode.kind === 'full'
299
+ ? projectSegments(baseSegments, startMs, totalDurationMs, geometry)
300
+ : [],
301
+ [geometry, baseSegments, startMs, totalDurationMs]
302
+ );
303
+
304
+ const workflowStatus = (span.attributes.data as Record<string, unknown>)
305
+ ?.status as string | undefined;
306
+ const isErrored = span.status.code === 2 || workflowStatus === 'failed';
307
+ const colors = getResourceColor(span.resource);
308
+ const fallbackBg = isErrored
309
+ ? (colors.errorBg ?? 'var(--ds-red-200)')
310
+ : colors.bg;
311
+ const fallbackBorder = isErrored
312
+ ? (colors.errorBorder ?? 'var(--ds-red-500)')
313
+ : colors.border;
314
+
315
+ const totalLabel = formatDuration(totalDurationMs);
316
+ const showTotalLabel =
317
+ geometry.visiblePixelWidth >= Math.max(40, totalLabel.length * 6 + 12);
318
+
319
+ const handleClick = useCallback(() => {
320
+ onSelect(span.spanId);
321
+ }, [onSelect, span.spanId]);
322
+
323
+ return (
324
+ <div
325
+ role="treeitem"
326
+ aria-selected={isSelected}
327
+ aria-expanded={isSelected}
328
+ aria-level={1}
329
+ className={cn(
330
+ 'group/timeline-row h-10 relative flex items-center hover:bg-gray-100 aria-selected:bg-gray-100 aria-selected:hover:bg-gray-200 transition-opacity',
331
+ isDimmed && 'opacity-35'
332
+ )}
333
+ onClick={handleClick}
334
+ >
335
+ <div className="absolute inset-y-0" style={TIMELINE_INSET_STYLE}>
336
+ <div
337
+ className="absolute top-1/2 h-6 -translate-y-1/2 rounded-[0.25rem]"
338
+ style={getBarPositionStyle(geometry)}
339
+ >
340
+ {geometry.mode.kind === 'arrow' ? (
341
+ <BoundaryArrow direction={geometry.mode.direction} />
342
+ ) : geometry.mode.kind === 'tiny' ? (
343
+ <div
344
+ className="h-6 rounded-[0.25rem] border"
345
+ style={{ background: fallbackBg, borderColor: fallbackBorder }}
346
+ />
347
+ ) : segments.length > 0 ? (
348
+ <SegmentBar segments={segments} />
349
+ ) : (
350
+ <PlainBar
351
+ bg={fallbackBg}
352
+ border={fallbackBorder}
353
+ label={showTotalLabel ? totalLabel : null}
354
+ />
355
+ )}
356
+ </div>
357
+ </div>
358
+ </div>
359
+ );
360
+ });
361
+
362
+ export { TimelineBar };
363
+
364
+ // ---------------------------------------------------------------------------
365
+ // DeltaIndicator (Alt-key gap overlay)
366
+ // ---------------------------------------------------------------------------
367
+
368
+ // Row and indicator sizes are local to DeltaIndicator since it's the only
369
+ // place that needs to compute Y positions from a row index. ROW_HEIGHT must
370
+ // match the `h-10` (40px) row used in TimelineBar.
371
+ const DELTA_ROW_HEIGHT_PX = 40;
372
+ const DELTA_CAP_HEIGHT_PX = 8;
373
+ // Vertical offset to sit the indicator inside the gap between row N and N+1,
374
+ // aligned with where the bar starts in the next row (rows center a 24px bar
375
+ // inside 40px, so bars start ~8px from the top of the row).
376
+ const DELTA_ROW_OFFSET_PX = 8;
377
+
378
+ const DeltaIndicator = memo(function DeltaIndicator({
379
+ leftFrac,
380
+ rightFrac,
381
+ label,
382
+ rowIndex,
383
+ }: {
384
+ leftFrac: number;
385
+ rightFrac: number;
386
+ label: string;
387
+ rowIndex: number;
388
+ }) {
389
+ const centerY = DELTA_ROW_OFFSET_PX + (rowIndex + 1) * DELTA_ROW_HEIGHT_PX;
390
+
391
+ return (
392
+ <div
393
+ className="absolute pointer-events-none"
394
+ style={{
395
+ left: `${leftFrac * 100}%`,
396
+ width: `${(rightFrac - leftFrac) * 100}%`,
397
+ top: centerY - DELTA_CAP_HEIGHT_PX / 2,
398
+ height: DELTA_CAP_HEIGHT_PX,
399
+ }}
400
+ >
401
+ <div className="absolute left-0 top-0 w-px h-full bg-amber-800" />
402
+ <div className="absolute left-0 right-0 top-1/2 h-px bg-amber-800" />
403
+ <div className="absolute right-0 top-0 w-px h-full bg-amber-800" />
404
+ <span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-label-12 leading-none whitespace-nowrap rounded-xs px-1 py-0.5 text-gray-100 bg-amber-800">
405
+ {label}
406
+ </span>
407
+ </div>
408
+ );
409
+ });
410
+
411
+ // ---------------------------------------------------------------------------
412
+ // TimelineHeader
413
+ // ---------------------------------------------------------------------------
414
+
415
+ export function TimelineHeader({
416
+ markers,
417
+ hoverInfo,
418
+ }: {
419
+ markers: TimeMarker[];
420
+ hoverInfo?: { fraction: number; label: string } | null;
421
+ }): ReactNode {
422
+ return (
423
+ <div className="relative bg-background-100 border-b border-gray-alpha-400 h-10 min-h-10 flex items-end px-4 pb-1">
424
+ <div className="relative h-full flex-1">
425
+ {markers.map((m) => (
426
+ <span
427
+ key={`${m.position}-${m.label}`}
428
+ className="absolute bottom-1 font-mono text-xs font-normal leading-4 text-gray-900 whitespace-nowrap"
429
+ style={{ left: `${m.position * 100}%` }}
430
+ >
431
+ {m.label}
432
+ </span>
433
+ ))}
434
+ {hoverInfo && (
435
+ <span
436
+ className="absolute top-1 pointer-events-none z-10 font-mono text-[11px] leading-4 text-gray-1000 whitespace-nowrap bg-background-100 border border-gray-alpha-400 rounded px-1 -translate-x-1/2"
437
+ style={{ left: `${hoverInfo.fraction * 100}%` }}
438
+ >
439
+ {hoverInfo.label}
440
+ </span>
441
+ )}
442
+ </div>
443
+ </div>
444
+ );
445
+ }
446
+
447
+ // ---------------------------------------------------------------------------
448
+ // Timeline
449
+ // ---------------------------------------------------------------------------
450
+
451
+ export function Timeline({
452
+ spans,
453
+ viewStart,
454
+ viewEnd,
455
+ markers,
456
+ selectedId,
457
+ searchResult,
458
+ onSelect,
459
+ hoverFraction,
460
+ altHeld = false,
461
+ }: {
462
+ spans: Span[];
463
+ viewStart: number;
464
+ viewEnd: number;
465
+ markers: TimeMarker[];
466
+ selectedId: string | null;
467
+ searchResult: SpanSearchResult;
468
+ onSelect: (spanId: string) => void;
469
+ hoverFraction?: number | null;
470
+ altHeld?: boolean;
471
+ }): ReactNode {
472
+ const containerRef = useRef<HTMLDivElement>(null);
473
+ const [containerWidth, setContainerWidth] = useState(0);
474
+ const viewDuration = viewEnd - viewStart;
475
+ const timelineWidth = Math.max(0, containerWidth - TIMELINE_PADDING_PX * 2);
476
+
477
+ useEffect(() => {
478
+ const el = containerRef.current;
479
+ if (!el) return;
480
+ const ro = new ResizeObserver(([entry]) => {
481
+ setContainerWidth(entry.contentRect.width);
482
+ });
483
+ ro.observe(el);
484
+ return () => ro.disconnect();
485
+ }, []);
486
+
487
+ const gaps = useMemo(
488
+ () => computeSpanGaps(spans, viewStart, viewEnd),
489
+ [spans, viewStart, viewEnd]
490
+ );
491
+
492
+ return (
493
+ <div ref={containerRef} className="relative h-full overflow-hidden">
494
+ <div
495
+ aria-hidden
496
+ className="absolute inset-y-0 pointer-events-none"
497
+ style={TIMELINE_INSET_STYLE}
498
+ >
499
+ {markers.map((marker) =>
500
+ // Skip the "0s" origin marker since the left edge already implies it.
501
+ Math.abs(marker.value) > 0.000001 ? (
502
+ <div
503
+ key={`${marker.position}-${marker.label}`}
504
+ className="absolute top-0 bottom-0 w-px bg-gray-alpha-300"
505
+ style={{ left: `${marker.position * 100}%` }}
506
+ />
507
+ ) : null
508
+ )}
509
+ </div>
510
+ {hoverFraction != null && (
511
+ <div
512
+ className="absolute inset-y-0 pointer-events-none z-10"
513
+ style={TIMELINE_INSET_STYLE}
514
+ >
515
+ <div
516
+ className="absolute top-0 bottom-0 w-px bg-gray-alpha-500"
517
+ style={{ left: `${hoverFraction * 100}%` }}
518
+ />
519
+ </div>
520
+ )}
521
+ {spans.map((span) => (
522
+ <TimelineBar
523
+ key={span.spanId}
524
+ span={span}
525
+ viewStart={viewStart}
526
+ viewDuration={viewDuration}
527
+ containerWidth={timelineWidth}
528
+ isSelected={selectedId === span.spanId}
529
+ isDimmed={isSpanDimmedBySearch(span.spanId, searchResult)}
530
+ onSelect={onSelect}
531
+ />
532
+ ))}
533
+ {altHeld && (
534
+ <div
535
+ aria-hidden
536
+ className="absolute inset-y-0 pointer-events-none"
537
+ style={TIMELINE_INSET_STYLE}
538
+ >
539
+ {gaps.map((gap) => (
540
+ <DeltaIndicator
541
+ key={gap.rowIndex}
542
+ leftFrac={gap.leftFrac}
543
+ rightFrac={gap.rightFrac}
544
+ label={formatDuration(gap.gapMs, true)}
545
+ rowIndex={gap.rowIndex}
546
+ />
547
+ ))}
548
+ </div>
549
+ )}
550
+ </div>
551
+ );
552
+ }
@@ -0,0 +1,92 @@
1
+ 'use client';
2
+
3
+ import {
4
+ createContext,
5
+ type ReactNode,
6
+ useCallback,
7
+ useContext,
8
+ useEffect,
9
+ useMemo,
10
+ useState,
11
+ } from 'react';
12
+ import type { Span } from '../trace-viewer/types';
13
+
14
+ interface ActiveSpanContextValue {
15
+ activeSpanId: string | null;
16
+ activeSpan: Span | null;
17
+ setActiveSpan: (spanId: string) => void;
18
+ clearActiveSpan: () => void;
19
+ }
20
+
21
+ const ActiveSpanContext = createContext<ActiveSpanContextValue | null>(null);
22
+ ActiveSpanContext.displayName = 'ActiveSpanContext';
23
+
24
+ export function ActiveSpanProvider({
25
+ spans,
26
+ children,
27
+ }: {
28
+ spans: Span[];
29
+ children: ReactNode;
30
+ }) {
31
+ const [activeSpanId, setActiveSpanId] = useState<string | null>(
32
+ spans[0]?.spanId ?? null
33
+ );
34
+
35
+ useEffect(() => {
36
+ setActiveSpanId((currentSpanId) => {
37
+ if (!currentSpanId) {
38
+ return null;
39
+ }
40
+
41
+ const hasCurrentSpan = spans.some(
42
+ (span) => span.spanId === currentSpanId
43
+ );
44
+ if (hasCurrentSpan) {
45
+ return currentSpanId;
46
+ }
47
+
48
+ return null;
49
+ });
50
+ }, [spans]);
51
+
52
+ const activeSpan = useMemo(() => {
53
+ if (!activeSpanId) {
54
+ return null;
55
+ }
56
+
57
+ return spans.find((span) => span.spanId === activeSpanId) ?? null;
58
+ }, [activeSpanId, spans]);
59
+
60
+ const setActiveSpan = useCallback((spanId: string) => {
61
+ setActiveSpanId(spanId);
62
+ }, []);
63
+
64
+ const clearActiveSpan = useCallback(() => {
65
+ setActiveSpanId(null);
66
+ }, []);
67
+
68
+ const value = useMemo<ActiveSpanContextValue>(
69
+ () => ({
70
+ activeSpanId,
71
+ activeSpan,
72
+ setActiveSpan,
73
+ clearActiveSpan,
74
+ }),
75
+ [activeSpanId, activeSpan, setActiveSpan, clearActiveSpan]
76
+ );
77
+
78
+ return (
79
+ <ActiveSpanContext.Provider value={value}>
80
+ {children}
81
+ </ActiveSpanContext.Provider>
82
+ );
83
+ }
84
+
85
+ export const useActiveSpan = (): ActiveSpanContextValue => {
86
+ const context = useContext(ActiveSpanContext);
87
+ if (!context) {
88
+ throw new Error('useActiveSpan must be used within ActiveSpanProvider');
89
+ }
90
+
91
+ return context;
92
+ };
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import { X } from 'lucide-react';
4
+ import type { ReactNode } from 'react';
5
+ import type { Span } from '../trace-viewer/types';
6
+ import { formatDuration, getHighResInMs } from '../trace-viewer/util/timing';
7
+ import { IconButton } from '../ui/icon-button';
8
+ import { getSpanDurationMs } from './utils';
9
+
10
+ interface DetailPanelProps {
11
+ span: Span;
12
+ rootStart: number;
13
+ onClose: () => void;
14
+ }
15
+
16
+ export function DetailPanel({
17
+ span,
18
+ rootStart,
19
+ onClose,
20
+ }: DetailPanelProps): ReactNode {
21
+ const startMs = getHighResInMs(span.startTime);
22
+ const durationMs = getSpanDurationMs(span);
23
+ const offsetMs = startMs - rootStart;
24
+
25
+ return (
26
+ <aside className="grid h-full max-h-full grid-rows-[2.5rem_1fr] bg-background-200">
27
+ <div className="flex items-center justify-between px-3 border-b border-gray-alpha-400">
28
+ <span className="text-sm font-medium text-gray-1000 truncate">
29
+ {span.name}
30
+ </span>
31
+ <IconButton aria-label="Close span details" onClick={onClose}>
32
+ <X className="w-4 h-4" />
33
+ </IconButton>
34
+ </div>
35
+ <div className="overflow-y-auto p-3 space-y-3">
36
+ <dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5 text-sm">
37
+ <dt className="text-gray-900">Resource</dt>
38
+ <dd className="text-gray-1000 font-mono">{span.resource}</dd>
39
+ <dt className="text-gray-900">Duration</dt>
40
+ <dd className="text-gray-1000 tabular-nums font-mono">
41
+ {formatDuration(durationMs)}
42
+ </dd>
43
+ <dt className="text-gray-900">Offset</dt>
44
+ <dd className="text-gray-1000 tabular-nums font-mono">
45
+ +{formatDuration(offsetMs)}
46
+ </dd>
47
+ <dt className="text-gray-900">Status</dt>
48
+ <dd className="text-gray-1000 font-mono">
49
+ {span.status.code === 2 ? 'Error' : 'OK'}
50
+ </dd>
51
+ </dl>
52
+ </div>
53
+ </aside>
54
+ );
55
+ }