@principal-ai/subsystems-react 0.17.0

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 (496) hide show
  1. package/README.md +93 -0
  2. package/dist/components/ConfigurationSelector.d.ts +35 -0
  3. package/dist/components/ConfigurationSelector.d.ts.map +1 -0
  4. package/dist/components/ConfigurationSelector.js +63 -0
  5. package/dist/components/ConfigurationSelector.js.map +1 -0
  6. package/dist/components/GraphRenderer.d.ts +295 -0
  7. package/dist/components/GraphRenderer.d.ts.map +1 -0
  8. package/dist/components/GraphRenderer.js +2008 -0
  9. package/dist/components/GraphRenderer.js.map +1 -0
  10. package/dist/components/MultiCanvasRenderer.d.ts +75 -0
  11. package/dist/components/MultiCanvasRenderer.d.ts.map +1 -0
  12. package/dist/components/MultiCanvasRenderer.js +203 -0
  13. package/dist/components/MultiCanvasRenderer.js.map +1 -0
  14. package/dist/components/NodeTooltip.d.ts +24 -0
  15. package/dist/components/NodeTooltip.d.ts.map +1 -0
  16. package/dist/components/NodeTooltip.js +160 -0
  17. package/dist/components/NodeTooltip.js.map +1 -0
  18. package/dist/components/SelectionSidebar.d.ts +18 -0
  19. package/dist/components/SelectionSidebar.d.ts.map +1 -0
  20. package/dist/components/SelectionSidebar.js +190 -0
  21. package/dist/components/SelectionSidebar.js.map +1 -0
  22. package/dist/components/SequenceDiagramRenderer.d.ts +77 -0
  23. package/dist/components/SequenceDiagramRenderer.d.ts.map +1 -0
  24. package/dist/components/SequenceDiagramRenderer.js +604 -0
  25. package/dist/components/SequenceDiagramRenderer.js.map +1 -0
  26. package/dist/components/WorkflowSequenceDiagram.d.ts +63 -0
  27. package/dist/components/WorkflowSequenceDiagram.d.ts.map +1 -0
  28. package/dist/components/WorkflowSequenceDiagram.js +147 -0
  29. package/dist/components/WorkflowSequenceDiagram.js.map +1 -0
  30. package/dist/components/dashboard/DashboardRenderer.d.ts +10 -0
  31. package/dist/components/dashboard/DashboardRenderer.d.ts.map +1 -0
  32. package/dist/components/dashboard/DashboardRenderer.js +179 -0
  33. package/dist/components/dashboard/DashboardRenderer.js.map +1 -0
  34. package/dist/components/dashboard/MetricPanel.d.ts +10 -0
  35. package/dist/components/dashboard/MetricPanel.d.ts.map +1 -0
  36. package/dist/components/dashboard/MetricPanel.js +103 -0
  37. package/dist/components/dashboard/MetricPanel.js.map +1 -0
  38. package/dist/components/dashboard/MockDataProvider.d.ts +30 -0
  39. package/dist/components/dashboard/MockDataProvider.d.ts.map +1 -0
  40. package/dist/components/dashboard/MockDataProvider.js +270 -0
  41. package/dist/components/dashboard/MockDataProvider.js.map +1 -0
  42. package/dist/components/dashboard/components/BarChart.d.ts +10 -0
  43. package/dist/components/dashboard/components/BarChart.d.ts.map +1 -0
  44. package/dist/components/dashboard/components/BarChart.js +167 -0
  45. package/dist/components/dashboard/components/BarChart.js.map +1 -0
  46. package/dist/components/dashboard/components/LineChart.d.ts +10 -0
  47. package/dist/components/dashboard/components/LineChart.d.ts.map +1 -0
  48. package/dist/components/dashboard/components/LineChart.js +141 -0
  49. package/dist/components/dashboard/components/LineChart.js.map +1 -0
  50. package/dist/components/dashboard/components/MetricCard.d.ts +9 -0
  51. package/dist/components/dashboard/components/MetricCard.d.ts.map +1 -0
  52. package/dist/components/dashboard/components/MetricCard.js +163 -0
  53. package/dist/components/dashboard/components/MetricCard.js.map +1 -0
  54. package/dist/components/dashboard/components/SourceLink.d.ts +9 -0
  55. package/dist/components/dashboard/components/SourceLink.d.ts.map +1 -0
  56. package/dist/components/dashboard/components/SourceLink.js +39 -0
  57. package/dist/components/dashboard/components/SourceLink.js.map +1 -0
  58. package/dist/components/dashboard/components/TimeRangeSelector.d.ts +9 -0
  59. package/dist/components/dashboard/components/TimeRangeSelector.d.ts.map +1 -0
  60. package/dist/components/dashboard/components/TimeRangeSelector.js +167 -0
  61. package/dist/components/dashboard/components/TimeRangeSelector.js.map +1 -0
  62. package/dist/components/dashboard/components/index.d.ts +6 -0
  63. package/dist/components/dashboard/components/index.d.ts.map +1 -0
  64. package/dist/components/dashboard/components/index.js +6 -0
  65. package/dist/components/dashboard/components/index.js.map +1 -0
  66. package/dist/components/dashboard/index.d.ts +6 -0
  67. package/dist/components/dashboard/index.d.ts.map +1 -0
  68. package/dist/components/dashboard/index.js +8 -0
  69. package/dist/components/dashboard/index.js.map +1 -0
  70. package/dist/components/dashboard/types.d.ts +74 -0
  71. package/dist/components/dashboard/types.d.ts.map +1 -0
  72. package/dist/components/dashboard/types.js +8 -0
  73. package/dist/components/dashboard/types.js.map +1 -0
  74. package/dist/components/session-events/SessionEventFeed.d.ts +34 -0
  75. package/dist/components/session-events/SessionEventFeed.d.ts.map +1 -0
  76. package/dist/components/session-events/SessionEventFeed.js +1196 -0
  77. package/dist/components/session-events/SessionEventFeed.js.map +1 -0
  78. package/dist/components/session-events/index.d.ts +7 -0
  79. package/dist/components/session-events/index.d.ts.map +1 -0
  80. package/dist/components/session-events/index.js +6 -0
  81. package/dist/components/session-events/index.js.map +1 -0
  82. package/dist/components/state-view/PipelineView.d.ts +14 -0
  83. package/dist/components/state-view/PipelineView.d.ts.map +1 -0
  84. package/dist/components/state-view/PipelineView.js +195 -0
  85. package/dist/components/state-view/PipelineView.js.map +1 -0
  86. package/dist/components/state-view/index.d.ts +14 -0
  87. package/dist/components/state-view/index.d.ts.map +1 -0
  88. package/dist/components/state-view/index.js +12 -0
  89. package/dist/components/state-view/index.js.map +1 -0
  90. package/dist/components/state-view/types.d.ts +188 -0
  91. package/dist/components/state-view/types.d.ts.map +1 -0
  92. package/dist/components/state-view/types.js +10 -0
  93. package/dist/components/state-view/types.js.map +1 -0
  94. package/dist/components/state-view/useStateView.d.ts +32 -0
  95. package/dist/components/state-view/useStateView.d.ts.map +1 -0
  96. package/dist/components/state-view/useStateView.js +129 -0
  97. package/dist/components/state-view/useStateView.js.map +1 -0
  98. package/dist/contexts/GraphEditContext.d.ts +20 -0
  99. package/dist/contexts/GraphEditContext.d.ts.map +1 -0
  100. package/dist/contexts/GraphEditContext.js +8 -0
  101. package/dist/contexts/GraphEditContext.js.map +1 -0
  102. package/dist/contexts/TooltipPortalContext.d.ts +8 -0
  103. package/dist/contexts/TooltipPortalContext.d.ts.map +1 -0
  104. package/dist/contexts/TooltipPortalContext.js +8 -0
  105. package/dist/contexts/TooltipPortalContext.js.map +1 -0
  106. package/dist/edges/CustomEdge.d.ts +24 -0
  107. package/dist/edges/CustomEdge.d.ts.map +1 -0
  108. package/dist/edges/CustomEdge.js +222 -0
  109. package/dist/edges/CustomEdge.js.map +1 -0
  110. package/dist/edges/GenericEdge.d.ts +18 -0
  111. package/dist/edges/GenericEdge.d.ts.map +1 -0
  112. package/dist/edges/GenericEdge.js +10 -0
  113. package/dist/edges/GenericEdge.js.map +1 -0
  114. package/dist/graphify/anchor.d.ts +28 -0
  115. package/dist/graphify/anchor.d.ts.map +1 -0
  116. package/dist/graphify/anchor.js +132 -0
  117. package/dist/graphify/anchor.js.map +1 -0
  118. package/dist/graphify/consolidated.d.ts +210 -0
  119. package/dist/graphify/consolidated.d.ts.map +1 -0
  120. package/dist/graphify/consolidated.js +11 -0
  121. package/dist/graphify/consolidated.js.map +1 -0
  122. package/dist/graphify/ids.d.ts +19 -0
  123. package/dist/graphify/ids.d.ts.map +1 -0
  124. package/dist/graphify/ids.js +46 -0
  125. package/dist/graphify/ids.js.map +1 -0
  126. package/dist/graphify/index.d.ts +18 -0
  127. package/dist/graphify/index.d.ts.map +1 -0
  128. package/dist/graphify/index.js +12 -0
  129. package/dist/graphify/index.js.map +1 -0
  130. package/dist/graphify/kind.d.ts +23 -0
  131. package/dist/graphify/kind.d.ts.map +1 -0
  132. package/dist/graphify/kind.js +77 -0
  133. package/dist/graphify/kind.js.map +1 -0
  134. package/dist/graphify/resolve.d.ts +48 -0
  135. package/dist/graphify/resolve.d.ts.map +1 -0
  136. package/dist/graphify/resolve.js +83 -0
  137. package/dist/graphify/resolve.js.map +1 -0
  138. package/dist/graphify/signature.d.ts +87 -0
  139. package/dist/graphify/signature.d.ts.map +1 -0
  140. package/dist/graphify/signature.js +276 -0
  141. package/dist/graphify/signature.js.map +1 -0
  142. package/dist/graphify/types.d.ts +141 -0
  143. package/dist/graphify/types.d.ts.map +1 -0
  144. package/dist/graphify/types.js +8 -0
  145. package/dist/graphify/types.js.map +1 -0
  146. package/dist/hooks/useElkLayout.d.ts +66 -0
  147. package/dist/hooks/useElkLayout.d.ts.map +1 -0
  148. package/dist/hooks/useElkLayout.js +136 -0
  149. package/dist/hooks/useElkLayout.js.map +1 -0
  150. package/dist/hooks/usePathBasedEvents.d.ts +42 -0
  151. package/dist/hooks/usePathBasedEvents.d.ts.map +1 -0
  152. package/dist/hooks/usePathBasedEvents.js +119 -0
  153. package/dist/hooks/usePathBasedEvents.js.map +1 -0
  154. package/dist/hooks/useSequenceLayout.d.ts +186 -0
  155. package/dist/hooks/useSequenceLayout.d.ts.map +1 -0
  156. package/dist/hooks/useSequenceLayout.js +327 -0
  157. package/dist/hooks/useSequenceLayout.js.map +1 -0
  158. package/dist/hooks/useUndoRedo.d.ts +80 -0
  159. package/dist/hooks/useUndoRedo.d.ts.map +1 -0
  160. package/dist/hooks/useUndoRedo.js +70 -0
  161. package/dist/hooks/useUndoRedo.js.map +1 -0
  162. package/dist/index.d.ts +73 -0
  163. package/dist/index.d.ts.map +1 -0
  164. package/dist/index.js +51 -0
  165. package/dist/index.js.map +1 -0
  166. package/dist/nodes/CustomNode.d.ts +28 -0
  167. package/dist/nodes/CustomNode.d.ts.map +1 -0
  168. package/dist/nodes/CustomNode.js +820 -0
  169. package/dist/nodes/CustomNode.js.map +1 -0
  170. package/dist/nodes/GenericNode.d.ts +20 -0
  171. package/dist/nodes/GenericNode.d.ts.map +1 -0
  172. package/dist/nodes/GenericNode.js +20 -0
  173. package/dist/nodes/GenericNode.js.map +1 -0
  174. package/dist/nodes/otel/OtelBoundaryNode.d.ts +55 -0
  175. package/dist/nodes/otel/OtelBoundaryNode.d.ts.map +1 -0
  176. package/dist/nodes/otel/OtelBoundaryNode.js +81 -0
  177. package/dist/nodes/otel/OtelBoundaryNode.js.map +1 -0
  178. package/dist/nodes/otel/OtelEventNode.d.ts +59 -0
  179. package/dist/nodes/otel/OtelEventNode.d.ts.map +1 -0
  180. package/dist/nodes/otel/OtelEventNode.js +82 -0
  181. package/dist/nodes/otel/OtelEventNode.js.map +1 -0
  182. package/dist/nodes/otel/OtelResourceNode.d.ts +53 -0
  183. package/dist/nodes/otel/OtelResourceNode.d.ts.map +1 -0
  184. package/dist/nodes/otel/OtelResourceNode.js +105 -0
  185. package/dist/nodes/otel/OtelResourceNode.js.map +1 -0
  186. package/dist/nodes/otel/OtelScopeNode.d.ts +53 -0
  187. package/dist/nodes/otel/OtelScopeNode.d.ts.map +1 -0
  188. package/dist/nodes/otel/OtelScopeNode.js +81 -0
  189. package/dist/nodes/otel/OtelScopeNode.js.map +1 -0
  190. package/dist/nodes/otel/OtelSpanConventionNode.d.ts +61 -0
  191. package/dist/nodes/otel/OtelSpanConventionNode.d.ts.map +1 -0
  192. package/dist/nodes/otel/OtelSpanConventionNode.js +131 -0
  193. package/dist/nodes/otel/OtelSpanConventionNode.js.map +1 -0
  194. package/dist/nodes/otel/index.d.ts +17 -0
  195. package/dist/nodes/otel/index.d.ts.map +1 -0
  196. package/dist/nodes/otel/index.js +13 -0
  197. package/dist/nodes/otel/index.js.map +1 -0
  198. package/dist/nodes/otel/shared/NodeBadges.d.ts +10 -0
  199. package/dist/nodes/otel/shared/NodeBadges.d.ts.map +1 -0
  200. package/dist/nodes/otel/shared/NodeBadges.js +178 -0
  201. package/dist/nodes/otel/shared/NodeBadges.js.map +1 -0
  202. package/dist/nodes/otel/shared/NodeContent.d.ts +10 -0
  203. package/dist/nodes/otel/shared/NodeContent.d.ts.map +1 -0
  204. package/dist/nodes/otel/shared/NodeContent.js +96 -0
  205. package/dist/nodes/otel/shared/NodeContent.js.map +1 -0
  206. package/dist/nodes/otel/shared/index.d.ts +8 -0
  207. package/dist/nodes/otel/shared/index.d.ts.map +1 -0
  208. package/dist/nodes/otel/shared/index.js +8 -0
  209. package/dist/nodes/otel/shared/index.js.map +1 -0
  210. package/dist/nodes/otel/shared/types.d.ts +129 -0
  211. package/dist/nodes/otel/shared/types.d.ts.map +1 -0
  212. package/dist/nodes/otel/shared/types.js +5 -0
  213. package/dist/nodes/otel/shared/types.js.map +1 -0
  214. package/dist/nodes/otel/shared/useNodeBehavior.d.ts +42 -0
  215. package/dist/nodes/otel/shared/useNodeBehavior.d.ts.map +1 -0
  216. package/dist/nodes/otel/shared/useNodeBehavior.js +61 -0
  217. package/dist/nodes/otel/shared/useNodeBehavior.js.map +1 -0
  218. package/dist/pierre/PierreFileView.d.ts +17 -0
  219. package/dist/pierre/PierreFileView.d.ts.map +1 -0
  220. package/dist/pierre/PierreFileView.js +46 -0
  221. package/dist/pierre/PierreFileView.js.map +1 -0
  222. package/dist/pierre/PierreSnippetView.d.ts +28 -0
  223. package/dist/pierre/PierreSnippetView.d.ts.map +1 -0
  224. package/dist/pierre/PierreSnippetView.js +105 -0
  225. package/dist/pierre/PierreSnippetView.js.map +1 -0
  226. package/dist/pierre/PierreThroughlineCodeView.d.ts +22 -0
  227. package/dist/pierre/PierreThroughlineCodeView.d.ts.map +1 -0
  228. package/dist/pierre/PierreThroughlineCodeView.js +206 -0
  229. package/dist/pierre/PierreThroughlineCodeView.js.map +1 -0
  230. package/dist/pierre/constructColors.d.ts +24 -0
  231. package/dist/pierre/constructColors.d.ts.map +1 -0
  232. package/dist/pierre/constructColors.js +91 -0
  233. package/dist/pierre/constructColors.js.map +1 -0
  234. package/dist/pierre/index.d.ts +12 -0
  235. package/dist/pierre/index.d.ts.map +1 -0
  236. package/dist/pierre/index.js +7 -0
  237. package/dist/pierre/index.js.map +1 -0
  238. package/dist/pierre/pierreBackground.d.ts +15 -0
  239. package/dist/pierre/pierreBackground.d.ts.map +1 -0
  240. package/dist/pierre/pierreBackground.js +31 -0
  241. package/dist/pierre/pierreBackground.js.map +1 -0
  242. package/dist/pierre/pierreFileLang.d.ts +27 -0
  243. package/dist/pierre/pierreFileLang.d.ts.map +1 -0
  244. package/dist/pierre/pierreFileLang.js +52 -0
  245. package/dist/pierre/pierreFileLang.js.map +1 -0
  246. package/dist/pierre/pierreSyntaxTheme.d.ts +11 -0
  247. package/dist/pierre/pierreSyntaxTheme.d.ts.map +1 -0
  248. package/dist/pierre/pierreSyntaxTheme.js +20 -0
  249. package/dist/pierre/pierreSyntaxTheme.js.map +1 -0
  250. package/dist/pierre/scrollAnchor.d.ts +5 -0
  251. package/dist/pierre/scrollAnchor.d.ts.map +1 -0
  252. package/dist/pierre/scrollAnchor.js +9 -0
  253. package/dist/pierre/scrollAnchor.js.map +1 -0
  254. package/dist/pierre/sliceSnippet.d.ts +10 -0
  255. package/dist/pierre/sliceSnippet.d.ts.map +1 -0
  256. package/dist/pierre/sliceSnippet.js +16 -0
  257. package/dist/pierre/sliceSnippet.js.map +1 -0
  258. package/dist/pierre/syntaxTokens.d.ts +11 -0
  259. package/dist/pierre/syntaxTokens.d.ts.map +1 -0
  260. package/dist/pierre/syntaxTokens.js +51 -0
  261. package/dist/pierre/syntaxTokens.js.map +1 -0
  262. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +8 -0
  263. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -0
  264. package/dist/stories/Subsystem/ComponentGraph/fixtures.js +116 -0
  265. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -0
  266. package/dist/subsystem/ComponentDeclaration.d.ts +87 -0
  267. package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -0
  268. package/dist/subsystem/ComponentDeclaration.js +374 -0
  269. package/dist/subsystem/ComponentDeclaration.js.map +1 -0
  270. package/dist/subsystem/FileDrawer.d.ts +17 -0
  271. package/dist/subsystem/FileDrawer.d.ts.map +1 -0
  272. package/dist/subsystem/FileDrawer.js +72 -0
  273. package/dist/subsystem/FileDrawer.js.map +1 -0
  274. package/dist/subsystem/GraphLayoutCover.d.ts +20 -0
  275. package/dist/subsystem/GraphLayoutCover.d.ts.map +1 -0
  276. package/dist/subsystem/GraphLayoutCover.js +42 -0
  277. package/dist/subsystem/GraphLayoutCover.js.map +1 -0
  278. package/dist/subsystem/SubsystemComponentGraph.d.ts +96 -0
  279. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -0
  280. package/dist/subsystem/SubsystemComponentGraph.js +1059 -0
  281. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -0
  282. package/dist/subsystem/SubsystemFileTree.d.ts +24 -0
  283. package/dist/subsystem/SubsystemFileTree.d.ts.map +1 -0
  284. package/dist/subsystem/SubsystemFileTree.js +150 -0
  285. package/dist/subsystem/SubsystemFileTree.js.map +1 -0
  286. package/dist/subsystem/declarationRef.d.ts +39 -0
  287. package/dist/subsystem/declarationRef.d.ts.map +1 -0
  288. package/dist/subsystem/declarationRef.js +32 -0
  289. package/dist/subsystem/declarationRef.js.map +1 -0
  290. package/dist/subsystem/formatDeclaration.d.ts +11 -0
  291. package/dist/subsystem/formatDeclaration.d.ts.map +1 -0
  292. package/dist/subsystem/formatDeclaration.js +146 -0
  293. package/dist/subsystem/formatDeclaration.js.map +1 -0
  294. package/dist/subsystem/model.d.ts +353 -0
  295. package/dist/subsystem/model.d.ts.map +1 -0
  296. package/dist/subsystem/model.js +503 -0
  297. package/dist/subsystem/model.js.map +1 -0
  298. package/dist/subsystem/nodes.d.ts +69 -0
  299. package/dist/subsystem/nodes.d.ts.map +1 -0
  300. package/dist/subsystem/nodes.js +289 -0
  301. package/dist/subsystem/nodes.js.map +1 -0
  302. package/dist/subsystem/paths.d.ts +63 -0
  303. package/dist/subsystem/paths.d.ts.map +1 -0
  304. package/dist/subsystem/paths.js +86 -0
  305. package/dist/subsystem/paths.js.map +1 -0
  306. package/dist/subsystem/tokenizeComponent.d.ts +20 -0
  307. package/dist/subsystem/tokenizeComponent.d.ts.map +1 -0
  308. package/dist/subsystem/tokenizeComponent.js +56 -0
  309. package/dist/subsystem/tokenizeComponent.js.map +1 -0
  310. package/dist/subsystem/tokenizeFormatted.d.ts +12 -0
  311. package/dist/subsystem/tokenizeFormatted.d.ts.map +1 -0
  312. package/dist/subsystem/tokenizeFormatted.js +13 -0
  313. package/dist/subsystem/tokenizeFormatted.js.map +1 -0
  314. package/dist/utils/animationMapping.d.ts +53 -0
  315. package/dist/utils/animationMapping.d.ts.map +1 -0
  316. package/dist/utils/animationMapping.js +126 -0
  317. package/dist/utils/animationMapping.js.map +1 -0
  318. package/dist/utils/canvasBounds.d.ts +50 -0
  319. package/dist/utils/canvasBounds.d.ts.map +1 -0
  320. package/dist/utils/canvasBounds.js +88 -0
  321. package/dist/utils/canvasBounds.js.map +1 -0
  322. package/dist/utils/elkLayout.d.ts +137 -0
  323. package/dist/utils/elkLayout.d.ts.map +1 -0
  324. package/dist/utils/elkLayout.js +596 -0
  325. package/dist/utils/elkLayout.js.map +1 -0
  326. package/dist/utils/graphConverter.d.ts +18 -0
  327. package/dist/utils/graphConverter.d.ts.map +1 -0
  328. package/dist/utils/graphConverter.js +114 -0
  329. package/dist/utils/graphConverter.js.map +1 -0
  330. package/dist/utils/iconResolver.d.ts +29 -0
  331. package/dist/utils/iconResolver.d.ts.map +1 -0
  332. package/dist/utils/iconResolver.js +30 -0
  333. package/dist/utils/iconResolver.js.map +1 -0
  334. package/dist/utils/orientationUtils.d.ts +19 -0
  335. package/dist/utils/orientationUtils.d.ts.map +1 -0
  336. package/dist/utils/orientationUtils.js +62 -0
  337. package/dist/utils/orientationUtils.js.map +1 -0
  338. package/package.json +80 -0
  339. package/src/components/ConfigurationSelector.tsx +133 -0
  340. package/src/components/GraphRenderer.tsx +3195 -0
  341. package/src/components/MultiCanvasRenderer.tsx +295 -0
  342. package/src/components/NodeTooltip.tsx +279 -0
  343. package/src/components/PendingChanges.test.tsx +375 -0
  344. package/src/components/SelectionSidebar.tsx +352 -0
  345. package/src/components/SequenceDiagramRenderer.tsx +1156 -0
  346. package/src/components/WorkflowSequenceDiagram.tsx +270 -0
  347. package/src/components/dashboard/DashboardRenderer.tsx +317 -0
  348. package/src/components/dashboard/MetricPanel.tsx +254 -0
  349. package/src/components/dashboard/MockDataProvider.ts +330 -0
  350. package/src/components/dashboard/components/BarChart.tsx +299 -0
  351. package/src/components/dashboard/components/LineChart.tsx +279 -0
  352. package/src/components/dashboard/components/MetricCard.tsx +270 -0
  353. package/src/components/dashboard/components/SourceLink.tsx +63 -0
  354. package/src/components/dashboard/components/TimeRangeSelector.tsx +280 -0
  355. package/src/components/dashboard/components/index.ts +5 -0
  356. package/src/components/dashboard/index.ts +47 -0
  357. package/src/components/dashboard/types.ts +126 -0
  358. package/src/components/session-events/SessionEventFeed.tsx +1797 -0
  359. package/src/components/session-events/index.ts +13 -0
  360. package/src/components/state-view/PipelineView.tsx +347 -0
  361. package/src/components/state-view/index.ts +14 -0
  362. package/src/components/state-view/types.ts +261 -0
  363. package/src/components/state-view/useStateView.ts +205 -0
  364. package/src/contexts/GraphEditContext.tsx +27 -0
  365. package/src/contexts/TooltipPortalContext.ts +8 -0
  366. package/src/edges/CustomEdge.tsx +402 -0
  367. package/src/edges/GenericEdge.tsx +36 -0
  368. package/src/graphify/anchor.test.ts +124 -0
  369. package/src/graphify/anchor.ts +160 -0
  370. package/src/graphify/consolidated.ts +246 -0
  371. package/src/graphify/ids.ts +48 -0
  372. package/src/graphify/index.ts +72 -0
  373. package/src/graphify/kind.test.ts +108 -0
  374. package/src/graphify/kind.ts +120 -0
  375. package/src/graphify/resolve.test.ts +89 -0
  376. package/src/graphify/resolve.ts +115 -0
  377. package/src/graphify/signature.test.ts +278 -0
  378. package/src/graphify/signature.ts +354 -0
  379. package/src/graphify/types.ts +164 -0
  380. package/src/hooks/useElkLayout.test.ts +134 -0
  381. package/src/hooks/useElkLayout.ts +191 -0
  382. package/src/hooks/usePathBasedEvents.ts +192 -0
  383. package/src/hooks/useSequenceLayout.ts +543 -0
  384. package/src/hooks/useUndoRedo.ts +144 -0
  385. package/src/index.ts +341 -0
  386. package/src/nodes/CustomNode.tsx +1231 -0
  387. package/src/nodes/GenericNode.tsx +55 -0
  388. package/src/nodes/otel/OtelBoundaryNode.tsx +224 -0
  389. package/src/nodes/otel/OtelEventNode.tsx +225 -0
  390. package/src/nodes/otel/OtelResourceNode.tsx +251 -0
  391. package/src/nodes/otel/OtelScopeNode.tsx +221 -0
  392. package/src/nodes/otel/OtelSpanConventionNode.tsx +295 -0
  393. package/src/nodes/otel/index.ts +23 -0
  394. package/src/nodes/otel/shared/NodeBadges.tsx +295 -0
  395. package/src/nodes/otel/shared/NodeContent.tsx +204 -0
  396. package/src/nodes/otel/shared/index.ts +8 -0
  397. package/src/nodes/otel/shared/types.ts +138 -0
  398. package/src/nodes/otel/shared/useNodeBehavior.ts +114 -0
  399. package/src/pierre/PierreFileView.tsx +81 -0
  400. package/src/pierre/PierreSnippetView.tsx +184 -0
  401. package/src/pierre/PierreThroughlineCodeView.tsx +304 -0
  402. package/src/pierre/constructColors.test.ts +52 -0
  403. package/src/pierre/constructColors.ts +112 -0
  404. package/src/pierre/index.ts +18 -0
  405. package/src/pierre/pierreBackground.ts +33 -0
  406. package/src/pierre/pierreFileLang.test.ts +26 -0
  407. package/src/pierre/pierreFileLang.ts +55 -0
  408. package/src/pierre/pierreSyntaxTheme.ts +23 -0
  409. package/src/pierre/scrollAnchor.test.ts +13 -0
  410. package/src/pierre/scrollAnchor.ts +11 -0
  411. package/src/pierre/sliceSnippet.test.ts +21 -0
  412. package/src/pierre/sliceSnippet.ts +31 -0
  413. package/src/pierre/syntaxTokens.test.ts +35 -0
  414. package/src/pierre/syntaxTokens.ts +60 -0
  415. package/src/stories/Audit/BoundaryNodes.stories.tsx +385 -0
  416. package/src/stories/Audit/CanvasEdgeTypes.stories.tsx +1448 -0
  417. package/src/stories/Audit/CanvasNodeTypes.stories.tsx +1049 -0
  418. package/src/stories/Audit/ColorPriority.stories.tsx +895 -0
  419. package/src/stories/Audit/DiamondBadges.stories.tsx +629 -0
  420. package/src/stories/Audit/NodeDimensionsTesting.stories.tsx +724 -0
  421. package/src/stories/Audit/NodeFieldsAudit.stories.tsx +996 -0
  422. package/src/stories/Audit/NodeFontSizeTesting.stories.tsx +715 -0
  423. package/src/stories/Audit/NodeShapes.stories.tsx +1208 -0
  424. package/src/stories/Audit/WideNodesTesting.stories.tsx +1045 -0
  425. package/src/stories/Components/DashboardRenderer.stories.tsx +263 -0
  426. package/src/stories/Components/GraphRenderer.stories.tsx +3236 -0
  427. package/src/stories/Components/MultiCanvasRenderer.stories.tsx +370 -0
  428. package/src/stories/Components/SequenceDiagram.stories.tsx +255 -0
  429. package/src/stories/Components/StateView.stories.tsx +417 -0
  430. package/src/stories/Components/TraceViewer.stories.tsx +442 -0
  431. package/src/stories/Components/WorkflowSequenceDiagram.stories.tsx +802 -0
  432. package/src/stories/Features/AnimationWorkshop.stories.tsx +727 -0
  433. package/src/stories/Features/ChangeTypeVisual.stories.tsx +67 -0
  434. package/src/stories/Features/EventDrivenAnimations.stories.tsx +513 -0
  435. package/src/stories/Features/MultiConfig.stories.tsx +539 -0
  436. package/src/stories/Introduction.mdx +34 -0
  437. package/src/stories/Layout/ElkEdgeRouting.stories.tsx +539 -0
  438. package/src/stories/Layout/GraphOrientation.stories.tsx +500 -0
  439. package/src/stories/Layout/MultiDirectionalConnections.stories.tsx +352 -0
  440. package/src/stories/Nodes/NodeDefinitionComparison.stories.tsx +560 -0
  441. package/src/stories/Nodes/NodeNameFallbacks.stories.tsx +491 -0
  442. package/src/stories/Nodes/NumericColors.stories.tsx +440 -0
  443. package/src/stories/OTEL/EventNameDisplay.stories.tsx +694 -0
  444. package/src/stories/OTEL/EventNameFallback.stories.tsx +578 -0
  445. package/src/stories/OTEL/OtelComponents.stories.tsx +422 -0
  446. package/src/stories/OTEL/OtelNodeTypesPrototype.stories.tsx +806 -0
  447. package/src/stories/OTEL/RawEventCards.stories.tsx +57 -0
  448. package/src/stories/OTEL/SpanBadges.stories.tsx +840 -0
  449. package/src/stories/OTEL/ValidationEventsCanvas.stories.tsx +209 -0
  450. package/src/stories/Pierre/CodeView.stories.tsx +219 -0
  451. package/src/stories/RealWorld/BacklogMdCanvases.stories.tsx +721 -0
  452. package/src/stories/RealWorld/FileCitySequence.stories.tsx +516 -0
  453. package/src/stories/RealWorld/RealCanvasFiles.stories.tsx +336 -0
  454. package/src/stories/Subsystem/ComponentDeclarationAudit.stories.tsx +321 -0
  455. package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +365 -0
  456. package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +137 -0
  457. package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +238 -0
  458. package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +315 -0
  459. package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +239 -0
  460. package/src/stories/Subsystem/ComponentGraph/FrameworkStereotype.stories.tsx +216 -0
  461. package/src/stories/Subsystem/ComponentGraph/GraphTitle.stories.tsx +70 -0
  462. package/src/stories/Subsystem/ComponentGraph/Processes.stories.tsx +68 -0
  463. package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +959 -0
  464. package/src/stories/Subsystem/ComponentGraph/Spotlights.stories.tsx +681 -0
  465. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +129 -0
  466. package/src/stories/__traces__/test-run.canvas.json +241 -0
  467. package/src/stories/data/activity-feed-analytics.dashboard.json +300 -0
  468. package/src/stories/data/file-city-images-transformed.otel.canvas.json +295 -0
  469. package/src/stories/data/file-city-workflows.json +269 -0
  470. package/src/stories/data/graph-converter-test-execution.json +443 -0
  471. package/src/stories/data/graph-converter-test.workflow.json +105 -0
  472. package/src/stories/data/graph-converter-validated-execution.json +58 -0
  473. package/src/stories/data/opencode-session-raw-events.json +273396 -0
  474. package/src/subsystem/ComponentDeclaration.tsx +643 -0
  475. package/src/subsystem/FileDrawer.tsx +106 -0
  476. package/src/subsystem/GraphLayoutCover.tsx +69 -0
  477. package/src/subsystem/SubsystemComponentGraph.tsx +1644 -0
  478. package/src/subsystem/SubsystemFileTree.tsx +190 -0
  479. package/src/subsystem/declarationRef.test.ts +33 -0
  480. package/src/subsystem/declarationRef.ts +63 -0
  481. package/src/subsystem/formatDeclaration.ts +174 -0
  482. package/src/subsystem/model.test.ts +244 -0
  483. package/src/subsystem/model.ts +850 -0
  484. package/src/subsystem/nodes.test.ts +56 -0
  485. package/src/subsystem/nodes.tsx +443 -0
  486. package/src/subsystem/paths.test.ts +123 -0
  487. package/src/subsystem/paths.ts +127 -0
  488. package/src/subsystem/tokenizeComponent.ts +70 -0
  489. package/src/subsystem/tokenizeFormatted.ts +19 -0
  490. package/src/utils/animationMapping.ts +166 -0
  491. package/src/utils/canvasBounds.ts +146 -0
  492. package/src/utils/elkLayout.test.ts +240 -0
  493. package/src/utils/elkLayout.ts +779 -0
  494. package/src/utils/graphConverter.ts +148 -0
  495. package/src/utils/iconResolver.tsx +53 -0
  496. package/src/utils/orientationUtils.ts +73 -0
@@ -0,0 +1,1797 @@
1
+ /**
2
+ * SessionEventFeed — a three-column diagnostic feed over one agent session's
3
+ * events, showing the raw → repo-normalized → accumulated (UI) pipeline
4
+ * for every row.
5
+ *
6
+ * Input rows match the principal-studio's `SessionEventRow` wire shape:
7
+ * { seq, type, raw, normalized, accumulated }
8
+ * `raw` is the agent-specific payload (opencode V1 sqlite rows, or durable
9
+ * reader blobs such as Cursor store.db messages), `normalized` its
10
+ * repo-normalized form, and `accumulated` the AgentSessionEvent the File City
11
+ * UI actually renders (null when the accumulator drops the event).
12
+ *
13
+ * Dark diagnostic palette — intentionally self-contained rather than themed.
14
+ */
15
+
16
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
17
+ import { useTheme } from '@principal-ade/industry-theme';
18
+ import { Check, Copy } from 'lucide-react';
19
+ import type {
20
+ AgentSessionEvent,
21
+ NormalizedPathInfo,
22
+ RepoNormalizedUniversalAgentSessionEvent,
23
+ V1RawEvent,
24
+ } from '@principal-ai/agent-monitoring';
25
+
26
+ export interface SessionEventFeedRow {
27
+ seq: number;
28
+ type: string;
29
+ raw: unknown;
30
+ normalized?: Record<string, unknown>;
31
+ accumulated: AgentSessionEvent | null;
32
+ }
33
+
34
+ export interface SessionEventFeedProps {
35
+ title?: string;
36
+ rows: SessionEventFeedRow[];
37
+ }
38
+
39
+ export interface SessionEventFeedGroupedProps {
40
+ title?: string;
41
+ rows: SessionEventFeedRow[];
42
+ }
43
+
44
+ type AnyRecord = Record<string, unknown>;
45
+ type NormalizedFeedEvent = Omit<RepoNormalizedUniversalAgentSessionEvent, 'raw'>;
46
+
47
+ /** Shared collapse state keyed by row seq. */
48
+ function useCollapse(seqs: number[], defaultCollapsed = false) {
49
+ const [collapsed, setCollapsed] = useState<Set<number>>(
50
+ () => (defaultCollapsed ? new Set(seqs) : new Set<number>()),
51
+ );
52
+ // When the feed grows (incremental paging appends rows), default newly added
53
+ // rows to collapsed so the invariant holds for rows that arrive after mount.
54
+ useEffect(() => {
55
+ if (!defaultCollapsed) return;
56
+ setCollapsed((prev) => {
57
+ let changed = false;
58
+ const next = new Set(prev);
59
+ for (const seq of seqs) {
60
+ if (!next.has(seq)) {
61
+ next.add(seq);
62
+ changed = true;
63
+ }
64
+ }
65
+ return changed ? next : prev;
66
+ });
67
+ }, [seqs, defaultCollapsed]);
68
+ const toggle = useCallback((seq: number) => {
69
+ setCollapsed((prev) => {
70
+ const next = new Set(prev);
71
+ if (next.has(seq)) next.delete(seq);
72
+ else next.add(seq);
73
+ return next;
74
+ });
75
+ }, []);
76
+ const setAll = useCallback(
77
+ (value: boolean) => setCollapsed(value ? new Set(seqs) : new Set<number>()),
78
+ [seqs],
79
+ );
80
+ return { collapsed, toggle, setAll };
81
+ }
82
+
83
+ function toRaw(raw: unknown): V1RawEvent | null {
84
+ if (raw === null || raw === undefined || typeof raw !== 'object') return null;
85
+ return raw as V1RawEvent;
86
+ }
87
+
88
+ function toNormalized(normalized?: Record<string, unknown>): NormalizedFeedEvent | undefined {
89
+ return normalized as NormalizedFeedEvent | undefined;
90
+ }
91
+
92
+ // =============================================================================
93
+ // Raw event → presentable pieces
94
+ // =============================================================================
95
+
96
+ function dataOf(e: V1RawEvent | null): AnyRecord {
97
+ return (e?.data ?? {}) as AnyRecord;
98
+ }
99
+
100
+ function partOf(e: V1RawEvent | null): AnyRecord | undefined {
101
+ return dataOf(e).part as AnyRecord | undefined;
102
+ }
103
+
104
+ /** True when the payload looks like an opencode V1 sqlite event row. */
105
+ function isV1RawEvent(e: V1RawEvent | null): boolean {
106
+ return typeof e?.type === 'string' && e.type.length > 0;
107
+ }
108
+
109
+ /**
110
+ * Cursor store.db blobs (and other durable readers) put the whole message in
111
+ * `raw` — no `type` / `data` envelope. Pull a presentable kind + summary from
112
+ * the common shapes CursorSessionReader emits:
113
+ * - chat message: `{ role, content, id? }`
114
+ * - tool wrapper: `{ message, block }` where block is tool-call / tool-result
115
+ * - session meta: `{ title?, cwd?, schemaVersion? }`
116
+ */
117
+ function durableRawKind(e: AnyRecord): string {
118
+ const block = e.block as AnyRecord | undefined;
119
+ if (block && typeof block === 'object') {
120
+ const toolName = (block.toolName ?? block.name) as string | undefined;
121
+ const blockType = block.type as string | undefined;
122
+ if (blockType === 'tool-call' || blockType === 'tool_use') {
123
+ return toolName ? `tool-call:${toolName}` : 'tool-call';
124
+ }
125
+ if (blockType === 'tool-result' || blockType === 'tool_result') {
126
+ return toolName ? `tool-result:${toolName}` : 'tool-result';
127
+ }
128
+ if (typeof blockType === 'string') return blockType;
129
+ }
130
+ if (typeof e.role === 'string') return e.role;
131
+ if (typeof e.title === 'string' || typeof e.cwd === 'string' || typeof e.schemaVersion === 'number') {
132
+ return 'session-meta';
133
+ }
134
+ return 'durable-raw';
135
+ }
136
+
137
+ function durableRawDescribe(e: AnyRecord): string {
138
+ const block = e.block as AnyRecord | undefined;
139
+ if (block && typeof block === 'object') {
140
+ const toolName = (block.toolName ?? block.name) as string | undefined;
141
+ const blockType = block.type as string | undefined;
142
+ if (blockType === 'tool-call' || blockType === 'tool_use') {
143
+ const args = block.args ?? block.input;
144
+ const summary = toolSummary(args);
145
+ return [toolName, summary].filter(Boolean).join(' · ') || 'tool call';
146
+ }
147
+ if (blockType === 'tool-result' || blockType === 'tool_result') {
148
+ const result = block.result;
149
+ const text =
150
+ typeof result === 'string'
151
+ ? result.slice(0, 120)
152
+ : result !== undefined
153
+ ? JSON.stringify(result).slice(0, 120)
154
+ : '';
155
+ return [toolName, text].filter(Boolean).join(' · ') || 'tool result';
156
+ }
157
+ }
158
+ if (typeof e.role === 'string') {
159
+ const content = e.content;
160
+ if (typeof content === 'string') {
161
+ const tagged = content.match(/<user_query>\s*([\s\S]*?)\s*<\/user_query>/i);
162
+ const text = (tagged?.[1] ?? content).trim().replace(/\s+/g, ' ');
163
+ return text.slice(0, 120) || e.role;
164
+ }
165
+ if (Array.isArray(content)) {
166
+ const texts = content
167
+ .filter((c): c is AnyRecord => !!c && typeof c === 'object')
168
+ .map((c) => (typeof c.text === 'string' ? c.text : c.toolName ?? c.name ?? c.type))
169
+ .filter(Boolean);
170
+ return texts.join(' · ').slice(0, 120) || e.role;
171
+ }
172
+ return e.role;
173
+ }
174
+ if (typeof e.title === 'string') return e.title;
175
+ if (typeof e.cwd === 'string') return e.cwd;
176
+ return 'durable raw event';
177
+ }
178
+
179
+ function eventTimestamp(e: V1RawEvent | null): number {
180
+ if (!e) return 0;
181
+ const d = dataOf(e);
182
+ if (typeof d.time === 'number') return d.time;
183
+ const info = d.info as AnyRecord | undefined;
184
+ const infoTime = info?.time as AnyRecord | undefined;
185
+ if (typeof infoTime?.created === 'number') return infoTime.created;
186
+ const part = partOf(e);
187
+ if (typeof part?.time === 'number') return part.time;
188
+ const state = part?.state as AnyRecord | undefined;
189
+ const stateTime = state?.time as AnyRecord | undefined;
190
+ if (typeof stateTime?.start === 'number') return stateTime.start;
191
+ if (typeof stateTime?.end === 'number') return stateTime.end;
192
+ return 0;
193
+ }
194
+
195
+ // =============================================================================
196
+ // Event categorization (drives the preset filters)
197
+ // =============================================================================
198
+
199
+ type EventCategory = 'session' | 'conversation' | 'tool' | 'step' | 'patch' | 'compaction';
200
+
201
+ const ALL_CATEGORIES: EventCategory[] = [
202
+ 'session',
203
+ 'conversation',
204
+ 'tool',
205
+ 'step',
206
+ 'patch',
207
+ 'compaction',
208
+ ];
209
+
210
+ function eventCategory(e: V1RawEvent | null): EventCategory {
211
+ if (!e) return 'session';
212
+ if (!isV1RawEvent(e)) {
213
+ const kind = durableRawKind(e as unknown as AnyRecord);
214
+ if (kind.startsWith('tool-call') || kind.startsWith('tool-result') || kind.startsWith('tool')) {
215
+ return 'tool';
216
+ }
217
+ if (kind === 'session-meta') return 'session';
218
+ return 'conversation';
219
+ }
220
+ switch (e.type) {
221
+ case 'session.created.1':
222
+ case 'session.updated.1':
223
+ return 'session';
224
+ case 'message.updated.1':
225
+ case 'message.removed.1':
226
+ case 'message.part.updated.1': {
227
+ const partType = partOf(e)?.type;
228
+ switch (partType) {
229
+ case 'text':
230
+ case 'reasoning':
231
+ return 'conversation';
232
+ case 'tool':
233
+ return 'tool';
234
+ case 'step-start':
235
+ case 'step-finish':
236
+ return 'step';
237
+ case 'patch':
238
+ return 'patch';
239
+ case 'compaction':
240
+ return 'compaction';
241
+ default:
242
+ return 'conversation';
243
+ }
244
+ }
245
+ default:
246
+ return 'conversation';
247
+ }
248
+ }
249
+
250
+ const PRESETS: Array<{ id: string; label: string; include: EventCategory[] }> = [
251
+ { id: 'all', label: 'All', include: ALL_CATEGORIES },
252
+ { id: 'activity', label: 'Activity', include: ['conversation', 'tool', 'patch'] },
253
+ { id: 'conversation', label: 'Conversation', include: ['conversation'] },
254
+ { id: 'tools', label: 'Tools', include: ['tool'] },
255
+ { id: 'noise', label: 'Session & steps', include: ['session', 'step', 'compaction'] },
256
+ ];
257
+
258
+ /** Granular key for a card (raw event type, or part type for parts). */
259
+ function eventKind(e: V1RawEvent | null): string {
260
+ if (!e) return 'no-raw';
261
+ if (!isV1RawEvent(e)) return durableRawKind(e as unknown as AnyRecord);
262
+ if (e.type === 'message.part.updated.1') {
263
+ return `part:${partOf(e)?.type ?? 'unknown'}`;
264
+ }
265
+ return e.type;
266
+ }
267
+
268
+ function kindLabel(kind: string | undefined | null): string {
269
+ if (!kind) return 'unknown';
270
+ if (kind.startsWith('part:')) return `part · ${kind.slice(5)}`;
271
+ if (kind.startsWith('tool-call:')) return `call · ${kind.slice('tool-call:'.length)}`;
272
+ if (kind.startsWith('tool-result:')) return `result · ${kind.slice('tool-result:'.length)}`;
273
+ return kind;
274
+ }
275
+
276
+ const KIND_COLORS: Record<string, string> = {
277
+ 'session.created.1': '#64748b',
278
+ 'session.updated.1': '#94a3b8',
279
+ 'message.updated.1': '#06b6d4',
280
+ 'message.removed.1': '#ef4444',
281
+ 'part:text': '#10b981',
282
+ 'part:reasoning': '#8b5cf6',
283
+ 'part:tool': '#3b82f6',
284
+ 'part:step-start': '#f59e0b',
285
+ 'part:step-finish': '#f59e0b',
286
+ 'part:patch': '#ef4444',
287
+ 'part:compaction': '#ec4899',
288
+ 'part:unknown': '#6b7280',
289
+ };
290
+
291
+ function kindColor(kind: string): string {
292
+ return KIND_COLORS[kind] ?? '#6b7280';
293
+ }
294
+
295
+ function describe(e: V1RawEvent | null): string {
296
+ if (!e) return 'finished';
297
+ if (!isV1RawEvent(e)) return durableRawDescribe(e as unknown as AnyRecord);
298
+ const d = dataOf(e);
299
+ const info = d.info as AnyRecord | undefined;
300
+ const part = partOf(e);
301
+ const state = part?.state as AnyRecord | undefined;
302
+
303
+ switch (e.type) {
304
+ case 'session.created.1':
305
+ return (info?.title as string) ?? 'Session created';
306
+ case 'session.updated.1': {
307
+ const bits: string[] = [];
308
+ const tokens = info?.tokens as AnyRecord | undefined;
309
+ if (typeof info?.cost === 'number') bits.push(`$${info.cost.toFixed(4)}`);
310
+ if (typeof tokens?.input === 'number') bits.push(`${tokens.input} in`);
311
+ if (typeof tokens?.output === 'number') bits.push(`${tokens.output} out`);
312
+ return bits.length > 0 ? `Session updated · ${bits.join(' · ')}` : 'Session updated';
313
+ }
314
+ case 'message.updated.1': {
315
+ const role = (info?.role as string) ?? 'message';
316
+ const model = (info?.model as AnyRecord | undefined)?.modelID as string | undefined;
317
+ return model ? `${role} message · ${model}` : `${role} message`;
318
+ }
319
+ case 'message.removed.1':
320
+ return `Removed ${d.messageID as string}`;
321
+ case 'message.part.updated.1': {
322
+ const partType = part?.type as string;
323
+ const toolName = part?.tool as string | undefined;
324
+ const status = state?.status as string | undefined;
325
+ switch (partType) {
326
+ case 'text':
327
+ return (part?.text as string) ?? 'Text';
328
+ case 'reasoning':
329
+ return (part?.text as string) || 'Reasoning';
330
+ case 'tool': {
331
+ const bits = [toolName, status];
332
+ const n = todoCountOf(state?.input);
333
+ if (n > 0) bits.push(`${n} todo${n === 1 ? '' : 's'}`);
334
+ return bits.filter(Boolean).join(' · ') || 'Tool';
335
+ }
336
+ case 'step-start':
337
+ return 'Step start';
338
+ case 'step-finish':
339
+ return part?.reason ? `Step finish · ${part.reason as string}` : 'Step finish';
340
+ case 'patch': {
341
+ const files = part?.files as string[] | undefined;
342
+ return `Patch · ${files?.length ?? 0} file${(files?.length ?? 0) === 1 ? '' : 's'}`;
343
+ }
344
+ case 'compaction':
345
+ return `Compaction${part?.auto ? ' · auto' : ''}`;
346
+ default:
347
+ return partType ?? 'Part';
348
+ }
349
+ }
350
+ default:
351
+ return e.type;
352
+ }
353
+ }
354
+
355
+ /** Number of todos carried by a todowrite tool input, if any. */
356
+ function todoCountOf(input: unknown): number {
357
+ if (input && typeof input === 'object') {
358
+ const todos = (input as AnyRecord)['todos'];
359
+ if (Array.isArray(todos)) return todos.length;
360
+ }
361
+ return 0;
362
+ }
363
+
364
+ function toolSummary(input: unknown): string {
365
+ if (input === null || input === undefined) return '';
366
+ if (typeof input === 'string') return input;
367
+ if (Array.isArray(input)) return JSON.stringify(input).slice(0, 120);
368
+ const obj = input as AnyRecord;
369
+ // A todowrite input carries the full todo list — summarize by count rather
370
+ // than dumping truncated JSON.
371
+ if (Array.isArray(obj['todos'])) return `${(obj['todos'] as unknown[]).length} todos`;
372
+ for (const key of ['command', 'filePath', 'file_path', 'path', 'pattern', 'include', 'tool']) {
373
+ if (typeof obj[key] === 'string' && obj[key] !== '') return obj[key] as string;
374
+ }
375
+ return JSON.stringify(input).slice(0, 120);
376
+ }
377
+
378
+ // =============================================================================
379
+ // Cards
380
+ // =============================================================================
381
+
382
+ function formatTime(ts: number): string {
383
+ if (!ts) return '';
384
+ return new Date(ts).toLocaleTimeString([], {
385
+ hour: '2-digit',
386
+ minute: '2-digit',
387
+ second: '2-digit',
388
+ });
389
+ }
390
+
391
+ function RawEventCard({
392
+ event,
393
+ runCount,
394
+ snapshotCount,
395
+ expanded,
396
+ onToggle,
397
+ }: {
398
+ event: V1RawEvent | null;
399
+ runCount?: number;
400
+ snapshotCount?: number;
401
+ expanded: boolean;
402
+ onToggle: () => void;
403
+ }) {
404
+ const { theme } = useTheme();
405
+ const kind = eventKind(event);
406
+ const d = dataOf(event);
407
+ const part = partOf(event);
408
+ const state = part?.state as AnyRecord | undefined;
409
+ const color = kindColor(kind);
410
+
411
+ if (!event) {
412
+ return (
413
+ <div
414
+ style={{
415
+ display: 'flex',
416
+ alignItems: 'center',
417
+ gap: 8,
418
+ padding: '10px 14px',
419
+ borderRadius: 0,
420
+ minWidth: 0,
421
+ overflow: 'hidden',
422
+ border: '1px dashed #1f2937',
423
+ backgroundColor: '#0d1117',
424
+ fontFamily: theme.fonts.body,
425
+ fontSize: 12,
426
+ }}
427
+ >
428
+ <span style={{ color: '#4b5563', fontStyle: 'italic' }}>no raw payload</span>
429
+ </div>
430
+ );
431
+ }
432
+
433
+ const body = ((): React.ReactNode => {
434
+ switch (event.type) {
435
+ case 'session.created.1': {
436
+ const info = d.info as AnyRecord | undefined;
437
+ return (
438
+ <MetaRows
439
+ rows={[
440
+ ['agent', info?.agent as string],
441
+ ['model', ((info?.model as AnyRecord | undefined)?.id as string) ?? ((info?.model as AnyRecord | undefined)?.modelID as string)],
442
+ ['directory', info?.directory as string],
443
+ ['version', info?.version as string],
444
+ ['slug', info?.slug as string],
445
+ ]}
446
+ />
447
+ );
448
+ }
449
+ case 'session.updated.1': {
450
+ const info = d.info as AnyRecord | undefined;
451
+ return (
452
+ <MetaRows
453
+ rows={[
454
+ ['cost', typeof info?.cost === 'number' ? `$${info.cost.toFixed(4)}` : undefined],
455
+ ['tokens', info?.tokens as AnyRecord | undefined],
456
+ ['title', info?.title as string],
457
+ ]}
458
+ />
459
+ );
460
+ }
461
+ case 'message.updated.1': {
462
+ const info = d.info as AnyRecord | undefined;
463
+ return (
464
+ <MetaRows
465
+ rows={[
466
+ ['role', info?.role as string],
467
+ ['agent', info?.agent as string],
468
+ ['model', info?.model as AnyRecord | undefined],
469
+ ['tokens', info?.tokens as AnyRecord | undefined],
470
+ ['messageID', info?.id as string],
471
+ ]}
472
+ />
473
+ );
474
+ }
475
+ case 'message.removed.1':
476
+ return <MetaRows rows={[['messageID', d.messageID as string]]} />;
477
+ case 'message.part.updated.1': {
478
+ switch (part?.type) {
479
+ case 'text':
480
+ case 'reasoning':
481
+ return <TextView text={part?.text as string | undefined} />;
482
+ case 'tool': {
483
+ const todos = (state?.input as AnyRecord | undefined)?.['todos'];
484
+ return (
485
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
486
+ <div style={{ fontSize: 12, color: '#9ca3af' }}>
487
+ callID {String(part?.callID ?? '')} · {String(state?.status ?? '')}
488
+ </div>
489
+ {Array.isArray(todos) ? <TodosView todos={todos} /> : null}
490
+ <ToolIO
491
+ input={state?.input}
492
+ output={state?.status === 'error' ? state?.error : state?.output}
493
+ />
494
+ </div>
495
+ );
496
+ }
497
+ case 'step-start':
498
+ return <MetaRows rows={[['snapshot', part?.snapshot as string]]} />;
499
+ case 'step-finish':
500
+ return (
501
+ <MetaRows
502
+ rows={[
503
+ ['reason', part?.reason as string],
504
+ ['cost', typeof part?.cost === 'number' ? `$${part.cost.toFixed(4)}` : undefined],
505
+ ['tokens', part?.tokens as AnyRecord | undefined],
506
+ ]}
507
+ />
508
+ );
509
+ case 'patch':
510
+ return (
511
+ <MetaRows
512
+ rows={[
513
+ ['files', part?.files as string[] | undefined],
514
+ ['hash', part?.hash as string],
515
+ ]}
516
+ />
517
+ );
518
+ case 'compaction':
519
+ return (
520
+ <MetaRows
521
+ rows={[
522
+ ['auto', typeof part?.auto === 'boolean' ? String(part.auto) : undefined],
523
+ ['overflow', typeof part?.overflow === 'boolean' ? String(part.overflow) : undefined],
524
+ ]}
525
+ />
526
+ );
527
+ default:
528
+ return <JsonView value={d} />;
529
+ }
530
+ }
531
+ default:
532
+ // Durable-agent raw blobs (Cursor store.db, etc.) have no V1 `data`
533
+ // envelope — dump the whole payload so the expanded card isn't empty.
534
+ return <JsonView value={isV1RawEvent(event) ? d : event} />;
535
+ }
536
+ })();
537
+
538
+ return (
539
+ <div
540
+ style={{
541
+ display: 'flex',
542
+ flexDirection: 'column',
543
+ gap: expanded ? 8 : 0,
544
+ padding: '10px 14px',
545
+ borderRadius: 0,
546
+ minWidth: 0,
547
+ overflow: 'hidden',
548
+ backgroundColor: expanded ? '#111827' : '#161b26',
549
+ fontFamily: theme.fonts.body,
550
+ fontSize: 13,
551
+ }}
552
+ >
553
+ <button
554
+ onClick={onToggle}
555
+ aria-expanded={expanded}
556
+ style={{
557
+ display: 'flex',
558
+ alignItems: 'center',
559
+ gap: 8,
560
+ width: '100%',
561
+ border: 'none',
562
+ background: 'none',
563
+ padding: 0,
564
+ cursor: 'pointer',
565
+ fontFamily: 'inherit',
566
+ textAlign: 'left',
567
+ }}
568
+ >
569
+ <span style={{ color: '#6b7280', fontSize: 11, whiteSpace: 'nowrap' }}>#{event.seq}</span>
570
+ <span
571
+ style={{
572
+ fontSize: 11,
573
+ fontWeight: 600,
574
+ textTransform: 'uppercase',
575
+ letterSpacing: 0.5,
576
+ color,
577
+ whiteSpace: 'nowrap',
578
+ }}
579
+ >
580
+ {kindLabel(kind)}
581
+ </span>
582
+ {runCount && runCount > 1 ? (
583
+ <span
584
+ title={`${runCount} separate runs of this command`}
585
+ style={{
586
+ fontSize: 11,
587
+ fontWeight: 700,
588
+ color: '#f59e0b',
589
+ whiteSpace: 'nowrap',
590
+ }}
591
+ >
592
+ ×{runCount}
593
+ </span>
594
+ ) : null}
595
+ {snapshotCount && snapshotCount > 1 ? (
596
+ <span
597
+ title={`${snapshotCount} raw event snapshots collapsed into this row`}
598
+ style={{
599
+ fontSize: 11,
600
+ fontWeight: 600,
601
+ color: '#60a5fa',
602
+ whiteSpace: 'nowrap',
603
+ }}
604
+ >
605
+ {snapshotCount} events
606
+ </span>
607
+ ) : null}
608
+ <span
609
+ style={{
610
+ flex: 1,
611
+ overflow: 'hidden',
612
+ textOverflow: 'ellipsis',
613
+ whiteSpace: 'nowrap',
614
+ color: '#e5e7eb',
615
+ }}
616
+ >
617
+ {describe(event)}
618
+ </span>
619
+ <span style={{ color: '#6b7280', fontSize: 11, whiteSpace: 'nowrap' }}>
620
+ {formatTime(eventTimestamp(event))}
621
+ </span>
622
+ </button>
623
+ {expanded ? body : null}
624
+ </div>
625
+ );
626
+ }
627
+
628
+ function prettifyEventType(t: string | undefined): string {
629
+ return (t ?? '').replace(/-/g, ' ');
630
+ }
631
+
632
+ function normalizedDescribe(n: NormalizedFeedEvent): string {
633
+ if (n.toolName) return [n.toolName, n.operation].filter(Boolean).join(' · ');
634
+ if (n.operation) return n.operation;
635
+ const d = n.data as AnyRecord | undefined;
636
+ if (typeof d?.message === 'string') return d.message.slice(0, 90);
637
+ return prettifyEventType(n.eventType);
638
+ }
639
+
640
+ function repoLabel(repo: {
641
+ root?: string;
642
+ owner?: string;
643
+ repo?: string;
644
+ remoteUrl?: string;
645
+ }): string {
646
+ const identity = repo.owner && repo.repo ? `${repo.owner}/${repo.repo}` : repo.repo ?? '';
647
+ return identity && repo.root ? `${identity} @ ${repo.root}` : repo.root ?? '';
648
+ }
649
+
650
+ function NormalizedEventCard({
651
+ event,
652
+ seq,
653
+ color,
654
+ expanded,
655
+ onToggle,
656
+ }: {
657
+ event: NormalizedFeedEvent;
658
+ seq: number;
659
+ color: string;
660
+ expanded: boolean;
661
+ onToggle: () => void;
662
+ }) {
663
+ const { theme } = useTheme();
664
+ return (
665
+ <div
666
+ style={{
667
+ display: 'flex',
668
+ flexDirection: 'column',
669
+ gap: expanded ? 8 : 0,
670
+ padding: '10px 14px',
671
+ borderRadius: 0,
672
+ minWidth: 0,
673
+ overflow: 'hidden',
674
+ backgroundColor: expanded ? '#0d1520' : '#101722',
675
+ fontFamily: theme.fonts.body,
676
+ fontSize: 13,
677
+ }}
678
+ >
679
+ <button
680
+ onClick={onToggle}
681
+ aria-expanded={expanded}
682
+ style={{
683
+ display: 'flex',
684
+ alignItems: 'center',
685
+ gap: 8,
686
+ width: '100%',
687
+ border: 'none',
688
+ background: 'none',
689
+ padding: 0,
690
+ cursor: 'pointer',
691
+ fontFamily: 'inherit',
692
+ textAlign: 'left',
693
+ }}
694
+ >
695
+ <span style={{ color: '#6b7280', fontSize: 11, whiteSpace: 'nowrap' }}>#{seq}</span>
696
+ <span
697
+ style={{
698
+ fontSize: 11,
699
+ fontWeight: 600,
700
+ textTransform: 'uppercase',
701
+ letterSpacing: 0.5,
702
+ color,
703
+ whiteSpace: 'nowrap',
704
+ }}
705
+ >
706
+ {prettifyEventType(event.eventType)}
707
+ </span>
708
+ <span
709
+ style={{
710
+ flex: 1,
711
+ overflow: 'hidden',
712
+ textOverflow: 'ellipsis',
713
+ whiteSpace: 'nowrap',
714
+ color: '#e5e7eb',
715
+ }}
716
+ >
717
+ {normalizedDescribe(event)}
718
+ </span>
719
+ <span style={{ color: '#6b7280', fontSize: 11, whiteSpace: 'nowrap' }}>
720
+ {formatTime(event.timestamp)}
721
+ </span>
722
+ </button>
723
+ {expanded ? (
724
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
725
+ <MetaRows
726
+ rows={[
727
+ ['eventType', event.eventType],
728
+ ['operation', event.operation],
729
+ ['toolName', event.toolName],
730
+ ['sessionId', event.sessionId],
731
+ ['workingDir', event.normalizedWorkingDirectory],
732
+ ['repository', event.repository ? repoLabel(event.repository) : undefined],
733
+ ]}
734
+ />
735
+ {event.files && event.files.length > 0 ? (
736
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
737
+ <div style={{ fontSize: 11, color: '#6b7280' }}>files ({event.files.length})</div>
738
+ {event.files.map((file, i) => (
739
+ <FileRow key={i} file={file} />
740
+ ))}
741
+ </div>
742
+ ) : null}
743
+ {event.toolInput !== undefined ? (
744
+ <ToolIO input={event.toolInput} output={event.toolOutput} />
745
+ ) : null}
746
+ </div>
747
+ ) : null}
748
+ </div>
749
+ );
750
+ }
751
+
752
+ function FileRow({ file }: { file: NormalizedPathInfo }) {
753
+ const { theme } = useTheme();
754
+ return (
755
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
756
+ <div
757
+ style={{
758
+ color: '#93c5fd',
759
+ fontSize: 12,
760
+ wordBreak: 'break-word',
761
+ fontFamily: theme.fonts.monospace,
762
+ }}
763
+ >
764
+ {file.displayPath}
765
+ </div>
766
+ <div style={{ color: '#6b7280', fontSize: 11, wordBreak: 'break-word' }}>
767
+ {file.context}
768
+ {file.repository
769
+ ? ` · ${file.repository.gitRoot} → ${file.repository.relativePath}`
770
+ : ` · ${file.absolutePath}`}
771
+ </div>
772
+ </div>
773
+ );
774
+ }
775
+
776
+ const OPERATION_COLORS: Record<string, string> = {
777
+ starting: '#6b7280',
778
+ prompting: '#a78bfa',
779
+ reading: '#a855f7',
780
+ grepping: '#e879f9',
781
+ globbing: '#14b8a6',
782
+ listing: '#f97316',
783
+ editing: '#22c55e',
784
+ tool: '#3b82f6',
785
+ errored: '#ef4444',
786
+ waiting: '#9ca3af',
787
+ finished: '#10b981',
788
+ compacting: '#ec4899',
789
+ subagent: '#f59e0b',
790
+ };
791
+
792
+ function AccumulatedEventCard({
793
+ event,
794
+ seq,
795
+ dropReason,
796
+ expanded,
797
+ onToggle,
798
+ }: {
799
+ event: AgentSessionEvent | null;
800
+ seq: number;
801
+ dropReason?: string;
802
+ expanded: boolean;
803
+ onToggle: () => void;
804
+ }) {
805
+ const { theme } = useTheme();
806
+ if (!event) {
807
+ return (
808
+ <div
809
+ style={{
810
+ display: 'flex',
811
+ alignItems: 'center',
812
+ gap: 8,
813
+ padding: '10px 14px',
814
+ borderRadius: 0,
815
+ minWidth: 0,
816
+ overflow: 'hidden',
817
+ border: '1px dashed #1f2937',
818
+ backgroundColor: '#0d1117',
819
+ fontFamily: theme.fonts.body,
820
+ fontSize: 12,
821
+ }}
822
+ >
823
+ <span style={{ color: '#374151', whiteSpace: 'nowrap' }}>#{seq}</span>
824
+ <span style={{ color: dropReason ? '#b45309' : '#4b5563', fontStyle: 'italic' }}>
825
+ {dropReason ?? 'no UI event (accumulator dropped)'}
826
+ </span>
827
+ </div>
828
+ );
829
+ }
830
+
831
+ const opColor = OPERATION_COLORS[event.operation] ?? '#6b7280';
832
+
833
+ return (
834
+ <div
835
+ style={{
836
+ display: 'flex',
837
+ flexDirection: 'column',
838
+ gap: expanded ? 8 : 0,
839
+ padding: '10px 14px',
840
+ borderRadius: 0,
841
+ minWidth: 0,
842
+ overflow: 'hidden',
843
+ backgroundColor: expanded ? '#121a13' : '#141b16',
844
+ fontFamily: theme.fonts.body,
845
+ fontSize: 13,
846
+ }}
847
+ >
848
+ <button
849
+ onClick={onToggle}
850
+ aria-expanded={expanded}
851
+ style={{
852
+ display: 'flex',
853
+ alignItems: 'center',
854
+ gap: 8,
855
+ width: '100%',
856
+ border: 'none',
857
+ background: 'none',
858
+ padding: 0,
859
+ cursor: 'pointer',
860
+ fontFamily: 'inherit',
861
+ textAlign: 'left',
862
+ }}
863
+ >
864
+ <span
865
+ style={{
866
+ width: 8,
867
+ height: 8,
868
+ borderRadius: '50%',
869
+ flexShrink: 0,
870
+ backgroundColor: event.sessionColor,
871
+ }}
872
+ />
873
+ <span style={{ color: '#6b7280', fontSize: 11, whiteSpace: 'nowrap' }}>#{seq}</span>
874
+ <span
875
+ style={{
876
+ fontSize: 11,
877
+ fontWeight: 600,
878
+ textTransform: 'uppercase',
879
+ letterSpacing: 0.5,
880
+ color: opColor,
881
+ whiteSpace: 'nowrap',
882
+ }}
883
+ >
884
+ {event.operation}
885
+ </span>
886
+ <span
887
+ style={{
888
+ flex: 1,
889
+ overflow: 'hidden',
890
+ textOverflow: 'ellipsis',
891
+ whiteSpace: 'nowrap',
892
+ color: '#e5e7eb',
893
+ }}
894
+ >
895
+ {event.description}
896
+ </span>
897
+ <span style={{ color: '#6b7280', fontSize: 11, whiteSpace: 'nowrap' }}>
898
+ {formatTime(event.timestamp)}
899
+ </span>
900
+ </button>
901
+ {expanded ? (
902
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
903
+ <MetaRows
904
+ rows={[
905
+ ['operation', event.operation],
906
+ ['session', event.sessionName],
907
+ ['contextTokens', event.contextTokens],
908
+ ['toolName', event.toolName],
909
+ ['subagent', event.subagentType],
910
+ ['childSessionId', event.childSessionId],
911
+ ]}
912
+ />
913
+ <div style={{ fontSize: 12, color: '#6b7280' }}>{event.description}</div>
914
+ {event.files.length > 0 ? <PathList label="files" paths={event.files} /> : null}
915
+ {event.dependencies.length > 0 ? (
916
+ <PathList label="dependencies" paths={event.dependencies} />
917
+ ) : null}
918
+ {event.layers.length > 0 ? (
919
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
920
+ <div style={{ fontSize: 11, color: '#6b7280' }}>layers</div>
921
+ {event.layers.map((layer) => (
922
+ <div
923
+ key={layer.id}
924
+ style={{ display: 'flex', gap: 6, alignItems: 'center', fontSize: 12 }}
925
+ >
926
+ <span
927
+ style={{
928
+ width: 8,
929
+ height: 8,
930
+ borderRadius: '50%',
931
+ backgroundColor: layer.color,
932
+ opacity: 0.8,
933
+ }}
934
+ />
935
+ <span style={{ color: '#d1d5db' }}>
936
+ {layer.name} ({layer.items.length})
937
+ </span>
938
+ </div>
939
+ ))}
940
+ </div>
941
+ ) : null}
942
+ </div>
943
+ ) : null}
944
+ </div>
945
+ );
946
+ }
947
+
948
+ function PathList({ label, paths }: { label: string; paths: NormalizedPathInfo[] }) {
949
+ const { theme } = useTheme();
950
+ return (
951
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
952
+ <div style={{ fontSize: 11, color: '#6b7280' }}>{label}</div>
953
+ {paths.map((p, i) => (
954
+ <div
955
+ key={i}
956
+ style={{ color: '#93c5fd', fontSize: 11.5, fontFamily: theme.fonts.monospace, wordBreak: 'break-word' }}
957
+ >
958
+ {p.displayPath}
959
+ </div>
960
+ ))}
961
+ </div>
962
+ );
963
+ }
964
+
965
+ function TextView({ text }: { text?: string }) {
966
+ if (!text) return <div style={{ color: '#6b7280', fontSize: 12 }}>(empty)</div>;
967
+ return (
968
+ <div
969
+ style={{
970
+ color: '#d1d5db',
971
+ fontSize: 12.5,
972
+ lineHeight: 1.5,
973
+ maxHeight: 120,
974
+ overflowY: 'auto',
975
+ whiteSpace: 'pre-wrap',
976
+ wordBreak: 'break-word',
977
+ }}
978
+ >
979
+ {text}
980
+ </div>
981
+ );
982
+ }
983
+
984
+ const TODO_STATUS_COLORS: Record<string, string> = {
985
+ completed: '#22c55e',
986
+ in_progress: '#f59e0b',
987
+ pending: '#6b7280',
988
+ };
989
+
990
+ function TodosView({ todos }: { todos: Array<Record<string, unknown>> }) {
991
+ return (
992
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
993
+ <div style={{ fontSize: 11, color: '#6b7280' }}>todos ({todos.length})</div>
994
+ {todos.map((t, i) => (
995
+ <div key={i} style={{ display: 'flex', gap: 8, fontSize: 12, alignItems: 'baseline' }}>
996
+ <span
997
+ style={{
998
+ color: TODO_STATUS_COLORS[String(t.status ?? '')] ?? '#6b7280',
999
+ fontWeight: 600,
1000
+ minWidth: 84,
1001
+ whiteSpace: 'nowrap',
1002
+ }}
1003
+ >
1004
+ {String(t.status ?? '')}
1005
+ </span>
1006
+ <span style={{ color: '#d1d5db', wordBreak: 'break-word' }}>{String(t.content ?? '')}</span>
1007
+ {typeof t.priority === 'string' && t.priority ? (
1008
+ <span style={{ color: '#6b7280', whiteSpace: 'nowrap' }}>· {t.priority}</span>
1009
+ ) : null}
1010
+ </div>
1011
+ ))}
1012
+ </div>
1013
+ );
1014
+ }
1015
+
1016
+ function ToolIO({ input, output }: { input?: unknown; output?: unknown }) { const summary = toolSummary(input);
1017
+ return (
1018
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
1019
+ {summary ? (
1020
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
1021
+ <span style={{ fontSize: 11, color: '#60a5fa', fontWeight: 600 }}>IN</span>
1022
+ <code style={{ color: '#93c5fd', fontSize: 12, wordBreak: 'break-word' }}>{summary}</code>
1023
+ </div>
1024
+ ) : null}
1025
+ {input !== undefined && input !== null && typeof input === 'object' ? (
1026
+ <CodeBlock value={JSON.stringify(input, null, 2)} />
1027
+ ) : null}
1028
+ {output !== undefined && output !== null ? (
1029
+ <>
1030
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
1031
+ <span style={{ fontSize: 11, color: '#34d399', fontWeight: 600 }}>OUT</span>
1032
+ <span style={{ fontSize: 11, color: '#6b7280' }}>
1033
+ {typeof output === 'string' ? `${output.length.toLocaleString()} chars` : 'object'}
1034
+ </span>
1035
+ </div>
1036
+ <CodeBlock value={typeof output === 'string' ? output : JSON.stringify(output, null, 2)} />
1037
+ </>
1038
+ ) : null}
1039
+ </div>
1040
+ );
1041
+ }
1042
+
1043
+ function CodeBlock({ value }: { value: string }) {
1044
+ return (
1045
+ <pre
1046
+ style={{
1047
+ margin: 0,
1048
+ padding: 8,
1049
+ borderRadius: 6,
1050
+ backgroundColor: '#0b1220',
1051
+ color: '#9ca3af',
1052
+ fontSize: 11.5,
1053
+ lineHeight: 1.45,
1054
+ maxHeight: 180,
1055
+ overflow: 'auto',
1056
+ whiteSpace: 'pre-wrap',
1057
+ wordBreak: 'break-word',
1058
+ }}
1059
+ >
1060
+ {value}
1061
+ </pre>
1062
+ );
1063
+ }
1064
+
1065
+ function MetaRows({ rows }: { rows: Array<[string, unknown]> }) {
1066
+ const present = rows.filter(([, v]) => v !== undefined && v !== null && v !== '');
1067
+ if (present.length === 0) return null;
1068
+ return (
1069
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
1070
+ {present.map(([label, value]) => (
1071
+ <div key={label} style={{ display: 'flex', gap: 8, fontSize: 12 }}>
1072
+ <span style={{ color: '#6b7280', minWidth: 76, whiteSpace: 'nowrap' }}>{label}</span>
1073
+ <span
1074
+ style={{
1075
+ color: '#d1d5db',
1076
+ wordBreak: 'break-word',
1077
+ whiteSpace: typeof value === 'string' ? 'pre-wrap' : 'nowrap',
1078
+ }}
1079
+ >
1080
+ {typeof value === 'string'
1081
+ ? value
1082
+ : Array.isArray(value)
1083
+ ? (value as unknown[]).join(', ')
1084
+ : JSON.stringify(value)}
1085
+ </span>
1086
+ </div>
1087
+ ))}
1088
+ </div>
1089
+ );
1090
+ }
1091
+
1092
+ function JsonView({ value }: { value: unknown }) {
1093
+ return <CodeBlock value={JSON.stringify(value, null, 2)} />;
1094
+ }
1095
+
1096
+ // =============================================================================
1097
+ // Feed
1098
+ // =============================================================================
1099
+
1100
+ type FeedItem = SessionEventFeedRow & {
1101
+ /** Distinct invocations (callIDs) merged into this row. */
1102
+ runCount?: number;
1103
+ /** Raw event snapshots collapsed into this row (pending/running/completed…). */
1104
+ snapshotCount?: number;
1105
+ /** Why the accumulator dropped this row's UI event, when it did. */
1106
+ dropReason?: string;
1107
+ /** CallIDs folded into this row — the accumulator emits at most one UI event
1108
+ * per invocation, so this stays the source of truth for `runCount`. */
1109
+ callIDs?: Set<string>;
1110
+ };
1111
+
1112
+ /**
1113
+ * Why the accumulator dropped a row's UI event, derived from the raw event
1114
+ * alone (no stream context needed). Rows that don't match a known category keep
1115
+ * the generic "dropped" label.
1116
+ */
1117
+ function dropReasonFor(raw: V1RawEvent | null): string | null {
1118
+ if (!raw) return null;
1119
+ if (raw.type === 'message.updated.1') {
1120
+ const role = (dataOf(raw).info as AnyRecord | undefined)?.role;
1121
+ if (role === 'assistant') {
1122
+ return 'assistant message — metadata only (feeds context/token tracking)';
1123
+ }
1124
+ return null; // user messages are handled by the re-save pass below
1125
+ }
1126
+ if (raw.type === 'message.removed.1') return 'message removed — no UI event';
1127
+ if (raw.type === 'message.part.updated.1') {
1128
+ switch (partOf(raw)?.type) {
1129
+ case 'reasoning':
1130
+ return 'model reasoning — not surfaced in the UI';
1131
+ case 'step-start':
1132
+ case 'step-finish':
1133
+ return 'step lifecycle — not surfaced in the UI';
1134
+ case 'text':
1135
+ return 'text content — not surfaced in the UI';
1136
+ case 'tool':
1137
+ return 'tool snapshot — folded into the tool run';
1138
+ default:
1139
+ return null;
1140
+ }
1141
+ }
1142
+ return null;
1143
+ }
1144
+
1145
+ /**
1146
+ * Annotate rows the accumulator drops with *why* they were dropped, walking in
1147
+ * stream order. opencode re-saves a user message on every change (seq 1, 6, 24,
1148
+ * 39, 51… share one messageID), so the first occurrence becomes the `prompting`
1149
+ * UI event and every re-save is deduped away — those read "re-save, ignored"
1150
+ * instead of a generic "dropped".
1151
+ */
1152
+ function annotateDrops(items: FeedItem[]): FeedItem[] {
1153
+ const seenUserMessageIDs = new Set<string>();
1154
+ return items.map((item) => {
1155
+ const raw = toRaw(item.raw);
1156
+ if (!raw) return item;
1157
+ // Track user message IDs on every occurrence — the first one is the real
1158
+ // prompt (it carries the accumulated UI event); later re-saves are deduped.
1159
+ if (raw.type === 'message.updated.1') {
1160
+ const info = dataOf(raw).info as AnyRecord | undefined;
1161
+ const messageID = info?.id;
1162
+ const role = info?.role;
1163
+ if (typeof messageID === 'string' && role === 'user') {
1164
+ if (seenUserMessageIDs.has(messageID)) {
1165
+ if (item.accumulated === null) {
1166
+ return {
1167
+ ...item,
1168
+ dropReason: 're-save of user message — already counted as a prompt',
1169
+ };
1170
+ }
1171
+ return item;
1172
+ }
1173
+ seenUserMessageIDs.add(messageID);
1174
+ }
1175
+ }
1176
+ if (item.accumulated !== null) return item;
1177
+ const reason = dropReasonFor(raw);
1178
+ if (reason) return { ...item, dropReason: reason };
1179
+ return item;
1180
+ });
1181
+ }
1182
+
1183
+ /**
1184
+ * Collapse a tool invocation's snapshots into a single row. The raw V1 stream
1185
+ * emits one part update per status flip (pending → running → completed) — and
1186
+ * those flips can be interleaved with other events (reasoning, text, session
1187
+ * bookkeeping) — so snapshots are matched by callID across the whole stream,
1188
+ * not just when consecutive. Distinct calls that repeat the same command and
1189
+ * are consecutive still fold into one ×N row.
1190
+ *
1191
+ * The merged row keeps the run's first seq as a stable key and adopts the
1192
+ * **latest** snapshot's raw/normalized payload (the completed/errored part
1193
+ * carries the tool output). The UI (accumulated) column instead prefers the
1194
+ * group's one non-null accumulated event — the accumulator dedupes to at most
1195
+ * one per invocation, and it's usually the *first* snapshot (e.g. todowrite's
1196
+ * pending emits the `tool` event; running/completed are dropped).
1197
+ */
1198
+ function groupToolRuns(items: SessionEventFeedRow[]): FeedItem[] {
1199
+ const out: FeedItem[] = [];
1200
+ const byCallID = new Map<string, FeedItem>();
1201
+ let lastGroup: FeedItem | null = null;
1202
+ let lastGroupCmd: string | null = null;
1203
+
1204
+ const adopt = (group: FeedItem, r: SessionEventFeedRow, cmd: string | null) => {
1205
+ group.snapshotCount = (group.snapshotCount ?? 1) + 1;
1206
+ group.raw = r.raw;
1207
+ group.normalized = r.normalized;
1208
+ const acc = r.accumulated;
1209
+ if (acc !== null) {
1210
+ // Prefer the run's meaningful accumulated. The pending snapshot of a
1211
+ // bash `ls` (empty input) emits a generic `tool` before the completed
1212
+ // snapshot's `listing` arrives — replace a generic `tool` with a more
1213
+ // specific operation, and never downgrade a specific one.
1214
+ if (
1215
+ group.accumulated === null ||
1216
+ (group.accumulated.operation === 'tool' && acc.operation !== 'tool')
1217
+ ) {
1218
+ group.accumulated = acc;
1219
+ }
1220
+ }
1221
+ group.type = r.type;
1222
+ lastGroup = group;
1223
+ lastGroupCmd = cmd;
1224
+ };
1225
+
1226
+ for (const r of items) {
1227
+ const raw = toRaw(r.raw);
1228
+ const isTool =
1229
+ !!raw && raw.type === 'message.part.updated.1' && partOf(raw)?.type === 'tool';
1230
+ const part = partOf(raw);
1231
+ const cmd = isTool
1232
+ ? toolSummary((part?.state as AnyRecord | undefined)?.input)
1233
+ : null;
1234
+ const callID = isTool ? (part?.callID as string | undefined) : undefined;
1235
+
1236
+ if (!isTool) {
1237
+ out.push(r);
1238
+ lastGroup = null;
1239
+ lastGroupCmd = null;
1240
+ continue;
1241
+ }
1242
+
1243
+ // Same invocation — fold this status snapshot in wherever it appears.
1244
+ const existing = callID ? byCallID.get(callID) : undefined;
1245
+ if (existing) {
1246
+ adopt(existing, r, cmd);
1247
+ continue;
1248
+ }
1249
+
1250
+ // A distinct call repeating the previous group's command folds into it.
1251
+ if (lastGroup && cmd !== null && cmd !== '' && cmd === lastGroupCmd) {
1252
+ if (callID) {
1253
+ lastGroup.callIDs = lastGroup.callIDs ?? new Set<string>();
1254
+ lastGroup.callIDs.add(callID);
1255
+ byCallID.set(callID, lastGroup);
1256
+ }
1257
+ lastGroup.runCount = lastGroup.callIDs ? lastGroup.callIDs.size : 1;
1258
+ adopt(lastGroup, r, cmd);
1259
+ continue;
1260
+ }
1261
+
1262
+ const group: FeedItem = {
1263
+ ...r,
1264
+ runCount: 1,
1265
+ snapshotCount: 1,
1266
+ callIDs: new Set(callID ? [callID] : []),
1267
+ };
1268
+ if (callID) byCallID.set(callID, group);
1269
+ out.push(group);
1270
+ lastGroup = group;
1271
+ lastGroupCmd = cmd;
1272
+ }
1273
+ return out;
1274
+ }
1275
+
1276
+ /**
1277
+ * Fold a user message's events into a single row keyed by messageID: the
1278
+ * original `message.updated.1` (which carries the `prompting` accumulated
1279
+ * event), every re-save of the same message (opencode re-saves a user message
1280
+ * on each change), and the text part(s) that carry its content (the
1281
+ * `notification` events sharing the same messageID). They all resolve to the
1282
+ * one `prompting` accumulated event, so they render as one row with a snapshot
1283
+ * count instead of a wall of "re-save" rows.
1284
+ */
1285
+ function groupPromptParts(items: FeedItem[]): FeedItem[] {
1286
+ const out: FeedItem[] = [];
1287
+ const byMessageID = new Map<string, FeedItem>();
1288
+ for (const item of items) {
1289
+ const raw = toRaw(item.raw);
1290
+ if (!raw) {
1291
+ out.push(item);
1292
+ continue;
1293
+ }
1294
+ if (raw.type === 'message.updated.1') {
1295
+ const info = dataOf(raw).info as AnyRecord | undefined;
1296
+ const messageID = info?.id;
1297
+ const role = info?.role;
1298
+ if (typeof messageID === 'string' && role === 'user') {
1299
+ const existing = byMessageID.get(messageID);
1300
+ if (existing) {
1301
+ // re-save of the same user message — fold into its prompt row
1302
+ existing.snapshotCount = (existing.snapshotCount ?? 1) + 1;
1303
+ if (existing.accumulated === null && item.accumulated !== null) {
1304
+ existing.accumulated = item.accumulated;
1305
+ }
1306
+ continue;
1307
+ }
1308
+ byMessageID.set(messageID, item);
1309
+ }
1310
+ out.push(item);
1311
+ continue;
1312
+ }
1313
+ if (raw.type === 'message.part.updated.1') {
1314
+ const part = partOf(raw);
1315
+ const messageID = part?.messageID;
1316
+ if (part?.type === 'text' && typeof messageID === 'string') {
1317
+ const existing = byMessageID.get(messageID);
1318
+ if (existing) {
1319
+ existing.snapshotCount = (existing.snapshotCount ?? 1) + 1;
1320
+ if (existing.accumulated === null && item.accumulated !== null) {
1321
+ existing.accumulated = item.accumulated;
1322
+ }
1323
+ continue; // absorb the text part into its message row
1324
+ }
1325
+ }
1326
+ out.push(item);
1327
+ continue;
1328
+ }
1329
+ out.push(item);
1330
+ }
1331
+ return out;
1332
+ }
1333
+
1334
+ export function SessionEventFeed({ title, rows }: SessionEventFeedProps) {
1335
+ const { theme } = useTheme();
1336
+ const items = useMemo(() => [...rows].sort((a, b) => a.seq - b.seq), [rows]);
1337
+
1338
+ const [active, setActive] = useState<string>('activity');
1339
+ const [groupTools, setGroupTools] = useState(true);
1340
+
1341
+ const displayItems = useMemo<FeedItem[]>(
1342
+ () => annotateDrops(groupPromptParts(groupTools ? groupToolRuns(items) : items)),
1343
+ [items, groupTools],
1344
+ );
1345
+ const seqs = useMemo(() => displayItems.map((r) => r.seq), [displayItems]);
1346
+ const { collapsed, toggle, setAll } = useCollapse(seqs, true);
1347
+
1348
+ const activePreset = PRESETS.find((p) => p.id === active) ?? PRESETS[0];
1349
+ // "Generic tools" surfaces the improvement backlog: tool rows whose
1350
+ // accumulated operation is still the generic `tool` fallback (e.g. bash
1351
+ // commands that haven't been given a listing/reading/… type yet).
1352
+ const [unprocessedOnly, setUnprocessedOnly] = useState(false);
1353
+ const isVisible = (r: FeedItem) =>
1354
+ unprocessedOnly
1355
+ ? r.accumulated?.operation === 'tool'
1356
+ : activePreset.include.includes(eventCategory(toRaw(r.raw)));
1357
+ const visibleEvents = displayItems.filter(isVisible);
1358
+
1359
+ const presetCounts = useMemo(() => {
1360
+ const map = new Map<string, number>();
1361
+ for (const preset of PRESETS) {
1362
+ map.set(preset.id, displayItems.filter((r) => preset.include.includes(eventCategory(toRaw(r.raw)))).length);
1363
+ }
1364
+ return map;
1365
+ }, [displayItems]);
1366
+
1367
+ const unprocessedCount = useMemo(
1368
+ () => displayItems.filter((r) => r.accumulated?.operation === 'tool').length,
1369
+ [displayItems],
1370
+ );
1371
+
1372
+ const expandedCount = visibleEvents.filter((r) => !collapsed.has(r.seq)).length;
1373
+
1374
+ return (
1375
+ <div style={{ padding: 24, backgroundColor: '#0d1117', minHeight: '100vh' }}>
1376
+ <div
1377
+ style={{
1378
+ display: 'flex',
1379
+ alignItems: 'baseline',
1380
+ gap: 12,
1381
+ marginBottom: 4,
1382
+ fontFamily: theme.fonts.body,
1383
+ }}
1384
+ >
1385
+ <h1 style={{ margin: 0, color: '#f3f4f6', fontSize: 18, fontWeight: 600 }}>{title}</h1>
1386
+ <span style={{ color: '#6b7280', fontSize: 12 }}>
1387
+ {items.length} raw events · {visibleEvents.length} visible · {expandedCount} expanded
1388
+ </span>
1389
+ </div>
1390
+ <div
1391
+ style={{
1392
+ display: 'flex',
1393
+ gap: 6,
1394
+ flexWrap: 'wrap',
1395
+ marginBottom: 16,
1396
+ fontFamily: theme.fonts.body,
1397
+ }}
1398
+ >
1399
+ <FilterChip label="all" active={active === 'all'} onClick={() => setActive('all')} />
1400
+ {PRESETS.filter((p) => p.id !== 'all').map((preset) => (
1401
+ <FilterChip
1402
+ key={preset.id}
1403
+ label={`${preset.label} (${presetCounts.get(preset.id) ?? 0})`}
1404
+ active={active === preset.id}
1405
+ onClick={() => setActive(preset.id)}
1406
+ />
1407
+ ))}
1408
+ <span style={{ flex: 1 }} />
1409
+ <FilterChip
1410
+ label={`Generic tools (${unprocessedCount})`}
1411
+ active={unprocessedOnly}
1412
+ onClick={() => setUnprocessedOnly((v) => !v)}
1413
+ />
1414
+ <FilterChip
1415
+ label={groupTools ? 'Group tool runs: on' : 'Group tool runs: off'}
1416
+ active={groupTools}
1417
+ onClick={() => setGroupTools((v) => !v)}
1418
+ />
1419
+ <FilterChip label="Expand all" active={false} onClick={() => setAll(false)} />
1420
+ <FilterChip label="Collapse all" active={false} onClick={() => setAll(true)} />
1421
+ </div>
1422
+ <ColumnHeaders />
1423
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
1424
+ {displayItems.map((r) => (
1425
+ <EventRow
1426
+ key={r.seq}
1427
+ row={r}
1428
+ sessionTitle={title}
1429
+ runCount={r.runCount}
1430
+ snapshotCount={r.snapshotCount}
1431
+ visible={isVisible(r)}
1432
+ expanded={!collapsed.has(r.seq)}
1433
+ onToggle={() => toggle(r.seq)}
1434
+ />
1435
+ ))}
1436
+ </div>
1437
+ </div>
1438
+ );
1439
+ }
1440
+
1441
+ export function SessionEventFeedGrouped({ title, rows }: SessionEventFeedGroupedProps) {
1442
+ const { theme } = useTheme();
1443
+ const items = useMemo<FeedItem[]>(
1444
+ () => annotateDrops(groupPromptParts([...rows].sort((a, b) => a.seq - b.seq))),
1445
+ [rows],
1446
+ );
1447
+ const byKind = useMemo(() => {
1448
+ const map = new Map<string, SessionEventFeedRow[]>();
1449
+ for (const r of items) {
1450
+ const kind = eventKind(toRaw(r.raw));
1451
+ map.set(kind, [...(map.get(kind) ?? []), r]);
1452
+ }
1453
+ return map;
1454
+ }, [items]);
1455
+
1456
+ return (
1457
+ <div style={{ padding: 24, backgroundColor: '#0d1117', minHeight: '100vh' }}>
1458
+ {title ? (
1459
+ <h1 style={{ margin: '0 0 12px', color: '#f3f4f6', fontSize: 18, fontWeight: 600, fontFamily: theme.fonts.body }}>
1460
+ {title}
1461
+ </h1>
1462
+ ) : null}
1463
+ <ColumnHeaders />
1464
+ {Array.from(byKind.entries())
1465
+ .sort((a, b) => b[1].length - a[1].length)
1466
+ .map(([kind, group]) => (
1467
+ <GroupSection key={kind} kind={kind} group={group} sessionTitle={title} />
1468
+ ))}
1469
+ </div>
1470
+ );
1471
+ }
1472
+
1473
+ function GroupSection({
1474
+ kind,
1475
+ group,
1476
+ sessionTitle,
1477
+ }: {
1478
+ kind: string;
1479
+ group: SessionEventFeedRow[];
1480
+ sessionTitle?: string;
1481
+ }) {
1482
+ const { theme } = useTheme();
1483
+ const seqs = useMemo(() => group.map((r) => r.seq), [group]);
1484
+ const { collapsed, toggle, setAll } = useCollapse(seqs, true);
1485
+
1486
+ return (
1487
+ <div style={{ marginBottom: 20 }}>
1488
+ <div
1489
+ style={{
1490
+ display: 'flex',
1491
+ alignItems: 'center',
1492
+ gap: 8,
1493
+ marginBottom: 8,
1494
+ fontFamily: theme.fonts.body,
1495
+ }}
1496
+ >
1497
+ <span style={{ color: kindColor(kind), fontWeight: 600, fontSize: 13 }}>
1498
+ {kindLabel(kind)}
1499
+ </span>
1500
+ <span style={{ color: '#6b7280', fontSize: 12 }}>{group.length}</span>
1501
+ <span style={{ flex: 1 }} />
1502
+ <FilterChip label="Expand all" active={false} onClick={() => setAll(false)} />
1503
+ <FilterChip label="Collapse all" active={false} onClick={() => setAll(true)} />
1504
+ </div>
1505
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
1506
+ {group.map((r) => (
1507
+ <EventRow
1508
+ key={r.seq}
1509
+ row={r}
1510
+ sessionTitle={sessionTitle}
1511
+ visible
1512
+ expanded={!collapsed.has(r.seq)}
1513
+ onToggle={() => toggle(r.seq)}
1514
+ />
1515
+ ))}
1516
+ </div>
1517
+ </div>
1518
+ );
1519
+ }
1520
+
1521
+ function ColumnHeaders() {
1522
+ const { theme } = useTheme();
1523
+ return (
1524
+ <div
1525
+ style={{
1526
+ position: 'sticky',
1527
+ top: 0,
1528
+ zIndex: 10,
1529
+ display: 'grid',
1530
+ gridTemplateColumns: '3px 1fr 1fr 1fr 28px',
1531
+ gap: 8,
1532
+ padding: '8px 14px',
1533
+ marginBottom: 8,
1534
+ backgroundColor: '#0d1117',
1535
+ borderBottom: '1px solid #1f2937',
1536
+ fontFamily: theme.fonts.body,
1537
+ }}
1538
+ >
1539
+ <span />
1540
+ <HeaderLabel>Raw event</HeaderLabel>
1541
+ <HeaderLabel>Repo-normalized</HeaderLabel>
1542
+ <HeaderLabel>UI event (accumulated)</HeaderLabel>
1543
+ <span />
1544
+ </div>
1545
+ );
1546
+ }
1547
+
1548
+ function HeaderLabel({ children }: { children: React.ReactNode }) {
1549
+ return (
1550
+ <span
1551
+ style={{
1552
+ fontSize: 11,
1553
+ fontWeight: 600,
1554
+ textTransform: 'uppercase',
1555
+ letterSpacing: 0.5,
1556
+ color: '#9ca3af',
1557
+ }}
1558
+ >
1559
+ {children}
1560
+ </span>
1561
+ );
1562
+ }
1563
+
1564
+ /**
1565
+ * Assemble a self-contained, discussion-ready text block for one row: enough
1566
+ * identifiers and detail (session, seq, raw → normalized → UI) that the block
1567
+ * carries the whole issue without the viewer open. Paste this into a chat, an
1568
+ * agent conversation, or an issue to talk about the event.
1569
+ */
1570
+ function buildRowContext(row: SessionEventFeedRow, sessionTitle?: string): string {
1571
+ const raw = toRaw(row.raw);
1572
+ const d = dataOf(raw);
1573
+ const part = partOf(raw);
1574
+ const state = part?.state as AnyRecord | undefined;
1575
+ const normalized = toNormalized(row.normalized);
1576
+ const acc = row.accumulated;
1577
+
1578
+ const lines: string[] = [];
1579
+ const runCount = (row as FeedItem).runCount;
1580
+ const snapshotCount = (row as FeedItem).snapshotCount;
1581
+ const runLabel = runCount && runCount > 1 ? ` ×${runCount} runs` : '';
1582
+ const snapLabel = snapshotCount && snapshotCount > 1 ? ` · ${snapshotCount} snapshots collapsed` : '';
1583
+ lines.push(`## Event #${row.seq} — ${kindLabel(eventKind(raw))}${runLabel}${snapLabel}`);
1584
+ lines.push(`_${formatTime(eventTimestamp(raw))} · \`${row.type}\`_`);
1585
+ lines.push('');
1586
+
1587
+ if (sessionTitle) lines.push(`**Session:** ${sessionTitle}`);
1588
+ const sessionID =
1589
+ (d.sessionID as string | undefined) ?? normalized?.sessionId ?? acc?.sessionId;
1590
+ if (sessionID) lines.push(`Session id: \`${sessionID}\``);
1591
+ lines.push('');
1592
+
1593
+ lines.push('**Raw:**');
1594
+ if (!raw) {
1595
+ lines.push('- no raw payload');
1596
+ } else {
1597
+ lines.push(`- event type: \`${raw.type}\``);
1598
+ if (typeof part?.type === 'string') lines.push(`- part type: \`${part.type}\``);
1599
+ if (typeof part?.callID === 'string') lines.push(`- callID: \`${part.callID}\``);
1600
+ if (typeof part?.tool === 'string') lines.push(`- tool: \`${part.tool}\``);
1601
+ if (typeof state?.status === 'string') lines.push(`- status: \`${state.status}\``);
1602
+ const inSummary = toolSummary(state?.input);
1603
+ if (inSummary) lines.push(`- input: \`${inSummary}\``);
1604
+ const out = state?.status === 'error' ? state?.error : state?.output;
1605
+ if (typeof out === 'string') {
1606
+ const preview =
1607
+ out.length > 500 ? `${out.slice(0, 500)}\n…[+${out.length - 500} chars]` : out;
1608
+ lines.push(`- output (${out.length.toLocaleString()} chars):\n\`\`\`\n${preview}\n\`\`\``);
1609
+ }
1610
+ }
1611
+ lines.push('');
1612
+
1613
+ if (normalized && typeof normalized.eventType === 'string') {
1614
+ lines.push('**Normalized:**');
1615
+ lines.push(`- eventType: \`${normalized.eventType}\``);
1616
+ if (typeof normalized.operation === 'string' && normalized.operation) {
1617
+ lines.push(`- operation: \`${normalized.operation}\``);
1618
+ }
1619
+ if (typeof normalized.toolName === 'string' && normalized.toolName) {
1620
+ lines.push(`- toolName: \`${normalized.toolName}\``);
1621
+ }
1622
+ const files = normalized.files ?? [];
1623
+ if (files.length > 0) {
1624
+ lines.push(`- files: ${files.map((f) => `\`${f.displayPath}\``).join(', ')}`);
1625
+ }
1626
+ lines.push('');
1627
+ }
1628
+
1629
+ if (acc) {
1630
+ lines.push('**UI event (accumulated):**');
1631
+ lines.push(`- operation: \`${acc.operation}\``);
1632
+ lines.push(`- description: ${acc.description}`);
1633
+ if (typeof acc.contextTokens === 'number' && acc.contextTokens > 0) {
1634
+ lines.push(`- contextTokens: ${acc.contextTokens}`);
1635
+ }
1636
+ if (acc.files.length > 0) {
1637
+ lines.push(`- files: ${acc.files.map((f) => f.displayPath).join(', ')}`);
1638
+ }
1639
+ } else {
1640
+ const dropReason = (row as FeedItem).dropReason;
1641
+ lines.push(`**UI event:** ${dropReason ?? 'none (accumulator dropped this event)'}`);
1642
+ }
1643
+
1644
+ return lines.join('\n');
1645
+ }
1646
+
1647
+ async function copyRowContext(row: SessionEventFeedRow, sessionTitle?: string): Promise<boolean> {
1648
+ const text = buildRowContext(row, sessionTitle);
1649
+ try {
1650
+ await navigator.clipboard.writeText(text);
1651
+ return true;
1652
+ } catch (err) {
1653
+ console.error('[session-events] copy failed:', err);
1654
+ return false;
1655
+ }
1656
+ }
1657
+
1658
+ function EventRow({
1659
+ row,
1660
+ sessionTitle,
1661
+ runCount,
1662
+ snapshotCount,
1663
+ visible,
1664
+ expanded,
1665
+ onToggle,
1666
+ }: {
1667
+ row: SessionEventFeedRow;
1668
+ sessionTitle?: string;
1669
+ runCount?: number;
1670
+ snapshotCount?: number;
1671
+ visible: boolean;
1672
+ expanded: boolean;
1673
+ onToggle: () => void;
1674
+ }) {
1675
+ const raw = toRaw(row.raw);
1676
+ const normalized = toNormalized(row.normalized);
1677
+ const { theme } = useTheme();
1678
+ const [copied, setCopied] = useState(false);
1679
+ if (!visible) return <FilteredOutLine raw={raw} />;
1680
+ const color = kindColor(eventKind(raw));
1681
+ const files = normalized?.files;
1682
+ const hasPaths = !!files && files.length > 0;
1683
+
1684
+ const onCopy = () => {
1685
+ void copyRowContext(row, sessionTitle).then((ok) => {
1686
+ if (!ok) return;
1687
+ setCopied(true);
1688
+ setTimeout(() => setCopied(false), 2000);
1689
+ });
1690
+ };
1691
+
1692
+ return (
1693
+ <div
1694
+ style={{
1695
+ display: 'grid',
1696
+ gridTemplateColumns: '3px 1fr 1fr 1fr 28px',
1697
+ gridAutoRows: 'min-content',
1698
+ gap: 8,
1699
+ }}
1700
+ >
1701
+ <div
1702
+ title={hasPaths ? `${files.length} normalized path(s)` : 'no paths to normalize'}
1703
+ style={{
1704
+ backgroundColor: hasPaths ? '#10b981' : '#232c39',
1705
+ }}
1706
+ />
1707
+ <RawEventCard event={raw} runCount={runCount} snapshotCount={snapshotCount} expanded={expanded} onToggle={onToggle} />
1708
+ {normalized ? (
1709
+ <NormalizedEventCard
1710
+ event={normalized}
1711
+ seq={row.seq}
1712
+ color={color}
1713
+ expanded={expanded}
1714
+ onToggle={onToggle}
1715
+ />
1716
+ ) : null}
1717
+ <AccumulatedEventCard
1718
+ event={row.accumulated}
1719
+ seq={row.seq}
1720
+ dropReason={(row as FeedItem).dropReason}
1721
+ expanded={expanded}
1722
+ onToggle={onToggle}
1723
+ />
1724
+ <button
1725
+ type="button"
1726
+ onClick={(e) => {
1727
+ e.preventDefault();
1728
+ e.stopPropagation();
1729
+ onCopy();
1730
+ }}
1731
+ title="Copy this row's context for a discussion"
1732
+ aria-label="Copy row context"
1733
+ style={{
1734
+ display: 'flex',
1735
+ alignItems: 'center',
1736
+ justifyContent: 'center',
1737
+ alignSelf: 'stretch',
1738
+ padding: 0,
1739
+ border: 'none',
1740
+ borderRadius: 4,
1741
+ appearance: 'none',
1742
+ cursor: 'pointer',
1743
+ background: copied ? 'rgba(16,185,129,0.12)' : 'transparent',
1744
+ color: copied ? '#10b981' : theme.colors.textTertiary,
1745
+ fontSize: 12,
1746
+ }}
1747
+ >
1748
+ {copied ? <Check size={13} /> : <Copy size={13} />}
1749
+ </button>
1750
+ </div>
1751
+ );
1752
+ }
1753
+
1754
+ function FilteredOutLine({ raw }: { raw: V1RawEvent | null }) {
1755
+ const color = kindColor(eventKind(raw));
1756
+ return (
1757
+ <div
1758
+ title={describe(raw)}
1759
+ style={{
1760
+ height: 2,
1761
+ backgroundColor: color,
1762
+ opacity: 0.16,
1763
+ cursor: 'default',
1764
+ }}
1765
+ />
1766
+ );
1767
+ }
1768
+
1769
+ function FilterChip({
1770
+ label,
1771
+ active,
1772
+ color,
1773
+ onClick,
1774
+ }: {
1775
+ label: string;
1776
+ active: boolean;
1777
+ color?: string;
1778
+ onClick: () => void;
1779
+ }) {
1780
+ return (
1781
+ <button
1782
+ onClick={onClick}
1783
+ style={{
1784
+ padding: '4px 10px',
1785
+ borderRadius: 999,
1786
+ border: `1px solid ${active ? color ?? '#3b82f6' : '#1f2937'}`,
1787
+ backgroundColor: active ? (color ?? '#3b82f6') + '22' : 'transparent',
1788
+ color: active ? '#f3f4f6' : '#9ca3af',
1789
+ fontSize: 11.5,
1790
+ cursor: 'pointer',
1791
+ fontFamily: 'inherit',
1792
+ }}
1793
+ >
1794
+ {label}
1795
+ </button>
1796
+ );
1797
+ }