@stigmer/react 3.0.9-dev.20260616060535 → 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 (426) 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/useAutoDisclosure.d.ts +34 -0
  178. package/internal/useAutoDisclosure.d.ts.map +1 -0
  179. package/internal/useAutoDisclosure.js +53 -0
  180. package/internal/useAutoDisclosure.js.map +1 -0
  181. package/internal/useElapsedSince.d.ts +30 -0
  182. package/internal/useElapsedSince.d.ts.map +1 -0
  183. package/internal/useElapsedSince.js +70 -0
  184. package/internal/useElapsedSince.js.map +1 -0
  185. package/internal/useIsOverflowing.d.ts +30 -0
  186. package/internal/useIsOverflowing.d.ts.map +1 -0
  187. package/internal/useIsOverflowing.js +47 -0
  188. package/internal/useIsOverflowing.js.map +1 -0
  189. package/internal/useIsTextTruncated.d.ts +24 -0
  190. package/internal/useIsTextTruncated.d.ts.map +1 -0
  191. package/internal/useIsTextTruncated.js +41 -0
  192. package/internal/useIsTextTruncated.js.map +1 -0
  193. package/internal/useKeyedSubmission.d.ts +49 -0
  194. package/internal/useKeyedSubmission.d.ts.map +1 -0
  195. package/internal/useKeyedSubmission.js +67 -0
  196. package/internal/useKeyedSubmission.js.map +1 -0
  197. package/package.json +4 -4
  198. package/session/SessionViewer.d.ts +2 -2
  199. package/session/SessionViewer.d.ts.map +1 -1
  200. package/session/SessionViewer.js +10 -4
  201. package/session/SessionViewer.js.map +1 -1
  202. package/session/index.d.ts +2 -0
  203. package/session/index.d.ts.map +1 -1
  204. package/session/index.js +1 -0
  205. package/session/index.js.map +1 -1
  206. package/session/inspector/ChangesTab.d.ts +8 -5
  207. package/session/inspector/ChangesTab.d.ts.map +1 -1
  208. package/session/inspector/ChangesTab.js +11 -8
  209. package/session/inspector/ChangesTab.js.map +1 -1
  210. package/session/inspector/SessionInspector.d.ts +4 -4
  211. package/session/inspector/SessionInspector.d.ts.map +1 -1
  212. package/session/inspector/SessionInspector.js +4 -4
  213. package/session/inspector/SessionInspector.js.map +1 -1
  214. package/session/inspector/index.d.ts +0 -2
  215. package/session/inspector/index.d.ts.map +1 -1
  216. package/session/inspector/index.js +0 -1
  217. package/session/inspector/index.js.map +1 -1
  218. package/session/inspector/useSessionInspector.d.ts +8 -4
  219. package/session/inspector/useSessionInspector.d.ts.map +1 -1
  220. package/session/inspector/useSessionInspector.js +27 -20
  221. package/session/inspector/useSessionInspector.js.map +1 -1
  222. package/session/useSessionConversation.d.ts +37 -3
  223. package/session/useSessionConversation.d.ts.map +1 -1
  224. package/session/useSessionConversation.js +18 -2
  225. package/session/useSessionConversation.js.map +1 -1
  226. package/session/useSessionFileChanges.d.ts +74 -0
  227. package/session/useSessionFileChanges.d.ts.map +1 -0
  228. package/session/useSessionFileChanges.js +178 -0
  229. package/session/useSessionFileChanges.js.map +1 -0
  230. package/src/__tests__/styles-border-layer-invariant.test.ts +105 -0
  231. package/src/execution/ApprovalCard.tsx +368 -230
  232. package/src/execution/ApprovalContext.ts +109 -0
  233. package/src/execution/ArtifactCard.tsx +10 -6
  234. package/src/execution/ArtifactPreviewModal.tsx +12 -6
  235. package/src/execution/EmptyChangeNotice.tsx +62 -0
  236. package/src/execution/FileChangesView.tsx +383 -0
  237. package/src/execution/FilePathLink.tsx +65 -12
  238. package/src/execution/FileReviewCard.tsx +1118 -0
  239. package/src/execution/FileReviewContext.ts +53 -0
  240. package/src/execution/FileReviewDock.tsx +157 -0
  241. package/src/execution/McpToolDetail.tsx +98 -32
  242. package/src/execution/MessageThread.tsx +374 -9
  243. package/src/execution/PlanArtifactCard.tsx +9 -6
  244. package/src/execution/PlanCompletionCard.tsx +1 -1
  245. package/src/execution/ResultView.tsx +333 -162
  246. package/src/execution/SubAgentSection.tsx +58 -11
  247. package/src/execution/TerminalSession.tsx +91 -0
  248. package/src/execution/TodoCard.tsx +186 -0
  249. package/src/execution/ToolArgsView.tsx +28 -17
  250. package/src/execution/ToolCallDetail.tsx +137 -26
  251. package/src/execution/ToolCallGroup.tsx +68 -296
  252. package/src/execution/ToolCallItem.tsx +265 -67
  253. package/src/execution/ToolRunGroup.tsx +302 -0
  254. package/src/execution/__tests__/ApprovalCard.test.tsx +395 -5
  255. package/src/execution/__tests__/ArtifactPreviewModal.test.tsx +0 -1
  256. package/src/execution/__tests__/EmptyChangeNotice.test.tsx +24 -0
  257. package/src/execution/__tests__/FileChangesView.test.tsx +296 -0
  258. package/src/execution/__tests__/FilePathLink.test.tsx +92 -0
  259. package/src/execution/__tests__/FileReviewCard.test.tsx +1044 -0
  260. package/src/execution/__tests__/FileReviewDock.test.tsx +134 -0
  261. package/src/execution/__tests__/McpToolDetail.test.tsx +79 -0
  262. package/src/execution/__tests__/MessageThread.test.tsx +324 -0
  263. package/src/execution/__tests__/PlanArtifactCard.test.tsx +37 -16
  264. package/src/execution/__tests__/ResultView.test.tsx +280 -2
  265. package/src/execution/__tests__/SubAgentSection.test.tsx +180 -0
  266. package/src/execution/__tests__/TerminalSession.test.tsx +72 -0
  267. package/src/execution/__tests__/TodoCard.test.tsx +102 -0
  268. package/src/execution/__tests__/ToolArgsView.test.tsx +48 -0
  269. package/src/execution/__tests__/ToolCallDetail.test.tsx +195 -0
  270. package/src/execution/__tests__/ToolCallGroup.test.tsx +132 -0
  271. package/src/execution/__tests__/ToolCallItem.test.tsx +521 -0
  272. package/src/execution/__tests__/ToolRunGroup.test.tsx +137 -0
  273. package/src/execution/__tests__/blanked-narration.test.tsx +149 -0
  274. package/src/execution/__tests__/buildThreadItems-approvals.test.ts +304 -0
  275. package/src/execution/__tests__/buildThreadItems-file-review.test.ts +331 -0
  276. package/src/execution/__tests__/file-path-resolver.test.ts +44 -0
  277. package/src/execution/__tests__/file-review-status.test.ts +343 -0
  278. package/src/execution/__tests__/segment-tool-calls.test.ts +124 -0
  279. package/src/execution/__tests__/subagent-file-review-badge.test.tsx +273 -0
  280. package/src/execution/__tests__/thread-keys.test.ts +195 -0
  281. package/src/execution/__tests__/tool-presenter.test.tsx +95 -0
  282. package/src/execution/__tests__/useArtifactDownload.test.tsx +67 -0
  283. package/src/execution/__tests__/useArtifactDownloadUrl.test.tsx +51 -0
  284. package/src/execution/__tests__/useFileChangeContent.test.tsx +240 -0
  285. package/src/execution/__tests__/useFileReview.test.tsx +236 -0
  286. package/src/execution/__tests__/useSubmitApproval.test.tsx +165 -0
  287. package/src/execution/__tests__/useToolOutputContent.test.tsx +82 -0
  288. package/src/execution/approval-provenance.ts +8 -0
  289. package/src/execution/artifact-utils.ts +0 -18
  290. package/src/execution/execution-phases.ts +4 -16
  291. package/src/execution/file-path-resolver.ts +38 -0
  292. package/src/execution/file-review-status.ts +219 -0
  293. package/src/execution/index.ts +84 -4
  294. package/src/execution/segment-tool-calls.ts +101 -0
  295. package/src/execution/tool-categories.ts +128 -1
  296. package/src/execution/tool-presenter.ts +58 -3
  297. package/src/execution/tool-rendering-primitives.tsx +11 -14
  298. package/src/execution/useArtifactDownload.ts +109 -0
  299. package/src/execution/useArtifactDownloadUrl.ts +116 -0
  300. package/src/execution/useFileChangeContent.ts +149 -0
  301. package/src/execution/useFileReview.ts +197 -0
  302. package/src/execution/useSubmitApproval.ts +49 -27
  303. package/src/execution/useToolOutputContent.ts +70 -0
  304. package/src/index.ts +60 -1
  305. package/src/internal/ApprovalPeekBar.tsx +73 -0
  306. package/src/internal/BoundedContent.tsx +97 -0
  307. package/src/internal/DecisionButton.tsx +127 -0
  308. package/src/internal/InCardDecisionError.tsx +47 -0
  309. package/src/internal/RevealToggle.tsx +91 -0
  310. package/src/internal/VirtualizedThread.tsx +28 -2
  311. package/src/internal/__tests__/ApprovalPeekBar.test.tsx +34 -0
  312. package/src/internal/__tests__/BoundedContent.test.tsx +92 -0
  313. package/src/internal/__tests__/DecisionButton.test.tsx +107 -0
  314. package/src/internal/__tests__/RevealToggle.test.tsx +61 -0
  315. package/src/internal/__tests__/useAutoDisclosure.test.ts +74 -0
  316. package/src/internal/__tests__/useElapsedSince.test.tsx +68 -0
  317. package/src/internal/__tests__/useIsOverflowing.test.ts +36 -0
  318. package/src/internal/__tests__/useKeyedSubmission.test.tsx +126 -0
  319. package/src/internal/useAutoDisclosure.ts +58 -0
  320. package/src/internal/useElapsedSince.ts +72 -0
  321. package/src/internal/useIsOverflowing.ts +61 -0
  322. package/src/internal/useIsTextTruncated.ts +55 -0
  323. package/src/internal/useKeyedSubmission.ts +101 -0
  324. package/src/session/SessionViewer.tsx +21 -4
  325. package/src/session/__tests__/SessionViewer-fileReviewDock.test.tsx +198 -0
  326. package/src/session/__tests__/audienceWiring.test.tsx +4 -0
  327. package/src/session/__tests__/useSessionFileChanges.test.ts +303 -0
  328. package/src/session/index.ts +3 -0
  329. package/src/session/inspector/ChangesTab.tsx +20 -17
  330. package/src/session/inspector/SessionInspector.tsx +12 -10
  331. package/src/session/inspector/__tests__/ArtifactsTab.test.tsx +0 -1
  332. package/src/session/inspector/__tests__/ChangesTab.test.tsx +38 -0
  333. package/src/session/inspector/__tests__/useSessionInspector.test.ts +46 -61
  334. package/src/session/inspector/index.ts +0 -3
  335. package/src/session/inspector/useSessionInspector.ts +27 -20
  336. package/src/session/useSessionConversation.ts +80 -3
  337. package/src/session/useSessionFileChanges.ts +207 -0
  338. package/src/styles.css +35 -10
  339. package/src/version-history/DiffViewer.tsx +10 -7
  340. package/src/version-history/UnifiedDiffView.tsx +66 -0
  341. package/src/version-history/__tests__/DiffViewer.test.tsx +57 -0
  342. package/src/version-history/__tests__/UnifiedDiffView.test.tsx +50 -0
  343. package/src/version-history/__tests__/computeDiff.test.ts +35 -0
  344. package/src/version-history/__tests__/parseUnifiedDiff.test.ts +82 -0
  345. package/src/version-history/computeDiff.ts +6 -37
  346. package/src/version-history/diff-hunks.ts +63 -0
  347. package/src/version-history/index.ts +1 -0
  348. package/src/version-history/parseUnifiedDiff.ts +34 -0
  349. package/src/workflow/WorkflowExecutionApprovalCard.tsx +49 -24
  350. package/src/workflow/WorkflowExecutionTimeline.tsx +15 -4
  351. package/src/workflow/WorkflowExecutionTimelineEvent.tsx +8 -3
  352. package/src/workflow/WorkflowExecutionViewer.tsx +72 -28
  353. package/src/workflow/WorkflowFileReviewList.tsx +152 -0
  354. package/src/workflow/WorkflowTaskApprovalCard.tsx +37 -85
  355. package/src/workflow/__tests__/ExecutionInspector.test.tsx +30 -2
  356. package/src/workflow/__tests__/WorkflowExecutionApprovalCard.test.tsx +129 -0
  357. package/src/workflow/__tests__/WorkflowFileReviewList.test.tsx +129 -0
  358. package/src/workflow/__tests__/WorkflowTaskApprovalCard.test.tsx +43 -11
  359. package/src/workflow/__tests__/useWorkflowExecutionActions.test.tsx +276 -0
  360. package/src/workflow/execution-inspector/ExecutionInspector.tsx +42 -18
  361. package/src/workflow/index.ts +5 -0
  362. package/src/workflow/useWorkflowExecutionActions.ts +177 -29
  363. package/styles.css +1 -1
  364. package/version-history/DiffViewer.d.ts.map +1 -1
  365. package/version-history/DiffViewer.js +9 -3
  366. package/version-history/DiffViewer.js.map +1 -1
  367. package/version-history/UnifiedDiffView.d.ts +29 -0
  368. package/version-history/UnifiedDiffView.d.ts.map +1 -0
  369. package/version-history/UnifiedDiffView.js +36 -0
  370. package/version-history/UnifiedDiffView.js.map +1 -0
  371. package/version-history/computeDiff.d.ts +3 -1
  372. package/version-history/computeDiff.d.ts.map +1 -1
  373. package/version-history/computeDiff.js +5 -35
  374. package/version-history/computeDiff.js.map +1 -1
  375. package/version-history/diff-hunks.d.ts +21 -0
  376. package/version-history/diff-hunks.d.ts.map +1 -0
  377. package/version-history/diff-hunks.js +58 -0
  378. package/version-history/diff-hunks.js.map +1 -0
  379. package/version-history/index.d.ts +1 -0
  380. package/version-history/index.d.ts.map +1 -1
  381. package/version-history/index.js +1 -0
  382. package/version-history/index.js.map +1 -1
  383. package/version-history/parseUnifiedDiff.d.ts +22 -0
  384. package/version-history/parseUnifiedDiff.d.ts.map +1 -0
  385. package/version-history/parseUnifiedDiff.js +34 -0
  386. package/version-history/parseUnifiedDiff.js.map +1 -0
  387. package/workflow/WorkflowExecutionApprovalCard.d.ts +8 -1
  388. package/workflow/WorkflowExecutionApprovalCard.d.ts.map +1 -1
  389. package/workflow/WorkflowExecutionApprovalCard.js +17 -3
  390. package/workflow/WorkflowExecutionApprovalCard.js.map +1 -1
  391. package/workflow/WorkflowExecutionTimeline.d.ts +11 -2
  392. package/workflow/WorkflowExecutionTimeline.d.ts.map +1 -1
  393. package/workflow/WorkflowExecutionTimeline.js +2 -2
  394. package/workflow/WorkflowExecutionTimeline.js.map +1 -1
  395. package/workflow/WorkflowExecutionTimelineEvent.d.ts +4 -1
  396. package/workflow/WorkflowExecutionTimelineEvent.d.ts.map +1 -1
  397. package/workflow/WorkflowExecutionTimelineEvent.js +2 -2
  398. package/workflow/WorkflowExecutionTimelineEvent.js.map +1 -1
  399. package/workflow/WorkflowExecutionViewer.d.ts.map +1 -1
  400. package/workflow/WorkflowExecutionViewer.js +18 -10
  401. package/workflow/WorkflowExecutionViewer.js.map +1 -1
  402. package/workflow/WorkflowFileReviewList.d.ts +46 -0
  403. package/workflow/WorkflowFileReviewList.d.ts.map +1 -0
  404. package/workflow/WorkflowFileReviewList.js +48 -0
  405. package/workflow/WorkflowFileReviewList.js.map +1 -0
  406. package/workflow/WorkflowTaskApprovalCard.d.ts +8 -1
  407. package/workflow/WorkflowTaskApprovalCard.d.ts.map +1 -1
  408. package/workflow/WorkflowTaskApprovalCard.js +12 -19
  409. package/workflow/WorkflowTaskApprovalCard.js.map +1 -1
  410. package/workflow/execution-inspector/ExecutionInspector.d.ts +21 -4
  411. package/workflow/execution-inspector/ExecutionInspector.d.ts.map +1 -1
  412. package/workflow/execution-inspector/ExecutionInspector.js +6 -3
  413. package/workflow/execution-inspector/ExecutionInspector.js.map +1 -1
  414. package/workflow/index.d.ts +1 -0
  415. package/workflow/index.d.ts.map +1 -1
  416. package/workflow/index.js +1 -0
  417. package/workflow/index.js.map +1 -1
  418. package/workflow/useWorkflowExecutionActions.d.ts +62 -4
  419. package/workflow/useWorkflowExecutionActions.d.ts.map +1 -1
  420. package/workflow/useWorkflowExecutionActions.js +94 -18
  421. package/workflow/useWorkflowExecutionActions.js.map +1 -1
  422. package/session/inspector/PlanTab.d.ts +0 -15
  423. package/session/inspector/PlanTab.d.ts.map +0 -1
  424. package/session/inspector/PlanTab.js +0 -19
  425. package/session/inspector/PlanTab.js.map +0 -1
  426. package/src/session/inspector/PlanTab.tsx +0 -31
@@ -0,0 +1,1044 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { render, screen, cleanup, fireEvent } from "@testing-library/react";
3
+ import { create } from "@bufbuild/protobuf";
4
+ import { FileContentSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
5
+ import {
6
+ CapturedFileChangeSchema,
7
+ FileChangeSetSchema,
8
+ FileDecisionSchema,
9
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
10
+ import {
11
+ DiffCompleteness,
12
+ FileCaptureClass,
13
+ FileChangeKind,
14
+ FileChangeSetStatus,
15
+ FileDecisionAction,
16
+ FileDecisionOrigin,
17
+ FileDecisionScope,
18
+ FileReviewBlockReason,
19
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
20
+ import type { UseFileChangeContentReturn } from "../useFileChangeContent";
21
+
22
+ // FileReviewCard renders FileChangeDiff (per file), which pulls in the
23
+ // offload-resolving content hook transitively. Mock it so the diff path renders
24
+ // without artifact-fetch context, then import the component after the mock.
25
+ let mockReturn: UseFileChangeContentReturn;
26
+ vi.mock("../useFileChangeContent", () => ({
27
+ useFileChangeContent: () => mockReturn,
28
+ }));
29
+
30
+ const { FileReviewCard } = await import("../FileReviewCard");
31
+ const { fileDecisionKey } = await import("../useFileReview");
32
+
33
+ function setContent(overrides?: Partial<UseFileChangeContentReturn>) {
34
+ mockReturn = {
35
+ beforeText: "old\n",
36
+ afterText: "new\n",
37
+ isBinary: false,
38
+ isLoading: false,
39
+ error: null,
40
+ isTruncated: false,
41
+ downloadUrl: null,
42
+ ...overrides,
43
+ };
44
+ }
45
+
46
+ beforeEach(() => setContent());
47
+ afterEach(cleanup);
48
+
49
+ const noop = () => {};
50
+
51
+ /**
52
+ * Open the bar's diff expander. A COMPLETE undecided set and a settled record
53
+ * start collapsed (the bar carries the verdict; the transcript's stamped rows
54
+ * carry the previews), so tests that assert on per-file content expand first —
55
+ * exactly the user's own path to the authoritative diffs.
56
+ */
57
+ function expandCard() {
58
+ fireEvent.click(
59
+ screen.getByRole("button", { name: /^(Review|Show)$/ }),
60
+ );
61
+ }
62
+
63
+ function inline(value: string, isBinary = false) {
64
+ return create(FileContentSchema, { body: { case: "inline", value }, isBinary });
65
+ }
66
+
67
+ function capturedChange(opts: {
68
+ id: string;
69
+ path: string;
70
+ fileDigest: string;
71
+ diffComplete?: boolean;
72
+ captureClass?: FileCaptureClass;
73
+ }) {
74
+ return create(CapturedFileChangeSchema, {
75
+ id: opts.id,
76
+ pathBefore: opts.path,
77
+ pathAfter: opts.path,
78
+ kind: FileChangeKind.MODIFY,
79
+ captureClass: opts.captureClass ?? FileCaptureClass.GIT_TRACKED,
80
+ before: inline("old\n"),
81
+ after: inline("new\n"),
82
+ fileDigest: opts.fileDigest,
83
+ diffComplete: opts.diffComplete ?? true,
84
+ });
85
+ }
86
+
87
+ /** A binary change (real wire shape): both sides present + flagged, incomplete. */
88
+ function capturedBinaryChange(opts: {
89
+ id: string;
90
+ path: string;
91
+ fileDigest: string;
92
+ captureClass?: FileCaptureClass;
93
+ }) {
94
+ return create(CapturedFileChangeSchema, {
95
+ id: opts.id,
96
+ pathBefore: opts.path,
97
+ pathAfter: opts.path,
98
+ kind: FileChangeKind.MODIFY,
99
+ captureClass: opts.captureClass ?? FileCaptureClass.GIT_TRACKED,
100
+ before: inline("\u0000old", true),
101
+ after: inline("\u0000new", true),
102
+ fileDigest: opts.fileDigest,
103
+ diffComplete: false,
104
+ });
105
+ }
106
+
107
+ /**
108
+ * A diff-unavailable change (real wire shape): content-less + incomplete — the
109
+ * shape both the secret gate and the size backstop produce. `blockedReason`
110
+ * carries the honest cause (doc 15); omit it to model a pre-doc-15/unknown row.
111
+ */
112
+ function capturedUnavailableChange(opts: {
113
+ id: string;
114
+ path: string;
115
+ fileDigest: string;
116
+ captureClass?: FileCaptureClass;
117
+ blockedReason?: FileReviewBlockReason;
118
+ }) {
119
+ return create(CapturedFileChangeSchema, {
120
+ id: opts.id,
121
+ pathBefore: opts.path,
122
+ pathAfter: opts.path,
123
+ kind: FileChangeKind.MODIFY,
124
+ captureClass: opts.captureClass ?? FileCaptureClass.GIT_IGNORED_CAPTURED,
125
+ fileDigest: opts.fileDigest,
126
+ diffComplete: false,
127
+ blockedReason: opts.blockedReason ?? FileReviewBlockReason.UNSPECIFIED,
128
+ });
129
+ }
130
+
131
+ function fileDecision(fileChangeId: string, action: FileDecisionAction) {
132
+ return create(FileDecisionSchema, {
133
+ changeSetId: "aex-1:0",
134
+ fileChangeId,
135
+ scope: FileDecisionScope.FILE,
136
+ action,
137
+ });
138
+ }
139
+
140
+ /** Single-file change set (the whole set IS the file — no per-file controls). */
141
+ function changeSet(opts?: {
142
+ diffCompleteness?: DiffCompleteness;
143
+ change?: ReturnType<typeof capturedChange>;
144
+ }) {
145
+ return create(FileChangeSetSchema, {
146
+ id: "aex-1:0",
147
+ status: FileChangeSetStatus.AWAITING_REVIEW,
148
+ aggregateDigest: "agg-1",
149
+ diffCompleteness: opts?.diffCompleteness ?? DiffCompleteness.COMPLETE,
150
+ changes: [
151
+ opts?.change ??
152
+ capturedChange({ id: "aex-1:0:src/a.ts", path: "src/a.ts", fileDigest: "d-a" }),
153
+ ],
154
+ });
155
+ }
156
+
157
+ /** Two-file change set with per-file Keep/Discard controls. */
158
+ function multiChangeSet(opts?: {
159
+ diffCompleteness?: DiffCompleteness;
160
+ fc1?: ReturnType<typeof capturedChange>;
161
+ fc2?: ReturnType<typeof capturedChange>;
162
+ decisions?: ReturnType<typeof fileDecision>[];
163
+ }) {
164
+ return create(FileChangeSetSchema, {
165
+ id: "aex-1:0",
166
+ status: FileChangeSetStatus.AWAITING_REVIEW,
167
+ aggregateDigest: "agg-1",
168
+ diffCompleteness: opts?.diffCompleteness ?? DiffCompleteness.COMPLETE,
169
+ changes: [
170
+ opts?.fc1 ?? capturedChange({ id: "fc1", path: "src/a.ts", fileDigest: "d-fc1" }),
171
+ opts?.fc2 ?? capturedChange({ id: "fc2", path: "src/b.ts", fileDigest: "d-fc2" }),
172
+ ],
173
+ decisions: opts?.decisions ?? [],
174
+ });
175
+ }
176
+
177
+ describe("FileReviewCard", () => {
178
+ describe("single-file set (bulk only)", () => {
179
+ it("renders the change set summary and file diff", () => {
180
+ render(<FileReviewCard fileChangeSet={changeSet()} onSubmit={noop} />);
181
+ expect(screen.getByText(/awaiting review/i)).toBeTruthy();
182
+ expect(
183
+ screen.getByRole("alert", { name: /review 1 file change/i }),
184
+ ).toBeTruthy();
185
+ });
186
+
187
+ it("shows no per-file Keep/Discard radios for a single file", () => {
188
+ render(<FileReviewCard fileChangeSet={changeSet()} onSubmit={noop} />);
189
+ expect(screen.queryByRole("radio")).toBeNull();
190
+ // The bulk label is singular (no misleading "all" for one file).
191
+ expect(screen.getByRole("button", { name: "Approve" })).toBeTruthy();
192
+ expect(screen.getByRole("button", { name: "Reject" })).toBeTruthy();
193
+ });
194
+
195
+ it("submits a CHANGE_SET APPROVE bound to the aggregate digest on Approve", () => {
196
+ const onSubmit = vi.fn();
197
+ render(<FileReviewCard fileChangeSet={changeSet()} onSubmit={onSubmit} />);
198
+
199
+ fireEvent.click(screen.getByRole("button", { name: "Approve" }));
200
+
201
+ expect(onSubmit).toHaveBeenCalledTimes(1);
202
+ expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.APPROVE, {
203
+ scope: FileDecisionScope.CHANGE_SET,
204
+ expectedDigest: "agg-1",
205
+ // A COMPLETE set carries no acknowledgment (nothing unreviewable).
206
+ acknowledgeUnreviewable: false,
207
+ });
208
+ });
209
+
210
+ it("submits a CHANGE_SET REJECT on Reject", () => {
211
+ const onSubmit = vi.fn();
212
+ render(<FileReviewCard fileChangeSet={changeSet()} onSubmit={onSubmit} />);
213
+
214
+ fireEvent.click(screen.getByRole("button", { name: "Reject" }));
215
+
216
+ expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.REJECT, {
217
+ scope: FileDecisionScope.CHANGE_SET,
218
+ expectedDigest: "agg-1",
219
+ acknowledgeUnreviewable: false,
220
+ });
221
+ });
222
+
223
+ it("switches a single INCOMPLETE file to the per-file control (no CHANGE_SET footer)", () => {
224
+ // A lone binary set is BINARY_SUMMARY_ONLY, but "Keep all" is a multi-file
225
+ // affordance: with a single file the bulk footer is dropped and the per-file
226
+ // "Keep anyway" / Discard control (DD-16) is the only decision surface.
227
+ render(
228
+ <FileReviewCard
229
+ fileChangeSet={changeSet({
230
+ diffCompleteness: DiffCompleteness.BINARY_SUMMARY_ONLY,
231
+ change: capturedBinaryChange({ id: "aex-1:0:img.png", path: "img.png", fileDigest: "d" }),
232
+ })}
233
+ onSubmit={noop}
234
+ />,
235
+ );
236
+
237
+ // No bulk footer for a single incomplete file.
238
+ expect(screen.queryByRole("button", { name: "Approve" })).toBeNull();
239
+ expect(screen.queryByRole("button", { name: "Reject" })).toBeNull();
240
+ // The per-file control is present, with an enabled binary "Keep anyway".
241
+ expect(
242
+ (screen.getByRole("radio", { name: /Keep img.png anyway/i }) as HTMLButtonElement).disabled,
243
+ ).toBe(false);
244
+ expect(
245
+ (screen.getByRole("radio", { name: "Discard img.png" }) as HTMLButtonElement).disabled,
246
+ ).toBe(false);
247
+ });
248
+ });
249
+
250
+ describe("multi-file set (per-file controls)", () => {
251
+ it("renders a Keep/Discard radio per file plus whole-set controls", () => {
252
+ render(<FileReviewCard fileChangeSet={multiChangeSet()} onSubmit={noop} />);
253
+ expandCard();
254
+
255
+ expect(screen.getByRole("radio", { name: "Keep src/a.ts" })).toBeTruthy();
256
+ expect(screen.getByRole("radio", { name: "Discard src/a.ts" })).toBeTruthy();
257
+ expect(screen.getByRole("radio", { name: "Keep src/b.ts" })).toBeTruthy();
258
+ expect(screen.getByRole("radio", { name: "Discard src/b.ts" })).toBeTruthy();
259
+ expect(screen.getByRole("button", { name: "Approve all" })).toBeTruthy();
260
+ expect(screen.getByRole("button", { name: "Reject all" })).toBeTruthy();
261
+ });
262
+
263
+ it("submits a FILE APPROVE bound to the file digest on Keep", () => {
264
+ const onSubmit = vi.fn();
265
+ render(<FileReviewCard fileChangeSet={multiChangeSet()} onSubmit={onSubmit} />);
266
+ expandCard();
267
+
268
+ fireEvent.click(screen.getByRole("radio", { name: "Keep src/a.ts" }));
269
+
270
+ expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.APPROVE, {
271
+ scope: FileDecisionScope.FILE,
272
+ fileChangeId: "fc1",
273
+ expectedDigest: "d-fc1",
274
+ acknowledgeUnreviewable: false,
275
+ });
276
+ });
277
+
278
+ it("submits a FILE REJECT bound to the file digest on Discard", () => {
279
+ const onSubmit = vi.fn();
280
+ render(<FileReviewCard fileChangeSet={multiChangeSet()} onSubmit={onSubmit} />);
281
+ expandCard();
282
+
283
+ fireEvent.click(screen.getByRole("radio", { name: "Discard src/b.ts" }));
284
+
285
+ expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.REJECT, {
286
+ scope: FileDecisionScope.FILE,
287
+ fileChangeId: "fc2",
288
+ expectedDigest: "d-fc2",
289
+ acknowledgeUnreviewable: false,
290
+ });
291
+ });
292
+
293
+ it("disables Keep (but not Discard) for an incomplete file, leaving complete files keepable", () => {
294
+ const onSubmit = vi.fn();
295
+ render(
296
+ <FileReviewCard
297
+ fileChangeSet={multiChangeSet({
298
+ diffCompleteness: DiffCompleteness.PARTIAL_BLOCKED,
299
+ fc2: capturedUnavailableChange({ id: "fc2", path: "src/b.ts", fileDigest: "d-fc2" }),
300
+ })}
301
+ onSubmit={onSubmit}
302
+ />,
303
+ );
304
+
305
+ // The unavailable file can only be discarded.
306
+ expect(
307
+ (screen.getByRole("radio", { name: "Keep src/b.ts" }) as HTMLButtonElement)
308
+ .disabled,
309
+ ).toBe(true);
310
+ expect(
311
+ (screen.getByRole("radio", { name: "Discard src/b.ts" }) as HTMLButtonElement)
312
+ .disabled,
313
+ ).toBe(false);
314
+ // The complete file is still keepable individually.
315
+ expect(
316
+ (screen.getByRole("radio", { name: "Keep src/a.ts" }) as HTMLButtonElement)
317
+ .disabled,
318
+ ).toBe(false);
319
+ // The whole-set Approve is blocked while the set is not COMPLETE.
320
+ expect(
321
+ (screen.getByRole("button", { name: "Approve all" }) as HTMLButtonElement)
322
+ .disabled,
323
+ ).toBe(true);
324
+ });
325
+
326
+ it("shows a committed verdict as checked and flips it on the other option", () => {
327
+ const onSubmit = vi.fn();
328
+ render(
329
+ <FileReviewCard
330
+ fileChangeSet={multiChangeSet({
331
+ decisions: [fileDecision("fc1", FileDecisionAction.APPROVE)],
332
+ })}
333
+ onSubmit={onSubmit}
334
+ />,
335
+ );
336
+
337
+ expect(
338
+ screen.getByRole("radio", { name: "Keep src/a.ts" }).getAttribute("aria-checked"),
339
+ ).toBe("true");
340
+
341
+ // Flipping is allowed: clicking Discard records a new (last-wins) decision.
342
+ fireEvent.click(screen.getByRole("radio", { name: "Discard src/a.ts" }));
343
+ expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.REJECT, {
344
+ scope: FileDecisionScope.FILE,
345
+ fileChangeId: "fc1",
346
+ expectedDigest: "d-fc1",
347
+ acknowledgeUnreviewable: false,
348
+ });
349
+ });
350
+
351
+ it("reflects last-write-wins: a later REJECT supersedes an earlier APPROVE", () => {
352
+ render(
353
+ <FileReviewCard
354
+ fileChangeSet={multiChangeSet({
355
+ decisions: [
356
+ fileDecision("fc1", FileDecisionAction.APPROVE),
357
+ fileDecision("fc1", FileDecisionAction.REJECT),
358
+ ],
359
+ })}
360
+ onSubmit={noop}
361
+ />,
362
+ );
363
+
364
+ expect(
365
+ screen.getByRole("radio", { name: "Discard src/a.ts" }).getAttribute("aria-checked"),
366
+ ).toBe("true");
367
+ expect(
368
+ screen.getByRole("radio", { name: "Keep src/a.ts" }).getAttribute("aria-checked"),
369
+ ).toBe("false");
370
+ });
371
+
372
+ it("shows review progress for a partially-decided set", () => {
373
+ render(
374
+ <FileReviewCard
375
+ fileChangeSet={multiChangeSet({
376
+ decisions: [fileDecision("fc1", FileDecisionAction.APPROVE)],
377
+ })}
378
+ onSubmit={noop}
379
+ />,
380
+ );
381
+ expect(screen.getByText(/1 of 2 files reviewed/i)).toBeTruthy();
382
+ });
383
+
384
+ it("relabels the bulk action to 'remaining' once some files are decided", () => {
385
+ const onSubmit = vi.fn();
386
+ render(
387
+ <FileReviewCard
388
+ fileChangeSet={multiChangeSet({
389
+ decisions: [fileDecision("fc1", FileDecisionAction.APPROVE)],
390
+ })}
391
+ onSubmit={onSubmit}
392
+ />,
393
+ );
394
+
395
+ const approveRemaining = screen.getByRole("button", { name: "Approve remaining" });
396
+ expect(approveRemaining).toBeTruthy();
397
+ expect(screen.getByRole("button", { name: "Reject remaining" })).toBeTruthy();
398
+
399
+ fireEvent.click(approveRemaining);
400
+ expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.APPROVE, {
401
+ scope: FileDecisionScope.CHANGE_SET,
402
+ expectedDigest: "agg-1",
403
+ acknowledgeUnreviewable: false,
404
+ });
405
+ });
406
+
407
+ it("disables a file's options while that file's decision is in flight", () => {
408
+ const submitting = new Set([fileDecisionKey("aex-1:0", "fc1")]);
409
+ render(
410
+ <FileReviewCard
411
+ fileChangeSet={multiChangeSet()}
412
+ onSubmit={noop}
413
+ submittingDecisionKeys={submitting}
414
+ />,
415
+ );
416
+ expandCard();
417
+
418
+ expect(
419
+ (screen.getByRole("radio", { name: "Keep src/a.ts" }) as HTMLButtonElement)
420
+ .disabled,
421
+ ).toBe(true);
422
+ expect(
423
+ (screen.getByRole("radio", { name: "Discard src/a.ts" }) as HTMLButtonElement)
424
+ .disabled,
425
+ ).toBe(true);
426
+ // A different file is unaffected.
427
+ expect(
428
+ (screen.getByRole("radio", { name: "Keep src/b.ts" }) as HTMLButtonElement)
429
+ .disabled,
430
+ ).toBe(false);
431
+ });
432
+
433
+ it("disables the bulk action while a whole-set decision is in flight", () => {
434
+ const submitting = new Set(["aex-1:0"]);
435
+ render(
436
+ <FileReviewCard
437
+ fileChangeSet={multiChangeSet()}
438
+ onSubmit={noop}
439
+ submittingDecisionKeys={submitting}
440
+ />,
441
+ );
442
+
443
+ expect(
444
+ (screen.getByRole("button", { name: "Approve all" }) as HTMLButtonElement)
445
+ .disabled,
446
+ ).toBe(true);
447
+ });
448
+ });
449
+
450
+ describe("decision errors (in-card surfacing)", () => {
451
+ it("renders no error element when the map is empty (default path)", () => {
452
+ const { container } = render(
453
+ <FileReviewCard fileChangeSet={changeSet()} onSubmit={noop} />,
454
+ );
455
+ expect(container.querySelector('[data-cursor-target="file-review-error"]')).toBeNull();
456
+ expect(
457
+ container.querySelector('[data-cursor-target="file-review-file-error"]'),
458
+ ).toBeNull();
459
+ });
460
+
461
+ it("surfaces a whole-set failure beside the bulk buttons", () => {
462
+ const decisionErrors = new Map([["aex-1:0", new Error("digest mismatch")]]);
463
+ render(
464
+ <FileReviewCard
465
+ fileChangeSet={changeSet()}
466
+ onSubmit={noop}
467
+ decisionErrors={decisionErrors}
468
+ />,
469
+ );
470
+ const err = screen.getByText(/Couldn.t submit decision — digest mismatch/);
471
+ expect(err.getAttribute("data-cursor-target")).toBe("file-review-error");
472
+ });
473
+
474
+ it("surfaces a per-file failure under the right file row only", () => {
475
+ const decisionErrors = new Map([
476
+ [fileDecisionKey("aex-1:0", "fc1"), new Error("network down")],
477
+ ]);
478
+ render(
479
+ <FileReviewCard
480
+ fileChangeSet={multiChangeSet()}
481
+ onSubmit={noop}
482
+ decisionErrors={decisionErrors}
483
+ />,
484
+ );
485
+ expandCard();
486
+ // Exactly one per-file error, and it carries fc1's message.
487
+ const fileErrors = screen.getAllByText(/Couldn.t save/);
488
+ expect(fileErrors).toHaveLength(1);
489
+ expect(fileErrors[0].textContent).toMatch(/network down/);
490
+ // The whole-set error is NOT shown (the failure was per-file).
491
+ expect(screen.queryByText(/Couldn.t submit decision/)).toBeNull();
492
+ });
493
+ });
494
+
495
+ describe("blocked / partial states (Slice 6)", () => {
496
+ it("labels a binary file and offers an enabled 'Keep anyway' (DD-16)", () => {
497
+ const onSubmit = vi.fn();
498
+ render(
499
+ <FileReviewCard
500
+ fileChangeSet={multiChangeSet({
501
+ diffCompleteness: DiffCompleteness.BINARY_SUMMARY_ONLY,
502
+ fc2: capturedBinaryChange({ id: "fc2", path: "img.png", fileDigest: "d-fc2" }),
503
+ })}
504
+ onSubmit={onSubmit}
505
+ />,
506
+ );
507
+
508
+ // Target the per-file note specifically ("...to review"), not the bulk
509
+ // keep-all notice which also mentions "no text diff".
510
+ expect(screen.getByText(/Binary file.*no text diff to review/i)).toBeTruthy();
511
+ // A binary is keepable via an explicit, enabled "Keep anyway".
512
+ const keepAnyway = screen.getByRole("radio", {
513
+ name: /Keep img.png anyway/i,
514
+ }) as HTMLButtonElement;
515
+ expect(keepAnyway.disabled).toBe(false);
516
+ expect(
517
+ (screen.getByRole("radio", { name: "Discard img.png" }) as HTMLButtonElement).disabled,
518
+ ).toBe(false);
519
+ // The reviewable sibling keeps its plain "Keep".
520
+ expect(
521
+ (screen.getByRole("radio", { name: "Keep src/a.ts" }) as HTMLButtonElement).disabled,
522
+ ).toBe(false);
523
+
524
+ // Clicking "Keep anyway" submits an acknowledged FILE approve.
525
+ fireEvent.click(keepAnyway);
526
+ expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.APPROVE, {
527
+ scope: FileDecisionScope.FILE,
528
+ fileChangeId: "fc2",
529
+ expectedDigest: "d-fc2",
530
+ acknowledgeUnreviewable: true,
531
+ });
532
+ });
533
+
534
+ it("labels a diff-unavailable file and associates the reason via aria-describedby", () => {
535
+ render(
536
+ <FileReviewCard
537
+ fileChangeSet={multiChangeSet({
538
+ diffCompleteness: DiffCompleteness.PARTIAL_BLOCKED,
539
+ fc2: capturedUnavailableChange({ id: "fc2", path: "src/b.ts", fileDigest: "d-fc2" }),
540
+ })}
541
+ onSubmit={noop}
542
+ />,
543
+ );
544
+
545
+ const note = screen.getByText(/full diff isn.t available to review/i);
546
+ expect(note).toBeTruthy();
547
+ // The blocked file's radiogroup points at exactly that note (a11y).
548
+ const group = screen.getByRole("radiogroup", { name: "Decision for src/b.ts" });
549
+ const describedBy = group.getAttribute("aria-describedby");
550
+ expect(describedBy).toBeTruthy();
551
+ expect(note.getAttribute("id")).toBe(describedBy);
552
+ // The reviewable sibling's group carries no such description.
553
+ const okGroup = screen.getByRole("radiogroup", { name: "Decision for src/a.ts" });
554
+ expect(okGroup.getAttribute("aria-describedby")).toBeFalsy();
555
+ });
556
+
557
+ it("gives a secret-withheld file its own honest copy (doc 15)", () => {
558
+ render(
559
+ <FileReviewCard
560
+ fileChangeSet={multiChangeSet({
561
+ diffCompleteness: DiffCompleteness.PARTIAL_BLOCKED,
562
+ fc2: capturedUnavailableChange({
563
+ id: "fc2",
564
+ path: ".env",
565
+ fileDigest: "d-fc2",
566
+ blockedReason: FileReviewBlockReason.SECRET_WITHHELD,
567
+ }),
568
+ })}
569
+ onSubmit={noop}
570
+ />,
571
+ );
572
+ expect(screen.getByText(/looks like a secret/i)).toBeTruthy();
573
+ // It is NOT the generic copy — the cause is now honest, not agnostic.
574
+ expect(screen.queryByText(/full diff isn.t available to review/i)).toBeNull();
575
+ });
576
+
577
+ it("gives a size-elided file its own honest copy, distinct from secret (doc 15)", () => {
578
+ render(
579
+ <FileReviewCard
580
+ fileChangeSet={multiChangeSet({
581
+ diffCompleteness: DiffCompleteness.PARTIAL_BLOCKED,
582
+ fc2: capturedUnavailableChange({
583
+ id: "fc2",
584
+ path: "src/huge.ts",
585
+ fileDigest: "d-fc2",
586
+ blockedReason: FileReviewBlockReason.SIZE_ELIDED,
587
+ }),
588
+ })}
589
+ onSubmit={noop}
590
+ />,
591
+ );
592
+ expect(screen.getByText(/too large to display/i)).toBeTruthy();
593
+ expect(screen.queryByText(/looks like a secret/i)).toBeNull();
594
+ });
595
+
596
+ it("shows a provenance badge for a gitignored capture", () => {
597
+ render(
598
+ <FileReviewCard
599
+ fileChangeSet={changeSet({
600
+ change: capturedChange({
601
+ id: "x",
602
+ path: ".env.local",
603
+ fileDigest: "d",
604
+ captureClass: FileCaptureClass.GIT_IGNORED_CAPTURED,
605
+ }),
606
+ })}
607
+ onSubmit={noop}
608
+ />,
609
+ );
610
+ expandCard();
611
+ const badge = screen.getByText("gitignored");
612
+ expect(badge).toBeTruthy();
613
+ expect(badge.getAttribute("aria-label")).toMatch(/ignored by git/i);
614
+ });
615
+
616
+ it("shows no provenance badge for an ordinary git-tracked change", () => {
617
+ render(<FileReviewCard fileChangeSet={changeSet()} onSubmit={noop} />);
618
+ expect(screen.queryByText("gitignored")).toBeNull();
619
+ expect(screen.queryByText("outside git")).toBeNull();
620
+ });
621
+
622
+ it("uses unavailable-specific set copy when a diff-unavailable file is present", () => {
623
+ render(
624
+ <FileReviewCard
625
+ fileChangeSet={multiChangeSet({
626
+ diffCompleteness: DiffCompleteness.PARTIAL_BLOCKED,
627
+ fc2: capturedUnavailableChange({ id: "fc2", path: "src/b.ts", fileDigest: "d-fc2" }),
628
+ })}
629
+ onSubmit={noop}
630
+ />,
631
+ );
632
+ expect(screen.getByText(/isn.t available to review, so the whole set/i)).toBeTruthy();
633
+ });
634
+
635
+ it("offers per-file granular control alongside 'Keep all' in a binary-only set (DD-17)", () => {
636
+ // A binary-only set is keepable in one shot, but the per-file escape hatch
637
+ // stays: keep the reviewable text file on its own, keep-anyway or discard
638
+ // the binary. Bulk "Keep all" and the granular rows coexist.
639
+ render(
640
+ <FileReviewCard
641
+ fileChangeSet={multiChangeSet({
642
+ diffCompleteness: DiffCompleteness.BINARY_SUMMARY_ONLY,
643
+ fc2: capturedBinaryChange({ id: "fc2", path: "img.png", fileDigest: "d-fc2" }),
644
+ })}
645
+ onSubmit={noop}
646
+ />,
647
+ );
648
+ expect(screen.getByRole("button", { name: "Keep all" })).toBeTruthy();
649
+ expect(screen.getByRole("radio", { name: "Keep src/a.ts" })).toBeTruthy();
650
+ expect(screen.getByRole("radio", { name: /Keep img.png anyway/i })).toBeTruthy();
651
+ });
652
+
653
+ it("keeps a single-file binary via an acknowledged FILE approve; discard still works", () => {
654
+ const onSubmit = vi.fn();
655
+ render(
656
+ <FileReviewCard
657
+ fileChangeSet={changeSet({
658
+ diffCompleteness: DiffCompleteness.BINARY_SUMMARY_ONLY,
659
+ change: capturedBinaryChange({ id: "aex-1:0:img.png", path: "img.png", fileDigest: "d" }),
660
+ })}
661
+ onSubmit={onSubmit}
662
+ />,
663
+ );
664
+
665
+ // No CHANGE_SET footer for a single incomplete file ("Keep all" is a
666
+ // multi-file affordance); the per-file control carries an enabled "Keep anyway".
667
+ expect(screen.queryByRole("button", { name: "Approve" })).toBeNull();
668
+ fireEvent.click(screen.getByRole("radio", { name: /Keep img.png anyway/i }));
669
+ expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.APPROVE, {
670
+ scope: FileDecisionScope.FILE,
671
+ fileChangeId: "aex-1:0:img.png",
672
+ expectedDigest: "d",
673
+ acknowledgeUnreviewable: true,
674
+ });
675
+
676
+ fireEvent.click(screen.getByRole("radio", { name: "Discard img.png" }));
677
+ expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.REJECT, {
678
+ scope: FileDecisionScope.FILE,
679
+ fileChangeId: "aex-1:0:img.png",
680
+ expectedDigest: "d",
681
+ acknowledgeUnreviewable: false,
682
+ });
683
+ });
684
+
685
+ it("offers an enabled 'Keep all' for a BINARY_SUMMARY_ONLY set that acknowledges the whole set (DD-17)", () => {
686
+ // A set whose only blocker is binary files is keepable in one action: the
687
+ // bulk approve is enabled, relabeled "Keep all", and carries the whole-set
688
+ // acknowledgment bound to the aggregate digest.
689
+ const onSubmit = vi.fn();
690
+ render(
691
+ <FileReviewCard
692
+ fileChangeSet={multiChangeSet({
693
+ diffCompleteness: DiffCompleteness.BINARY_SUMMARY_ONLY,
694
+ fc2: capturedBinaryChange({ id: "fc2", path: "img.png", fileDigest: "d-fc2" }),
695
+ })}
696
+ onSubmit={onSubmit}
697
+ />,
698
+ );
699
+
700
+ const keepAll = screen.getByRole("button", { name: "Keep all" }) as HTMLButtonElement;
701
+ expect(keepAll.disabled).toBe(false);
702
+ // The notice explains the enabled keep-all rather than blocking it.
703
+ expect(screen.getByText(/Keep all keeps every file as-is/i)).toBeTruthy();
704
+
705
+ fireEvent.click(keepAll);
706
+ expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.APPROVE, {
707
+ scope: FileDecisionScope.CHANGE_SET,
708
+ expectedDigest: "agg-1",
709
+ acknowledgeUnreviewable: true,
710
+ });
711
+ });
712
+
713
+ it("still disables the whole-set Approve when a file is truly unavailable (not binary-only)", () => {
714
+ // A PARTIAL_BLOCKED set (a secret/elided file with no keepable bytes) can
715
+ // never be kept in one shot — the bulk Approve stays disabled.
716
+ render(
717
+ <FileReviewCard
718
+ fileChangeSet={multiChangeSet({
719
+ diffCompleteness: DiffCompleteness.PARTIAL_BLOCKED,
720
+ fc2: capturedUnavailableChange({ id: "fc2", path: "src/b.ts", fileDigest: "d-fc2" }),
721
+ })}
722
+ onSubmit={noop}
723
+ />,
724
+ );
725
+ expect(
726
+ (screen.getByRole("button", { name: "Approve all" }) as HTMLButtonElement).disabled,
727
+ ).toBe(true);
728
+ });
729
+ });
730
+
731
+ describe("read-only settled card (interactive=false)", () => {
732
+ it("shows a passive record with no decision controls", () => {
733
+ render(<FileReviewCard fileChangeSet={changeSet()} interactive={false} />);
734
+ // A neutral group + settled header, never the actionable "Review" alert.
735
+ expect(screen.getByRole("group", { name: /1 file change/i })).toBeTruthy();
736
+ expect(screen.getByText("File changes")).toBeTruthy();
737
+ expect(screen.queryByText("Review file changes")).toBeNull();
738
+ // No decision affordances at all.
739
+ expect(
740
+ screen.queryByRole("button", { name: /approve|reject|keep/i }),
741
+ ).toBeNull();
742
+ expect(screen.queryByRole("radio")).toBeNull();
743
+ });
744
+
745
+ it("renders without an onSubmit callback", () => {
746
+ expect(() =>
747
+ render(<FileReviewCard fileChangeSet={changeSet()} interactive={false} />),
748
+ ).not.toThrow();
749
+ });
750
+
751
+ it("renders each file's committed verdict (Kept / Discarded)", () => {
752
+ render(
753
+ <FileReviewCard
754
+ fileChangeSet={multiChangeSet({
755
+ decisions: [
756
+ fileDecision("fc1", FileDecisionAction.APPROVE),
757
+ fileDecision("fc2", FileDecisionAction.REJECT),
758
+ ],
759
+ })}
760
+ interactive={false}
761
+ />,
762
+ );
763
+ // The collapsed bar already tells the verdict story in one line…
764
+ expect(screen.getByText("1 kept · 1 discarded")).toBeTruthy();
765
+ // …and expanding shows each file's committed verdict beside its diff.
766
+ expandCard();
767
+ expect(screen.getByText("Kept")).toBeTruthy();
768
+ expect(screen.getByText("Discarded")).toBeTruthy();
769
+ expect(screen.queryByRole("radio")).toBeNull();
770
+ });
771
+
772
+ it("labels an undecided file 'Not reviewed'", () => {
773
+ render(<FileReviewCard fileChangeSet={changeSet()} interactive={false} />);
774
+ expandCard();
775
+ expect(screen.getByText("Not reviewed")).toBeTruthy();
776
+ });
777
+
778
+ it("reads every file as kept when the set was decided via a whole-set approve", () => {
779
+ // A "Keep all" set carries only a CHANGE_SET decision; the settled record
780
+ // folds it onto every file (the reconcile's own precedence) rather than
781
+ // showing "Not reviewed" for files without their own FILE decision.
782
+ const bulkApprove = create(FileDecisionSchema, {
783
+ changeSetId: "aex-1:0",
784
+ scope: FileDecisionScope.CHANGE_SET,
785
+ action: FileDecisionAction.APPROVE,
786
+ });
787
+ render(
788
+ <FileReviewCard
789
+ fileChangeSet={multiChangeSet({ decisions: [bulkApprove] })}
790
+ interactive={false}
791
+ />,
792
+ );
793
+ expect(screen.getByText("2 kept")).toBeTruthy();
794
+ expandCard();
795
+ expect(screen.getAllByText("Kept")).toHaveLength(2);
796
+ expect(screen.queryByText("Not reviewed")).toBeNull();
797
+ });
798
+
799
+ it("labels a policy auto-kept set 'Kept automatically' — never as a human review (DD-28)", () => {
800
+ // The approved-command auto-keep authors a CHANGE_SET approve with origin
801
+ // POLICY_APPROVED_COMMAND. The record must say the platform kept it
802
+ // because the user approved the command — not read like a human decided
803
+ // it here ("2 kept").
804
+ const policyKeep = create(FileDecisionSchema, {
805
+ changeSetId: "aex-1:0",
806
+ scope: FileDecisionScope.CHANGE_SET,
807
+ action: FileDecisionAction.APPROVE,
808
+ origin: FileDecisionOrigin.POLICY_APPROVED_COMMAND,
809
+ });
810
+ render(
811
+ <FileReviewCard
812
+ fileChangeSet={multiChangeSet({ decisions: [policyKeep] })}
813
+ interactive={false}
814
+ />,
815
+ );
816
+ expect(
817
+ screen.getByText("Kept automatically — produced by a command you approved"),
818
+ ).toBeTruthy();
819
+ expect(screen.queryByText("2 kept")).toBeNull();
820
+ // The per-file verdicts still fold as kept (the decision is a real
821
+ // CHANGE_SET approve), so the expanded record stays consistent.
822
+ expandCard();
823
+ expect(screen.getAllByText("Kept")).toHaveLength(2);
824
+ });
825
+
826
+ it("a human whole-set approve keeps the plain verdict summary (origin USER)", () => {
827
+ const humanKeep = create(FileDecisionSchema, {
828
+ changeSetId: "aex-1:0",
829
+ scope: FileDecisionScope.CHANGE_SET,
830
+ action: FileDecisionAction.APPROVE,
831
+ origin: FileDecisionOrigin.USER,
832
+ });
833
+ render(
834
+ <FileReviewCard
835
+ fileChangeSet={multiChangeSet({ decisions: [humanKeep] })}
836
+ interactive={false}
837
+ />,
838
+ );
839
+ expect(screen.getByText("2 kept")).toBeTruthy();
840
+ expect(screen.queryByText(/Kept automatically/)).toBeNull();
841
+ });
842
+ });
843
+
844
+ // The session thread passes showDiffs={false}: its stamped edit rows already
845
+ // show every diff in place, so the expanded body is a compact file list and
846
+ // the card is purely the decision surface. Decision semantics (scopes,
847
+ // digests, acknowledgments) must be byte-identical to diff mode — these
848
+ // cases mirror the decision cases above without the diff DOM. The default
849
+ // (showDiffs unset) stays covered by every other suite in this file.
850
+ describe("list mode (showDiffs=false)", () => {
851
+ function expandFiles() {
852
+ fireEvent.click(screen.getByRole("button", { name: "Files" }));
853
+ }
854
+
855
+ it("labels the expander 'Files' and renders inert list rows with no diff DOM", () => {
856
+ render(
857
+ <FileReviewCard
858
+ fileChangeSet={multiChangeSet()}
859
+ onSubmit={noop}
860
+ showDiffs={false}
861
+ />,
862
+ );
863
+ expandFiles();
864
+ expect(document.querySelector('[data-cursor-target="file-diff"]')).toBeNull();
865
+ expect(
866
+ document.querySelectorAll('[data-cursor-target="file-review-list-row"]'),
867
+ ).toHaveLength(2);
868
+ // Filename-first path text, full path available on hover — and no
869
+ // link/copy affordance (the list is an inventory, not navigation).
870
+ expect(screen.getByText("a.ts")).toBeTruthy();
871
+ expect(screen.getByText("b.ts")).toBeTruthy();
872
+ expect(screen.getByTitle("src/a.ts")).toBeTruthy();
873
+ expect(screen.queryByRole("link")).toBeNull();
874
+ });
875
+
876
+ it("marks each file with its change-kind letter (A/M/D)", () => {
877
+ const added = create(CapturedFileChangeSchema, {
878
+ id: "fc-add",
879
+ pathAfter: "src/new.ts",
880
+ kind: FileChangeKind.ADD,
881
+ captureClass: FileCaptureClass.GIT_TRACKED,
882
+ after: inline("new\n"),
883
+ fileDigest: "d-add",
884
+ diffComplete: true,
885
+ });
886
+ const deleted = create(CapturedFileChangeSchema, {
887
+ id: "fc-del",
888
+ pathBefore: "src/old.ts",
889
+ kind: FileChangeKind.DELETE,
890
+ captureClass: FileCaptureClass.GIT_TRACKED,
891
+ before: inline("old\n"),
892
+ fileDigest: "d-del",
893
+ diffComplete: true,
894
+ });
895
+ render(
896
+ <FileReviewCard
897
+ fileChangeSet={multiChangeSet({ fc1: added, fc2: deleted })}
898
+ onSubmit={noop}
899
+ showDiffs={false}
900
+ />,
901
+ );
902
+ expandFiles();
903
+ expect(screen.getByLabelText("added").textContent).toBe("A");
904
+ expect(screen.getByLabelText("deleted").textContent).toBe("D");
905
+ });
906
+
907
+ it("shows a rename's source path in the row", () => {
908
+ const renamed = create(CapturedFileChangeSchema, {
909
+ id: "fc-ren",
910
+ pathBefore: "src/old-name.ts",
911
+ pathAfter: "src/new-name.ts",
912
+ kind: FileChangeKind.RENAME,
913
+ captureClass: FileCaptureClass.GIT_TRACKED,
914
+ before: inline("x\n"),
915
+ after: inline("x\n"),
916
+ fileDigest: "d-ren",
917
+ diffComplete: true,
918
+ });
919
+ render(
920
+ <FileReviewCard
921
+ fileChangeSet={changeSet({ change: renamed })}
922
+ onSubmit={noop}
923
+ showDiffs={false}
924
+ />,
925
+ );
926
+ expandFiles();
927
+ expect(screen.getByText(/src\/old-name\.ts/)).toBeTruthy();
928
+ expect(screen.getByText("new-name.ts")).toBeTruthy();
929
+ });
930
+
931
+ it("submits a FILE APPROVE bound to the file digest from a list row", () => {
932
+ const onSubmit = vi.fn();
933
+ render(
934
+ <FileReviewCard
935
+ fileChangeSet={multiChangeSet()}
936
+ onSubmit={onSubmit}
937
+ showDiffs={false}
938
+ />,
939
+ );
940
+ expandFiles();
941
+ fireEvent.click(screen.getByRole("radio", { name: "Keep src/a.ts" }));
942
+ expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.APPROVE, {
943
+ scope: FileDecisionScope.FILE,
944
+ fileChangeId: "fc1",
945
+ expectedDigest: "d-fc1",
946
+ acknowledgeUnreviewable: false,
947
+ });
948
+ });
949
+
950
+ it("submits a CHANGE_SET APPROVE bound to the aggregate digest from the bar", () => {
951
+ const onSubmit = vi.fn();
952
+ render(
953
+ <FileReviewCard
954
+ fileChangeSet={multiChangeSet()}
955
+ onSubmit={onSubmit}
956
+ showDiffs={false}
957
+ />,
958
+ );
959
+ fireEvent.click(screen.getByRole("button", { name: "Approve all" }));
960
+ expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.APPROVE, {
961
+ scope: FileDecisionScope.CHANGE_SET,
962
+ expectedDigest: "agg-1",
963
+ acknowledgeUnreviewable: false,
964
+ });
965
+ });
966
+
967
+ it("keeps the acknowledged binary 'Keep anyway' flow, reason note intact", () => {
968
+ const binary = capturedBinaryChange({
969
+ id: "fc-bin",
970
+ path: "logo.png",
971
+ fileDigest: "d-bin",
972
+ });
973
+ const onSubmit = vi.fn();
974
+ render(
975
+ <FileReviewCard
976
+ fileChangeSet={changeSet({
977
+ change: binary,
978
+ diffCompleteness: DiffCompleteness.BINARY_SUMMARY_ONLY,
979
+ })}
980
+ onSubmit={onSubmit}
981
+ showDiffs={false}
982
+ />,
983
+ );
984
+ // An incomplete set starts expanded in list mode too — with no diff to
985
+ // lean on, the reason note IS the per-file context for the decision.
986
+ expect(document.querySelector('[data-cursor-target="file-diff"]')).toBeNull();
987
+ const note = document.querySelector(
988
+ '[data-cursor-target="file-review-block-reason"]',
989
+ );
990
+ expect(note?.textContent).toMatch(/binary file/i);
991
+ fireEvent.click(screen.getByRole("radio", { name: /keep logo\.png anyway/i }));
992
+ expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.APPROVE, {
993
+ scope: FileDecisionScope.FILE,
994
+ fileChangeId: "fc-bin",
995
+ expectedDigest: "d-bin",
996
+ acknowledgeUnreviewable: true,
997
+ });
998
+ });
999
+
1000
+ it("keeps the secret-withheld discard-only flow, honest copy intact", () => {
1001
+ const secret = capturedUnavailableChange({
1002
+ id: "fc-sec",
1003
+ path: ".env",
1004
+ fileDigest: "d-sec",
1005
+ blockedReason: FileReviewBlockReason.SECRET_WITHHELD,
1006
+ });
1007
+ render(
1008
+ <FileReviewCard
1009
+ fileChangeSet={changeSet({
1010
+ change: secret,
1011
+ diffCompleteness: DiffCompleteness.PARTIAL_BLOCKED,
1012
+ })}
1013
+ onSubmit={noop}
1014
+ showDiffs={false}
1015
+ />,
1016
+ );
1017
+ expect(
1018
+ (screen.getByRole("radio", { name: /keep \.env/i }) as HTMLButtonElement)
1019
+ .disabled,
1020
+ ).toBe(true);
1021
+ expect(screen.getByText(/contents withheld/i)).toBeTruthy();
1022
+ });
1023
+
1024
+ it("renders the settled record as verdict-badged list rows, no diffs", () => {
1025
+ render(
1026
+ <FileReviewCard
1027
+ fileChangeSet={multiChangeSet({
1028
+ decisions: [
1029
+ fileDecision("fc1", FileDecisionAction.APPROVE),
1030
+ fileDecision("fc2", FileDecisionAction.REJECT),
1031
+ ],
1032
+ })}
1033
+ interactive={false}
1034
+ showDiffs={false}
1035
+ />,
1036
+ );
1037
+ fireEvent.click(screen.getByRole("button", { name: "Show" }));
1038
+ expect(document.querySelector('[data-cursor-target="file-diff"]')).toBeNull();
1039
+ expect(screen.getByText("Kept")).toBeTruthy();
1040
+ expect(screen.getByText("Discarded")).toBeTruthy();
1041
+ expect(screen.queryByRole("radio")).toBeNull();
1042
+ });
1043
+ });
1044
+ });