@workflow/web-shared 5.0.0-beta.2 → 5.0.0-beta.21

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