@workflow/web-shared 5.0.0-beta.1 → 5.0.0-beta.11

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 (298) 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 +219 -125
  6. package/dist/components/hook-actions.js +1 -1
  7. package/dist/components/index.d.ts +5 -1
  8. package/dist/components/index.d.ts.map +1 -1
  9. package/dist/components/index.js +6 -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 +44 -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 +15 -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 +105 -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 +221 -0
  34. package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.d.ts +2 -0
  35. package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.d.ts.map +1 -0
  36. package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.js +19 -0
  37. package/dist/components/new-trace-viewer/components/use-row-window.d.ts +20 -0
  38. package/dist/components/new-trace-viewer/components/use-row-window.d.ts.map +1 -0
  39. package/dist/components/new-trace-viewer/components/use-row-window.js +69 -0
  40. package/dist/components/new-trace-viewer/context.d.ts +15 -0
  41. package/dist/components/new-trace-viewer/context.d.ts.map +1 -0
  42. package/dist/components/new-trace-viewer/context.js +47 -0
  43. package/dist/components/new-trace-viewer/detail-panel.d.ts +10 -0
  44. package/dist/components/new-trace-viewer/detail-panel.d.ts.map +1 -0
  45. package/dist/components/new-trace-viewer/detail-panel.js +13 -0
  46. package/dist/components/new-trace-viewer/icons.d.ts +6 -0
  47. package/dist/components/new-trace-viewer/icons.d.ts.map +1 -0
  48. package/dist/components/new-trace-viewer/icons.js +15 -0
  49. package/dist/components/new-trace-viewer/search.d.ts +19 -0
  50. package/dist/components/new-trace-viewer/search.d.ts.map +1 -0
  51. package/dist/components/new-trace-viewer/search.js +134 -0
  52. package/dist/components/new-trace-viewer/trace-viewer.d.ts +11 -0
  53. package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -0
  54. package/dist/components/new-trace-viewer/trace-viewer.js +400 -0
  55. package/dist/components/new-trace-viewer/types.d.ts +1 -0
  56. package/dist/components/new-trace-viewer/types.d.ts.map +1 -0
  57. package/dist/components/new-trace-viewer/types.js +2 -0
  58. package/dist/components/new-trace-viewer/util/constants.d.ts +1 -0
  59. package/dist/components/new-trace-viewer/util/constants.d.ts.map +1 -0
  60. package/dist/components/new-trace-viewer/util/constants.js +2 -0
  61. package/dist/components/new-trace-viewer/utils.d.ts +46 -0
  62. package/dist/components/new-trace-viewer/utils.d.ts.map +1 -0
  63. package/dist/components/new-trace-viewer/utils.js +330 -0
  64. package/dist/components/run-trace-view.d.ts +2 -1
  65. package/dist/components/run-trace-view.d.ts.map +1 -1
  66. package/dist/components/run-trace-view.js +3 -3
  67. package/dist/components/sidebar/attribute-panel.d.ts +7 -1
  68. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  69. package/dist/components/sidebar/attribute-panel.js +131 -70
  70. package/dist/components/sidebar/conversation-view.js +1 -1
  71. package/dist/components/sidebar/copyable-data-block.d.ts +1 -0
  72. package/dist/components/sidebar/copyable-data-block.d.ts.map +1 -1
  73. package/dist/components/sidebar/copyable-data-block.js +16 -15
  74. package/dist/components/sidebar/detail-card.d.ts +5 -6
  75. package/dist/components/sidebar/detail-card.d.ts.map +1 -1
  76. package/dist/components/sidebar/detail-card.js +32 -16
  77. package/dist/components/sidebar/entity-detail-panel.d.ts +3 -1
  78. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  79. package/dist/components/sidebar/entity-detail-panel.js +20 -37
  80. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  81. package/dist/components/sidebar/events-list.js +21 -15
  82. package/dist/components/sidebar/resolve-hook-modal.js +1 -1
  83. package/dist/components/sidebar/sidebar-data-context.d.ts +29 -0
  84. package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -0
  85. package/dist/components/sidebar/sidebar-data-context.js +19 -0
  86. package/dist/components/stream-viewer.d.ts +1 -1
  87. package/dist/components/stream-viewer.d.ts.map +1 -1
  88. package/dist/components/stream-viewer.js +3 -27
  89. package/dist/components/trace-viewer/components/map.js +1 -1
  90. package/dist/components/trace-viewer/components/markers.js +1 -1
  91. package/dist/components/trace-viewer/components/node.js +1 -1
  92. package/dist/components/trace-viewer/components/search-input.js +1 -1
  93. package/dist/components/trace-viewer/components/search.js +1 -1
  94. package/dist/components/trace-viewer/components/span-content.js +1 -1
  95. package/dist/components/trace-viewer/components/span-detail-panel.js +1 -1
  96. package/dist/components/trace-viewer/components/span-segments.js +1 -1
  97. package/dist/components/trace-viewer/components/span-strategies.js +1 -1
  98. package/dist/components/trace-viewer/components/ui.js +1 -1
  99. package/dist/components/trace-viewer/components/zoom-button.js +1 -1
  100. package/dist/components/trace-viewer/components/zoom-icons.js +1 -1
  101. package/dist/components/trace-viewer/context.js +1 -1
  102. package/dist/components/trace-viewer/index.js +1 -1
  103. package/dist/components/trace-viewer/trace-viewer.js +1 -1
  104. package/dist/components/trace-viewer/types.js +1 -1
  105. package/dist/components/trace-viewer/util/constants.js +1 -1
  106. package/dist/components/trace-viewer/util/scrollbar-width.js +1 -1
  107. package/dist/components/trace-viewer/util/timing.d.ts +1 -1
  108. package/dist/components/trace-viewer/util/timing.d.ts.map +1 -1
  109. package/dist/components/trace-viewer/util/timing.js +6 -9
  110. package/dist/components/trace-viewer/util/tree.js +1 -1
  111. package/dist/components/trace-viewer/util/use-immediate-style.js +1 -1
  112. package/dist/components/trace-viewer/util/use-streaming-spans.js +1 -1
  113. package/dist/components/trace-viewer/util/use-trackpad-zoom.js +1 -1
  114. package/dist/components/trace-viewer/worker.js +1 -1
  115. package/dist/components/trace-viewer-new.d.ts +14 -0
  116. package/dist/components/trace-viewer-new.d.ts.map +1 -0
  117. package/dist/components/trace-viewer-new.js +22 -0
  118. package/dist/components/ui/alert.js +1 -1
  119. package/dist/components/ui/button.d.ts +10 -0
  120. package/dist/components/ui/button.d.ts.map +1 -0
  121. package/dist/components/ui/button.js +46 -0
  122. package/dist/components/ui/card.js +1 -1
  123. package/dist/components/ui/data-inspector.d.ts +39 -1
  124. package/dist/components/ui/data-inspector.d.ts.map +1 -1
  125. package/dist/components/ui/data-inspector.js +207 -22
  126. package/dist/components/ui/decrypt-button.js +1 -1
  127. package/dist/components/ui/error-card.js +1 -1
  128. package/dist/components/ui/error-stack-block.d.ts.map +1 -1
  129. package/dist/components/ui/error-stack-block.js +27 -19
  130. package/dist/components/ui/icon-button.d.ts +19 -0
  131. package/dist/components/ui/icon-button.d.ts.map +1 -0
  132. package/dist/components/ui/icon-button.js +29 -0
  133. package/dist/components/ui/inspector-theme.d.ts +9 -10
  134. package/dist/components/ui/inspector-theme.d.ts.map +1 -1
  135. package/dist/components/ui/inspector-theme.js +39 -72
  136. package/dist/components/ui/kbd.d.ts +6 -0
  137. package/dist/components/ui/kbd.d.ts.map +1 -0
  138. package/dist/components/ui/kbd.js +6 -0
  139. package/dist/components/ui/load-more-button.js +1 -1
  140. package/dist/components/ui/menu-dropdown.js +1 -1
  141. package/dist/components/ui/skeleton.js +1 -1
  142. package/dist/components/ui/spinner.js +1 -1
  143. package/dist/components/ui/timestamp-tooltip.js +1 -1
  144. package/dist/components/ui/tooltip.d.ts +10 -0
  145. package/dist/components/ui/tooltip.d.ts.map +1 -0
  146. package/dist/components/ui/tooltip.js +12 -0
  147. package/dist/components/workflow-trace-view.d.ts +3 -1
  148. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  149. package/dist/components/workflow-trace-view.js +10 -5
  150. package/dist/components/workflow-traces/event-colors.js +1 -1
  151. package/dist/components/workflow-traces/trace-colors.js +1 -1
  152. package/dist/components/workflow-traces/trace-span-construction.d.ts +2 -2
  153. package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
  154. package/dist/components/workflow-traces/trace-span-construction.js +19 -6
  155. package/dist/components/workflow-traces/trace-time-utils.js +1 -1
  156. package/dist/hooks/use-dark-mode.js +1 -1
  157. package/dist/hooks/use-intersection-observer.d.ts +17 -0
  158. package/dist/hooks/use-intersection-observer.d.ts.map +1 -0
  159. package/dist/hooks/use-intersection-observer.js +29 -0
  160. package/dist/hooks/use-load-more-on-scroll.d.ts +21 -0
  161. package/dist/hooks/use-load-more-on-scroll.d.ts.map +1 -0
  162. package/dist/hooks/use-load-more-on-scroll.js +28 -0
  163. package/dist/hooks/use-reduced-motion.d.ts +10 -0
  164. package/dist/hooks/use-reduced-motion.d.ts.map +1 -0
  165. package/dist/hooks/use-reduced-motion.js +29 -0
  166. package/dist/index.d.ts +3 -1
  167. package/dist/index.d.ts.map +1 -1
  168. package/dist/index.js +3 -2
  169. package/dist/lib/event-analysis.js +1 -1
  170. package/dist/lib/event-materialization.js +1 -1
  171. package/dist/lib/exact-event-search-id.d.ts +25 -0
  172. package/dist/lib/exact-event-search-id.d.ts.map +1 -0
  173. package/dist/lib/exact-event-search-id.js +39 -0
  174. package/dist/lib/hydration.d.ts +14 -1
  175. package/dist/lib/hydration.d.ts.map +1 -1
  176. package/dist/lib/hydration.js +157 -4
  177. package/dist/lib/stream-display.d.ts +18 -0
  178. package/dist/lib/stream-display.d.ts.map +1 -0
  179. package/dist/lib/stream-display.js +30 -0
  180. package/dist/lib/toast.js +1 -1
  181. package/dist/lib/trace-builder.d.ts.map +1 -1
  182. package/dist/lib/trace-builder.js +3 -2
  183. package/dist/lib/utils.d.ts +9 -9
  184. package/dist/lib/utils.d.ts.map +1 -1
  185. package/dist/lib/utils.js +27 -32
  186. package/dist/styles.css +475 -1
  187. package/package.json +6 -4
  188. package/src/components/event-list-view.tsx +421 -262
  189. package/src/components/index.ts +13 -1
  190. package/src/components/new-trace-viewer/components/copy-button.tsx +68 -0
  191. package/src/components/new-trace-viewer/components/event-list.tsx +135 -0
  192. package/src/components/new-trace-viewer/components/middle-truncate/copy-selection.ts +114 -0
  193. package/src/components/new-trace-viewer/components/middle-truncate/middle-truncate.tsx +157 -0
  194. package/src/components/new-trace-viewer/components/middle-truncate/truncate.ts +132 -0
  195. package/src/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.ts +167 -0
  196. package/src/components/new-trace-viewer/components/split-pane.tsx +198 -0
  197. package/src/components/new-trace-viewer/components/timeline.tsx +560 -0
  198. package/src/components/new-trace-viewer/components/trace-viewer-skeleton.tsx +94 -0
  199. package/src/components/new-trace-viewer/components/use-row-window.ts +93 -0
  200. package/src/components/new-trace-viewer/context.tsx +90 -0
  201. package/src/components/new-trace-viewer/detail-panel.tsx +55 -0
  202. package/src/components/new-trace-viewer/icons.tsx +79 -0
  203. package/src/components/new-trace-viewer/search.ts +178 -0
  204. package/src/components/new-trace-viewer/trace-viewer.tsx +762 -0
  205. package/src/components/new-trace-viewer/types.ts +0 -0
  206. package/src/components/new-trace-viewer/util/constants.ts +0 -0
  207. package/src/components/new-trace-viewer/utils.ts +482 -0
  208. package/src/components/run-trace-view.tsx +3 -0
  209. package/src/components/sidebar/attribute-panel.tsx +240 -196
  210. package/src/components/sidebar/copyable-data-block.tsx +49 -27
  211. package/src/components/sidebar/detail-card.tsx +119 -59
  212. package/src/components/sidebar/entity-detail-panel.tsx +92 -148
  213. package/src/components/sidebar/events-list.tsx +51 -76
  214. package/src/components/sidebar/sidebar-data-context.tsx +62 -0
  215. package/src/components/stream-viewer.tsx +18 -40
  216. package/src/components/trace-viewer/util/timing.ts +5 -9
  217. package/src/components/trace-viewer-new.tsx +57 -0
  218. package/src/components/ui/button.tsx +68 -0
  219. package/src/components/ui/data-inspector.tsx +427 -31
  220. package/src/components/ui/error-stack-block.tsx +39 -30
  221. package/src/components/ui/icon-button.tsx +53 -0
  222. package/src/components/ui/inspector-theme.ts +39 -81
  223. package/src/components/ui/kbd.tsx +21 -0
  224. package/src/components/ui/tooltip.tsx +47 -0
  225. package/src/components/workflow-trace-view.tsx +12 -2
  226. package/src/components/workflow-traces/trace-span-construction.ts +24 -6
  227. package/src/hooks/use-intersection-observer.ts +57 -0
  228. package/src/hooks/use-load-more-on-scroll.ts +49 -0
  229. package/src/hooks/use-reduced-motion.ts +34 -0
  230. package/src/index.ts +9 -0
  231. package/src/lib/exact-event-search-id.ts +67 -0
  232. package/src/lib/hydration.ts +169 -1
  233. package/src/lib/stream-display.ts +48 -0
  234. package/src/lib/trace-builder.ts +2 -1
  235. package/src/lib/utils.ts +27 -32
  236. package/src/styles.css +475 -1
  237. package/dist/components/error-boundary.js.map +0 -1
  238. package/dist/components/event-list-view.js.map +0 -1
  239. package/dist/components/hook-actions.js.map +0 -1
  240. package/dist/components/index.js.map +0 -1
  241. package/dist/components/run-trace-view.js.map +0 -1
  242. package/dist/components/sidebar/attribute-panel.js.map +0 -1
  243. package/dist/components/sidebar/conversation-view.js.map +0 -1
  244. package/dist/components/sidebar/copyable-data-block.js.map +0 -1
  245. package/dist/components/sidebar/detail-card.js.map +0 -1
  246. package/dist/components/sidebar/entity-detail-panel.js.map +0 -1
  247. package/dist/components/sidebar/events-list.js.map +0 -1
  248. package/dist/components/sidebar/resolve-hook-modal.js.map +0 -1
  249. package/dist/components/stream-viewer.js.map +0 -1
  250. package/dist/components/trace-viewer/components/map.js.map +0 -1
  251. package/dist/components/trace-viewer/components/markers.js.map +0 -1
  252. package/dist/components/trace-viewer/components/node.js.map +0 -1
  253. package/dist/components/trace-viewer/components/search-input.js.map +0 -1
  254. package/dist/components/trace-viewer/components/search.js.map +0 -1
  255. package/dist/components/trace-viewer/components/span-content.js.map +0 -1
  256. package/dist/components/trace-viewer/components/span-detail-panel.js.map +0 -1
  257. package/dist/components/trace-viewer/components/span-segments.js.map +0 -1
  258. package/dist/components/trace-viewer/components/span-strategies.js.map +0 -1
  259. package/dist/components/trace-viewer/components/ui.js.map +0 -1
  260. package/dist/components/trace-viewer/components/zoom-button.js.map +0 -1
  261. package/dist/components/trace-viewer/components/zoom-icons.js.map +0 -1
  262. package/dist/components/trace-viewer/context.js.map +0 -1
  263. package/dist/components/trace-viewer/index.js.map +0 -1
  264. package/dist/components/trace-viewer/trace-viewer.js.map +0 -1
  265. package/dist/components/trace-viewer/types.js.map +0 -1
  266. package/dist/components/trace-viewer/util/constants.js.map +0 -1
  267. package/dist/components/trace-viewer/util/scrollbar-width.js.map +0 -1
  268. package/dist/components/trace-viewer/util/timing.js.map +0 -1
  269. package/dist/components/trace-viewer/util/tree.js.map +0 -1
  270. package/dist/components/trace-viewer/util/use-immediate-style.js.map +0 -1
  271. package/dist/components/trace-viewer/util/use-streaming-spans.js.map +0 -1
  272. package/dist/components/trace-viewer/util/use-trackpad-zoom.js.map +0 -1
  273. package/dist/components/trace-viewer/worker.js.map +0 -1
  274. package/dist/components/ui/alert.js.map +0 -1
  275. package/dist/components/ui/card.js.map +0 -1
  276. package/dist/components/ui/data-inspector.js.map +0 -1
  277. package/dist/components/ui/decrypt-button.js.map +0 -1
  278. package/dist/components/ui/error-card.js.map +0 -1
  279. package/dist/components/ui/error-stack-block.js.map +0 -1
  280. package/dist/components/ui/inspector-theme.js.map +0 -1
  281. package/dist/components/ui/load-more-button.js.map +0 -1
  282. package/dist/components/ui/menu-dropdown.js.map +0 -1
  283. package/dist/components/ui/skeleton.js.map +0 -1
  284. package/dist/components/ui/spinner.js.map +0 -1
  285. package/dist/components/ui/timestamp-tooltip.js.map +0 -1
  286. package/dist/components/workflow-trace-view.js.map +0 -1
  287. package/dist/components/workflow-traces/event-colors.js.map +0 -1
  288. package/dist/components/workflow-traces/trace-colors.js.map +0 -1
  289. package/dist/components/workflow-traces/trace-span-construction.js.map +0 -1
  290. package/dist/components/workflow-traces/trace-time-utils.js.map +0 -1
  291. package/dist/hooks/use-dark-mode.js.map +0 -1
  292. package/dist/index.js.map +0 -1
  293. package/dist/lib/event-analysis.js.map +0 -1
  294. package/dist/lib/event-materialization.js.map +0 -1
  295. package/dist/lib/hydration.js.map +0 -1
  296. package/dist/lib/toast.js.map +0 -1
  297. package/dist/lib/trace-builder.js.map +0 -1
  298. package/dist/lib/utils.js.map +0 -1
@@ -0,0 +1,762 @@
1
+ 'use client';
2
+
3
+ import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name';
4
+ import {
5
+ ChevronDown,
6
+ ChevronUp,
7
+ RotateCcw,
8
+ Search,
9
+ X,
10
+ ZoomIn,
11
+ ZoomOut,
12
+ } from 'lucide-react';
13
+ import {
14
+ type ReactNode,
15
+ useCallback,
16
+ useDeferredValue,
17
+ useEffect,
18
+ useMemo,
19
+ useRef,
20
+ useState,
21
+ } from 'react';
22
+ import { useLoadMoreOnScroll } from '../../hooks/use-load-more-on-scroll';
23
+ import { useReducedMotion } from '../../hooks/use-reduced-motion';
24
+ import { ErrorBoundary } from '../error-boundary';
25
+ import {
26
+ EntityDetailPanel,
27
+ type SelectedSpanInfo,
28
+ } from '../sidebar/entity-detail-panel';
29
+ import { useSidebarDataOptional } from '../sidebar/sidebar-data-context';
30
+ import type { Trace } from '../trace-viewer/types';
31
+ import { formatDuration, getHighResInMs } from '../trace-viewer/util/timing';
32
+ import { IconButton } from '../ui/icon-button';
33
+ import { Spinner } from '../ui/spinner';
34
+ import { Kbd } from '../ui/kbd';
35
+ import EventList from './components/event-list';
36
+ import { SplitPane } from './components/split-pane';
37
+ import {
38
+ TIMELINE_PADDING_PX,
39
+ Timeline,
40
+ TimelineHeader,
41
+ } from './components/timeline';
42
+ import {
43
+ Tooltip,
44
+ TooltipContent,
45
+ TooltipProvider,
46
+ TooltipTrigger,
47
+ } from '../ui/tooltip';
48
+ import { ActiveSpanProvider, useActiveSpan } from './context';
49
+ import { DetailPanel } from './detail-panel';
50
+ import { searchSpans } from './search';
51
+ import { computeRootBounds, computeTimeMarkers } from './utils';
52
+
53
+ interface NewTraceViewerProps {
54
+ trace: Trace;
55
+ onLoadMore?: () => void | Promise<void>;
56
+ hasMore?: boolean;
57
+ isLoadingMore?: boolean;
58
+ }
59
+
60
+ const MIN_VIEWPORT_MS = 0.001;
61
+
62
+ interface Viewport {
63
+ start: number;
64
+ end: number;
65
+ }
66
+
67
+ function useAnimatedViewport(initial: Viewport) {
68
+ const [viewport, setViewportState] = useState<Viewport>(initial);
69
+ const animRef = useRef<{
70
+ raf: number;
71
+ from: Viewport;
72
+ to: Viewport;
73
+ start: number;
74
+ } | null>(null);
75
+ const currentRef = useRef(initial);
76
+ currentRef.current = viewport;
77
+ const reducedMotion = useReducedMotion();
78
+
79
+ const cancel = useCallback(() => {
80
+ if (animRef.current) {
81
+ cancelAnimationFrame(animRef.current.raf);
82
+ animRef.current = null;
83
+ }
84
+ }, []);
85
+
86
+ const animateTo = useCallback(
87
+ (target: Viewport) => {
88
+ cancel();
89
+
90
+ if (reducedMotion) {
91
+ currentRef.current = target;
92
+ setViewportState(target);
93
+ return;
94
+ }
95
+
96
+ const from = currentRef.current;
97
+ const anim = { raf: 0, from, to: target, start: performance.now() };
98
+
99
+ const tick = () => {
100
+ const t = Math.min((performance.now() - anim.start) / 150, 1);
101
+ const e = 1 - (1 - t) * (1 - t);
102
+ setViewportState({
103
+ start: anim.from.start + (anim.to.start - anim.from.start) * e,
104
+ end: anim.from.end + (anim.to.end - anim.from.end) * e,
105
+ });
106
+ if (t < 1) anim.raf = requestAnimationFrame(tick);
107
+ else animRef.current = null;
108
+ };
109
+
110
+ animRef.current = anim;
111
+ anim.raf = requestAnimationFrame(tick);
112
+ },
113
+ [cancel, reducedMotion]
114
+ );
115
+
116
+ const setViewport = useCallback(
117
+ (update: Viewport | ((prev: Viewport) => Viewport)) => {
118
+ cancel();
119
+ if (typeof update === 'function') {
120
+ setViewportState((prev) => {
121
+ const next = update(prev);
122
+ currentRef.current = next;
123
+ return next;
124
+ });
125
+ } else {
126
+ currentRef.current = update;
127
+ setViewportState(update);
128
+ }
129
+ },
130
+ [cancel]
131
+ );
132
+
133
+ useEffect(() => cancel, [cancel]);
134
+
135
+ return { viewport, setViewport, animateTo };
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Hook: bridge ActiveSpanContext + SidebarDataContext → SelectedSpanInfo
140
+ // ---------------------------------------------------------------------------
141
+
142
+ function useSelectedSpanInfo(): SelectedSpanInfo | null {
143
+ const { activeSpan } = useActiveSpan();
144
+ const sidebar = useSidebarDataOptional();
145
+
146
+ return useMemo(() => {
147
+ if (!activeSpan || !sidebar) return null;
148
+
149
+ const correlationId = activeSpan.spanId;
150
+ const rawEvents = correlationId
151
+ ? sidebar.events.filter((e) => e.correlationId === correlationId)
152
+ : [];
153
+
154
+ return {
155
+ data: activeSpan.attributes?.data,
156
+ resource: activeSpan.attributes?.resource as string | undefined,
157
+ spanId: activeSpan.spanId,
158
+ rawEvents,
159
+ };
160
+ }, [activeSpan, sidebar]);
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Root component
165
+ // ---------------------------------------------------------------------------
166
+
167
+ export function NewTraceViewer({
168
+ trace,
169
+ onLoadMore,
170
+ hasMore,
171
+ isLoadingMore,
172
+ }: NewTraceViewerProps): ReactNode {
173
+ return (
174
+ <TooltipProvider delayDuration={300}>
175
+ <ActiveSpanProvider spans={trace.spans}>
176
+ <NewTraceViewerContent
177
+ trace={trace}
178
+ onLoadMore={onLoadMore}
179
+ hasMore={hasMore}
180
+ isLoadingMore={isLoadingMore}
181
+ />
182
+ </ActiveSpanProvider>
183
+ </TooltipProvider>
184
+ );
185
+ }
186
+
187
+ function NewTraceViewerContent({
188
+ trace,
189
+ onLoadMore,
190
+ hasMore,
191
+ isLoadingMore,
192
+ }: NewTraceViewerProps): ReactNode {
193
+ const { activeSpan, activeSpanId, setActiveSpan, clearActiveSpan } =
194
+ useActiveSpan();
195
+
196
+ const sidebar = useSidebarDataOptional();
197
+ const selectedSpan = useSelectedSpanInfo();
198
+
199
+ const [searchQuery, setSearchQuery] = useState('');
200
+ const deferredSearchQuery = useDeferredValue(searchQuery);
201
+
202
+ const searchResult = useMemo(
203
+ () => searchSpans(trace.spans, deferredSearchQuery),
204
+ [trace.spans, deferredSearchQuery]
205
+ );
206
+
207
+ const root = useMemo(() => computeRootBounds(trace.spans), [trace.spans]);
208
+
209
+ const scrollContainerRef = useRef<HTMLDivElement>(null);
210
+ const loadMore = useCallback(() => {
211
+ void onLoadMore?.();
212
+ }, [onLoadMore]);
213
+ const loadMoreSentinelRef = useLoadMoreOnScroll(loadMore, {
214
+ hasMore: Boolean(onLoadMore && hasMore),
215
+ isLoadingMore: Boolean(isLoadingMore),
216
+ rootRef: scrollContainerRef,
217
+ });
218
+
219
+ const { viewport, setViewport, animateTo } = useAnimatedViewport({
220
+ start: root.startTime,
221
+ end: root.startTime + root.duration,
222
+ });
223
+
224
+ const prevRootRef = useRef<Viewport>({
225
+ start: root.startTime,
226
+ end: root.startTime + root.duration,
227
+ });
228
+
229
+ useEffect(() => {
230
+ const prevRoot = prevRootRef.current;
231
+ const newStart = root.startTime;
232
+ const newEnd = root.startTime + root.duration;
233
+
234
+ setViewport((prev) => {
235
+ const wasAtFullExtent =
236
+ Math.abs(prev.start - prevRoot.start) < 0.01 &&
237
+ Math.abs(prev.end - prevRoot.end) < 0.01;
238
+
239
+ if (wasAtFullExtent) {
240
+ return { start: newStart, end: newEnd };
241
+ }
242
+ return prev;
243
+ });
244
+
245
+ prevRootRef.current = { start: newStart, end: newEnd };
246
+ }, [root.startTime, root.duration, setViewport]);
247
+
248
+ const viewDuration = viewport.end - viewport.start;
249
+
250
+ const timeMarkers = useMemo(
251
+ () => computeTimeMarkers(viewDuration, viewport.start - root.startTime),
252
+ [viewDuration, viewport.start, root.startTime]
253
+ );
254
+
255
+ const resetZoom = useCallback(() => {
256
+ animateTo({ start: root.startTime, end: root.startTime + root.duration });
257
+ }, [animateTo, root.startTime, root.duration]);
258
+
259
+ const ZOOM_FACTOR = 0.5;
260
+
261
+ const zoomBy = useCallback(
262
+ (factor: number) => {
263
+ const rootS = root.startTime;
264
+ const rootE = root.startTime + root.duration;
265
+ const rootD = root.duration;
266
+
267
+ setViewport((prev) => {
268
+ const prevDuration = prev.end - prev.start;
269
+ const center = (prev.start + prev.end) / 2;
270
+ const newDuration = Math.max(
271
+ MIN_VIEWPORT_MS,
272
+ Math.min(rootD, prevDuration * factor)
273
+ );
274
+ let newStart = center - newDuration / 2;
275
+ let newEnd = center + newDuration / 2;
276
+
277
+ if (newStart < rootS) {
278
+ newStart = rootS;
279
+ newEnd = rootS + newDuration;
280
+ }
281
+ if (newEnd > rootE) {
282
+ newEnd = rootE;
283
+ newStart = Math.max(rootS, rootE - newDuration);
284
+ }
285
+
286
+ return { start: newStart, end: newEnd };
287
+ });
288
+ },
289
+ [setViewport, root.startTime, root.duration]
290
+ );
291
+
292
+ const zoomIn = useCallback(() => zoomBy(ZOOM_FACTOR), [zoomBy]);
293
+ const zoomOut = useCallback(() => zoomBy(1 / ZOOM_FACTOR), [zoomBy]);
294
+
295
+ const handleSelectSpan = useCallback(
296
+ (spanId: string) => {
297
+ if (spanId === activeSpanId) {
298
+ clearActiveSpan();
299
+ return;
300
+ }
301
+ setActiveSpan(spanId);
302
+
303
+ const span = trace.spans.find((s) => s.spanId === spanId);
304
+ if (!span) return;
305
+
306
+ const spanStart = getHighResInMs(span.startTime);
307
+ const spanEnd = getHighResInMs(span.endTime);
308
+ const spanDuration = spanEnd - spanStart;
309
+
310
+ const rootS = root.startTime;
311
+ const rootE = root.startTime + root.duration;
312
+ const rootD = root.duration;
313
+
314
+ if (spanDuration > rootD * 0.8) {
315
+ animateTo({ start: rootS, end: rootE });
316
+ return;
317
+ }
318
+
319
+ const padding = Math.max(spanDuration * 0.2, MIN_VIEWPORT_MS / 2);
320
+ let newStart = spanStart - padding;
321
+ let newEnd = spanEnd + padding;
322
+
323
+ if (newEnd - newStart < MIN_VIEWPORT_MS) {
324
+ const center = (spanStart + spanEnd) / 2;
325
+ newStart = center - MIN_VIEWPORT_MS / 2;
326
+ newEnd = center + MIN_VIEWPORT_MS / 2;
327
+ }
328
+
329
+ if (newStart < rootS) {
330
+ const duration = newEnd - newStart;
331
+ newStart = rootS;
332
+ newEnd = Math.min(rootE, rootS + duration);
333
+ }
334
+ if (newEnd > rootE) {
335
+ const duration = newEnd - newStart;
336
+ newEnd = rootE;
337
+ newStart = Math.max(rootS, rootE - duration);
338
+ }
339
+
340
+ animateTo({ start: newStart, end: newEnd });
341
+ },
342
+ [
343
+ animateTo,
344
+ setActiveSpan,
345
+ clearActiveSpan,
346
+ activeSpanId,
347
+ trace.spans,
348
+ root.startTime,
349
+ root.duration,
350
+ ]
351
+ );
352
+
353
+ const [altHeld, setAltHeld] = useState(false);
354
+
355
+ useEffect(() => {
356
+ const handleSidebarNavKey = (e: KeyboardEvent): void => {
357
+ const target = e.target as HTMLElement | null;
358
+ if (
359
+ target instanceof HTMLInputElement ||
360
+ target instanceof HTMLTextAreaElement ||
361
+ target?.isContentEditable
362
+ ) {
363
+ return;
364
+ }
365
+ const targetId =
366
+ e.key === 'k' ? prevSpanIdRef.current : nextSpanIdRef.current;
367
+ if (targetId) {
368
+ e.preventDefault();
369
+ handleSelectSpanRef.current(targetId);
370
+ }
371
+ };
372
+
373
+ const onKeyDown = (e: KeyboardEvent): void => {
374
+ if (e.key === 'Escape') {
375
+ clearActiveSpan();
376
+ } else if (e.key === 'Alt') {
377
+ e.preventDefault();
378
+ setAltHeld(true);
379
+ } else if (e.key === 'j' || e.key === 'k') {
380
+ handleSidebarNavKey(e);
381
+ }
382
+ };
383
+ const onKeyUp = (e: KeyboardEvent): void => {
384
+ if (e.key === 'Alt') setAltHeld(false);
385
+ };
386
+ const onBlur = (): void => setAltHeld(false);
387
+
388
+ window.addEventListener('keydown', onKeyDown);
389
+ window.addEventListener('keyup', onKeyUp);
390
+ window.addEventListener('blur', onBlur);
391
+ return () => {
392
+ window.removeEventListener('keydown', onKeyDown);
393
+ window.removeEventListener('keyup', onKeyUp);
394
+ window.removeEventListener('blur', onBlur);
395
+ };
396
+ }, [clearActiveSpan]);
397
+
398
+ const timelineRef = useRef<HTMLDivElement>(null);
399
+ const [hoverFraction, setHoverFraction] = useState<number | null>(null);
400
+
401
+ const hoverInfo = useMemo(() => {
402
+ if (hoverFraction == null) return null;
403
+ const absTime = viewport.start + hoverFraction * viewDuration;
404
+ const offset = absTime - root.startTime;
405
+ return { fraction: hoverFraction, label: formatDuration(offset, true) };
406
+ }, [hoverFraction, viewport.start, viewDuration, root.startTime]);
407
+
408
+ const handleTimelineMouseMove = useCallback(
409
+ (e: React.MouseEvent<HTMLDivElement>) => {
410
+ const el = timelineRef.current;
411
+ if (!el) return;
412
+ const rect = el.getBoundingClientRect();
413
+ const contentWidth = rect.width - TIMELINE_PADDING_PX * 2;
414
+ if (contentWidth <= 0) return;
415
+ const fraction = Math.max(
416
+ 0,
417
+ Math.min(
418
+ 1,
419
+ (e.clientX - rect.left - TIMELINE_PADDING_PX) / contentWidth
420
+ )
421
+ );
422
+ setHoverFraction(fraction);
423
+ },
424
+ []
425
+ );
426
+
427
+ const handleTimelineMouseLeave = useCallback(() => {
428
+ setHoverFraction(null);
429
+ }, []);
430
+
431
+ useEffect(() => {
432
+ const el = timelineRef.current;
433
+ if (!el) return;
434
+
435
+ const rootS = root.startTime;
436
+ const rootE = root.startTime + root.duration;
437
+ const rootD = root.duration;
438
+ if (rootD <= 0) return;
439
+
440
+ const onWheel = (e: WheelEvent): void => {
441
+ const isZoomGesture = e.ctrlKey || e.metaKey;
442
+ const hasDeltaX = Math.abs(e.deltaX) > Math.abs(e.deltaY);
443
+
444
+ if (!isZoomGesture && !hasDeltaX) return;
445
+ e.preventDefault();
446
+
447
+ const rect = el.getBoundingClientRect();
448
+ const contentWidth = rect.width - TIMELINE_PADDING_PX * 2;
449
+ if (contentWidth <= 0) return;
450
+
451
+ if (isZoomGesture) {
452
+ let dy = e.deltaY;
453
+ if (e.deltaMode === 1) dy *= 16;
454
+
455
+ const cursorFraction = Math.max(
456
+ 0,
457
+ Math.min(
458
+ 1,
459
+ (e.clientX - rect.left - TIMELINE_PADDING_PX) / contentWidth
460
+ )
461
+ );
462
+ const isMouseWheel = e.deltaMode === 1 || Math.abs(e.deltaY) >= 50;
463
+ const scaleFactor = Math.pow(2, dy / (isMouseWheel ? 200 : 60));
464
+
465
+ setViewport((prev) => {
466
+ const prevDuration = prev.end - prev.start;
467
+ const cursorTime = prev.start + cursorFraction * prevDuration;
468
+ const newDuration = Math.max(
469
+ MIN_VIEWPORT_MS,
470
+ Math.min(rootD, prevDuration * scaleFactor)
471
+ );
472
+
473
+ let newStart = cursorTime - cursorFraction * newDuration;
474
+ let newEnd = newStart + newDuration;
475
+
476
+ if (newStart < rootS) {
477
+ newStart = rootS;
478
+ newEnd = rootS + newDuration;
479
+ }
480
+ if (newEnd > rootE) {
481
+ newEnd = rootE;
482
+ newStart = Math.max(rootS, rootE - newDuration);
483
+ }
484
+
485
+ return { start: newStart, end: newEnd };
486
+ });
487
+ } else {
488
+ let dx = e.deltaX;
489
+ if (e.deltaMode === 1) dx *= 16;
490
+
491
+ setViewport((prev) => {
492
+ const prevDuration = prev.end - prev.start;
493
+ const panAmount = (dx / contentWidth) * prevDuration;
494
+
495
+ let newStart = prev.start + panAmount;
496
+ let newEnd = prev.end + panAmount;
497
+
498
+ if (newStart < rootS) {
499
+ newStart = rootS;
500
+ newEnd = rootS + prevDuration;
501
+ }
502
+ if (newEnd > rootE) {
503
+ newEnd = rootE;
504
+ newStart = Math.max(rootS, rootE - prevDuration);
505
+ }
506
+
507
+ return { start: newStart, end: newEnd };
508
+ });
509
+ }
510
+ };
511
+
512
+ el.addEventListener('wheel', onWheel, { passive: false });
513
+ return () => el.removeEventListener('wheel', onWheel);
514
+ }, [root.startTime, root.duration, setViewport]);
515
+
516
+ // Derive the selected span name and metadata for the panel header
517
+ const selectedSpanName = useMemo(() => {
518
+ if (!selectedSpan?.data) return 'Details';
519
+ const data = selectedSpan.data as Record<string, unknown>;
520
+ if (selectedSpan.resource === 'hook') {
521
+ return (data.token as string | undefined) ?? (data.hookId as string);
522
+ }
523
+
524
+ const stepName = data.stepName as string | undefined;
525
+ const workflowName = data.workflowName as string | undefined;
526
+ return (
527
+ (stepName ? parseStepName(stepName)?.shortName : undefined) ??
528
+ (workflowName ? parseWorkflowName(workflowName)?.shortName : undefined) ??
529
+ stepName ??
530
+ workflowName ??
531
+ (data.hookId as string) ??
532
+ 'Details'
533
+ );
534
+ }, [selectedSpan?.data, selectedSpan?.resource]);
535
+
536
+ const { prevSpanId, nextSpanId } = useMemo(() => {
537
+ if (!activeSpanId) return { prevSpanId: null, nextSpanId: null };
538
+ const i = trace.spans.findIndex((s) => s.spanId === activeSpanId);
539
+ if (i === -1) return { prevSpanId: null, nextSpanId: null };
540
+ return {
541
+ prevSpanId: trace.spans[i - 1]?.spanId ?? null,
542
+ nextSpanId: trace.spans[i + 1]?.spanId ?? null,
543
+ };
544
+ }, [activeSpanId, trace.spans]);
545
+
546
+ const handleSelectPrevSpan = useCallback(() => {
547
+ if (prevSpanId) handleSelectSpan(prevSpanId);
548
+ }, [prevSpanId, handleSelectSpan]);
549
+
550
+ const handleSelectNextSpan = useCallback(() => {
551
+ if (nextSpanId) handleSelectSpan(nextSpanId);
552
+ }, [nextSpanId, handleSelectSpan]);
553
+
554
+ const prevSpanIdRef = useRef(prevSpanId);
555
+ const nextSpanIdRef = useRef(nextSpanId);
556
+ const handleSelectSpanRef = useRef(handleSelectSpan);
557
+ prevSpanIdRef.current = prevSpanId;
558
+ nextSpanIdRef.current = nextSpanId;
559
+ handleSelectSpanRef.current = handleSelectSpan;
560
+
561
+ return (
562
+ <div
563
+ data-pane="pane-root"
564
+ data-has-detail={activeSpan ? '' : undefined}
565
+ 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%)]"
566
+ >
567
+ <div
568
+ id="trace-parent"
569
+ className="grid grid-rows-[1fr] h-full min-h-0 overflow-hidden relative bg-background-100"
570
+ >
571
+ <SplitPane
572
+ scrollContainerRef={scrollContainerRef}
573
+ startHeader={
574
+ <div className="bg-background-100 border-b border-gray-alpha-400 h-10 min-h-10 flex items-center pl-4 pr-2 gap-1.5">
575
+ <Search className="w-3.5 h-3.5 shrink-0 text-gray-800" />
576
+ <input
577
+ id="trace-viewer-search"
578
+ name="trace-viewer-search"
579
+ type="text"
580
+ value={searchQuery}
581
+ onChange={(e) => setSearchQuery(e.target.value)}
582
+ onKeyDown={(e) => {
583
+ if (e.key === 'Escape' && searchQuery) {
584
+ e.preventDefault();
585
+ e.stopPropagation();
586
+ setSearchQuery('');
587
+ }
588
+ }}
589
+ placeholder="Search spans..."
590
+ aria-label="Search spans"
591
+ className="flex-1 min-w-0 bg-transparent text-sm text-gray-1000 placeholder:text-gray-800 outline-none"
592
+ />
593
+ {searchQuery && (
594
+ <button
595
+ type="button"
596
+ aria-label="Clear search"
597
+ onClick={() => setSearchQuery('')}
598
+ className="-mr-2 hidden h-full max-w-full shrink-0 cursor-pointer items-center rounded-r-md border-0 bg-transparent px-2.5 font-inherit text-base text-gray-900 no-underline transition-colors duration-150 ease-in hover:text-gray-1000 focus-visible:-outline-offset-1 focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--ds-focus-color)] min-[961px]:flex"
599
+ >
600
+ <kbd className="inline-flex h-5 min-h-5 min-w-5 items-center justify-center rounded border border-gray-alpha-400 bg-background-100 px-1 font-sans text-[13px] font-medium leading-[1.7em] text-gray-900">
601
+ Esc
602
+ </kbd>
603
+ </button>
604
+ )}
605
+ </div>
606
+ }
607
+ endHeader={
608
+ <TimelineHeader markers={timeMarkers} hoverInfo={hoverInfo} />
609
+ }
610
+ >
611
+ <div className="block overflow-visible">
612
+ <EventList
613
+ spans={trace.spans}
614
+ activeSpanId={activeSpanId}
615
+ searchResult={searchResult}
616
+ onSelectSpan={handleSelectSpan}
617
+ />
618
+ <div ref={loadMoreSentinelRef} className="flex justify-center">
619
+ {isLoadingMore ? (
620
+ <div className="flex items-center justify-center gap-2 py-3 text-sm text-gray-800">
621
+ <Spinner size={14} />
622
+ <span>Loading spans…</span>
623
+ </div>
624
+ ) : null}
625
+ </div>
626
+ </div>
627
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: timeline hover and wheel gestures are pointer-only annotations */}
628
+ <div
629
+ ref={timelineRef}
630
+ className="block min-h-0 overflow-visible relative"
631
+ onDoubleClick={resetZoom}
632
+ onMouseMove={handleTimelineMouseMove}
633
+ onMouseLeave={handleTimelineMouseLeave}
634
+ >
635
+ <Timeline
636
+ spans={trace.spans}
637
+ viewStart={viewport.start}
638
+ viewEnd={viewport.end}
639
+ markers={timeMarkers}
640
+ selectedId={activeSpanId}
641
+ searchResult={searchResult}
642
+ onSelect={handleSelectSpan}
643
+ hoverFraction={hoverFraction}
644
+ altHeld={altHeld}
645
+ />
646
+ </div>
647
+ </SplitPane>
648
+ <div className="absolute right-3 bottom-3 z-[5] flex items-center border border-gray-alpha-400 rounded-md bg-background-100 shadow-sm overflow-hidden divide-x divide-gray-alpha-400">
649
+ <IconButton
650
+ variant="muted"
651
+ size="small"
652
+ onClick={zoomOut}
653
+ aria-label="Zoom out"
654
+ >
655
+ <ZoomOut className="w-4 h-4" />
656
+ </IconButton>
657
+ <IconButton
658
+ variant="muted"
659
+ size="small"
660
+ onClick={resetZoom}
661
+ aria-label="Reset zoom"
662
+ >
663
+ <RotateCcw className="w-3.5 h-3.5" />
664
+ </IconButton>
665
+ <IconButton
666
+ variant="muted"
667
+ size="small"
668
+ onClick={zoomIn}
669
+ aria-label="Zoom in"
670
+ >
671
+ <ZoomIn className="w-4 h-4" />
672
+ </IconButton>
673
+ </div>
674
+ </div>
675
+
676
+ {/* Detail panel */}
677
+ {activeSpan && sidebar ? (
678
+ <aside className="flex flex-col h-full max-h-full bg-background-100 border-l border-gray-alpha-400 overflow-auto">
679
+ {/* Panel header */}
680
+ <div className="flex items-center justify-between gap-2 shrink-0 px-4 pt-3 pb-3">
681
+ <span className="text-label-14 font-medium text-gray-1000 truncate block">
682
+ {selectedSpanName}
683
+ </span>
684
+ <div className="flex items-center gap-0.5 shrink-0">
685
+ <Tooltip>
686
+ <TooltipTrigger asChild>
687
+ <IconButton
688
+ aria-label="Navigate up"
689
+ aria-keyshortcuts="K"
690
+ onClick={handleSelectPrevSpan}
691
+ disabled={!prevSpanId}
692
+ >
693
+ <ChevronUp className="w-4 h-4" />
694
+ </IconButton>
695
+ </TooltipTrigger>
696
+ {prevSpanId ? (
697
+ <TooltipContent>
698
+ Navigate up
699
+ <Kbd>K</Kbd>
700
+ </TooltipContent>
701
+ ) : null}
702
+ </Tooltip>
703
+ <Tooltip>
704
+ <TooltipTrigger asChild>
705
+ <IconButton
706
+ aria-label="Navigate down"
707
+ aria-keyshortcuts="J"
708
+ onClick={handleSelectNextSpan}
709
+ disabled={!nextSpanId}
710
+ >
711
+ <ChevronDown className="w-4 h-4" />
712
+ </IconButton>
713
+ </TooltipTrigger>
714
+ {nextSpanId ? (
715
+ <TooltipContent>
716
+ Navigate down
717
+ <Kbd>J</Kbd>
718
+ </TooltipContent>
719
+ ) : null}
720
+ </Tooltip>
721
+ <div aria-hidden className="w-px h-4 bg-gray-alpha-400 mx-1" />
722
+ <IconButton
723
+ aria-label="Close span details"
724
+ aria-keyshortcuts="Escape"
725
+ onClick={clearActiveSpan}
726
+ >
727
+ <X className="w-4 h-4" />
728
+ </IconButton>
729
+ </div>
730
+ </div>
731
+ {/* Panel body */}
732
+ <div className="flex-1 overflow-y-auto">
733
+ <ErrorBoundary>
734
+ <EntityDetailPanel
735
+ run={sidebar.run}
736
+ onStreamClick={sidebar.onStreamClick}
737
+ onRunClick={sidebar.onRunClick}
738
+ spanDetailData={sidebar.spanDetailData}
739
+ spanDetailError={sidebar.spanDetailError}
740
+ spanDetailLoading={sidebar.spanDetailLoading}
741
+ onSpanSelect={sidebar.onSpanSelect}
742
+ onWakeUpSleep={sidebar.onWakeUpSleep}
743
+ onLoadEventData={sidebar.onLoadEventData}
744
+ onResolveHook={sidebar.onResolveHook}
745
+ encryptionKey={sidebar.encryptionKey}
746
+ onDecrypt={sidebar.onDecrypt}
747
+ isDecrypting={sidebar.isDecrypting}
748
+ selectedSpan={selectedSpan}
749
+ />
750
+ </ErrorBoundary>
751
+ </div>
752
+ </aside>
753
+ ) : activeSpan ? (
754
+ <DetailPanel
755
+ span={activeSpan}
756
+ rootStart={root.startTime}
757
+ onClose={clearActiveSpan}
758
+ />
759
+ ) : null}
760
+ </div>
761
+ );
762
+ }