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

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
@@ -4,13 +4,16 @@ import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
4
4
  import clsx from 'clsx';
5
5
  import { Send, Zap } from 'lucide-react';
6
6
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
7
- import { isEncryptedMarker } from '../../lib/hydration';
8
7
  import { useToast } from '../../lib/toast';
9
8
  import { DecryptClickContext } from '../ui/data-inspector';
10
- import { DecryptButton } from '../ui/decrypt-button';
11
9
  import { AttributePanel } from './attribute-panel';
12
10
  import { EventsList } from './events-list';
13
11
  import { ResolveHookModal } from './resolve-hook-modal';
12
+ import { useSidebarDataOptional } from './sidebar-data-context';
13
+ import {
14
+ mergeSpanDetail,
15
+ spanDetailMatchesSelection,
16
+ } from './span-detail-merge';
14
17
 
15
18
  // Type guards for runtime validation of span attribute data
16
19
  function isStep(data: unknown): data is Step {
@@ -69,7 +72,6 @@ export function EntityDetailPanel({
69
72
  onDecrypt,
70
73
  isDecrypting = false,
71
74
  selectedSpan,
72
- hasEncryptedData = false,
73
75
  }: {
74
76
  run: WorkflowRun;
75
77
  /** Callback when a stream reference is clicked */
@@ -108,8 +110,6 @@ export function EntityDetailPanel({
108
110
  isDecrypting?: boolean;
109
111
  /** Info about the currently selected span from the trace viewer */
110
112
  selectedSpan: SelectedSpanInfo | null;
111
- /** Run-level hint: the run contains encrypted data (from probe). */
112
- hasEncryptedData?: boolean;
113
113
  }): React.JSX.Element | null {
114
114
  const toast = useToast();
115
115
  const [stoppingSleep, setStoppingSleep] = useState(false);
@@ -121,6 +121,9 @@ export function EntityDetailPanel({
121
121
  new Set()
122
122
  );
123
123
 
124
+ const sidebar = useSidebarDataOptional();
125
+ const hasEncryptedData = Boolean(sidebar?.hasEncryptedData && !encryptionKey);
126
+
124
127
  const data = selectedSpan?.data;
125
128
  const rawEvents = selectedSpan?.rawEvents;
126
129
  const rawEventsLength = rawEvents?.length ?? 0;
@@ -216,30 +219,27 @@ export function EntityDetailPanel({
216
219
  const error = spanDetailError ?? undefined;
217
220
  const loading = spanDetailLoading ?? false;
218
221
 
219
- const hasEncryptedFields = useMemo(() => {
220
- if (!spanDetailData) return false;
221
- const d = spanDetailData as Record<string, unknown>;
222
- return (
223
- isEncryptedMarker(d.input) ||
224
- isEncryptedMarker(d.output) ||
225
- isEncryptedMarker(d.error) ||
226
- isEncryptedMarker(d.metadata)
227
- );
228
- }, [spanDetailData]);
222
+ const matchedSpanDetailData = useMemo(
223
+ () =>
224
+ spanDetailMatchesSelection(spanDetailData, resource, resourceId)
225
+ ? spanDetailData
226
+ : null,
227
+ [spanDetailData, resource, resourceId]
228
+ );
229
229
 
230
230
  // Get the hook token for resolving (prefer fetched data, then hooks array fallback)
231
231
  const hookToken = useMemo(() => {
232
232
  if (resource !== 'hook' || !resourceId) return undefined;
233
233
  // 1. Try the externally-fetched detail data first
234
- if (isHook(spanDetailData) && spanDetailData.token) {
235
- return spanDetailData.token;
234
+ if (isHook(matchedSpanDetailData) && matchedSpanDetailData.token) {
235
+ return matchedSpanDetailData.token;
236
236
  }
237
237
  // 2. Try the span's inline data (reconstructed from hook_created event)
238
238
  if (isHook(data) && (data as Hook).token) {
239
239
  return (data as Hook).token;
240
240
  }
241
241
  return undefined;
242
- }, [resource, resourceId, spanDetailData, data]);
242
+ }, [resource, resourceId, matchedSpanDetailData, data]);
243
243
 
244
244
  useEffect(() => {
245
245
  if (error && selectedSpan && resource) {
@@ -301,7 +301,7 @@ export function EntityDetailPanel({
301
301
 
302
302
  try {
303
303
  setResolvingHook(true);
304
- const candidate = spanDetailData ?? data;
304
+ const candidate = matchedSpanDetailData ?? data;
305
305
  const hook = isHook(candidate) ? candidate : undefined;
306
306
  await onResolveHook(hookToken, payload, hook);
307
307
  toast.success('Hook resolved', {
@@ -322,17 +322,18 @@ export function EntityDetailPanel({
322
322
  setResolvingHook(false);
323
323
  }
324
324
  },
325
- [onResolveHook, hookToken, resolvingHook, spanDetailData, data]
325
+ [onResolveHook, hookToken, resolvingHook, matchedSpanDetailData, data]
326
326
  );
327
327
 
328
- // Prefer externally-fetched details when available. For sleep spans, the
329
- // host fetches full correlated events (withData=true) and materializes a wait
330
- // entity, so this includes resumeAt/completedAt without bloating trace payloads.
331
- const displayData = (spanDetailData ?? data) as
332
- | WorkflowRun
333
- | Step
334
- | Hook
335
- | Event;
328
+ const displayData = useMemo(
329
+ () =>
330
+ mergeSpanDetail(data, matchedSpanDetailData) as
331
+ | WorkflowRun
332
+ | Step
333
+ | Hook
334
+ | Event,
335
+ [data, matchedSpanDetailData]
336
+ );
336
337
 
337
338
  const moduleSpecifier = useMemo(() => {
338
339
  const displayRecord = displayData as Record<string, unknown>;
@@ -354,65 +355,18 @@ export function EntityDetailPanel({
354
355
  return null;
355
356
  }
356
357
 
357
- const resourceLabel = resource.charAt(0).toUpperCase() + resource.slice(1);
358
358
  const hasPendingActions =
359
359
  (resource === 'sleep' && canWakeUp) ||
360
360
  (resource === 'hook' && canResolveHook);
361
- const runStateLabel = run.completedAt ? 'Completed' : 'Live';
362
361
 
363
362
  return (
364
363
  <div className="flex h-full flex-col">
365
- <div
366
- className="border-b px-3 py-3"
367
- style={{ borderColor: 'var(--ds-gray-200)' }}
368
- >
369
- <div className="flex items-start justify-between gap-2">
370
- <div className="min-w-0">
371
- <div className="flex items-center gap-2">
372
- <span
373
- className="inline-flex items-center rounded-full border px-2 py-0.5 text-[13px] font-medium"
374
- style={{
375
- borderColor: 'var(--ds-gray-300)',
376
- color: 'var(--ds-gray-900)',
377
- backgroundColor: 'var(--ds-background-100)',
378
- }}
379
- >
380
- {resourceLabel}
381
- </span>
382
- <span
383
- className="text-[13px]"
384
- style={{
385
- color: run.completedAt
386
- ? 'var(--ds-gray-700)'
387
- : 'var(--ds-green-800)',
388
- }}
389
- >
390
- {runStateLabel}
391
- </span>
392
- </div>
393
- <p
394
- className="mt-1 truncate font-mono text-[13px]"
395
- style={{ color: 'var(--ds-gray-700)' }}
396
- title={resourceId}
397
- >
398
- {resourceId}
399
- </p>
400
- </div>
401
- {(hasEncryptedFields || hasEncryptedData || encryptionKey) &&
402
- onDecrypt && (
403
- <DecryptButton
404
- decrypted={!!encryptionKey}
405
- loading={isDecrypting}
406
- onClick={onDecrypt}
407
- />
408
- )}
409
- </div>
410
- </div>
411
-
412
364
  <DecryptClickContext.Provider
413
- value={onDecrypt ? { onDecrypt, isDecrypting } : undefined}
365
+ value={
366
+ onDecrypt ? { onDecrypt, isDecrypting, hasEncryptedData } : undefined
367
+ }
414
368
  >
415
- <div className="flex-1 overflow-y-auto px-3 pt-3 pb-8">
369
+ <div className="flex-1 overflow-y-auto px-4 pb-8">
416
370
  {hasPendingActions && (
417
371
  <div
418
372
  className="mb-4 rounded-lg border p-2"
@@ -477,38 +431,28 @@ export function EntityDetailPanel({
477
431
  </div>
478
432
  )}
479
433
 
480
- <div className="space-y-4">
481
- <section>
482
- <h3
483
- className="mb-2 text-[13px] font-medium uppercase tracking-wide"
484
- style={{ color: 'var(--ds-gray-700)' }}
485
- >
486
- Details
487
- </h3>
488
- <AttributePanel
489
- data={displayData}
490
- moduleSpecifier={moduleSpecifier}
491
- expiredAt={run.expiredAt}
492
- isLoading={loading}
493
- error={error ?? undefined}
494
- onStreamClick={onStreamClick}
495
- onRunClick={onRunClick}
496
- onDecrypt={onDecrypt}
497
- isDecrypting={isDecrypting}
498
- resource={resource}
499
- />
500
- </section>
501
-
502
- {resource !== 'run' && rawEvents && (
503
- <section>
504
- <EventsList
505
- events={rawEvents}
506
- onLoadEventData={onLoadEventData}
507
- encryptionKey={encryptionKey}
508
- />
509
- </section>
510
- )}
511
- </div>
434
+ <AttributePanel
435
+ data={displayData}
436
+ moduleSpecifier={moduleSpecifier}
437
+ expiredAt={run.expiredAt}
438
+ isLoading={loading}
439
+ error={error ?? undefined}
440
+ onStreamClick={onStreamClick}
441
+ onRunClick={onRunClick}
442
+ onDecrypt={onDecrypt}
443
+ isDecrypting={isDecrypting}
444
+ resource={resource}
445
+ />
446
+
447
+ {rawEvents && (
448
+ <EventsList
449
+ events={rawEvents}
450
+ onLoadEventData={onLoadEventData}
451
+ onStreamClick={onStreamClick}
452
+ onRunClick={onRunClick}
453
+ encryptionKey={encryptionKey}
454
+ />
455
+ )}
512
456
  </div>
513
457
  </DecryptClickContext.Provider>
514
458
 
@@ -1,16 +1,14 @@
1
1
  'use client';
2
2
 
3
3
  import { EVENT_DATA_REF_FIELDS, type Event } from '@workflow/world';
4
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
- import { isExpiredMarker } from '../../lib/hydration';
4
+ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
5
+ import { hasEncryptedFields, isExpiredMarker } from '../../lib/hydration';
6
+ import { RunClickContext, StreamClickContext } from '../ui/data-inspector';
6
7
  import { ErrorCard } from '../ui/error-card';
7
- import {
8
- ErrorStackBlock,
9
- isStructuredErrorWithStack,
10
- } from '../ui/error-stack-block';
8
+ import { ErrorStackBlock, isStructuredError } from '../ui/error-stack-block';
11
9
  import { Skeleton } from '../ui/skeleton';
12
- import { localMillisecondTime } from './attribute-panel';
13
- import { CopyableDataBlock } from './copyable-data-block';
10
+ import { AttrSetEventBlock } from './attributes-block';
11
+ import { CopyableDataBlock, EncryptedDataBlock } from './copyable-data-block';
14
12
  import { DetailCard } from './detail-card';
15
13
 
16
14
  /**
@@ -33,6 +31,7 @@ const DATA_EVENT_TYPES = new Set([
33
31
  'run_failed',
34
32
  'wait_created',
35
33
  'wait_completed',
34
+ 'attr_set',
36
35
  ]);
37
36
 
38
37
  /**
@@ -94,7 +93,7 @@ function EventItem({
94
93
 
95
94
  // When the encryption key changes and this event was previously expanded,
96
95
  // re-load the data so it gets decrypted
97
- useEffect(() => {
96
+ useLayoutEffect(() => {
98
97
  if (!encryptionKey || !wasExpandedRef.current) return;
99
98
  loadedDataRef.current = null;
100
99
  setLoadedData(null);
@@ -102,25 +101,27 @@ function EventItem({
102
101
  }, [encryptionKey, loadEventData]);
103
102
 
104
103
  const createdAt = new Date(event.createdAt);
104
+ const createdAtTime = createdAt.toLocaleTimeString(undefined, {
105
+ hour: 'numeric',
106
+ minute: 'numeric',
107
+ second: 'numeric',
108
+ });
105
109
 
106
110
  const displayPayload = isLoading ? loadedData : mergedDisplay;
107
111
 
108
112
  return (
109
113
  <DetailCard
110
- summaryClassName="text-base py-2"
114
+ variant="card"
115
+ summaryClassName="px-3 py-2"
111
116
  summary={
112
- <>
113
- <span
114
- className="font-medium"
115
- style={{ color: 'var(--ds-gray-1000)' }}
116
- >
117
+ <div className="flex w-full items-center justify-between gap-3">
118
+ <span className="text-gray-1000 text-label-12 font-mono">
117
119
  {event.eventType}
118
- </span>{' '}
119
- -{' '}
120
- <span style={{ color: 'var(--ds-gray-700)' }}>
121
- {localMillisecondTime(createdAt.getTime())}
122
120
  </span>
123
- </>
121
+ <span className="shrink-0 text-label-13 text-gray-900">
122
+ {createdAtTime}
123
+ </span>
124
+ </div>
124
125
  }
125
126
  onToggle={
126
127
  canHaveData
@@ -131,44 +132,17 @@ function EventItem({
131
132
  }
132
133
  >
133
134
  {/* Event attributes */}
134
- <div
135
- className="flex flex-col divide-y rounded-md border overflow-hidden"
136
- style={{
137
- borderColor: 'var(--ds-gray-300)',
138
- backgroundColor: 'var(--ds-gray-100)',
139
- }}
140
- >
141
- <div
142
- className="flex min-h-[32px] items-center justify-between gap-4 px-2.5 py-1.5"
143
- style={{ borderColor: 'var(--ds-gray-300)' }}
144
- >
145
- <span className="text-[14px]" style={{ color: 'var(--ds-gray-700)' }}>
146
- eventId
147
- </span>
148
- <span
149
- className="max-w-[70%] truncate text-right text-[13px] font-mono"
150
- style={{ color: 'var(--ds-gray-1000)' }}
151
- title={event.eventId}
152
- >
135
+ <div className="flex flex-col bg-background-200 [&:has(+_*)]:border-b [&:has(+_*)]:border-gray-alpha-400">
136
+ <div className="flex items-center justify-between gap-2 py-2 px-3">
137
+ <span className="text-label-12 text-gray-900">Event ID</span>
138
+ <span className="max-w-[70%] truncate text-right text-label-12 font-mono">
153
139
  {event.eventId}
154
140
  </span>
155
141
  </div>
156
142
  {event.correlationId && (
157
- <div
158
- className="flex min-h-[32px] items-center justify-between gap-4 px-2.5 py-1.5"
159
- style={{ borderColor: 'var(--ds-gray-300)' }}
160
- >
161
- <span
162
- className="text-[14px]"
163
- style={{ color: 'var(--ds-gray-700)' }}
164
- >
165
- correlationId
166
- </span>
167
- <span
168
- className="max-w-[70%] truncate text-right text-[13px] font-mono"
169
- style={{ color: 'var(--ds-gray-1000)' }}
170
- title={event.correlationId}
171
- >
143
+ <div className="flex items-center justify-between gap-2 py-2 px-3">
144
+ <span className="text-label-12 text-gray-900">Correlation ID</span>
145
+ <span className="max-w-[70%] truncate text-right text-label-12 font-mono">
172
146
  {event.correlationId}
173
147
  </span>
174
148
  </div>
@@ -177,12 +151,7 @@ function EventItem({
177
151
 
178
152
  {/* Loading state */}
179
153
  {isLoading && (
180
- <div
181
- className="mt-2 rounded-md border p-3"
182
- style={{
183
- borderColor: 'var(--ds-gray-300)',
184
- }}
185
- >
154
+ <div className="p-3">
186
155
  <Skeleton className="h-4 w-[35%]" />
187
156
  <Skeleton className="mt-2 h-4 w-[90%]" />
188
157
  <Skeleton className="mt-2 h-4 w-[75%]" />
@@ -200,7 +169,7 @@ function EventItem({
200
169
 
201
170
  {/* Event data */}
202
171
  {displayPayload != null && (
203
- <div className="mt-2">
172
+ <div className="[&>div]:border-none [&>div]:rounded-none">
204
173
  <EventDataBlock eventType={event.eventType} data={displayPayload} />
205
174
  </div>
206
175
  )}
@@ -227,8 +196,8 @@ function hasOnlyExpiredFields(data: unknown, eventType: string): boolean {
227
196
  }
228
197
 
229
198
  /**
230
- * Renders event data, using ErrorStackBlock for error events that contain
231
- * a structured error with a stack trace, and CopyableDataBlock otherwise.
199
+ * Renders event data, using ErrorStackBlock for error events that contain a
200
+ * structured error, and CopyableDataBlock otherwise.
232
201
  */
233
202
  function EventDataBlock({
234
203
  eventType,
@@ -255,6 +224,16 @@ function EventDataBlock({
255
224
  );
256
225
  }
257
226
 
227
+ if (hasEncryptedFields({ eventType, eventData: data })) {
228
+ return <EncryptedDataBlock />;
229
+ }
230
+
231
+ // Attribute changes — render the changed keys and the writer instead of
232
+ // the raw JSON payload.
233
+ if (eventType === 'attr_set') {
234
+ return <AttrSetEventBlock data={data} />;
235
+ }
236
+
258
237
  // For error events (step_failed, step_retrying), the eventData has the shape
259
238
  // { error: StructuredError, stack?: string, ... }. Check both the top-level
260
239
  // value and the nested `error` field for a stack trace.
@@ -266,12 +245,12 @@ function EventDataBlock({
266
245
  const record = data as Record<string, unknown>;
267
246
 
268
247
  // Check the nested `error` field first (the StructuredError)
269
- if (isStructuredErrorWithStack(record.error)) {
248
+ if (isStructuredError(record.error)) {
270
249
  return <ErrorStackBlock value={record.error} />;
271
250
  }
272
251
 
273
- // Some error formats put the stack at the top level of eventData
274
- if (isStructuredErrorWithStack(record)) {
252
+ // Some error formats put the message/stack at the top level of eventData.
253
+ if (isStructuredError(record)) {
275
254
  return <ErrorStackBlock value={record} />;
276
255
  }
277
256
  }
@@ -286,6 +265,8 @@ export function EventsList({
286
265
  isLoading = false,
287
266
  error,
288
267
  onLoadEventData,
268
+ onStreamClick,
269
+ onRunClick,
289
270
  encryptionKey,
290
271
  }: {
291
272
  events: Event[];
@@ -295,6 +276,8 @@ export function EventsList({
295
276
  correlationId: string,
296
277
  eventId: string
297
278
  ) => Promise<unknown | null>;
279
+ onStreamClick?: (streamId: string) => void;
280
+ onRunClick?: (runId: string) => void;
298
281
  /** When provided, signals that decryption is active (triggers re-load of expanded events) */
299
282
  encryptionKey?: Uint8Array;
300
283
  }) {
@@ -308,36 +291,42 @@ export function EventsList({
308
291
  [events]
309
292
  );
310
293
 
294
+ const hasEvents = sortedEvents.length > 0 && !error;
295
+
296
+ if (!hasEvents && !isLoading) {
297
+ return <DetailCard summary="Events" disabled />;
298
+ }
299
+
311
300
  return (
312
- <div className="mt-2" style={{ color: 'var(--ds-gray-1000)' }}>
313
- <h3
314
- className="text-heading-16 font-medium mt-4 mb-2"
315
- style={{ color: 'var(--ds-gray-1000)' }}
316
- >
317
- Events {!isLoading && `(${sortedEvents.length})`}
318
- </h3>
319
- {isLoading ? (
320
- <div className="flex flex-col gap-4">
321
- <Skeleton className="h-9 w-full rounded-md" />
322
- <Skeleton className="h-9 w-full rounded-md" />
323
- <Skeleton className="h-9 w-full rounded-md" />
324
- </div>
325
- ) : null}
326
- {!isLoading && !error && sortedEvents.length === 0 && (
327
- <div className="text-sm">No events found</div>
328
- )}
329
- {sortedEvents.length > 0 && !error ? (
330
- <div className="flex flex-col gap-4">
331
- {sortedEvents.map((event) => (
332
- <EventItem
333
- key={event.eventId}
334
- event={event}
335
- onLoadEventData={onLoadEventData}
336
- encryptionKey={encryptionKey}
337
- />
338
- ))}
339
- </div>
340
- ) : null}
341
- </div>
301
+ <RunClickContext.Provider value={onRunClick}>
302
+ <StreamClickContext.Provider value={onStreamClick}>
303
+ <DetailCard summary="Events" contentClassName="mb-0" defaultOpen>
304
+ {isLoading ? (
305
+ <div className="flex flex-col -mx-4">
306
+ {[0, 1, 2].map((i) => (
307
+ <div
308
+ key={i}
309
+ className="flex items-center justify-between gap-3 bg-background-200 px-4 py-2"
310
+ >
311
+ <Skeleton className="h-4 w-32 rounded" />
312
+ <Skeleton className="h-3 w-16 rounded" />
313
+ </div>
314
+ ))}
315
+ </div>
316
+ ) : (
317
+ <div className="flex flex-col -mx-4">
318
+ {sortedEvents.map((event) => (
319
+ <EventItem
320
+ key={event.eventId}
321
+ event={event}
322
+ onLoadEventData={onLoadEventData}
323
+ encryptionKey={encryptionKey}
324
+ />
325
+ ))}
326
+ </div>
327
+ )}
328
+ </DetailCard>
329
+ </StreamClickContext.Provider>
330
+ </RunClickContext.Provider>
342
331
  );
343
332
  }
@@ -0,0 +1,62 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext, type ReactNode } from 'react';
4
+ import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
5
+ import type { SpanSelectionInfo } from './entity-detail-panel';
6
+
7
+ export interface SidebarDataContextValue {
8
+ run: WorkflowRun;
9
+ events: Event[];
10
+ spanDetailData: WorkflowRun | Step | Hook | Event | null;
11
+ spanDetailError?: Error | null;
12
+ spanDetailLoading?: boolean;
13
+ onSpanSelect: (info: SpanSelectionInfo) => void;
14
+ onStreamClick?: (streamId: string) => void;
15
+ onRunClick?: (runId: string) => void;
16
+ onWakeUpSleep?: (
17
+ runId: string,
18
+ correlationId: string
19
+ ) => Promise<{ stoppedCount: number }>;
20
+ onLoadEventData?: (
21
+ correlationId: string,
22
+ eventId: string
23
+ ) => Promise<unknown | null>;
24
+ onResolveHook?: (
25
+ hookToken: string,
26
+ payload: unknown,
27
+ hook?: Hook
28
+ ) => Promise<void>;
29
+ encryptionKey?: Uint8Array;
30
+ onDecrypt?: () => void;
31
+ isDecrypting?: boolean;
32
+ hasEncryptedData?: boolean;
33
+ }
34
+
35
+ const SidebarDataContext = createContext<SidebarDataContextValue | null>(null);
36
+ SidebarDataContext.displayName = 'SidebarDataContext';
37
+
38
+ export function SidebarDataProvider({
39
+ value,
40
+ children,
41
+ }: {
42
+ value: SidebarDataContextValue;
43
+ children: ReactNode;
44
+ }) {
45
+ return (
46
+ <SidebarDataContext.Provider value={value}>
47
+ {children}
48
+ </SidebarDataContext.Provider>
49
+ );
50
+ }
51
+
52
+ export function useSidebarData(): SidebarDataContextValue {
53
+ const ctx = useContext(SidebarDataContext);
54
+ if (!ctx) {
55
+ throw new Error('useSidebarData must be used within a SidebarDataProvider');
56
+ }
57
+ return ctx;
58
+ }
59
+
60
+ export function useSidebarDataOptional(): SidebarDataContextValue | null {
61
+ return useContext(SidebarDataContext);
62
+ }
@@ -0,0 +1,65 @@
1
+ const hasField = (
2
+ value: object,
3
+ key: string
4
+ ): value is Record<string, unknown> => key in value;
5
+
6
+ /**
7
+ * Returns true when the fetched `detail` belongs to the current selection.
8
+ * The fetch lags selection, so it can briefly hold a previously selected span
9
+ * (even a different resource type); reject it before merging or its fields
10
+ * union into the wrong panel. Steps/hooks/waits carry their parent `runId`, so
11
+ * a `run` selection excludes objects identifiable as a child resource.
12
+ */
13
+ export function spanDetailMatchesSelection(
14
+ detail: unknown,
15
+ resource: string | undefined,
16
+ resourceId: string | undefined
17
+ ): boolean {
18
+ if (!detail || typeof detail !== 'object' || !resource || !resourceId) {
19
+ return false;
20
+ }
21
+ switch (resource) {
22
+ case 'step':
23
+ return hasField(detail, 'stepId') && detail.stepId === resourceId;
24
+ case 'hook':
25
+ return hasField(detail, 'hookId') && detail.hookId === resourceId;
26
+ case 'sleep':
27
+ return hasField(detail, 'waitId') && detail.waitId === resourceId;
28
+ case 'run':
29
+ return (
30
+ hasField(detail, 'runId') &&
31
+ !('stepId' in detail) &&
32
+ !('hookId' in detail) &&
33
+ !('waitId' in detail) &&
34
+ detail.runId === resourceId
35
+ );
36
+ default:
37
+ return false;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Merges fetched `detail` over the span's own data. The detail supplies the
43
+ * heavy fields the trace strips (input/output/error/metadata, sleep resumeAt);
44
+ * the span's identity, status, and event-derived timestamps stay authoritative
45
+ * so they don't flicker to the backend row's millisecond-different values.
46
+ */
47
+ export function mergeSpanDetail(spanData: unknown, detail: unknown): unknown {
48
+ if (!detail || typeof detail !== 'object') {
49
+ return spanData;
50
+ }
51
+ if (!spanData || typeof spanData !== 'object') {
52
+ return detail;
53
+ }
54
+ // Skip `undefined` span fields so they don't clobber a value the detail
55
+ // legitimately provides (e.g. a step's optional startedAt).
56
+ const merged: Record<string, unknown> = { ...detail };
57
+ for (const [key, value] of Object.entries(
58
+ spanData as Record<string, unknown>
59
+ )) {
60
+ if (value !== undefined) {
61
+ merged[key] = value;
62
+ }
63
+ }
64
+ return merged;
65
+ }