@workflow/web-shared 4.1.0-beta.47 → 4.1.0-beta.49

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 (301) hide show
  1. package/README.md +26 -52
  2. package/dist/components/error-boundary.d.ts.map +1 -0
  3. package/dist/{error-boundary.js → components/error-boundary.js} +1 -1
  4. package/dist/components/error-boundary.js.map +1 -0
  5. package/dist/{event-list-view.d.ts → components/event-list-view.d.ts} +2 -3
  6. package/dist/components/event-list-view.d.ts.map +1 -0
  7. package/dist/{event-list-view.js → components/event-list-view.js} +9 -17
  8. package/dist/components/event-list-view.js.map +1 -0
  9. package/dist/{hook-actions.d.ts → components/hook-actions.d.ts} +2 -3
  10. package/dist/components/hook-actions.d.ts.map +1 -0
  11. package/dist/{hook-actions.js → components/hook-actions.js} +3 -4
  12. package/dist/components/hook-actions.js.map +1 -0
  13. package/dist/components/index.d.ts +10 -0
  14. package/dist/components/index.d.ts.map +1 -0
  15. package/dist/components/index.js +8 -0
  16. package/dist/components/index.js.map +1 -0
  17. package/dist/components/run-trace-view.d.ts +22 -0
  18. package/dist/components/run-trace-view.d.ts.map +1 -0
  19. package/dist/components/run-trace-view.js +11 -0
  20. package/dist/components/run-trace-view.js.map +1 -0
  21. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -0
  22. package/dist/{sidebar → components/sidebar}/attribute-panel.js +3 -3
  23. package/dist/components/sidebar/attribute-panel.js.map +1 -0
  24. package/dist/components/sidebar/conversation-view.d.ts.map +1 -0
  25. package/dist/components/sidebar/conversation-view.js.map +1 -0
  26. package/dist/components/sidebar/detail-card.d.ts.map +1 -0
  27. package/dist/components/sidebar/detail-card.js.map +1 -0
  28. package/dist/components/sidebar/entity-detail-panel.d.ts +32 -0
  29. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -0
  30. package/dist/{sidebar → components/sidebar}/entity-detail-panel.js +61 -49
  31. package/dist/components/sidebar/entity-detail-panel.js.map +1 -0
  32. package/dist/components/sidebar/events-list.d.ts +8 -0
  33. package/dist/components/sidebar/events-list.d.ts.map +1 -0
  34. package/dist/components/sidebar/events-list.js +16 -0
  35. package/dist/components/sidebar/events-list.js.map +1 -0
  36. package/dist/components/sidebar/resolve-hook-modal.d.ts.map +1 -0
  37. package/dist/components/sidebar/resolve-hook-modal.js.map +1 -0
  38. package/dist/components/stream-viewer.d.ts +18 -0
  39. package/dist/components/stream-viewer.d.ts.map +1 -0
  40. package/dist/{stream-viewer.js → components/stream-viewer.js} +1 -59
  41. package/dist/components/stream-viewer.js.map +1 -0
  42. package/dist/components/trace-viewer/components/map.d.ts.map +1 -0
  43. package/dist/components/trace-viewer/components/map.js.map +1 -0
  44. package/dist/components/trace-viewer/components/markers.d.ts.map +1 -0
  45. package/dist/components/trace-viewer/components/markers.js.map +1 -0
  46. package/dist/components/trace-viewer/components/node.d.ts.map +1 -0
  47. package/dist/components/trace-viewer/components/node.js.map +1 -0
  48. package/dist/components/trace-viewer/components/search-input.d.ts.map +1 -0
  49. package/dist/components/trace-viewer/components/search-input.js.map +1 -0
  50. package/dist/components/trace-viewer/components/search.d.ts.map +1 -0
  51. package/dist/components/trace-viewer/components/search.js.map +1 -0
  52. package/dist/components/trace-viewer/components/span-detail-panel.d.ts.map +1 -0
  53. package/dist/components/trace-viewer/components/span-detail-panel.js.map +1 -0
  54. package/dist/components/trace-viewer/components/ui.d.ts.map +1 -0
  55. package/dist/components/trace-viewer/components/ui.js.map +1 -0
  56. package/dist/components/trace-viewer/components/zoom-button.d.ts.map +1 -0
  57. package/dist/components/trace-viewer/components/zoom-button.js.map +1 -0
  58. package/dist/components/trace-viewer/components/zoom-icons.d.ts.map +1 -0
  59. package/dist/components/trace-viewer/components/zoom-icons.js.map +1 -0
  60. package/dist/components/trace-viewer/context.d.ts.map +1 -0
  61. package/dist/components/trace-viewer/context.js.map +1 -0
  62. package/dist/components/trace-viewer/index.d.ts.map +1 -0
  63. package/dist/components/trace-viewer/index.js.map +1 -0
  64. package/dist/components/trace-viewer/trace-viewer.d.ts.map +1 -0
  65. package/dist/components/trace-viewer/trace-viewer.js.map +1 -0
  66. package/dist/components/trace-viewer/types.d.ts.map +1 -0
  67. package/dist/components/trace-viewer/types.js.map +1 -0
  68. package/dist/components/trace-viewer/util/constants.d.ts.map +1 -0
  69. package/dist/components/trace-viewer/util/constants.js.map +1 -0
  70. package/dist/components/trace-viewer/util/scrollbar-width.d.ts.map +1 -0
  71. package/dist/components/trace-viewer/util/scrollbar-width.js.map +1 -0
  72. package/dist/{trace-viewer → components/trace-viewer}/util/timing.d.ts +1 -1
  73. package/dist/components/trace-viewer/util/timing.d.ts.map +1 -0
  74. package/dist/{trace-viewer → components/trace-viewer}/util/timing.js +1 -1
  75. package/dist/components/trace-viewer/util/timing.js.map +1 -0
  76. package/dist/components/trace-viewer/util/tree.d.ts.map +1 -0
  77. package/dist/components/trace-viewer/util/tree.js.map +1 -0
  78. package/dist/components/trace-viewer/util/use-immediate-style.d.ts.map +1 -0
  79. package/dist/components/trace-viewer/util/use-immediate-style.js.map +1 -0
  80. package/dist/components/trace-viewer/util/use-streaming-spans.d.ts.map +1 -0
  81. package/dist/components/trace-viewer/util/use-streaming-spans.js.map +1 -0
  82. package/dist/components/trace-viewer/util/use-trackpad-zoom.d.ts.map +1 -0
  83. package/dist/components/trace-viewer/util/use-trackpad-zoom.js.map +1 -0
  84. package/dist/components/trace-viewer/worker.d.ts.map +1 -0
  85. package/dist/components/trace-viewer/worker.js.map +1 -0
  86. package/dist/components/workflow-trace-view.d.ts +24 -0
  87. package/dist/components/workflow-trace-view.d.ts.map +1 -0
  88. package/dist/components/workflow-trace-view.js +152 -0
  89. package/dist/components/workflow-trace-view.js.map +1 -0
  90. package/dist/components/workflow-traces/event-colors.d.ts.map +1 -0
  91. package/dist/components/workflow-traces/event-colors.js.map +1 -0
  92. package/dist/components/workflow-traces/trace-colors.d.ts.map +1 -0
  93. package/dist/components/workflow-traces/trace-colors.js.map +1 -0
  94. package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -0
  95. package/dist/components/workflow-traces/trace-span-construction.js.map +1 -0
  96. package/dist/components/workflow-traces/trace-time-utils.d.ts.map +1 -0
  97. package/dist/components/workflow-traces/trace-time-utils.js.map +1 -0
  98. package/dist/index.d.ts +3 -13
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +2 -9
  101. package/dist/index.js.map +1 -1
  102. package/package.json +15 -11
  103. package/src/components/error-boundary.tsx +79 -0
  104. package/src/components/event-list-view.tsx +429 -0
  105. package/src/components/hook-actions.tsx +167 -0
  106. package/src/components/index.d.ts +1 -0
  107. package/src/components/index.ts +23 -0
  108. package/src/components/run-trace-view.tsx +75 -0
  109. package/src/components/sidebar/attribute-panel.tsx +938 -0
  110. package/src/components/sidebar/conversation-view.tsx +235 -0
  111. package/src/components/sidebar/detail-card.tsx +43 -0
  112. package/src/components/sidebar/entity-detail-panel.tsx +338 -0
  113. package/src/components/sidebar/events-list.tsx +119 -0
  114. package/src/components/sidebar/resolve-hook-modal.tsx +219 -0
  115. package/src/components/stream-viewer.tsx +143 -0
  116. package/src/components/trace-viewer/components/map.tsx +226 -0
  117. package/src/components/trace-viewer/components/markers.tsx +564 -0
  118. package/src/components/trace-viewer/components/node.tsx +259 -0
  119. package/src/components/trace-viewer/components/search-input.tsx +52 -0
  120. package/src/components/trace-viewer/components/search.tsx +47 -0
  121. package/src/components/trace-viewer/components/span-detail-panel.tsx +650 -0
  122. package/src/components/trace-viewer/components/ui.tsx +156 -0
  123. package/src/components/trace-viewer/components/zoom-button.tsx +61 -0
  124. package/src/components/trace-viewer/components/zoom-icons.tsx +65 -0
  125. package/src/components/trace-viewer/context.tsx +633 -0
  126. package/src/components/trace-viewer/index.tsx +4 -0
  127. package/src/components/trace-viewer/modules.d.ts +16 -0
  128. package/src/components/trace-viewer/trace-viewer.module.css +1292 -0
  129. package/src/components/trace-viewer/trace-viewer.tsx +448 -0
  130. package/src/components/trace-viewer/types.ts +234 -0
  131. package/src/components/trace-viewer/util/constants.ts +8 -0
  132. package/src/components/trace-viewer/util/scrollbar-width.ts +13 -0
  133. package/src/components/trace-viewer/util/timing.ts +33 -0
  134. package/src/components/trace-viewer/util/tree.ts +277 -0
  135. package/src/components/trace-viewer/util/use-immediate-style.ts +38 -0
  136. package/src/components/trace-viewer/util/use-streaming-spans.ts +415 -0
  137. package/src/components/trace-viewer/util/use-trackpad-zoom.tsx +51 -0
  138. package/src/components/trace-viewer/worker.ts +128 -0
  139. package/src/components/ui/alert.tsx +59 -0
  140. package/src/components/ui/card.tsx +88 -0
  141. package/src/components/ui/error-card.tsx +65 -0
  142. package/src/components/ui/skeleton.tsx +15 -0
  143. package/src/components/workflow-trace-view.tsx +306 -0
  144. package/src/components/workflow-traces/event-colors.ts +94 -0
  145. package/src/components/workflow-traces/trace-colors.ts +112 -0
  146. package/src/components/workflow-traces/trace-span-construction.ts +299 -0
  147. package/src/components/workflow-traces/trace-time-utils.ts +50 -0
  148. package/src/hooks/use-dark-mode.ts +34 -0
  149. package/src/index.d.ts +1 -0
  150. package/src/index.ts +29 -0
  151. package/src/lib/event-analysis.ts +231 -0
  152. package/src/lib/utils.ts +166 -0
  153. package/dist/api/workflow-api-client.d.ts +0 -543
  154. package/dist/api/workflow-api-client.d.ts.map +0 -1
  155. package/dist/api/workflow-api-client.js +0 -953
  156. package/dist/api/workflow-api-client.js.map +0 -1
  157. package/dist/api/workflow-server-actions.d.ts +0 -230
  158. package/dist/api/workflow-server-actions.d.ts.map +0 -1
  159. package/dist/api/workflow-server-actions.js +0 -861
  160. package/dist/api/workflow-server-actions.js.map +0 -1
  161. package/dist/error-boundary.d.ts.map +0 -1
  162. package/dist/error-boundary.js.map +0 -1
  163. package/dist/event-list-view.d.ts.map +0 -1
  164. package/dist/event-list-view.js.map +0 -1
  165. package/dist/hook-actions.d.ts.map +0 -1
  166. package/dist/hook-actions.js.map +0 -1
  167. package/dist/run-trace-view.d.ts +0 -8
  168. package/dist/run-trace-view.d.ts.map +0 -1
  169. package/dist/run-trace-view.js +0 -15
  170. package/dist/run-trace-view.js.map +0 -1
  171. package/dist/sidebar/attribute-panel.d.ts.map +0 -1
  172. package/dist/sidebar/attribute-panel.js.map +0 -1
  173. package/dist/sidebar/conversation-view.d.ts.map +0 -1
  174. package/dist/sidebar/conversation-view.js.map +0 -1
  175. package/dist/sidebar/detail-card.d.ts.map +0 -1
  176. package/dist/sidebar/detail-card.js.map +0 -1
  177. package/dist/sidebar/entity-detail-panel.d.ts +0 -12
  178. package/dist/sidebar/entity-detail-panel.d.ts.map +0 -1
  179. package/dist/sidebar/entity-detail-panel.js.map +0 -1
  180. package/dist/sidebar/events-list.d.ts +0 -9
  181. package/dist/sidebar/events-list.d.ts.map +0 -1
  182. package/dist/sidebar/events-list.js +0 -36
  183. package/dist/sidebar/events-list.js.map +0 -1
  184. package/dist/sidebar/resolve-hook-modal.d.ts.map +0 -1
  185. package/dist/sidebar/resolve-hook-modal.js.map +0 -1
  186. package/dist/stream-viewer.d.ts +0 -13
  187. package/dist/stream-viewer.d.ts.map +0 -1
  188. package/dist/stream-viewer.js.map +0 -1
  189. package/dist/trace-viewer/components/map.d.ts.map +0 -1
  190. package/dist/trace-viewer/components/map.js.map +0 -1
  191. package/dist/trace-viewer/components/markers.d.ts.map +0 -1
  192. package/dist/trace-viewer/components/markers.js.map +0 -1
  193. package/dist/trace-viewer/components/node.d.ts.map +0 -1
  194. package/dist/trace-viewer/components/node.js.map +0 -1
  195. package/dist/trace-viewer/components/search-input.d.ts.map +0 -1
  196. package/dist/trace-viewer/components/search-input.js.map +0 -1
  197. package/dist/trace-viewer/components/search.d.ts.map +0 -1
  198. package/dist/trace-viewer/components/search.js.map +0 -1
  199. package/dist/trace-viewer/components/span-detail-panel.d.ts.map +0 -1
  200. package/dist/trace-viewer/components/span-detail-panel.js.map +0 -1
  201. package/dist/trace-viewer/components/ui.d.ts.map +0 -1
  202. package/dist/trace-viewer/components/ui.js.map +0 -1
  203. package/dist/trace-viewer/components/zoom-button.d.ts.map +0 -1
  204. package/dist/trace-viewer/components/zoom-button.js.map +0 -1
  205. package/dist/trace-viewer/components/zoom-icons.d.ts.map +0 -1
  206. package/dist/trace-viewer/components/zoom-icons.js.map +0 -1
  207. package/dist/trace-viewer/context.d.ts.map +0 -1
  208. package/dist/trace-viewer/context.js.map +0 -1
  209. package/dist/trace-viewer/index.d.ts.map +0 -1
  210. package/dist/trace-viewer/index.js.map +0 -1
  211. package/dist/trace-viewer/trace-viewer.d.ts.map +0 -1
  212. package/dist/trace-viewer/trace-viewer.js.map +0 -1
  213. package/dist/trace-viewer/types.d.ts.map +0 -1
  214. package/dist/trace-viewer/types.js.map +0 -1
  215. package/dist/trace-viewer/util/constants.d.ts.map +0 -1
  216. package/dist/trace-viewer/util/constants.js.map +0 -1
  217. package/dist/trace-viewer/util/scrollbar-width.d.ts.map +0 -1
  218. package/dist/trace-viewer/util/scrollbar-width.js.map +0 -1
  219. package/dist/trace-viewer/util/timing.d.ts.map +0 -1
  220. package/dist/trace-viewer/util/timing.js.map +0 -1
  221. package/dist/trace-viewer/util/tree.d.ts.map +0 -1
  222. package/dist/trace-viewer/util/tree.js.map +0 -1
  223. package/dist/trace-viewer/util/use-immediate-style.d.ts.map +0 -1
  224. package/dist/trace-viewer/util/use-immediate-style.js.map +0 -1
  225. package/dist/trace-viewer/util/use-streaming-spans.d.ts.map +0 -1
  226. package/dist/trace-viewer/util/use-streaming-spans.js.map +0 -1
  227. package/dist/trace-viewer/util/use-trackpad-zoom.d.ts.map +0 -1
  228. package/dist/trace-viewer/util/use-trackpad-zoom.js.map +0 -1
  229. package/dist/trace-viewer/worker.d.ts.map +0 -1
  230. package/dist/trace-viewer/worker.js.map +0 -1
  231. package/dist/workflow-trace-view.d.ts +0 -14
  232. package/dist/workflow-trace-view.d.ts.map +0 -1
  233. package/dist/workflow-trace-view.js +0 -135
  234. package/dist/workflow-trace-view.js.map +0 -1
  235. package/dist/workflow-traces/event-colors.d.ts.map +0 -1
  236. package/dist/workflow-traces/event-colors.js.map +0 -1
  237. package/dist/workflow-traces/trace-colors.d.ts.map +0 -1
  238. package/dist/workflow-traces/trace-colors.js.map +0 -1
  239. package/dist/workflow-traces/trace-span-construction.d.ts.map +0 -1
  240. package/dist/workflow-traces/trace-span-construction.js.map +0 -1
  241. package/dist/workflow-traces/trace-time-utils.d.ts.map +0 -1
  242. package/dist/workflow-traces/trace-time-utils.js.map +0 -1
  243. package/server/README.md +0 -1
  244. package/server/package.json +0 -4
  245. /package/dist/{error-boundary.d.ts → components/error-boundary.d.ts} +0 -0
  246. /package/dist/{sidebar → components/sidebar}/attribute-panel.d.ts +0 -0
  247. /package/dist/{sidebar → components/sidebar}/conversation-view.d.ts +0 -0
  248. /package/dist/{sidebar → components/sidebar}/conversation-view.js +0 -0
  249. /package/dist/{sidebar → components/sidebar}/detail-card.d.ts +0 -0
  250. /package/dist/{sidebar → components/sidebar}/detail-card.js +0 -0
  251. /package/dist/{sidebar → components/sidebar}/resolve-hook-modal.d.ts +0 -0
  252. /package/dist/{sidebar → components/sidebar}/resolve-hook-modal.js +0 -0
  253. /package/dist/{trace-viewer → components/trace-viewer}/components/map.d.ts +0 -0
  254. /package/dist/{trace-viewer → components/trace-viewer}/components/map.js +0 -0
  255. /package/dist/{trace-viewer → components/trace-viewer}/components/markers.d.ts +0 -0
  256. /package/dist/{trace-viewer → components/trace-viewer}/components/markers.js +0 -0
  257. /package/dist/{trace-viewer → components/trace-viewer}/components/node.d.ts +0 -0
  258. /package/dist/{trace-viewer → components/trace-viewer}/components/node.js +0 -0
  259. /package/dist/{trace-viewer → components/trace-viewer}/components/search-input.d.ts +0 -0
  260. /package/dist/{trace-viewer → components/trace-viewer}/components/search-input.js +0 -0
  261. /package/dist/{trace-viewer → components/trace-viewer}/components/search.d.ts +0 -0
  262. /package/dist/{trace-viewer → components/trace-viewer}/components/search.js +0 -0
  263. /package/dist/{trace-viewer → components/trace-viewer}/components/span-detail-panel.d.ts +0 -0
  264. /package/dist/{trace-viewer → components/trace-viewer}/components/span-detail-panel.js +0 -0
  265. /package/dist/{trace-viewer → components/trace-viewer}/components/ui.d.ts +0 -0
  266. /package/dist/{trace-viewer → components/trace-viewer}/components/ui.js +0 -0
  267. /package/dist/{trace-viewer → components/trace-viewer}/components/zoom-button.d.ts +0 -0
  268. /package/dist/{trace-viewer → components/trace-viewer}/components/zoom-button.js +0 -0
  269. /package/dist/{trace-viewer → components/trace-viewer}/components/zoom-icons.d.ts +0 -0
  270. /package/dist/{trace-viewer → components/trace-viewer}/components/zoom-icons.js +0 -0
  271. /package/dist/{trace-viewer → components/trace-viewer}/context.d.ts +0 -0
  272. /package/dist/{trace-viewer → components/trace-viewer}/context.js +0 -0
  273. /package/dist/{trace-viewer → components/trace-viewer}/index.d.ts +0 -0
  274. /package/dist/{trace-viewer → components/trace-viewer}/index.js +0 -0
  275. /package/dist/{trace-viewer → components/trace-viewer}/trace-viewer.d.ts +0 -0
  276. /package/dist/{trace-viewer → components/trace-viewer}/trace-viewer.js +0 -0
  277. /package/dist/{trace-viewer → components/trace-viewer}/trace-viewer.module.css +0 -0
  278. /package/dist/{trace-viewer → components/trace-viewer}/types.d.ts +0 -0
  279. /package/dist/{trace-viewer → components/trace-viewer}/types.js +0 -0
  280. /package/dist/{trace-viewer → components/trace-viewer}/util/constants.d.ts +0 -0
  281. /package/dist/{trace-viewer → components/trace-viewer}/util/constants.js +0 -0
  282. /package/dist/{trace-viewer → components/trace-viewer}/util/scrollbar-width.d.ts +0 -0
  283. /package/dist/{trace-viewer → components/trace-viewer}/util/scrollbar-width.js +0 -0
  284. /package/dist/{trace-viewer → components/trace-viewer}/util/tree.d.ts +0 -0
  285. /package/dist/{trace-viewer → components/trace-viewer}/util/tree.js +0 -0
  286. /package/dist/{trace-viewer → components/trace-viewer}/util/use-immediate-style.d.ts +0 -0
  287. /package/dist/{trace-viewer → components/trace-viewer}/util/use-immediate-style.js +0 -0
  288. /package/dist/{trace-viewer → components/trace-viewer}/util/use-streaming-spans.d.ts +0 -0
  289. /package/dist/{trace-viewer → components/trace-viewer}/util/use-streaming-spans.js +0 -0
  290. /package/dist/{trace-viewer → components/trace-viewer}/util/use-trackpad-zoom.d.ts +0 -0
  291. /package/dist/{trace-viewer → components/trace-viewer}/util/use-trackpad-zoom.js +0 -0
  292. /package/dist/{trace-viewer → components/trace-viewer}/worker.d.ts +0 -0
  293. /package/dist/{trace-viewer → components/trace-viewer}/worker.js +0 -0
  294. /package/dist/{workflow-traces → components/workflow-traces}/event-colors.d.ts +0 -0
  295. /package/dist/{workflow-traces → components/workflow-traces}/event-colors.js +0 -0
  296. /package/dist/{workflow-traces → components/workflow-traces}/trace-colors.d.ts +0 -0
  297. /package/dist/{workflow-traces → components/workflow-traces}/trace-colors.js +0 -0
  298. /package/dist/{workflow-traces → components/workflow-traces}/trace-span-construction.d.ts +0 -0
  299. /package/dist/{workflow-traces → components/workflow-traces}/trace-span-construction.js +0 -0
  300. /package/dist/{workflow-traces → components/workflow-traces}/trace-time-utils.d.ts +0 -0
  301. /package/dist/{workflow-traces → components/workflow-traces}/trace-time-utils.js +0 -0
@@ -0,0 +1,938 @@
1
+ 'use client';
2
+
3
+ import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name';
4
+ import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
5
+ import type { ModelMessage } from 'ai';
6
+ import type { ReactNode } from 'react';
7
+ import { createContext, useContext, useMemo, useState } from 'react';
8
+ import { ErrorCard } from '../ui/error-card';
9
+ import { useDarkMode } from '../../hooks/use-dark-mode';
10
+ import { extractConversation, isDoStreamStep } from '../../lib/utils';
11
+ import { ConversationView } from './conversation-view';
12
+ import { DetailCard } from './detail-card';
13
+
14
+ /**
15
+ * Tab button for conversation/JSON toggle
16
+ */
17
+ function TabButton({
18
+ active,
19
+ onClick,
20
+ children,
21
+ }: {
22
+ active: boolean;
23
+ onClick: () => void;
24
+ children: ReactNode;
25
+ }) {
26
+ return (
27
+ <button
28
+ type="button"
29
+ onClick={onClick}
30
+ className="px-3 py-1.5 text-[11px] font-medium transition-colors -mb-px"
31
+ style={{
32
+ // Explicit styles to prevent app-level button overrides when web-shared
33
+ // is embedded in a self-hosted app.
34
+ backgroundColor: 'transparent',
35
+ borderTop: 'none',
36
+ borderLeft: 'none',
37
+ borderRight: 'none',
38
+ borderBottom: `2px solid ${active ? 'var(--ds-blue-600)' : 'transparent'}`,
39
+ borderRadius: 0,
40
+ outline: 'none',
41
+ boxShadow: 'none',
42
+ cursor: 'pointer',
43
+ color: active ? 'var(--ds-gray-1000)' : 'var(--ds-gray-600)',
44
+ }}
45
+ >
46
+ {children}
47
+ </button>
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Tabbed view for conversation and raw JSON
53
+ */
54
+ function ConversationWithTabs({
55
+ conversation,
56
+ args,
57
+ }: {
58
+ conversation: ModelMessage[];
59
+ args: unknown[];
60
+ }) {
61
+ const [activeTab, setActiveTab] = useState<'conversation' | 'json'>(
62
+ 'conversation'
63
+ );
64
+
65
+ return (
66
+ <DetailCard summary={`Input (${conversation.length} messages)`}>
67
+ <div
68
+ className="rounded-md border"
69
+ style={{
70
+ borderColor: 'var(--ds-gray-300)',
71
+ backgroundColor: 'transparent',
72
+ }}
73
+ >
74
+ <div
75
+ className="flex gap-1 border-b"
76
+ style={{
77
+ borderColor: 'var(--ds-gray-300)',
78
+ backgroundColor: 'transparent',
79
+ }}
80
+ >
81
+ <TabButton
82
+ active={activeTab === 'conversation'}
83
+ onClick={() => setActiveTab('conversation')}
84
+ >
85
+ Conversation
86
+ </TabButton>
87
+ <TabButton
88
+ active={activeTab === 'json'}
89
+ onClick={() => setActiveTab('json')}
90
+ >
91
+ Raw JSON
92
+ </TabButton>
93
+ </div>
94
+
95
+ {activeTab === 'conversation' ? (
96
+ <ConversationView messages={conversation} />
97
+ ) : (
98
+ <div className="p-3">
99
+ {Array.isArray(args)
100
+ ? args.map((v, i) => (
101
+ <div className="mt-2 first:mt-0" key={i}>
102
+ {JsonBlock(v)}
103
+ </div>
104
+ ))
105
+ : JsonBlock(args)}
106
+ </div>
107
+ )}
108
+ </div>
109
+ </DetailCard>
110
+ );
111
+ }
112
+
113
+ /**
114
+ * Context for stream click handler
115
+ */
116
+ const StreamClickContext = createContext<
117
+ ((streamId: string) => void) | undefined
118
+ >(undefined);
119
+
120
+ /**
121
+ * Marker for stream reference objects that can be rendered as links
122
+ * This is duplicated from @workflow/core/observability to avoid pulling in
123
+ * Node.js dependencies into the client bundle.
124
+ */
125
+ const STREAM_REF_TYPE = '__workflow_stream_ref__';
126
+
127
+ /**
128
+ * A stream reference object that contains the stream ID and can be
129
+ * detected in the UI to render as a clickable link
130
+ */
131
+ interface StreamRef {
132
+ __type: typeof STREAM_REF_TYPE;
133
+ streamId: string;
134
+ }
135
+
136
+ /**
137
+ * Check if a value is a StreamRef object
138
+ *
139
+ */
140
+ const isStreamRef = (value: unknown): value is StreamRef => {
141
+ // TODO: This is duplicated from @workflow/core/observability, but can't be pulled
142
+ // in client-side code because it's a Node.js dependency.
143
+ return (
144
+ value !== null &&
145
+ typeof value === 'object' &&
146
+ '__type' in value &&
147
+ value.__type === STREAM_REF_TYPE &&
148
+ 'streamId' in value &&
149
+ typeof value.streamId === 'string'
150
+ );
151
+ };
152
+
153
+ /**
154
+ * Marker for custom class instance references.
155
+ * This is duplicated from @workflow/core/observability to avoid pulling in
156
+ * Node.js dependencies into the client bundle.
157
+ */
158
+ const CLASS_INSTANCE_REF_TYPE = '__workflow_class_instance_ref__';
159
+
160
+ /**
161
+ * A class instance reference object that contains the class name and serialized data.
162
+ * Used in o11y when a custom class instance is encountered but the class is not
163
+ * registered for deserialization.
164
+ */
165
+ interface ClassInstanceRef {
166
+ __type: typeof CLASS_INSTANCE_REF_TYPE;
167
+ className: string;
168
+ classId: string;
169
+ data: unknown;
170
+ }
171
+
172
+ /**
173
+ * Check if a value is a ClassInstanceRef object
174
+ */
175
+ const isClassInstanceRef = (value: unknown): value is ClassInstanceRef => {
176
+ return (
177
+ value !== null &&
178
+ typeof value === 'object' &&
179
+ '__type' in value &&
180
+ value.__type === CLASS_INSTANCE_REF_TYPE &&
181
+ 'className' in value &&
182
+ typeof value.className === 'string'
183
+ );
184
+ };
185
+
186
+ import ColorHash from 'color-hash';
187
+
188
+ /**
189
+ * Color hash instance configured for nice saturation and lightness.
190
+ * Returns HSL values which we can transform for different use cases.
191
+ */
192
+ const colorHash = new ColorHash({
193
+ saturation: [0.5, 0.6, 0.7],
194
+ lightness: [0.4, 0.5, 0.6],
195
+ });
196
+
197
+ /**
198
+ * Convert HSL to CSS hsl() string
199
+ */
200
+ const hslToString = (h: number, s: number, l: number): string => {
201
+ return `hsl(${h}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`;
202
+ };
203
+
204
+ /**
205
+ * Get consistent colors for a class ID using color-hash and HSL transformations.
206
+ * Adjusts colors based on light/dark mode for optimal appearance.
207
+ */
208
+ const getClassColors = (
209
+ classId: string,
210
+ isDark: boolean
211
+ ): { header: string; body: string; text: string } => {
212
+ const [h, s, l] = colorHash.hsl(classId);
213
+
214
+ if (isDark) {
215
+ // Dark mode: vibrant header, dark body, light text
216
+ return {
217
+ header: hslToString(h, s, Math.min(l + 0.1, 0.6)), // Slightly brighter header
218
+ body: hslToString(h, s * 0.8, 0.15), // Very dark, slightly desaturated body
219
+ text: hslToString(h, s * 0.6, 0.8), // Light, slightly desaturated text
220
+ };
221
+ } else {
222
+ // Light mode: vibrant header, light body, dark text
223
+ return {
224
+ header: hslToString(h, s, l), // Use base color for header
225
+ body: hslToString(h, s * 0.4, 0.95), // Very light, desaturated body
226
+ text: hslToString(h, s * 0.8, 0.25), // Dark, saturated text
227
+ };
228
+ }
229
+ };
230
+
231
+ /**
232
+ * Renders a ClassInstanceRef as a styled card showing the class name and serialized data.
233
+ * The header color is determined by hashing the classId for visual distinction.
234
+ * Reacts to theme changes for proper dark/light mode support.
235
+ */
236
+ const ClassInstanceRefDisplay = ({
237
+ classInstanceRef,
238
+ }: {
239
+ classInstanceRef: ClassInstanceRef;
240
+ }) => {
241
+ const isDark = useDarkMode();
242
+ const colors = getClassColors(classInstanceRef.classId, isDark);
243
+
244
+ return (
245
+ <div
246
+ className="inline-flex flex-col rounded text-[11px] font-mono my-1"
247
+ style={{
248
+ backgroundColor: colors.body,
249
+ border: `1px solid ${colors.header}`,
250
+ }}
251
+ >
252
+ <div
253
+ className="flex items-center gap-1.5 px-2 py-1 rounded-t"
254
+ style={{
255
+ backgroundColor: colors.header,
256
+ color: '#FFFFFF',
257
+ }}
258
+ title={`Custom class: ${classInstanceRef.classId}`}
259
+ >
260
+ <svg
261
+ xmlns="http://www.w3.org/2000/svg"
262
+ width="12"
263
+ height="12"
264
+ viewBox="0 0 24 24"
265
+ fill="none"
266
+ stroke="currentColor"
267
+ strokeWidth="2"
268
+ strokeLinecap="round"
269
+ strokeLinejoin="round"
270
+ >
271
+ <title>Class instance</title>
272
+ <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
273
+ <polyline points="3.27 6.96 12 12.01 20.73 6.96" />
274
+ <line x1="12" y1="22.08" x2="12" y2="12" />
275
+ </svg>
276
+ <span className="font-semibold">{classInstanceRef.className}</span>
277
+ </div>
278
+ <pre
279
+ className="px-2 py-1.5 overflow-x-auto whitespace-pre-wrap"
280
+ style={{ color: colors.text }}
281
+ >
282
+ {JSON.stringify(classInstanceRef.data, null, 2)}
283
+ </pre>
284
+ </div>
285
+ );
286
+ };
287
+
288
+ /**
289
+ * Renders a StreamRef as a styled link/badge
290
+ */
291
+ const StreamRefDisplay = ({ streamRef }: { streamRef: StreamRef }) => {
292
+ const onStreamClick = useContext(StreamClickContext);
293
+
294
+ const handleClick = () => {
295
+ if (onStreamClick) {
296
+ onStreamClick(streamRef.streamId);
297
+ }
298
+ };
299
+
300
+ return (
301
+ <button
302
+ type="button"
303
+ onClick={handleClick}
304
+ className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-mono cursor-pointer hover:opacity-80 transition-opacity"
305
+ style={{
306
+ backgroundColor: 'var(--ds-blue-200)',
307
+ color: 'var(--ds-blue-900)',
308
+ border: '1px solid var(--ds-blue-400)',
309
+ }}
310
+ title={`Click to view stream: ${streamRef.streamId}`}
311
+ >
312
+ <svg
313
+ xmlns="http://www.w3.org/2000/svg"
314
+ width="10"
315
+ height="10"
316
+ viewBox="0 0 24 24"
317
+ fill="none"
318
+ stroke="currentColor"
319
+ strokeWidth="2"
320
+ strokeLinecap="round"
321
+ strokeLinejoin="round"
322
+ >
323
+ <title>Stream icon</title>
324
+ <path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" />
325
+ <circle cx="12" cy="12" r="3" />
326
+ </svg>
327
+ {streamRef.streamId.length > 40
328
+ ? `${streamRef.streamId.slice(0, 20)}...${streamRef.streamId.slice(
329
+ -15
330
+ )}`
331
+ : streamRef.streamId}
332
+ </button>
333
+ );
334
+ };
335
+
336
+ /**
337
+ * Recursively transforms a value for JSON display, replacing StreamRef and
338
+ * ClassInstanceRef objects with placeholder strings that can be identified
339
+ * and replaced with React elements.
340
+ */
341
+ const transformValueForDisplay = (
342
+ value: unknown
343
+ ): {
344
+ json: string;
345
+ streamRefs: Map<string, StreamRef>;
346
+ classInstanceRefs: Map<string, ClassInstanceRef>;
347
+ } => {
348
+ const streamRefs = new Map<string, StreamRef>();
349
+ const classInstanceRefs = new Map<string, ClassInstanceRef>();
350
+ let counter = 0;
351
+
352
+ const transform = (v: unknown): unknown => {
353
+ if (isStreamRef(v)) {
354
+ const placeholder = `__STREAM_REF_${counter++}__`;
355
+ streamRefs.set(placeholder, v);
356
+ return placeholder;
357
+ }
358
+ if (isClassInstanceRef(v)) {
359
+ const placeholder = `__CLASS_INSTANCE_REF_${counter++}__`;
360
+ classInstanceRefs.set(placeholder, v);
361
+ return placeholder;
362
+ }
363
+ if (Array.isArray(v)) {
364
+ return v.map(transform);
365
+ }
366
+ if (v !== null && typeof v === 'object') {
367
+ const result: Record<string, unknown> = {};
368
+ for (const [key, val] of Object.entries(v)) {
369
+ result[key] = transform(val);
370
+ }
371
+ return result;
372
+ }
373
+ return v;
374
+ };
375
+
376
+ const transformed = transform(value);
377
+ return {
378
+ json: JSON.stringify(transformed, null, 2),
379
+ streamRefs,
380
+ classInstanceRefs,
381
+ };
382
+ };
383
+
384
+ const JsonBlock = (value: unknown) => {
385
+ const { json, streamRefs, classInstanceRefs } =
386
+ transformValueForDisplay(value);
387
+
388
+ // If no special refs, just render plain JSON
389
+ if (streamRefs.size === 0 && classInstanceRefs.size === 0) {
390
+ return (
391
+ <pre
392
+ className="text-[11px] overflow-x-auto rounded-md border p-3"
393
+ style={{
394
+ borderColor: 'var(--ds-gray-300)',
395
+ backgroundColor: 'var(--ds-gray-100)',
396
+ color: 'var(--ds-gray-1000)',
397
+ }}
398
+ >
399
+ <code>{json}</code>
400
+ </pre>
401
+ );
402
+ }
403
+
404
+ // Build a combined map of all placeholders to their React elements
405
+ const placeholderComponents = new Map<string, ReactNode>();
406
+ let keyIndex = 0;
407
+
408
+ for (const [placeholder, streamRef] of streamRefs) {
409
+ placeholderComponents.set(
410
+ placeholder,
411
+ <StreamRefDisplay key={keyIndex++} streamRef={streamRef} />
412
+ );
413
+ }
414
+
415
+ for (const [placeholder, classInstanceRef] of classInstanceRefs) {
416
+ placeholderComponents.set(
417
+ placeholder,
418
+ <ClassInstanceRefDisplay
419
+ key={keyIndex++}
420
+ classInstanceRef={classInstanceRef}
421
+ />
422
+ );
423
+ }
424
+
425
+ // Split the JSON by all placeholders and render with React elements
426
+ const parts: ReactNode[] = [];
427
+ let remaining = json;
428
+
429
+ // Process placeholders in order of their appearance in the string
430
+ while (remaining.length > 0) {
431
+ let earliestIndex = -1;
432
+ let earliestPlaceholder = '';
433
+ let earliestComponent: ReactNode = null;
434
+
435
+ // Find the earliest placeholder in the remaining string
436
+ for (const [placeholder, component] of placeholderComponents) {
437
+ const index = remaining.indexOf(`"${placeholder}"`);
438
+ if (index !== -1 && (earliestIndex === -1 || index < earliestIndex)) {
439
+ earliestIndex = index;
440
+ earliestPlaceholder = placeholder;
441
+ earliestComponent = component;
442
+ }
443
+ }
444
+
445
+ if (earliestIndex === -1) {
446
+ // No more placeholders found, add the rest
447
+ parts.push(remaining);
448
+ break;
449
+ }
450
+
451
+ // Add text before the placeholder
452
+ if (earliestIndex > 0) {
453
+ parts.push(remaining.slice(0, earliestIndex));
454
+ }
455
+
456
+ // Add the component
457
+ parts.push(earliestComponent);
458
+
459
+ // Move past the placeholder
460
+ remaining = remaining.slice(earliestIndex + earliestPlaceholder.length + 2); // +2 for quotes
461
+ }
462
+
463
+ return (
464
+ <pre
465
+ className="text-[11px] overflow-x-auto rounded-md border p-3"
466
+ style={{
467
+ borderColor: 'var(--ds-gray-300)',
468
+ backgroundColor: 'var(--ds-gray-100)',
469
+ color: 'var(--ds-gray-1000)',
470
+ }}
471
+ >
472
+ <code>{parts}</code>
473
+ </pre>
474
+ );
475
+ };
476
+
477
+ type AttributeKey =
478
+ | keyof Step
479
+ | keyof WorkflowRun
480
+ | keyof Hook
481
+ | keyof Event
482
+ | 'eventData'
483
+ | 'resumeAt'
484
+ | 'expiredAt'
485
+ | 'workflowCoreVersion';
486
+
487
+ const attributeOrder: AttributeKey[] = [
488
+ 'workflowName',
489
+ 'stepName',
490
+ 'status',
491
+ 'stepId',
492
+ 'hookId',
493
+ 'eventId',
494
+ 'runId',
495
+ 'attempt',
496
+ 'token',
497
+ 'correlationId',
498
+ 'eventType',
499
+ 'deploymentId',
500
+ 'specVersion',
501
+ 'workflowCoreVersion',
502
+ 'ownerId',
503
+ 'projectId',
504
+ 'environment',
505
+ 'executionContext',
506
+ 'createdAt',
507
+ 'startedAt',
508
+ 'updatedAt',
509
+ 'completedAt',
510
+ 'expiredAt',
511
+ 'retryAfter',
512
+ 'error',
513
+ 'metadata',
514
+ 'eventData',
515
+ 'input',
516
+ 'output',
517
+ 'resumeAt',
518
+ ];
519
+
520
+ const sortByAttributeOrder = (a: string, b: string): number => {
521
+ const aIndex = attributeOrder.indexOf(a as AttributeKey) || 0;
522
+ const bIndex = attributeOrder.indexOf(b as AttributeKey) || 0;
523
+ return aIndex - bIndex;
524
+ };
525
+
526
+ /**
527
+ * Display names for attributes that should render differently from their key.
528
+ */
529
+ const attributeDisplayNames: Partial<Record<AttributeKey, string>> = {
530
+ workflowCoreVersion: '@workflow/core version',
531
+ };
532
+
533
+ /**
534
+ * Get the display name for an attribute key.
535
+ */
536
+ const getAttributeDisplayName = (attribute: string): string => {
537
+ return attributeDisplayNames[attribute as AttributeKey] ?? attribute;
538
+ };
539
+
540
+ export const localMillisecondTime = (value: unknown): string => {
541
+ let date: Date;
542
+ if (value instanceof Date) {
543
+ date = value;
544
+ } else if (typeof value === 'number') {
545
+ date = new Date(value);
546
+ } else if (typeof value === 'string') {
547
+ date = new Date(value);
548
+ } else {
549
+ date = new Date(String(value));
550
+ }
551
+
552
+ // e.g. 12/17/2025, 9:08:55.182 AM
553
+ return date.toLocaleString(undefined, {
554
+ year: 'numeric',
555
+ month: 'numeric',
556
+ day: 'numeric',
557
+ hour: 'numeric',
558
+ minute: 'numeric',
559
+ second: 'numeric',
560
+ fractionalSecondDigits: 3,
561
+ });
562
+ };
563
+
564
+ interface DisplayContext {
565
+ stepName?: string;
566
+ }
567
+
568
+ const attributeToDisplayFn: Record<
569
+ AttributeKey,
570
+ (value: unknown, context?: DisplayContext) => null | string | ReactNode
571
+ > = {
572
+ // Names that need pretty-printing
573
+ workflowName: (value: unknown) =>
574
+ parseWorkflowName(String(value))?.shortName ?? '?',
575
+ stepName: (value: unknown) => parseStepName(String(value))?.shortName ?? '?',
576
+ // IDs
577
+ runId: (value: unknown) => String(value),
578
+ stepId: (value: unknown) => String(value),
579
+ hookId: (value: unknown) => String(value),
580
+ eventId: (value: unknown) => String(value),
581
+ // Run/step details
582
+ status: (value: unknown) => String(value),
583
+ attempt: (value: unknown) => String(value),
584
+ // Hook details
585
+ token: (value: unknown) => String(value),
586
+ // Event details
587
+ eventType: (value: unknown) => String(value),
588
+ correlationId: (value: unknown) => String(value),
589
+ // Project details
590
+ deploymentId: (value: unknown) => String(value),
591
+ specVersion: (value: unknown) => String(value),
592
+ workflowCoreVersion: (value: unknown) => String(value),
593
+ // Tenancy (we don't show these)
594
+ ownerId: (_value: unknown) => null,
595
+ projectId: (_value: unknown) => null,
596
+ environment: (_value: unknown) => null,
597
+ executionContext: (_value: unknown) => null,
598
+ // Dates
599
+ // TODO: relative time with tooltips for ISO times
600
+ createdAt: localMillisecondTime,
601
+ startedAt: localMillisecondTime,
602
+ updatedAt: localMillisecondTime,
603
+ completedAt: localMillisecondTime,
604
+ expiredAt: localMillisecondTime,
605
+ retryAfter: localMillisecondTime,
606
+ resumeAt: localMillisecondTime,
607
+ // Resolved attributes, won't actually use this function
608
+ metadata: JsonBlock,
609
+ input: (value: unknown, context?: DisplayContext) => {
610
+ // Check if input has args + closure vars structure
611
+ if (value && typeof value === 'object' && 'args' in value) {
612
+ const { args, closureVars } = value as {
613
+ args: unknown[];
614
+ closureVars?: Record<string, unknown>;
615
+ };
616
+ const argCount = Array.isArray(args) ? args.length : 0;
617
+ const hasClosureVars = closureVars && Object.keys(closureVars).length > 0;
618
+
619
+ // Check if this is a doStreamStep - show conversation view with tabs
620
+ if (context?.stepName && isDoStreamStep(context.stepName)) {
621
+ const conversation = extractConversation(args);
622
+ if (conversation && conversation.length > 0) {
623
+ return (
624
+ <>
625
+ <ConversationWithTabs conversation={conversation} args={args} />
626
+ {hasClosureVars && (
627
+ <DetailCard summary="Closure Variables">
628
+ {JsonBlock(closureVars)}
629
+ </DetailCard>
630
+ )}
631
+ </>
632
+ );
633
+ }
634
+ }
635
+
636
+ return (
637
+ <>
638
+ <DetailCard summary={`Input (${argCount} arguments)`}>
639
+ {Array.isArray(args)
640
+ ? args.map((v, i) => (
641
+ <div className="mt-2" key={i}>
642
+ {JsonBlock(v)}
643
+ </div>
644
+ ))
645
+ : JsonBlock(args)}
646
+ </DetailCard>
647
+ {hasClosureVars && (
648
+ <DetailCard summary="Closure Variables">
649
+ {JsonBlock(closureVars)}
650
+ </DetailCard>
651
+ )}
652
+ </>
653
+ );
654
+ }
655
+
656
+ // Fallback: treat as plain array or object
657
+ const argCount = Array.isArray(value) ? value.length : 0;
658
+ return (
659
+ <DetailCard summary={`Input (${argCount} arguments)`}>
660
+ {Array.isArray(value)
661
+ ? value.map((v, i) => (
662
+ <div className="mt-2" key={i}>
663
+ {JsonBlock(v)}
664
+ </div>
665
+ ))
666
+ : JsonBlock(value)}
667
+ </DetailCard>
668
+ );
669
+ },
670
+ output: (value: unknown) => {
671
+ return <DetailCard summary="Output">{JsonBlock(value)}</DetailCard>;
672
+ },
673
+ error: (value: unknown) => {
674
+ // Handle structured error format
675
+ if (value && typeof value === 'object' && 'message' in value) {
676
+ const error = value as {
677
+ message: string;
678
+ stack?: string;
679
+ code?: string;
680
+ };
681
+
682
+ return (
683
+ <DetailCard summary="Error">
684
+ <div className="flex flex-col gap-2">
685
+ {/* Show code if it exists */}
686
+ {error.code && (
687
+ <div>
688
+ <span
689
+ className="text-[11px] font-medium"
690
+ style={{ color: 'var(--ds-gray-700)' }}
691
+ >
692
+ Error Code:{' '}
693
+ </span>
694
+ <code
695
+ className="text-[11px]"
696
+ style={{ color: 'var(--ds-gray-1000)' }}
697
+ >
698
+ {error.code}
699
+ </code>
700
+ </div>
701
+ )}
702
+ {/* Show stack if available, otherwise just the message */}
703
+ <pre
704
+ className="text-[11px] overflow-x-auto rounded-md border p-3"
705
+ style={{
706
+ borderColor: 'var(--ds-gray-300)',
707
+ backgroundColor: 'var(--ds-gray-100)',
708
+ color: 'var(--ds-gray-1000)',
709
+ whiteSpace: 'pre-wrap',
710
+ }}
711
+ >
712
+ <code>{error.stack || error.message}</code>
713
+ </pre>
714
+ </div>
715
+ </DetailCard>
716
+ );
717
+ }
718
+
719
+ // Fallback for plain string errors
720
+ return (
721
+ <DetailCard summary="Error">
722
+ <pre
723
+ className="text-[11px] overflow-x-auto rounded-md border p-3"
724
+ style={{
725
+ borderColor: 'var(--ds-gray-300)',
726
+ backgroundColor: 'var(--ds-gray-100)',
727
+ color: 'var(--ds-gray-1000)',
728
+ whiteSpace: 'pre-wrap',
729
+ }}
730
+ >
731
+ <code>{String(value)}</code>
732
+ </pre>
733
+ </DetailCard>
734
+ );
735
+ },
736
+ eventData: (value: unknown) => {
737
+ return <DetailCard summary="Event Data">{JsonBlock(value)}</DetailCard>;
738
+ },
739
+ };
740
+
741
+ const resolvableAttributes = [
742
+ 'input',
743
+ 'output',
744
+ 'error',
745
+ 'metadata',
746
+ 'eventData',
747
+ ];
748
+
749
+ const ExpiredDataMessage = () => (
750
+ <div
751
+ className="text-copy-12 rounded-md border p-4 my-2"
752
+ style={{
753
+ borderColor: 'var(--ds-gray-300)',
754
+ backgroundColor: 'var(--ds-gray-100)',
755
+ color: 'var(--ds-gray-700)',
756
+ }}
757
+ >
758
+ <span>The data for this run has expired and is no longer available.</span>
759
+ </div>
760
+ );
761
+
762
+ export const AttributeBlock = ({
763
+ attribute,
764
+ value,
765
+ isLoading,
766
+ inline = false,
767
+ context,
768
+ }: {
769
+ attribute: string;
770
+ value: unknown;
771
+ isLoading?: boolean;
772
+ inline?: boolean;
773
+ context?: DisplayContext;
774
+ }) => {
775
+ const displayFn =
776
+ attributeToDisplayFn[attribute as keyof typeof attributeToDisplayFn];
777
+ if (!displayFn) {
778
+ return null;
779
+ }
780
+ const displayValue = displayFn(value, context);
781
+ if (!displayValue) {
782
+ return null;
783
+ }
784
+
785
+ if (inline) {
786
+ return (
787
+ <div className="flex items-center gap-1.5">
788
+ <span
789
+ className="text-[11px] font-medium"
790
+ style={{ color: 'var(--ds-gray-700)' }}
791
+ >
792
+ {attribute}
793
+ </span>
794
+ <span className="text-[11px]" style={{ color: 'var(--ds-gray-1000)' }}>
795
+ {displayValue}
796
+ </span>
797
+ </div>
798
+ );
799
+ }
800
+
801
+ return (
802
+ <div className="relative">
803
+ {typeof isLoading === 'boolean' && isLoading && (
804
+ <div className="absolute top-9 right-4">
805
+ <div
806
+ className="animate-spin rounded-full h-4 w-4 border-b-2"
807
+ style={{ borderColor: 'var(--ds-gray-900)' }}
808
+ />
809
+ </div>
810
+ )}
811
+ <div key={attribute} className="flex flex-col gap-0 my-2">
812
+ <span
813
+ className="text-xs font-medium"
814
+ style={{ color: 'var(--ds-gray-700)' }}
815
+ >
816
+ {attribute}
817
+ </span>
818
+ <span className="text-xs" style={{ color: 'var(--ds-gray-1000)' }}>
819
+ {displayValue}
820
+ </span>
821
+ </div>
822
+ </div>
823
+ );
824
+ };
825
+
826
+ export const AttributePanel = ({
827
+ data,
828
+ isLoading,
829
+ error,
830
+ expiredAt,
831
+ onStreamClick,
832
+ }: {
833
+ data: Record<string, unknown>;
834
+ isLoading?: boolean;
835
+ error?: Error;
836
+ expiredAt?: string | Date;
837
+ /** Callback when a stream reference is clicked */
838
+ onStreamClick?: (streamId: string) => void;
839
+ }) => {
840
+ // Extract workflowCoreVersion from executionContext for display
841
+ const displayData = useMemo(() => {
842
+ const result = { ...data };
843
+ const execCtx = data.executionContext as
844
+ | Record<string, unknown>
845
+ | undefined;
846
+ if (execCtx?.workflowCoreVersion) {
847
+ result.workflowCoreVersion = execCtx.workflowCoreVersion;
848
+ }
849
+ return result;
850
+ }, [data]);
851
+ const hasExpired = expiredAt != null && new Date(expiredAt) < new Date();
852
+ const basicAttributes = Object.keys(displayData)
853
+ .filter((key) => !resolvableAttributes.includes(key))
854
+ .sort(sortByAttributeOrder);
855
+ const resolvedAttributes = Object.keys(displayData)
856
+ .filter((key) => resolvableAttributes.includes(key))
857
+ .sort(sortByAttributeOrder);
858
+
859
+ // Filter out attributes that return null
860
+ const visibleBasicAttributes = basicAttributes.filter((attribute) => {
861
+ const displayFn =
862
+ attributeToDisplayFn[attribute as keyof typeof attributeToDisplayFn];
863
+ if (!displayFn) return false;
864
+ const displayValue = displayFn(
865
+ displayData[attribute as keyof typeof displayData]
866
+ );
867
+ return displayValue !== null;
868
+ });
869
+
870
+ // Memoize context object to avoid object reconstruction on render
871
+ const displayContext = useMemo(
872
+ () => ({
873
+ stepName: displayData.stepName as string | undefined,
874
+ }),
875
+ [displayData.stepName]
876
+ );
877
+
878
+ return (
879
+ <StreamClickContext.Provider value={onStreamClick}>
880
+ <div>
881
+ {/* Basic attributes in a vertical layout with border */}
882
+ {visibleBasicAttributes.length > 0 && (
883
+ <div
884
+ className="flex flex-col divide-y rounded-lg border mb-3 overflow-hidden"
885
+ style={{
886
+ borderColor: 'var(--ds-gray-300)',
887
+ backgroundColor: 'var(--ds-gray-100)',
888
+ }}
889
+ >
890
+ {visibleBasicAttributes.map((attribute) => (
891
+ <div
892
+ key={attribute}
893
+ className="flex items-center justify-between px-3 py-1.5"
894
+ style={{
895
+ borderColor: 'var(--ds-gray-300)',
896
+ }}
897
+ >
898
+ <span
899
+ className="text-[11px] font-medium"
900
+ style={{ color: 'var(--ds-gray-700)' }}
901
+ >
902
+ {getAttributeDisplayName(attribute)}
903
+ </span>
904
+ <span
905
+ className="text-[11px] font-mono"
906
+ style={{ color: 'var(--ds-gray-1000)' }}
907
+ >
908
+ {attributeToDisplayFn[
909
+ attribute as keyof typeof attributeToDisplayFn
910
+ ]?.(displayData[attribute as keyof typeof displayData])}
911
+ </span>
912
+ </div>
913
+ ))}
914
+ </div>
915
+ )}
916
+ {error ? (
917
+ <ErrorCard
918
+ title="Failed to load resource details"
919
+ details={error.message}
920
+ className="my-4"
921
+ />
922
+ ) : hasExpired ? (
923
+ <ExpiredDataMessage />
924
+ ) : (
925
+ resolvedAttributes.map((attribute) => (
926
+ <AttributeBlock
927
+ isLoading={isLoading}
928
+ key={attribute}
929
+ attribute={attribute}
930
+ value={displayData[attribute as keyof typeof displayData]}
931
+ context={displayContext}
932
+ />
933
+ ))
934
+ )}
935
+ </div>
936
+ </StreamClickContext.Provider>
937
+ );
938
+ };