@stigmer/react 3.0.9-dev.20260615153829 → 3.1.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 (439) hide show
  1. package/execution/ApprovalCard.d.ts +47 -3
  2. package/execution/ApprovalCard.d.ts.map +1 -1
  3. package/execution/ApprovalCard.js +138 -70
  4. package/execution/ApprovalCard.js.map +1 -1
  5. package/execution/ApprovalContext.d.ts +72 -0
  6. package/execution/ApprovalContext.d.ts.map +1 -0
  7. package/execution/ApprovalContext.js +41 -0
  8. package/execution/ApprovalContext.js.map +1 -0
  9. package/execution/ArtifactCard.d.ts.map +1 -1
  10. package/execution/ArtifactCard.js +3 -1
  11. package/execution/ArtifactCard.js.map +1 -1
  12. package/execution/ArtifactPreviewModal.d.ts.map +1 -1
  13. package/execution/ArtifactPreviewModal.js +5 -3
  14. package/execution/ArtifactPreviewModal.js.map +1 -1
  15. package/execution/EmptyChangeNotice.d.ts +38 -0
  16. package/execution/EmptyChangeNotice.d.ts.map +1 -0
  17. package/execution/EmptyChangeNotice.js +27 -0
  18. package/execution/EmptyChangeNotice.js.map +1 -0
  19. package/execution/FileChangesView.d.ts +81 -0
  20. package/execution/FileChangesView.d.ts.map +1 -0
  21. package/execution/FileChangesView.js +164 -0
  22. package/execution/FileChangesView.js.map +1 -0
  23. package/execution/FilePathLink.d.ts +24 -4
  24. package/execution/FilePathLink.d.ts.map +1 -1
  25. package/execution/FilePathLink.js +31 -9
  26. package/execution/FilePathLink.js.map +1 -1
  27. package/execution/FileReviewCard.d.ts +111 -0
  28. package/execution/FileReviewCard.d.ts.map +1 -0
  29. package/execution/FileReviewCard.js +558 -0
  30. package/execution/FileReviewCard.js.map +1 -0
  31. package/execution/FileReviewContext.d.ts +35 -0
  32. package/execution/FileReviewContext.d.ts.map +1 -0
  33. package/execution/FileReviewContext.js +23 -0
  34. package/execution/FileReviewContext.js.map +1 -0
  35. package/execution/FileReviewDock.d.ts +73 -0
  36. package/execution/FileReviewDock.d.ts.map +1 -0
  37. package/execution/FileReviewDock.js +65 -0
  38. package/execution/FileReviewDock.js.map +1 -0
  39. package/execution/McpToolDetail.d.ts.map +1 -1
  40. package/execution/McpToolDetail.js +38 -5
  41. package/execution/McpToolDetail.js.map +1 -1
  42. package/execution/MessageThread.d.ts +42 -3
  43. package/execution/MessageThread.d.ts.map +1 -1
  44. package/execution/MessageThread.js +263 -18
  45. package/execution/MessageThread.js.map +1 -1
  46. package/execution/PlanArtifactCard.d.ts.map +1 -1
  47. package/execution/PlanArtifactCard.js +3 -1
  48. package/execution/PlanArtifactCard.js.map +1 -1
  49. package/execution/PlanCompletionCard.d.ts +1 -1
  50. package/execution/PlanCompletionCard.js +1 -1
  51. package/execution/ResultView.d.ts +19 -1
  52. package/execution/ResultView.d.ts.map +1 -1
  53. package/execution/ResultView.js +141 -40
  54. package/execution/ResultView.js.map +1 -1
  55. package/execution/SubAgentSection.d.ts.map +1 -1
  56. package/execution/SubAgentSection.js +45 -6
  57. package/execution/SubAgentSection.js.map +1 -1
  58. package/execution/TerminalSession.d.ts +34 -0
  59. package/execution/TerminalSession.d.ts.map +1 -0
  60. package/execution/TerminalSession.js +34 -0
  61. package/execution/TerminalSession.js.map +1 -0
  62. package/execution/TodoCard.d.ts +53 -0
  63. package/execution/TodoCard.d.ts.map +1 -0
  64. package/execution/TodoCard.js +77 -0
  65. package/execution/TodoCard.js.map +1 -0
  66. package/execution/ToolArgsView.d.ts +7 -1
  67. package/execution/ToolArgsView.d.ts.map +1 -1
  68. package/execution/ToolArgsView.js +10 -9
  69. package/execution/ToolArgsView.js.map +1 -1
  70. package/execution/ToolCallDetail.d.ts +10 -1
  71. package/execution/ToolCallDetail.d.ts.map +1 -1
  72. package/execution/ToolCallDetail.js +66 -19
  73. package/execution/ToolCallDetail.js.map +1 -1
  74. package/execution/ToolCallGroup.d.ts +28 -18
  75. package/execution/ToolCallGroup.d.ts.map +1 -1
  76. package/execution/ToolCallGroup.js +39 -148
  77. package/execution/ToolCallGroup.js.map +1 -1
  78. package/execution/ToolCallItem.d.ts +36 -8
  79. package/execution/ToolCallItem.d.ts.map +1 -1
  80. package/execution/ToolCallItem.js +140 -21
  81. package/execution/ToolCallItem.js.map +1 -1
  82. package/execution/ToolRunGroup.d.ts +48 -0
  83. package/execution/ToolRunGroup.d.ts.map +1 -0
  84. package/execution/ToolRunGroup.js +168 -0
  85. package/execution/ToolRunGroup.js.map +1 -0
  86. package/execution/approval-provenance.d.ts +2 -0
  87. package/execution/approval-provenance.d.ts.map +1 -0
  88. package/execution/approval-provenance.js +6 -0
  89. package/execution/approval-provenance.js.map +1 -0
  90. package/execution/artifact-utils.d.ts +0 -10
  91. package/execution/artifact-utils.d.ts.map +1 -1
  92. package/execution/artifact-utils.js +0 -17
  93. package/execution/artifact-utils.js.map +1 -1
  94. package/execution/execution-phases.d.ts +1 -6
  95. package/execution/execution-phases.d.ts.map +1 -1
  96. package/execution/execution-phases.js +4 -14
  97. package/execution/execution-phases.js.map +1 -1
  98. package/execution/file-path-resolver.d.ts +28 -0
  99. package/execution/file-path-resolver.d.ts.map +1 -1
  100. package/execution/file-path-resolver.js +24 -0
  101. package/execution/file-path-resolver.js.map +1 -1
  102. package/execution/file-review-status.d.ts +127 -0
  103. package/execution/file-review-status.d.ts.map +1 -0
  104. package/execution/file-review-status.js +135 -0
  105. package/execution/file-review-status.js.map +1 -0
  106. package/execution/index.d.ts +39 -5
  107. package/execution/index.d.ts.map +1 -1
  108. package/execution/index.js +23 -3
  109. package/execution/index.js.map +1 -1
  110. package/execution/segment-tool-calls.d.ts +46 -0
  111. package/execution/segment-tool-calls.d.ts.map +1 -0
  112. package/execution/segment-tool-calls.js +71 -0
  113. package/execution/segment-tool-calls.js.map +1 -0
  114. package/execution/tool-categories.d.ts +62 -1
  115. package/execution/tool-categories.d.ts.map +1 -1
  116. package/execution/tool-categories.js +96 -2
  117. package/execution/tool-categories.js.map +1 -1
  118. package/execution/tool-presenter.d.ts +38 -2
  119. package/execution/tool-presenter.d.ts.map +1 -1
  120. package/execution/tool-presenter.js +23 -1
  121. package/execution/tool-presenter.js.map +1 -1
  122. package/execution/tool-rendering-primitives.d.ts.map +1 -1
  123. package/execution/tool-rendering-primitives.js +3 -2
  124. package/execution/tool-rendering-primitives.js.map +1 -1
  125. package/execution/useArtifactDownload.d.ts +44 -0
  126. package/execution/useArtifactDownload.d.ts.map +1 -0
  127. package/execution/useArtifactDownload.js +80 -0
  128. package/execution/useArtifactDownload.js.map +1 -0
  129. package/execution/useArtifactDownloadUrl.d.ts +67 -0
  130. package/execution/useArtifactDownloadUrl.d.ts.map +1 -0
  131. package/execution/useArtifactDownloadUrl.js +59 -0
  132. package/execution/useArtifactDownloadUrl.js.map +1 -0
  133. package/execution/useFileChangeContent.d.ts +61 -0
  134. package/execution/useFileChangeContent.d.ts.map +1 -0
  135. package/execution/useFileChangeContent.js +89 -0
  136. package/execution/useFileChangeContent.js.map +1 -0
  137. package/execution/useFileReview.d.ts +89 -0
  138. package/execution/useFileReview.d.ts.map +1 -0
  139. package/execution/useFileReview.js +106 -0
  140. package/execution/useFileReview.js.map +1 -0
  141. package/execution/useSubmitApproval.d.ts +17 -2
  142. package/execution/useSubmitApproval.d.ts.map +1 -1
  143. package/execution/useSubmitApproval.js +27 -23
  144. package/execution/useSubmitApproval.js.map +1 -1
  145. package/execution/useToolOutputContent.d.ts +44 -0
  146. package/execution/useToolOutputContent.d.ts.map +1 -0
  147. package/execution/useToolOutputContent.js +31 -0
  148. package/execution/useToolOutputContent.js.map +1 -0
  149. package/index.d.ts +6 -6
  150. package/index.d.ts.map +1 -1
  151. package/index.js +3 -3
  152. package/index.js.map +1 -1
  153. package/internal/ApprovalPeekBar.d.ts +23 -0
  154. package/internal/ApprovalPeekBar.d.ts.map +1 -0
  155. package/internal/ApprovalPeekBar.js +25 -0
  156. package/internal/ApprovalPeekBar.js.map +1 -0
  157. package/internal/BoundedContent.d.ts +49 -0
  158. package/internal/BoundedContent.d.ts.map +1 -0
  159. package/internal/BoundedContent.js +50 -0
  160. package/internal/BoundedContent.js.map +1 -0
  161. package/internal/DecisionButton.d.ts +47 -0
  162. package/internal/DecisionButton.d.ts.map +1 -0
  163. package/internal/DecisionButton.js +42 -0
  164. package/internal/DecisionButton.js.map +1 -0
  165. package/internal/InCardDecisionError.d.ts +31 -0
  166. package/internal/InCardDecisionError.d.ts.map +1 -0
  167. package/internal/InCardDecisionError.js +23 -0
  168. package/internal/InCardDecisionError.js.map +1 -0
  169. package/internal/RevealToggle.d.ts +36 -0
  170. package/internal/RevealToggle.d.ts.map +1 -0
  171. package/internal/RevealToggle.js +32 -0
  172. package/internal/RevealToggle.js.map +1 -0
  173. package/internal/VirtualizedThread.d.ts +7 -1
  174. package/internal/VirtualizedThread.d.ts.map +1 -1
  175. package/internal/VirtualizedThread.js +7 -3
  176. package/internal/VirtualizedThread.js.map +1 -1
  177. package/internal/code-highlight.d.ts +24 -0
  178. package/internal/code-highlight.d.ts.map +1 -0
  179. package/internal/code-highlight.js +115 -0
  180. package/internal/code-highlight.js.map +1 -0
  181. package/internal/markdown-components.d.ts.map +1 -1
  182. package/internal/markdown-components.js +13 -2
  183. package/internal/markdown-components.js.map +1 -1
  184. package/internal/useAutoDisclosure.d.ts +34 -0
  185. package/internal/useAutoDisclosure.d.ts.map +1 -0
  186. package/internal/useAutoDisclosure.js +53 -0
  187. package/internal/useAutoDisclosure.js.map +1 -0
  188. package/internal/useElapsedSince.d.ts +30 -0
  189. package/internal/useElapsedSince.d.ts.map +1 -0
  190. package/internal/useElapsedSince.js +70 -0
  191. package/internal/useElapsedSince.js.map +1 -0
  192. package/internal/useIsOverflowing.d.ts +30 -0
  193. package/internal/useIsOverflowing.d.ts.map +1 -0
  194. package/internal/useIsOverflowing.js +47 -0
  195. package/internal/useIsOverflowing.js.map +1 -0
  196. package/internal/useIsTextTruncated.d.ts +24 -0
  197. package/internal/useIsTextTruncated.d.ts.map +1 -0
  198. package/internal/useIsTextTruncated.js +41 -0
  199. package/internal/useIsTextTruncated.js.map +1 -0
  200. package/internal/useKeyedSubmission.d.ts +49 -0
  201. package/internal/useKeyedSubmission.d.ts.map +1 -0
  202. package/internal/useKeyedSubmission.js +67 -0
  203. package/internal/useKeyedSubmission.js.map +1 -0
  204. package/package.json +7 -4
  205. package/session/SessionViewer.d.ts +2 -2
  206. package/session/SessionViewer.d.ts.map +1 -1
  207. package/session/SessionViewer.js +10 -4
  208. package/session/SessionViewer.js.map +1 -1
  209. package/session/index.d.ts +2 -0
  210. package/session/index.d.ts.map +1 -1
  211. package/session/index.js +1 -0
  212. package/session/index.js.map +1 -1
  213. package/session/inspector/ChangesTab.d.ts +8 -5
  214. package/session/inspector/ChangesTab.d.ts.map +1 -1
  215. package/session/inspector/ChangesTab.js +11 -8
  216. package/session/inspector/ChangesTab.js.map +1 -1
  217. package/session/inspector/SessionInspector.d.ts +4 -4
  218. package/session/inspector/SessionInspector.d.ts.map +1 -1
  219. package/session/inspector/SessionInspector.js +4 -4
  220. package/session/inspector/SessionInspector.js.map +1 -1
  221. package/session/inspector/index.d.ts +0 -2
  222. package/session/inspector/index.d.ts.map +1 -1
  223. package/session/inspector/index.js +0 -1
  224. package/session/inspector/index.js.map +1 -1
  225. package/session/inspector/useSessionInspector.d.ts +8 -4
  226. package/session/inspector/useSessionInspector.d.ts.map +1 -1
  227. package/session/inspector/useSessionInspector.js +27 -20
  228. package/session/inspector/useSessionInspector.js.map +1 -1
  229. package/session/useSessionConversation.d.ts +37 -3
  230. package/session/useSessionConversation.d.ts.map +1 -1
  231. package/session/useSessionConversation.js +18 -2
  232. package/session/useSessionConversation.js.map +1 -1
  233. package/session/useSessionFileChanges.d.ts +74 -0
  234. package/session/useSessionFileChanges.d.ts.map +1 -0
  235. package/session/useSessionFileChanges.js +178 -0
  236. package/session/useSessionFileChanges.js.map +1 -0
  237. package/src/__tests__/styles-border-layer-invariant.test.ts +105 -0
  238. package/src/execution/ApprovalCard.tsx +368 -230
  239. package/src/execution/ApprovalContext.ts +109 -0
  240. package/src/execution/ArtifactCard.tsx +10 -6
  241. package/src/execution/ArtifactPreviewModal.tsx +12 -6
  242. package/src/execution/EmptyChangeNotice.tsx +62 -0
  243. package/src/execution/FileChangesView.tsx +383 -0
  244. package/src/execution/FilePathLink.tsx +65 -12
  245. package/src/execution/FileReviewCard.tsx +1118 -0
  246. package/src/execution/FileReviewContext.ts +53 -0
  247. package/src/execution/FileReviewDock.tsx +157 -0
  248. package/src/execution/McpToolDetail.tsx +98 -32
  249. package/src/execution/MessageThread.tsx +374 -9
  250. package/src/execution/PlanArtifactCard.tsx +9 -6
  251. package/src/execution/PlanCompletionCard.tsx +1 -1
  252. package/src/execution/ResultView.tsx +333 -162
  253. package/src/execution/SubAgentSection.tsx +58 -11
  254. package/src/execution/TerminalSession.tsx +91 -0
  255. package/src/execution/TodoCard.tsx +186 -0
  256. package/src/execution/ToolArgsView.tsx +28 -17
  257. package/src/execution/ToolCallDetail.tsx +137 -26
  258. package/src/execution/ToolCallGroup.tsx +68 -296
  259. package/src/execution/ToolCallItem.tsx +265 -67
  260. package/src/execution/ToolRunGroup.tsx +302 -0
  261. package/src/execution/__tests__/ApprovalCard.test.tsx +395 -5
  262. package/src/execution/__tests__/ArtifactContentRenderer.test.tsx +15 -0
  263. package/src/execution/__tests__/ArtifactPreviewModal.test.tsx +0 -1
  264. package/src/execution/__tests__/EmptyChangeNotice.test.tsx +24 -0
  265. package/src/execution/__tests__/FileChangesView.test.tsx +296 -0
  266. package/src/execution/__tests__/FilePathLink.test.tsx +92 -0
  267. package/src/execution/__tests__/FileReviewCard.test.tsx +1044 -0
  268. package/src/execution/__tests__/FileReviewDock.test.tsx +134 -0
  269. package/src/execution/__tests__/McpToolDetail.test.tsx +79 -0
  270. package/src/execution/__tests__/MessageThread.test.tsx +324 -0
  271. package/src/execution/__tests__/PlanArtifactCard.test.tsx +37 -16
  272. package/src/execution/__tests__/ResultView.test.tsx +280 -2
  273. package/src/execution/__tests__/SubAgentSection.test.tsx +180 -0
  274. package/src/execution/__tests__/TerminalSession.test.tsx +72 -0
  275. package/src/execution/__tests__/TodoCard.test.tsx +102 -0
  276. package/src/execution/__tests__/ToolArgsView.test.tsx +48 -0
  277. package/src/execution/__tests__/ToolCallDetail.test.tsx +195 -0
  278. package/src/execution/__tests__/ToolCallGroup.test.tsx +132 -0
  279. package/src/execution/__tests__/ToolCallItem.test.tsx +521 -0
  280. package/src/execution/__tests__/ToolRunGroup.test.tsx +137 -0
  281. package/src/execution/__tests__/blanked-narration.test.tsx +149 -0
  282. package/src/execution/__tests__/buildThreadItems-approvals.test.ts +304 -0
  283. package/src/execution/__tests__/buildThreadItems-file-review.test.ts +331 -0
  284. package/src/execution/__tests__/file-path-resolver.test.ts +44 -0
  285. package/src/execution/__tests__/file-review-status.test.ts +343 -0
  286. package/src/execution/__tests__/message-entry.test.tsx +16 -0
  287. package/src/execution/__tests__/segment-tool-calls.test.ts +124 -0
  288. package/src/execution/__tests__/subagent-file-review-badge.test.tsx +273 -0
  289. package/src/execution/__tests__/thread-keys.test.ts +195 -0
  290. package/src/execution/__tests__/tool-presenter.test.tsx +95 -0
  291. package/src/execution/__tests__/useArtifactDownload.test.tsx +67 -0
  292. package/src/execution/__tests__/useArtifactDownloadUrl.test.tsx +51 -0
  293. package/src/execution/__tests__/useFileChangeContent.test.tsx +240 -0
  294. package/src/execution/__tests__/useFileReview.test.tsx +236 -0
  295. package/src/execution/__tests__/useSubmitApproval.test.tsx +165 -0
  296. package/src/execution/__tests__/useToolOutputContent.test.tsx +82 -0
  297. package/src/execution/approval-provenance.ts +8 -0
  298. package/src/execution/artifact-utils.ts +0 -18
  299. package/src/execution/execution-phases.ts +4 -16
  300. package/src/execution/file-path-resolver.ts +38 -0
  301. package/src/execution/file-review-status.ts +219 -0
  302. package/src/execution/index.ts +84 -4
  303. package/src/execution/segment-tool-calls.ts +101 -0
  304. package/src/execution/tool-categories.ts +128 -1
  305. package/src/execution/tool-presenter.ts +58 -3
  306. package/src/execution/tool-rendering-primitives.tsx +11 -14
  307. package/src/execution/useArtifactDownload.ts +109 -0
  308. package/src/execution/useArtifactDownloadUrl.ts +116 -0
  309. package/src/execution/useFileChangeContent.ts +149 -0
  310. package/src/execution/useFileReview.ts +197 -0
  311. package/src/execution/useSubmitApproval.ts +49 -27
  312. package/src/execution/useToolOutputContent.ts +70 -0
  313. package/src/index.ts +60 -1
  314. package/src/internal/ApprovalPeekBar.tsx +73 -0
  315. package/src/internal/BoundedContent.tsx +97 -0
  316. package/src/internal/DecisionButton.tsx +127 -0
  317. package/src/internal/InCardDecisionError.tsx +47 -0
  318. package/src/internal/RevealToggle.tsx +91 -0
  319. package/src/internal/VirtualizedThread.tsx +28 -2
  320. package/src/internal/__tests__/ApprovalPeekBar.test.tsx +34 -0
  321. package/src/internal/__tests__/BoundedContent.test.tsx +92 -0
  322. package/src/internal/__tests__/DecisionButton.test.tsx +107 -0
  323. package/src/internal/__tests__/RevealToggle.test.tsx +61 -0
  324. package/src/internal/__tests__/code-highlight.test.tsx +59 -0
  325. package/src/internal/__tests__/markdown-components.test.tsx +68 -2
  326. package/src/internal/__tests__/useAutoDisclosure.test.ts +74 -0
  327. package/src/internal/__tests__/useElapsedSince.test.tsx +68 -0
  328. package/src/internal/__tests__/useIsOverflowing.test.ts +36 -0
  329. package/src/internal/__tests__/useKeyedSubmission.test.tsx +126 -0
  330. package/src/internal/code-highlight.tsx +120 -0
  331. package/src/internal/markdown-components.tsx +20 -3
  332. package/src/internal/useAutoDisclosure.ts +58 -0
  333. package/src/internal/useElapsedSince.ts +72 -0
  334. package/src/internal/useIsOverflowing.ts +61 -0
  335. package/src/internal/useIsTextTruncated.ts +55 -0
  336. package/src/internal/useKeyedSubmission.ts +101 -0
  337. package/src/session/SessionViewer.tsx +21 -4
  338. package/src/session/__tests__/SessionViewer-fileReviewDock.test.tsx +198 -0
  339. package/src/session/__tests__/audienceWiring.test.tsx +4 -0
  340. package/src/session/__tests__/useSessionFileChanges.test.ts +303 -0
  341. package/src/session/index.ts +3 -0
  342. package/src/session/inspector/ChangesTab.tsx +20 -17
  343. package/src/session/inspector/SessionInspector.tsx +12 -10
  344. package/src/session/inspector/__tests__/ArtifactsTab.test.tsx +0 -1
  345. package/src/session/inspector/__tests__/ChangesTab.test.tsx +38 -0
  346. package/src/session/inspector/__tests__/useSessionInspector.test.ts +46 -61
  347. package/src/session/inspector/index.ts +0 -3
  348. package/src/session/inspector/useSessionInspector.ts +27 -20
  349. package/src/session/useSessionConversation.ts +80 -3
  350. package/src/session/useSessionFileChanges.ts +207 -0
  351. package/src/styles.css +123 -10
  352. package/src/version-history/DiffViewer.tsx +10 -7
  353. package/src/version-history/UnifiedDiffView.tsx +66 -0
  354. package/src/version-history/__tests__/DiffViewer.test.tsx +57 -0
  355. package/src/version-history/__tests__/UnifiedDiffView.test.tsx +50 -0
  356. package/src/version-history/__tests__/computeDiff.test.ts +35 -0
  357. package/src/version-history/__tests__/parseUnifiedDiff.test.ts +82 -0
  358. package/src/version-history/computeDiff.ts +6 -37
  359. package/src/version-history/diff-hunks.ts +63 -0
  360. package/src/version-history/index.ts +1 -0
  361. package/src/version-history/parseUnifiedDiff.ts +34 -0
  362. package/src/workflow/WorkflowExecutionApprovalCard.tsx +49 -24
  363. package/src/workflow/WorkflowExecutionTimeline.tsx +15 -4
  364. package/src/workflow/WorkflowExecutionTimelineEvent.tsx +8 -3
  365. package/src/workflow/WorkflowExecutionViewer.tsx +72 -28
  366. package/src/workflow/WorkflowFileReviewList.tsx +152 -0
  367. package/src/workflow/WorkflowTaskApprovalCard.tsx +37 -85
  368. package/src/workflow/__tests__/ExecutionInspector.test.tsx +30 -2
  369. package/src/workflow/__tests__/WorkflowExecutionApprovalCard.test.tsx +129 -0
  370. package/src/workflow/__tests__/WorkflowFileReviewList.test.tsx +129 -0
  371. package/src/workflow/__tests__/WorkflowTaskApprovalCard.test.tsx +43 -11
  372. package/src/workflow/__tests__/useWorkflowExecutionActions.test.tsx +276 -0
  373. package/src/workflow/execution-inspector/ExecutionInspector.tsx +42 -18
  374. package/src/workflow/index.ts +5 -0
  375. package/src/workflow/useWorkflowExecutionActions.ts +177 -29
  376. package/styles.css +1 -1
  377. package/version-history/DiffViewer.d.ts.map +1 -1
  378. package/version-history/DiffViewer.js +9 -3
  379. package/version-history/DiffViewer.js.map +1 -1
  380. package/version-history/UnifiedDiffView.d.ts +29 -0
  381. package/version-history/UnifiedDiffView.d.ts.map +1 -0
  382. package/version-history/UnifiedDiffView.js +36 -0
  383. package/version-history/UnifiedDiffView.js.map +1 -0
  384. package/version-history/computeDiff.d.ts +3 -1
  385. package/version-history/computeDiff.d.ts.map +1 -1
  386. package/version-history/computeDiff.js +5 -35
  387. package/version-history/computeDiff.js.map +1 -1
  388. package/version-history/diff-hunks.d.ts +21 -0
  389. package/version-history/diff-hunks.d.ts.map +1 -0
  390. package/version-history/diff-hunks.js +58 -0
  391. package/version-history/diff-hunks.js.map +1 -0
  392. package/version-history/index.d.ts +1 -0
  393. package/version-history/index.d.ts.map +1 -1
  394. package/version-history/index.js +1 -0
  395. package/version-history/index.js.map +1 -1
  396. package/version-history/parseUnifiedDiff.d.ts +22 -0
  397. package/version-history/parseUnifiedDiff.d.ts.map +1 -0
  398. package/version-history/parseUnifiedDiff.js +34 -0
  399. package/version-history/parseUnifiedDiff.js.map +1 -0
  400. package/workflow/WorkflowExecutionApprovalCard.d.ts +8 -1
  401. package/workflow/WorkflowExecutionApprovalCard.d.ts.map +1 -1
  402. package/workflow/WorkflowExecutionApprovalCard.js +17 -3
  403. package/workflow/WorkflowExecutionApprovalCard.js.map +1 -1
  404. package/workflow/WorkflowExecutionTimeline.d.ts +11 -2
  405. package/workflow/WorkflowExecutionTimeline.d.ts.map +1 -1
  406. package/workflow/WorkflowExecutionTimeline.js +2 -2
  407. package/workflow/WorkflowExecutionTimeline.js.map +1 -1
  408. package/workflow/WorkflowExecutionTimelineEvent.d.ts +4 -1
  409. package/workflow/WorkflowExecutionTimelineEvent.d.ts.map +1 -1
  410. package/workflow/WorkflowExecutionTimelineEvent.js +2 -2
  411. package/workflow/WorkflowExecutionTimelineEvent.js.map +1 -1
  412. package/workflow/WorkflowExecutionViewer.d.ts.map +1 -1
  413. package/workflow/WorkflowExecutionViewer.js +18 -10
  414. package/workflow/WorkflowExecutionViewer.js.map +1 -1
  415. package/workflow/WorkflowFileReviewList.d.ts +46 -0
  416. package/workflow/WorkflowFileReviewList.d.ts.map +1 -0
  417. package/workflow/WorkflowFileReviewList.js +48 -0
  418. package/workflow/WorkflowFileReviewList.js.map +1 -0
  419. package/workflow/WorkflowTaskApprovalCard.d.ts +8 -1
  420. package/workflow/WorkflowTaskApprovalCard.d.ts.map +1 -1
  421. package/workflow/WorkflowTaskApprovalCard.js +12 -19
  422. package/workflow/WorkflowTaskApprovalCard.js.map +1 -1
  423. package/workflow/execution-inspector/ExecutionInspector.d.ts +21 -4
  424. package/workflow/execution-inspector/ExecutionInspector.d.ts.map +1 -1
  425. package/workflow/execution-inspector/ExecutionInspector.js +6 -3
  426. package/workflow/execution-inspector/ExecutionInspector.js.map +1 -1
  427. package/workflow/index.d.ts +1 -0
  428. package/workflow/index.d.ts.map +1 -1
  429. package/workflow/index.js +1 -0
  430. package/workflow/index.js.map +1 -1
  431. package/workflow/useWorkflowExecutionActions.d.ts +62 -4
  432. package/workflow/useWorkflowExecutionActions.d.ts.map +1 -1
  433. package/workflow/useWorkflowExecutionActions.js +94 -18
  434. package/workflow/useWorkflowExecutionActions.js.map +1 -1
  435. package/session/inspector/PlanTab.d.ts +0 -15
  436. package/session/inspector/PlanTab.d.ts.map +0 -1
  437. package/session/inspector/PlanTab.js +0 -19
  438. package/session/inspector/PlanTab.js.map +0 -1
  439. package/src/session/inspector/PlanTab.tsx +0 -31
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { renderHook, waitFor } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import type { Stigmer } from "@stigmer/sdk";
5
+ import { StigmerContext } from "../../context";
6
+ import { useArtifactDownloadUrl } from "../useArtifactDownloadUrl";
7
+
8
+ function wrapperFor(stigmer: Stigmer) {
9
+ return ({ children }: { children: ReactNode }) => (
10
+ <StigmerContext.Provider value={stigmer}>{children}</StigmerContext.Provider>
11
+ );
12
+ }
13
+
14
+ function makeStigmer(getArtifactDownloadUrl = vi.fn()) {
15
+ return { agentExecution: { getArtifactDownloadUrl } } as unknown as Stigmer;
16
+ }
17
+
18
+ describe("useArtifactDownloadUrl", () => {
19
+ it("mints a fresh URL on demand from the stable storage key", async () => {
20
+ const fn = vi.fn().mockResolvedValue({ downloadUrl: "https://fresh/1" });
21
+ const { result } = renderHook(
22
+ () => useArtifactDownloadUrl("aex_1", "artifacts/aex_1/k1"),
23
+ { wrapper: wrapperFor(makeStigmer(fn)) },
24
+ );
25
+
26
+ await waitFor(() => expect(result.current.url).toBe("https://fresh/1"));
27
+ expect(fn).toHaveBeenCalledTimes(1);
28
+ const req = fn.mock.calls[0][0];
29
+ expect(req.executionId).toBe("aex_1");
30
+ expect(req.storageKey).toBe("artifacts/aex_1/k1");
31
+ });
32
+
33
+ it("stays idle when disabled", () => {
34
+ const fn = vi.fn();
35
+ const { result } = renderHook(
36
+ () => useArtifactDownloadUrl("aex_1", "artifacts/aex_1/k2", { enabled: false }),
37
+ { wrapper: wrapperFor(makeStigmer(fn)) },
38
+ );
39
+ expect(result.current.url).toBeNull();
40
+ expect(fn).not.toHaveBeenCalled();
41
+ });
42
+
43
+ it("skips fetching when either id is null", () => {
44
+ const fn = vi.fn();
45
+ const { result } = renderHook(() => useArtifactDownloadUrl(null, null), {
46
+ wrapper: wrapperFor(makeStigmer(fn)),
47
+ });
48
+ expect(result.current.url).toBeNull();
49
+ expect(fn).not.toHaveBeenCalled();
50
+ });
51
+ });
@@ -0,0 +1,240 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { renderHook } from "@testing-library/react";
3
+ import { create } from "@bufbuild/protobuf";
4
+ import {
5
+ FileChangeSchema,
6
+ FileContentSchema,
7
+ ToolCallOutputRefSchema,
8
+ type FileContent,
9
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
10
+ import {
11
+ FileChangeCaptureLevel,
12
+ FileChangeType,
13
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
14
+ import type { UseArtifactContentReturn } from "../useArtifactContent";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Mock the offload fetch — keyed by storageKey so each side resolves independently.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ const responses = new Map<string, Partial<UseArtifactContentReturn>>();
21
+ const calls: Array<{ executionId: string | null; storageKey: string | null }> = [];
22
+
23
+ vi.mock("../useArtifactContent", () => ({
24
+ useArtifactContent: (executionId: string | null, storageKey: string | null) => {
25
+ calls.push({ executionId, storageKey });
26
+ const base: UseArtifactContentReturn = {
27
+ content: null,
28
+ contentType: null,
29
+ isTruncated: false,
30
+ isLoading: false,
31
+ isRefetching: false,
32
+ error: null,
33
+ refetch: () => {},
34
+ };
35
+ if (!storageKey) return base;
36
+ return { ...base, ...responses.get(storageKey) };
37
+ },
38
+ }));
39
+
40
+ // The download fallback is resolved on demand from the storage key (gated on
41
+ // truncation), so the test drives it through the URL hook rather than a baked ref.
42
+ const downloadUrls = new Map<string, string>();
43
+ const urlCalls: Array<{
44
+ executionId: string | null;
45
+ storageKey: string | null;
46
+ enabled: boolean;
47
+ }> = [];
48
+
49
+ vi.mock("../useArtifactDownloadUrl", () => ({
50
+ useArtifactDownloadUrl: (
51
+ executionId: string | null,
52
+ storageKey: string | null,
53
+ options?: { enabled?: boolean },
54
+ ) => {
55
+ const enabled = options?.enabled ?? true;
56
+ urlCalls.push({ executionId, storageKey, enabled });
57
+ const url = enabled && storageKey ? downloadUrls.get(storageKey) ?? null : null;
58
+ return { url, isLoading: false, isRefetching: false, error: null, refetch: () => {} };
59
+ },
60
+ }));
61
+
62
+ const { useFileChangeContent, execIdFromStorageKey } = await import(
63
+ "../useFileChangeContent"
64
+ );
65
+
66
+ beforeEach(() => {
67
+ responses.clear();
68
+ calls.length = 0;
69
+ downloadUrls.clear();
70
+ urlCalls.length = 0;
71
+ });
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Fixtures
75
+ // ---------------------------------------------------------------------------
76
+
77
+ function inlineSide(value: string): FileContent {
78
+ return create(FileContentSchema, { body: { case: "inline", value } });
79
+ }
80
+
81
+ function refSide(storageKey: string): FileContent {
82
+ return create(FileContentSchema, {
83
+ body: {
84
+ case: "ref",
85
+ value: create(ToolCallOutputRefSchema, { storageKey }),
86
+ },
87
+ });
88
+ }
89
+
90
+ function binarySide(): FileContent {
91
+ return create(FileContentSchema, { isBinary: true });
92
+ }
93
+
94
+ function wholeFile(before: FileContent | undefined, after: FileContent | undefined) {
95
+ return create(FileChangeSchema, {
96
+ path: "src/a.ts",
97
+ changeType: FileChangeType.MODIFY,
98
+ captureLevel: FileChangeCaptureLevel.WHOLE_FILE,
99
+ before,
100
+ after,
101
+ });
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // execIdFromStorageKey
106
+ // ---------------------------------------------------------------------------
107
+
108
+ describe("execIdFromStorageKey", () => {
109
+ it("parses the execution id from an artifacts key", () => {
110
+ expect(
111
+ execIdFromStorageKey("artifacts/exec-123/toolcalls/tc.0.before.txt"),
112
+ ).toBe("exec-123");
113
+ });
114
+
115
+ it("returns null for an unexpected shape", () => {
116
+ expect(execIdFromStorageKey("nope")).toBeNull();
117
+ expect(execIdFromStorageKey("artifacts//x")).toBeNull();
118
+ });
119
+ });
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // useFileChangeContent
123
+ // ---------------------------------------------------------------------------
124
+
125
+ describe("useFileChangeContent", () => {
126
+ it("resolves inline sides directly without fetching", () => {
127
+ const change = wholeFile(inlineSide("old"), inlineSide("new"));
128
+ const { result } = renderHook(() => useFileChangeContent(change));
129
+
130
+ expect(result.current.beforeText).toBe("old");
131
+ expect(result.current.afterText).toBe("new");
132
+ expect(result.current.isLoading).toBe(false);
133
+ // Both sides issue a (skipped) call with null storageKey.
134
+ expect(calls.every((c) => c.storageKey === null)).toBe(true);
135
+ });
136
+
137
+ it("treats an absent side as an empty file", () => {
138
+ const change = wholeFile(undefined, inlineSide("created"));
139
+ change.changeType = FileChangeType.CREATE;
140
+ const { result } = renderHook(() => useFileChangeContent(change));
141
+ expect(result.current.beforeText).toBe("");
142
+ expect(result.current.afterText).toBe("created");
143
+ });
144
+
145
+ it("fetches an offloaded side and parses the execId from its storageKey", () => {
146
+ const key = "artifacts/exec-9/toolcalls/tc.0.after.txt";
147
+ responses.set(key, { content: "fetched-after" });
148
+ const change = wholeFile(inlineSide("old"), refSide(key));
149
+
150
+ const { result } = renderHook(() => useFileChangeContent(change));
151
+
152
+ expect(result.current.afterText).toBe("fetched-after");
153
+ const afterCall = calls.find((c) => c.storageKey === key);
154
+ expect(afterCall?.executionId).toBe("exec-9");
155
+ });
156
+
157
+ it("resolves both sides independently when each is offloaded under a different execId", () => {
158
+ const beforeKey = "artifacts/exec-before/toolcalls/tc.0.before.txt";
159
+ const afterKey = "artifacts/exec-after/toolcalls/tc.0.after.txt";
160
+ responses.set(beforeKey, { content: "fetched-before" });
161
+ responses.set(afterKey, { content: "fetched-after" });
162
+ const change = wholeFile(refSide(beforeKey), refSide(afterKey));
163
+
164
+ const { result } = renderHook(() => useFileChangeContent(change));
165
+
166
+ expect(result.current.beforeText).toBe("fetched-before");
167
+ expect(result.current.afterText).toBe("fetched-after");
168
+ expect(result.current.isLoading).toBe(false);
169
+ // Each side derives its own execId from its own key.
170
+ expect(calls.find((c) => c.storageKey === beforeKey)?.executionId).toBe("exec-before");
171
+ expect(calls.find((c) => c.storageKey === afterKey)?.executionId).toBe("exec-after");
172
+ });
173
+
174
+ it("resolves a mix of an offloaded before and an inline after", () => {
175
+ const beforeKey = "artifacts/exec-9/toolcalls/tc.0.before.txt";
176
+ responses.set(beforeKey, { content: "fetched-before" });
177
+ const change = wholeFile(refSide(beforeKey), inlineSide("new"));
178
+
179
+ const { result } = renderHook(() => useFileChangeContent(change));
180
+
181
+ expect(result.current.beforeText).toBe("fetched-before");
182
+ expect(result.current.afterText).toBe("new");
183
+ expect(result.current.isLoading).toBe(false);
184
+ });
185
+
186
+ it("stays in loading while either side is still in flight, even if the other is ready", () => {
187
+ const beforeKey = "artifacts/exec-9/toolcalls/tc.0.before.txt";
188
+ const afterKey = "artifacts/exec-9/toolcalls/tc.0.after.txt";
189
+ responses.set(beforeKey, { content: "ready" });
190
+ responses.set(afterKey, { content: null, isLoading: true });
191
+ const change = wholeFile(refSide(beforeKey), refSide(afterKey));
192
+
193
+ const { result } = renderHook(() => useFileChangeContent(change));
194
+
195
+ // A diff needs both sides; one side still loading keeps the whole view loading.
196
+ expect(result.current.isLoading).toBe(true);
197
+ });
198
+
199
+ it("reports loading while an offloaded side is in flight", () => {
200
+ const key = "artifacts/exec-9/toolcalls/tc.0.after.txt";
201
+ responses.set(key, { content: null, isLoading: true });
202
+ const change = wholeFile(inlineSide("old"), refSide(key));
203
+
204
+ const { result } = renderHook(() => useFileChangeContent(change));
205
+
206
+ expect(result.current.isLoading).toBe(true);
207
+ expect(result.current.afterText).toBeNull();
208
+ });
209
+
210
+ it("surfaces truncation and resolves the download url on demand for an oversized offloaded side", () => {
211
+ const key = "artifacts/exec-9/toolcalls/tc.0.after.txt";
212
+ responses.set(key, { content: "head", isTruncated: true });
213
+ downloadUrls.set(key, "https://dl/full");
214
+ const change = wholeFile(inlineSide("old"), refSide(key));
215
+
216
+ const { result } = renderHook(() => useFileChangeContent(change));
217
+
218
+ expect(result.current.isTruncated).toBe(true);
219
+ expect(result.current.downloadUrl).toBe("https://dl/full");
220
+ // The URL is resolved from the stable key, gated on truncation.
221
+ expect(urlCalls.some((c) => c.storageKey === key && c.enabled)).toBe(true);
222
+ });
223
+
224
+ it("flags binary changes and yields empty text", () => {
225
+ const change = wholeFile(binarySide(), binarySide());
226
+ const { result } = renderHook(() => useFileChangeContent(change));
227
+ expect(result.current.isBinary).toBe(true);
228
+ expect(result.current.beforeText).toBe("");
229
+ expect(result.current.afterText).toBe("");
230
+ });
231
+
232
+ it("propagates a fetch error", () => {
233
+ const key = "artifacts/exec-9/toolcalls/tc.0.after.txt";
234
+ responses.set(key, { error: new Error("boom") });
235
+ const change = wholeFile(inlineSide("old"), refSide(key));
236
+
237
+ const { result } = renderHook(() => useFileChangeContent(change));
238
+ expect(result.current.error?.message).toBe("boom");
239
+ });
240
+ });
@@ -0,0 +1,236 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import type { Stigmer } from "@stigmer/sdk";
5
+ import {
6
+ FileDecisionAction,
7
+ FileDecisionScope,
8
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
9
+ import { StigmerContext } from "../../context";
10
+ import { useFileReview, fileDecisionKey } from "../useFileReview";
11
+
12
+ const mockSubmitFileDecision = vi.fn();
13
+
14
+ function makeMockClient(): Stigmer {
15
+ return {
16
+ agentExecution: { submitFileDecision: mockSubmitFileDecision },
17
+ } as unknown as Stigmer;
18
+ }
19
+
20
+ function createWrapper(client: Stigmer) {
21
+ return function Wrapper({ children }: { children: ReactNode }) {
22
+ return <StigmerContext.Provider value={client}>{children}</StigmerContext.Provider>;
23
+ };
24
+ }
25
+
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ });
29
+
30
+ describe("useFileReview", () => {
31
+ it("submits a whole-set decision as CHANGE_SET scope bound to the aggregate digest", async () => {
32
+ mockSubmitFileDecision.mockResolvedValueOnce({});
33
+
34
+ const { result } = renderHook(() => useFileReview(), {
35
+ wrapper: createWrapper(makeMockClient()),
36
+ });
37
+
38
+ await act(async () => {
39
+ await result.current.submitFileDecision("aex-1", "aex-1:0", FileDecisionAction.APPROVE, {
40
+ expectedDigest: "agg-1",
41
+ });
42
+ });
43
+
44
+ expect(mockSubmitFileDecision).toHaveBeenCalledTimes(1);
45
+ expect(mockSubmitFileDecision.mock.calls[0][0]).toMatchObject({
46
+ agentExecutionId: "aex-1",
47
+ changeSetId: "aex-1:0",
48
+ scope: FileDecisionScope.CHANGE_SET,
49
+ fileChangeId: "",
50
+ action: FileDecisionAction.APPROVE,
51
+ expectedDigest: "agg-1",
52
+ });
53
+ });
54
+
55
+ it("defaults to FILE scope when a fileChangeId is supplied", async () => {
56
+ mockSubmitFileDecision.mockResolvedValueOnce({});
57
+
58
+ const { result } = renderHook(() => useFileReview(), {
59
+ wrapper: createWrapper(makeMockClient()),
60
+ });
61
+
62
+ await act(async () => {
63
+ await result.current.submitFileDecision("aex-1", "aex-1:0", FileDecisionAction.REJECT, {
64
+ fileChangeId: "aex-1:0:src/a.ts",
65
+ expectedDigest: "file-1",
66
+ });
67
+ });
68
+
69
+ expect(mockSubmitFileDecision.mock.calls[0][0]).toMatchObject({
70
+ scope: FileDecisionScope.FILE,
71
+ fileChangeId: "aex-1:0:src/a.ts",
72
+ action: FileDecisionAction.REJECT,
73
+ expectedDigest: "file-1",
74
+ });
75
+ });
76
+
77
+ it("threads acknowledge_unreviewable onto the input (the binary 'Keep anyway' path)", async () => {
78
+ mockSubmitFileDecision.mockResolvedValueOnce({});
79
+
80
+ const { result } = renderHook(() => useFileReview(), {
81
+ wrapper: createWrapper(makeMockClient()),
82
+ });
83
+
84
+ await act(async () => {
85
+ await result.current.submitFileDecision("aex-1", "aex-1:0", FileDecisionAction.APPROVE, {
86
+ fileChangeId: "aex-1:0:logo.png",
87
+ expectedDigest: "d-bin",
88
+ acknowledgeUnreviewable: true,
89
+ });
90
+ });
91
+
92
+ expect(mockSubmitFileDecision.mock.calls[0][0]).toMatchObject({
93
+ scope: FileDecisionScope.FILE,
94
+ fileChangeId: "aex-1:0:logo.png",
95
+ action: FileDecisionAction.APPROVE,
96
+ expectedDigest: "d-bin",
97
+ acknowledgeUnreviewable: true,
98
+ });
99
+ });
100
+
101
+ it("defaults acknowledge_unreviewable to false when omitted", async () => {
102
+ mockSubmitFileDecision.mockResolvedValueOnce({});
103
+
104
+ const { result } = renderHook(() => useFileReview(), {
105
+ wrapper: createWrapper(makeMockClient()),
106
+ });
107
+
108
+ await act(async () => {
109
+ await result.current.submitFileDecision("aex-1", "aex-1:0", FileDecisionAction.APPROVE, {
110
+ expectedDigest: "agg-1",
111
+ });
112
+ });
113
+
114
+ expect(mockSubmitFileDecision.mock.calls[0][0].acknowledgeUnreviewable).toBe(false);
115
+ });
116
+
117
+ it("tracks submitting decision keys while in flight, then clears them", async () => {
118
+ let resolve!: () => void;
119
+ mockSubmitFileDecision.mockReturnValueOnce(
120
+ new Promise<void>((r) => {
121
+ resolve = r;
122
+ }),
123
+ );
124
+
125
+ const { result } = renderHook(() => useFileReview(), {
126
+ wrapper: createWrapper(makeMockClient()),
127
+ });
128
+
129
+ let pending: Promise<void>;
130
+ act(() => {
131
+ pending = result.current.submitFileDecision("aex-1", "aex-1:0", FileDecisionAction.APPROVE);
132
+ });
133
+
134
+ expect(result.current.submittingDecisionKeys.has(fileDecisionKey("aex-1:0"))).toBe(true);
135
+
136
+ await act(async () => {
137
+ resolve();
138
+ await pending;
139
+ });
140
+
141
+ expect(result.current.submittingDecisionKeys.size).toBe(0);
142
+ });
143
+
144
+ it("surfaces the error and rethrows on failure, keyed by the whole-set key", async () => {
145
+ mockSubmitFileDecision.mockRejectedValueOnce(new Error("digest mismatch"));
146
+
147
+ const { result } = renderHook(() => useFileReview(), {
148
+ wrapper: createWrapper(makeMockClient()),
149
+ });
150
+
151
+ await act(async () => {
152
+ await expect(
153
+ result.current.submitFileDecision("aex-1", "aex-1:0", FileDecisionAction.APPROVE),
154
+ ).rejects.toThrow("digest mismatch");
155
+ });
156
+
157
+ // Scalar mirror (single-error consumers).
158
+ expect(result.current.error).toBeInstanceOf(Error);
159
+ expect(result.current.error!.message).toBe("digest mismatch");
160
+ // Keyed map (per-control surfacing) — under the whole-set key.
161
+ expect(result.current.decisionErrors.get(fileDecisionKey("aex-1:0"))?.message).toBe(
162
+ "digest mismatch",
163
+ );
164
+ expect(result.current.submittingDecisionKeys.size).toBe(0);
165
+
166
+ act(() => result.current.clearError());
167
+ expect(result.current.error).toBeNull();
168
+ expect(result.current.decisionErrors.size).toBe(0);
169
+ });
170
+
171
+ it("keys a per-file failure under setId:fileChangeId, leaving other keys untouched", async () => {
172
+ mockSubmitFileDecision.mockRejectedValueOnce(new Error("network down"));
173
+
174
+ const { result } = renderHook(() => useFileReview(), {
175
+ wrapper: createWrapper(makeMockClient()),
176
+ });
177
+
178
+ await act(async () => {
179
+ await expect(
180
+ result.current.submitFileDecision("aex-1", "aex-1:0", FileDecisionAction.REJECT, {
181
+ fileChangeId: "fc1",
182
+ }),
183
+ ).rejects.toThrow("network down");
184
+ });
185
+
186
+ const perFileKey = fileDecisionKey("aex-1:0", "fc1");
187
+ expect(result.current.decisionErrors.get(perFileKey)?.message).toBe("network down");
188
+ // The whole-set key (and any other file) is unaffected.
189
+ expect(result.current.decisionErrors.has(fileDecisionKey("aex-1:0"))).toBe(false);
190
+ });
191
+
192
+ it("clears one key on a retry-start and via clearDecisionError, without touching siblings", async () => {
193
+ mockSubmitFileDecision
194
+ .mockRejectedValueOnce(new Error("fail-a"))
195
+ .mockRejectedValueOnce(new Error("fail-b"));
196
+
197
+ const { result } = renderHook(() => useFileReview(), {
198
+ wrapper: createWrapper(makeMockClient()),
199
+ });
200
+
201
+ // Two distinct files fail.
202
+ await act(async () => {
203
+ await expect(
204
+ result.current.submitFileDecision("aex-1", "aex-1:0", FileDecisionAction.REJECT, {
205
+ fileChangeId: "fc1",
206
+ }),
207
+ ).rejects.toThrow("fail-a");
208
+ });
209
+ await act(async () => {
210
+ await expect(
211
+ result.current.submitFileDecision("aex-1", "aex-1:0", FileDecisionAction.REJECT, {
212
+ fileChangeId: "fc2",
213
+ }),
214
+ ).rejects.toThrow("fail-b");
215
+ });
216
+
217
+ const keyA = fileDecisionKey("aex-1:0", "fc1");
218
+ const keyB = fileDecisionKey("aex-1:0", "fc2");
219
+ expect(result.current.decisionErrors.get(keyA)?.message).toBe("fail-a");
220
+ expect(result.current.decisionErrors.get(keyB)?.message).toBe("fail-b");
221
+
222
+ // Retrying fc1 succeeds: its error clears at submit-start, fc2's stays.
223
+ mockSubmitFileDecision.mockResolvedValueOnce({});
224
+ await act(async () => {
225
+ await result.current.submitFileDecision("aex-1", "aex-1:0", FileDecisionAction.APPROVE, {
226
+ fileChangeId: "fc1",
227
+ });
228
+ });
229
+ expect(result.current.decisionErrors.has(keyA)).toBe(false);
230
+ expect(result.current.decisionErrors.get(keyB)?.message).toBe("fail-b");
231
+
232
+ // clearDecisionError removes only the targeted key.
233
+ act(() => result.current.clearDecisionError(keyB));
234
+ expect(result.current.decisionErrors.size).toBe(0);
235
+ });
236
+ });
@@ -0,0 +1,165 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import type { Stigmer } from "@stigmer/sdk";
5
+ import { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
6
+ import { StigmerContext } from "../../context";
7
+ import { useSubmitApproval } from "../useSubmitApproval";
8
+
9
+ const mockSubmitApproval = vi.fn();
10
+
11
+ function makeMockClient(): Stigmer {
12
+ return {
13
+ agentExecution: { submitApproval: mockSubmitApproval },
14
+ } as unknown as Stigmer;
15
+ }
16
+
17
+ function createWrapper(client: Stigmer) {
18
+ return function Wrapper({ children }: { children: ReactNode }) {
19
+ return <StigmerContext.Provider value={client}>{children}</StigmerContext.Provider>;
20
+ };
21
+ }
22
+
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ });
26
+
27
+ describe("useSubmitApproval", () => {
28
+ it("forwards the decision with the bound execution + tool call ids", async () => {
29
+ mockSubmitApproval.mockResolvedValueOnce({});
30
+
31
+ const { result } = renderHook(() => useSubmitApproval(), {
32
+ wrapper: createWrapper(makeMockClient()),
33
+ });
34
+
35
+ await act(async () => {
36
+ await result.current.submitApproval("aex-1", "tc-1", ApprovalAction.APPROVE, "ok");
37
+ });
38
+
39
+ expect(mockSubmitApproval).toHaveBeenCalledTimes(1);
40
+ expect(mockSubmitApproval.mock.calls[0][0]).toMatchObject({
41
+ agentExecutionId: "aex-1",
42
+ toolCallId: "tc-1",
43
+ action: ApprovalAction.APPROVE,
44
+ comment: "ok",
45
+ });
46
+ });
47
+
48
+ it("tracks submitting tool-call ids while in flight, then clears them", async () => {
49
+ let resolve!: () => void;
50
+ mockSubmitApproval.mockReturnValueOnce(
51
+ new Promise<void>((r) => {
52
+ resolve = r;
53
+ }),
54
+ );
55
+
56
+ const { result } = renderHook(() => useSubmitApproval(), {
57
+ wrapper: createWrapper(makeMockClient()),
58
+ });
59
+
60
+ let pending: Promise<void>;
61
+ act(() => {
62
+ pending = result.current.submitApproval("aex-1", "tc-1", ApprovalAction.APPROVE);
63
+ });
64
+
65
+ expect(result.current.submittingToolCallIds.has("tc-1")).toBe(true);
66
+
67
+ await act(async () => {
68
+ resolve();
69
+ await pending;
70
+ });
71
+
72
+ expect(result.current.submittingToolCallIds.size).toBe(0);
73
+ });
74
+
75
+ it("records the failure keyed by tool call id (and the scalar mirror), then rethrows", async () => {
76
+ mockSubmitApproval.mockRejectedValueOnce(new Error("gate already resolved"));
77
+
78
+ const { result } = renderHook(() => useSubmitApproval(), {
79
+ wrapper: createWrapper(makeMockClient()),
80
+ });
81
+
82
+ await act(async () => {
83
+ await expect(
84
+ result.current.submitApproval("aex-1", "tc-1", ApprovalAction.APPROVE),
85
+ ).rejects.toThrow("gate already resolved");
86
+ });
87
+
88
+ // Keyed map (per-gate surfacing).
89
+ expect(result.current.errorsByToolCallId.get("tc-1")?.message).toBe(
90
+ "gate already resolved",
91
+ );
92
+ // Scalar mirror (single-error / ink consumers).
93
+ expect(result.current.error?.message).toBe("gate already resolved");
94
+ // In-flight state is released even on failure.
95
+ expect(result.current.submittingToolCallIds.size).toBe(0);
96
+
97
+ act(() => result.current.clearError());
98
+ expect(result.current.error).toBeNull();
99
+ expect(result.current.errorsByToolCallId.size).toBe(0);
100
+ });
101
+
102
+ it("keys two failures independently — one gate's error never clobbers another", async () => {
103
+ mockSubmitApproval
104
+ .mockRejectedValueOnce(new Error("fail-a"))
105
+ .mockRejectedValueOnce(new Error("fail-b"));
106
+
107
+ const { result } = renderHook(() => useSubmitApproval(), {
108
+ wrapper: createWrapper(makeMockClient()),
109
+ });
110
+
111
+ await act(async () => {
112
+ await expect(
113
+ result.current.submitApproval("aex-1", "tc-a", ApprovalAction.APPROVE),
114
+ ).rejects.toThrow("fail-a");
115
+ });
116
+ await act(async () => {
117
+ await expect(
118
+ result.current.submitApproval("aex-1", "tc-b", ApprovalAction.REJECT),
119
+ ).rejects.toThrow("fail-b");
120
+ });
121
+
122
+ expect(result.current.errorsByToolCallId.get("tc-a")?.message).toBe("fail-a");
123
+ expect(result.current.errorsByToolCallId.get("tc-b")?.message).toBe("fail-b");
124
+ });
125
+
126
+ it("clears a gate's prior error at retry-start, leaving siblings untouched", async () => {
127
+ mockSubmitApproval
128
+ .mockRejectedValueOnce(new Error("fail-a"))
129
+ .mockRejectedValueOnce(new Error("fail-b"));
130
+
131
+ const { result } = renderHook(() => useSubmitApproval(), {
132
+ wrapper: createWrapper(makeMockClient()),
133
+ });
134
+
135
+ await act(async () => {
136
+ await expect(
137
+ result.current.submitApproval("aex-1", "tc-a", ApprovalAction.APPROVE),
138
+ ).rejects.toThrow("fail-a");
139
+ });
140
+ await act(async () => {
141
+ await expect(
142
+ result.current.submitApproval("aex-1", "tc-b", ApprovalAction.APPROVE),
143
+ ).rejects.toThrow("fail-b");
144
+ });
145
+
146
+ // Retrying tc-a succeeds: its error clears at submit-start; tc-b's persists.
147
+ mockSubmitApproval.mockResolvedValueOnce({});
148
+ await act(async () => {
149
+ await result.current.submitApproval("aex-1", "tc-a", ApprovalAction.APPROVE);
150
+ });
151
+
152
+ expect(result.current.errorsByToolCallId.has("tc-a")).toBe(false);
153
+ expect(result.current.errorsByToolCallId.get("tc-b")?.message).toBe("fail-b");
154
+ });
155
+
156
+ it("keeps a stable return reference across renders when nothing changes", () => {
157
+ const { result, rerender } = renderHook(() => useSubmitApproval(), {
158
+ wrapper: createWrapper(makeMockClient()),
159
+ });
160
+
161
+ const first = result.current;
162
+ rerender();
163
+ expect(result.current).toBe(first);
164
+ });
165
+ });