@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,97 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import type { ReactNode } from "react";
5
+ import { cn } from "@stigmer/theme";
6
+ import { useIsOverflowing } from "./useIsOverflowing";
7
+ import { RevealToggle } from "./RevealToggle";
8
+
9
+ /**
10
+ * The single shared height budget for a bounded tool-card block. Tall enough
11
+ * to show a screenshot or a handful of diff lines at a glance, short enough
12
+ * that a turn of several cards still scans as a timeline rather than a wall.
13
+ * One constant so the settled edit/write diff and the HITL approval gate diff
14
+ * are visually identical — change it here to retune both at once.
15
+ */
16
+ export const PREVIEW_MAX_HEIGHT = "max-h-48";
17
+
18
+ /** Props for {@link BoundedContent}. */
19
+ export interface BoundedContentProps {
20
+ /** The content to bound — a diff (the only consumer today). */
21
+ readonly children: ReactNode;
22
+ /**
23
+ * Tailwind `max-h-*` clamp applied while collapsed. Defaults to the shared
24
+ * {@link PREVIEW_MAX_HEIGHT} budget; override only with deliberate reason.
25
+ */
26
+ readonly maxHeightClass?: string;
27
+ /** Stable hook for e2e/visual targeting on the reveal control. */
28
+ readonly cursorTarget?: string;
29
+ /** Additional CSS class names for the root container. */
30
+ readonly className?: string;
31
+ }
32
+
33
+ /**
34
+ * Bounds a visual block to one shared height budget with a bottom fade and an
35
+ * in-place {@link RevealToggle} — the "clamp + fade + Show more" shape used by
36
+ * the settled edit/write diff and the approval gate diff.
37
+ *
38
+ * This is the **pixel-clamp** truncation mechanism, for content that has no
39
+ * internal cap of its own (a diff table). Text and terminal output instead use
40
+ * the **line-count** mechanism ({@link CollapsiblePre} / {@link
41
+ * CollapsibleCode}); both render the same {@link RevealToggle}, so the two
42
+ * mechanisms share one visual language without ever double-bounding the same
43
+ * block. Never wrap content that already self-truncates in this.
44
+ *
45
+ * The control and fade appear only when {@link useIsOverflowing} reports the
46
+ * content is actually clipped (or the user expanded it). The fade uses
47
+ * `from-background`, matching every surface that consumes this today; a surface
48
+ * on a different token must supply its own fade.
49
+ *
50
+ * @internal Not part of the public API.
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * <BoundedContent><FileChangeDiff change={change} /></BoundedContent>
55
+ * ```
56
+ */
57
+ export function BoundedContent({
58
+ children,
59
+ maxHeightClass = PREVIEW_MAX_HEIGHT,
60
+ cursorTarget,
61
+ className,
62
+ }: BoundedContentProps) {
63
+ const [expanded, setExpanded] = useState(false);
64
+
65
+ // The clamp is applied while collapsed; measurement is only meaningful then.
66
+ const clamped = !expanded;
67
+ const { ref, isOverflowing } = useIsOverflowing<HTMLDivElement>(clamped);
68
+
69
+ // The control appears only when there is something hidden — either the
70
+ // content overflows now, or it was expanded (so "Show less" stays reachable).
71
+ const showControl = isOverflowing || expanded;
72
+ const showFade = clamped && isOverflowing;
73
+
74
+ return (
75
+ <div className={className}>
76
+ <div
77
+ ref={ref}
78
+ className={cn("relative overflow-hidden", clamped && maxHeightClass)}
79
+ >
80
+ {children}
81
+ {showFade && (
82
+ <div
83
+ aria-hidden="true"
84
+ className="pointer-events-none absolute inset-x-0 bottom-0 h-8 bg-gradient-to-t from-background to-transparent"
85
+ />
86
+ )}
87
+ </div>
88
+ {showControl && (
89
+ <RevealToggle
90
+ expanded={expanded}
91
+ onToggle={() => setExpanded((v) => !v)}
92
+ cursorTarget={cursorTarget}
93
+ />
94
+ )}
95
+ </div>
96
+ );
97
+ }
@@ -0,0 +1,127 @@
1
+ "use client";
2
+
3
+ import { cn } from "@stigmer/theme";
4
+
5
+ /** The visual weight of a {@link DecisionButton}. */
6
+ export type DecisionVariant = "primary" | "ghost" | "danger";
7
+
8
+ /** Props for {@link DecisionButton}. */
9
+ export interface DecisionButtonProps {
10
+ /** Visible button text; also used as the accessible name. */
11
+ readonly label: string;
12
+ /**
13
+ * Visual weight. `primary` is the quiet neutral chip (the single recommended
14
+ * action), `ghost` is the lowest-weight neutral action, and `danger` is a
15
+ * ghost that reveals a destructive cue on hover/focus.
16
+ */
17
+ readonly variant: DecisionVariant;
18
+ /** Invoked on click. */
19
+ readonly onClick: () => void;
20
+ /** True while ANY decision RPC is in flight — disables every button. */
21
+ readonly isSubmitting?: boolean;
22
+ /** True when THIS button's RPC is the one in flight — shows the spinner. */
23
+ readonly isActive?: boolean;
24
+ /**
25
+ * Disables the button independent of {@link isSubmitting} — e.g. an action that
26
+ * is structurally unavailable (approving an incomplete, unreviewable diff).
27
+ */
28
+ readonly disabled?: boolean;
29
+ /** Extra classes (e.g. `ml-auto` to visually separate a demoted action). */
30
+ readonly className?: string;
31
+ /** Stable hook for e2e/visual targeting (`data-cursor-target`). */
32
+ readonly cursorTarget?: string;
33
+ /**
34
+ * Id of an element that explains this button — wired to `aria-describedby` so a
35
+ * disabled, structurally-unavailable action (e.g. approving an unreviewable
36
+ * change) is associated with the notice stating why.
37
+ */
38
+ readonly ariaDescribedby?: string;
39
+ }
40
+
41
+ const BASE = cn(
42
+ "inline-flex items-center justify-center gap-1.5 rounded-md px-3 py-1.5",
43
+ "text-xs font-medium transition-colors",
44
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
45
+ // Disabled dimming is ELEMENT opacity (allowed), never a `bg-token/NN` opacity
46
+ // modifier — each preset keeps full control of the token colors themselves.
47
+ "disabled:cursor-not-allowed disabled:opacity-50 disabled:pointer-events-none",
48
+ );
49
+
50
+ // Quiet, Cursor-grade, token-only.
51
+ const VARIANT: Record<DecisionVariant, string> = {
52
+ // Neutral chip: the single primary action. It reads as primary by fill-weight
53
+ // and border against the ghost actions — NOT by hue — so the hierarchy holds
54
+ // without color being the only channel (a11y).
55
+ primary:
56
+ "border border-border bg-accent text-accent-foreground hover:bg-accent-hover",
57
+ // The canonical SDK ghost: no fill at rest, neutral wash on hover.
58
+ ghost: "text-muted-foreground hover:text-foreground hover:bg-accent-hover",
59
+ // Quiet at rest; the destructive cue is revealed on hover AND keyboard focus
60
+ // (parity for keyboard users — not hover-only). A persistent red button is
61
+ // unnecessary: rejecting is the safe action, and a destructive tool gate is
62
+ // already flagged by the card's accent.
63
+ danger: cn(
64
+ "text-muted-foreground",
65
+ "hover:bg-destructive-subtle hover:text-destructive",
66
+ "focus-visible:bg-destructive-subtle focus-visible:text-destructive",
67
+ ),
68
+ };
69
+
70
+ /**
71
+ * The shared decision-action button for every approval surface — the agent
72
+ * tool gate ({@link ApprovalCard}) and the two workflow approval cards
73
+ * ({@link WorkflowTaskApprovalCard}, {@link WorkflowExecutionApprovalCard}).
74
+ *
75
+ * One source of truth for the quiet, Cursor-grade button system so the three
76
+ * surfaces can never visually drift, and so the spinner and base chrome live in
77
+ * exactly one place. Every visual property flows through `--stgm-*` tokens.
78
+ *
79
+ * @internal Not part of the public API.
80
+ */
81
+ export function DecisionButton({
82
+ label,
83
+ variant,
84
+ onClick,
85
+ isSubmitting = false,
86
+ isActive = false,
87
+ disabled = false,
88
+ className,
89
+ cursorTarget,
90
+ ariaDescribedby,
91
+ }: DecisionButtonProps) {
92
+ return (
93
+ <button
94
+ type="button"
95
+ disabled={isSubmitting || disabled}
96
+ onClick={onClick}
97
+ aria-label={label}
98
+ aria-describedby={ariaDescribedby}
99
+ data-cursor-target={cursorTarget}
100
+ className={cn(BASE, VARIANT[variant], className)}
101
+ >
102
+ {isActive && isSubmitting ? <SpinnerIcon /> : null}
103
+ {label}
104
+ </button>
105
+ );
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Inline SVG icon
110
+ // ---------------------------------------------------------------------------
111
+
112
+ function SpinnerIcon() {
113
+ return (
114
+ <svg
115
+ width="10"
116
+ height="10"
117
+ viewBox="0 0 12 12"
118
+ fill="none"
119
+ stroke="currentColor"
120
+ strokeWidth="1.5"
121
+ className="animate-spin"
122
+ aria-hidden="true"
123
+ >
124
+ <path d="M6 1.5A4.5 4.5 0 1 1 1.5 6" strokeLinecap="round" />
125
+ </svg>
126
+ );
127
+ }
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ /** Props for {@link InCardDecisionError}. */
4
+ export interface InCardDecisionErrorProps {
5
+ /** The failure to surface. Its `message` is shown verbatim (the actionable part). */
6
+ readonly error: Error;
7
+ /**
8
+ * The verb phrase completing "Couldn't ___" — e.g. `"submit decision"` or
9
+ * `"save"`. Kept short; the server `message` carries the detail.
10
+ */
11
+ readonly leadIn: string;
12
+ /** Stable hook for e2e/visual targeting (`data-cursor-target`). */
13
+ readonly cursorTarget: string;
14
+ }
15
+
16
+ /**
17
+ * The shared in-card failure notice for a decision that did not take — used by
18
+ * every decision surface that surfaces errors beside the control that failed:
19
+ * the agent tool gate ({@link ApprovalCard}/{@link ApprovalCardBody}) and the
20
+ * file-review card ({@link FileReviewCard}, whole-set and per-file).
21
+ *
22
+ * One source of truth so those surfaces can never drift in tone, copy shape, or
23
+ * accessibility. `role="alert"` (matching the thread's other inline failures)
24
+ * announces it the moment it appears, since it is a dynamic reaction to the
25
+ * reviewer's action — by the time it renders the optimistic verdict has already
26
+ * reverted, and this explains why. The server `message` is shown verbatim
27
+ * because it is the actionable part (e.g. a digest mismatch means the captured
28
+ * content moved on); the lead-in stays short. Every visual property flows
29
+ * through `--stgm-*` tokens.
30
+ *
31
+ * @internal Not part of the public API.
32
+ */
33
+ export function InCardDecisionError({
34
+ error,
35
+ leadIn,
36
+ cursorTarget,
37
+ }: InCardDecisionErrorProps) {
38
+ return (
39
+ <p
40
+ role="alert"
41
+ className="text-[11px] text-destructive"
42
+ data-cursor-target={cursorTarget}
43
+ >
44
+ Couldn&rsquo;t {leadIn} — {error.message}
45
+ </p>
46
+ );
47
+ }
@@ -0,0 +1,91 @@
1
+ "use client";
2
+
3
+ import { cn } from "@stigmer/theme";
4
+
5
+ /** Props for {@link RevealToggle}. */
6
+ export interface RevealToggleProps {
7
+ /** Whether the governed content is currently fully revealed. */
8
+ readonly expanded: boolean;
9
+ /** Flips the reveal state. */
10
+ readonly onToggle: () => void;
11
+ /** Label shown while collapsed. Defaults to `"Show more"`. */
12
+ readonly moreLabel?: string;
13
+ /** Label shown while expanded. Defaults to `"Show less"`. */
14
+ readonly lessLabel?: string;
15
+ /** Stable hook for e2e/visual targeting (`data-cursor-target`). */
16
+ readonly cursorTarget?: string;
17
+ /** Additional CSS class names for the control. */
18
+ readonly className?: string;
19
+ }
20
+
21
+ /**
22
+ * The single "reveal more / less" control for every bounded block on the
23
+ * execution surface — the diff/gate clamp ({@link BoundedContent}) and the
24
+ * line-count text/terminal blocks ({@link CollapsiblePre} / {@link
25
+ * CollapsibleCode}).
26
+ *
27
+ * It is the Cursor-style affordance the design calls for: a light, centered
28
+ * chevron at the bottom-middle (down to reveal, up to collapse) with a quiet
29
+ * label. Extracting it is what guarantees one disclosure *language* across two
30
+ * truncation *mechanisms* (pixel clamp for visual blocks, line count for text)
31
+ * — the surfaces can never visually drift because they render the same control.
32
+ *
33
+ * Presentational only: it owns no state. The caller holds the reveal state and
34
+ * passes `expanded` + `onToggle`. It carries `aria-expanded` for the block it
35
+ * governs; the owning tool-call row's header chevron (when present) carries the
36
+ * row-level disclosure, so the two axes never collide.
37
+ *
38
+ * @internal Not part of the public API.
39
+ */
40
+ export function RevealToggle({
41
+ expanded,
42
+ onToggle,
43
+ moreLabel = "Show more",
44
+ lessLabel = "Show less",
45
+ cursorTarget,
46
+ className,
47
+ }: RevealToggleProps) {
48
+ return (
49
+ <button
50
+ type="button"
51
+ onClick={onToggle}
52
+ aria-expanded={expanded}
53
+ data-cursor-target={cursorTarget}
54
+ className={cn(
55
+ "mt-1 flex w-full items-center justify-center gap-1 rounded py-0.5",
56
+ "text-xs font-medium text-muted-foreground transition-colors hover:text-foreground",
57
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
58
+ className,
59
+ )}
60
+ >
61
+ <span>{expanded ? lessLabel : moreLabel}</span>
62
+ <ChevronIcon expanded={expanded} />
63
+ </button>
64
+ );
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Inline SVG icon
69
+ // ---------------------------------------------------------------------------
70
+
71
+ function ChevronIcon({ expanded }: { expanded: boolean }) {
72
+ return (
73
+ <svg
74
+ width="10"
75
+ height="10"
76
+ viewBox="0 0 10 10"
77
+ fill="none"
78
+ stroke="currentColor"
79
+ strokeWidth="1.5"
80
+ strokeLinecap="round"
81
+ strokeLinejoin="round"
82
+ className={cn(
83
+ "shrink-0 transition-transform duration-150",
84
+ expanded && "rotate-180",
85
+ )}
86
+ aria-hidden="true"
87
+ >
88
+ <path d="M2.5 3.5L5 6.5L7.5 3.5" />
89
+ </svg>
90
+ );
91
+ }
@@ -18,8 +18,11 @@ import {
18
18
  } from "../execution/MessageThread";
19
19
  import { FilePathContext, type FilePathContextValue } from "../execution/FilePathContext";
20
20
  import { SandboxContext, type SandboxContextValue } from "../execution/SandboxContext";
21
+ import { ApprovalContext, type ApprovalContextValue } from "../execution/ApprovalContext";
22
+ import { FileReviewContext, type FileReviewContextValue } from "../execution/FileReviewContext";
21
23
  import { DevProfiler, useDomNodeCount } from "./dev";
22
24
  import { JumpToLatestButton } from "./JumpToLatestButton";
25
+ import { ApprovalPeekBar } from "./ApprovalPeekBar";
23
26
  import { ThreadItemWrapper } from "./ThreadItemWrapper";
24
27
 
25
28
  // ---------------------------------------------------------------------------
@@ -35,8 +38,12 @@ export interface VirtualizedThreadProps {
35
38
  comment?: string,
36
39
  ) => void;
37
40
  readonly submittingApprovalIds?: ReadonlySet<string>;
41
+ readonly approvalErrors?: ReadonlyMap<string, Error>;
38
42
  readonly filePathCtx: FilePathContextValue;
39
43
  readonly sandboxCtx: SandboxContextValue;
44
+ readonly approvalCtx: ApprovalContextValue;
45
+ readonly fileReviewCtx: FileReviewContextValue;
46
+ readonly unresolvedApprovalCount: number;
40
47
  readonly onBuildFromPlan?: () => void;
41
48
  readonly org?: string;
42
49
  readonly planActionsDisabled?: boolean;
@@ -98,8 +105,12 @@ export function VirtualizedThread({
98
105
  formatToolCallSummary,
99
106
  onApprovalSubmit,
100
107
  submittingApprovalIds,
108
+ approvalErrors,
101
109
  filePathCtx,
102
110
  sandboxCtx,
111
+ approvalCtx,
112
+ fileReviewCtx,
113
+ unresolvedApprovalCount,
103
114
  onBuildFromPlan,
104
115
  org,
105
116
  planActionsDisabled,
@@ -135,6 +146,7 @@ export function VirtualizedThread({
135
146
  formatToolCallSummary,
136
147
  onApprovalSubmit,
137
148
  submittingApprovalIds,
149
+ approvalErrors,
138
150
  onBuildFromPlan,
139
151
  org,
140
152
  planActionsDisabled,
@@ -142,13 +154,15 @@ export function VirtualizedThread({
142
154
  onRetryExecution,
143
155
  onEditMessage,
144
156
  }),
145
- [formatToolCallSummary, onApprovalSubmit, submittingApprovalIds, onBuildFromPlan, org, planActionsDisabled, onRetrySend, onRetryExecution, onEditMessage],
157
+ [formatToolCallSummary, onApprovalSubmit, submittingApprovalIds, approvalErrors, onBuildFromPlan, org, planActionsDisabled, onRetrySend, onRetryExecution, onEditMessage],
146
158
  );
147
159
 
148
160
  return (
149
161
  <>
150
162
  <SandboxContext.Provider value={sandboxCtx}>
151
163
  <FilePathContext.Provider value={filePathCtx}>
164
+ <ApprovalContext.Provider value={approvalCtx}>
165
+ <FileReviewContext.Provider value={fileReviewCtx}>
152
166
  <DevProfiler id="MessageThread:virtualized">
153
167
  <Virtuoso
154
168
  ref={virtuosoRef}
@@ -174,9 +188,21 @@ export function VirtualizedThread({
174
188
  )}
175
189
  />
176
190
  </DevProfiler>
191
+ </FileReviewContext.Provider>
192
+ </ApprovalContext.Provider>
177
193
  </FilePathContext.Provider>
178
194
  </SandboxContext.Provider>
179
- <JumpToLatestButton onClick={jumpToLatest} visible={!isAtBottom} />
195
+ {/* The peek bar takes the jump button's slot while approvals are pending,
196
+ so the two never overlap — a gate is the louder of the two signals. */}
197
+ <JumpToLatestButton
198
+ onClick={jumpToLatest}
199
+ visible={!isAtBottom && unresolvedApprovalCount === 0}
200
+ />
201
+ <ApprovalPeekBar
202
+ visible={!isAtBottom && unresolvedApprovalCount > 0}
203
+ count={unresolvedApprovalCount}
204
+ onClick={jumpToLatest}
205
+ />
180
206
  </>
181
207
  );
182
208
  }
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect, afterEach, vi } from "vitest";
2
+ import { render, screen, cleanup, fireEvent } from "@testing-library/react";
3
+ import { ApprovalPeekBar } from "../ApprovalPeekBar";
4
+
5
+ afterEach(cleanup);
6
+
7
+ describe("ApprovalPeekBar", () => {
8
+ it("is interactive and labelled when visible", () => {
9
+ render(<ApprovalPeekBar visible count={1} onClick={() => {}} />);
10
+ const btn = screen.getByRole("button");
11
+ expect(btn.getAttribute("aria-hidden")).toBe("false");
12
+ expect(btn.getAttribute("tabindex")).toBe("0");
13
+ expect(screen.getByText("1 approval needed")).toBeTruthy();
14
+ });
15
+
16
+ it("is inert (mounted for transitions) when not visible", () => {
17
+ render(<ApprovalPeekBar visible={false} count={2} onClick={() => {}} />);
18
+ const btn = screen.getByRole("button", { hidden: true });
19
+ expect(btn.getAttribute("aria-hidden")).toBe("true");
20
+ expect(btn.getAttribute("tabindex")).toBe("-1");
21
+ });
22
+
23
+ it("pluralizes the count", () => {
24
+ render(<ApprovalPeekBar visible count={3} onClick={() => {}} />);
25
+ expect(screen.getByText("3 approvals needed")).toBeTruthy();
26
+ });
27
+
28
+ it("invokes onClick (jump-to-latest) when pressed", () => {
29
+ const onClick = vi.fn();
30
+ render(<ApprovalPeekBar visible count={1} onClick={onClick} />);
31
+ fireEvent.click(screen.getByRole("button"));
32
+ expect(onClick).toHaveBeenCalledTimes(1);
33
+ });
34
+ });
@@ -0,0 +1,92 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { render, screen, cleanup, fireEvent } from "@testing-library/react";
3
+
4
+ // happy-dom does not compute layout, so the real overflow measurement always
5
+ // reports false there (scrollHeight === clientHeight === 0). We mock the hook so
6
+ // the overflow-driven branches (fade + reveal control) are deterministically
7
+ // testable here; the *rendered* clamp/fade is exercised against a real browser
8
+ // in test/e2e/tests/interactive-approval/tool-card-ux.spec.ts.
9
+ let mockOverflowing = false;
10
+ vi.mock("../useIsOverflowing", () => ({
11
+ useIsOverflowing: () => ({ ref: { current: null }, isOverflowing: mockOverflowing }),
12
+ }));
13
+
14
+ const { BoundedContent, PREVIEW_MAX_HEIGHT } = await import("../BoundedContent");
15
+
16
+ beforeEach(() => {
17
+ mockOverflowing = false;
18
+ });
19
+ afterEach(cleanup);
20
+
21
+ /** The collapsed clamp element: `overflow-hidden` + the shared budget. */
22
+ function clampEl(container: HTMLElement): Element | null {
23
+ return container.querySelector(`.overflow-hidden.${PREVIEW_MAX_HEIGHT}`);
24
+ }
25
+
26
+ describe("BoundedContent", () => {
27
+ it("renders its children", () => {
28
+ render(
29
+ <BoundedContent>
30
+ <p>diff body</p>
31
+ </BoundedContent>,
32
+ );
33
+ expect(screen.getByText("diff body")).toBeTruthy();
34
+ });
35
+
36
+ it("clamps the body to the single shared budget while collapsed", () => {
37
+ const { container } = render(
38
+ <BoundedContent>
39
+ <p>diff body</p>
40
+ </BoundedContent>,
41
+ );
42
+ expect(clampEl(container)).not.toBeNull();
43
+ });
44
+
45
+ it("shows no reveal control when the content fits the budget", () => {
46
+ mockOverflowing = false;
47
+ render(
48
+ <BoundedContent>
49
+ <p>short</p>
50
+ </BoundedContent>,
51
+ );
52
+ expect(screen.queryByRole("button")).toBeNull();
53
+ });
54
+
55
+ it("shows 'Show more' when the content overflows and expands it in place to 'Show less', dropping the clamp", () => {
56
+ mockOverflowing = true;
57
+ const { container } = render(
58
+ <BoundedContent>
59
+ <p>long body</p>
60
+ </BoundedContent>,
61
+ );
62
+
63
+ // Collapsed: clamped, with a "Show more" reveal that owns the disclosure.
64
+ const control = screen.getByRole("button", { name: /Show more/ });
65
+ expect(control.getAttribute("aria-expanded")).toBe("false");
66
+ expect(clampEl(container)).not.toBeNull();
67
+
68
+ // Expanding removes the clamp (natural height) and flips the control.
69
+ fireEvent.click(control);
70
+ expect(
71
+ screen.getByRole("button", { name: /Show less/ }).getAttribute("aria-expanded"),
72
+ ).toBe("true");
73
+ expect(clampEl(container)).toBeNull();
74
+
75
+ // Collapsing restores the clamp.
76
+ fireEvent.click(screen.getByRole("button", { name: /Show less/ }));
77
+ expect(screen.getByRole("button", { name: /Show more/ })).toBeTruthy();
78
+ expect(clampEl(container)).not.toBeNull();
79
+ });
80
+
81
+ it("forwards the e2e cursor-target hook onto the control", () => {
82
+ mockOverflowing = true;
83
+ render(
84
+ <BoundedContent cursorTarget="file-diff-expand">
85
+ <p>body</p>
86
+ </BoundedContent>,
87
+ );
88
+ expect(
89
+ screen.getByRole("button").getAttribute("data-cursor-target"),
90
+ ).toBe("file-diff-expand");
91
+ });
92
+ });
@@ -0,0 +1,107 @@
1
+ import { describe, it, expect, afterEach, vi } from "vitest";
2
+ import { render, screen, cleanup, fireEvent } from "@testing-library/react";
3
+ import { DecisionButton } from "../DecisionButton";
4
+
5
+ afterEach(cleanup);
6
+
7
+ // happy-dom cannot resolve `@layer`/the cascade, so these assert the class
8
+ // CONTRACT (the quiet, token-only treatment), not rendered pixels. The rendered
9
+ // outcome is covered by the e2e computed-style guard in tool-card-ux.spec.ts.
10
+ describe("DecisionButton", () => {
11
+ it("renders the label as the accessible name and invokes onClick", () => {
12
+ const onClick = vi.fn();
13
+ render(<DecisionButton label="Approve" variant="primary" onClick={onClick} />);
14
+ const btn = screen.getByRole("button", { name: "Approve" });
15
+ fireEvent.click(btn);
16
+ expect(onClick).toHaveBeenCalledTimes(1);
17
+ });
18
+
19
+ it("primary is a neutral chip (bordered fill), never the loud success green", () => {
20
+ render(<DecisionButton label="Approve" variant="primary" onClick={() => {}} />);
21
+ const cls = screen.getByRole("button").className;
22
+ expect(cls).toContain("border");
23
+ expect(cls).toContain("bg-accent");
24
+ expect(cls).toContain("text-accent-foreground");
25
+ expect(cls).not.toContain("bg-success");
26
+ });
27
+
28
+ it("ghost carries no fill at rest (lowest weight)", () => {
29
+ render(<DecisionButton label="Skip" variant="ghost" onClick={() => {}} />);
30
+ const cls = screen.getByRole("button").className;
31
+ expect(cls).toContain("text-muted-foreground");
32
+ // No resting background — only a neutral hover wash.
33
+ expect(cls).not.toMatch(/(?:^|\s)bg-/);
34
+ expect(cls).toContain("hover:bg-accent-hover");
35
+ });
36
+
37
+ it("danger is a quiet ghost that reveals the destructive cue on hover AND focus, never the loud red fill", () => {
38
+ render(<DecisionButton label="Reject" variant="danger" onClick={() => {}} />);
39
+ const cls = screen.getByRole("button").className;
40
+ expect(cls).toContain("text-muted-foreground");
41
+ expect(cls).not.toMatch(/(?:^|\s)bg-/); // transparent at rest
42
+ expect(cls).toContain("hover:text-destructive");
43
+ expect(cls).toContain("hover:bg-destructive-subtle");
44
+ // Keyboard parity: the cue is not hover-only.
45
+ expect(cls).toContain("focus-visible:text-destructive");
46
+ expect(cls).not.toContain("bg-destructive text-destructive-foreground");
47
+ });
48
+
49
+ it("uses NO `bg-token/NN` opacity modifiers in any variant", () => {
50
+ for (const variant of ["primary", "ghost", "danger"] as const) {
51
+ const { unmount } = render(
52
+ <DecisionButton label="x" variant={variant} onClick={() => {}} />,
53
+ );
54
+ const cls = screen.getByRole("button").className;
55
+ // No utility ends in `/<digits>` (e.g. bg-success/90, hover:bg-destructive/10).
56
+ expect(cls).not.toMatch(/\/\d+(?:\s|$)/);
57
+ unmount();
58
+ }
59
+ });
60
+
61
+ it("disables every button while a submission is in flight", () => {
62
+ render(
63
+ <DecisionButton label="Approve" variant="primary" onClick={() => {}} isSubmitting />,
64
+ );
65
+ expect((screen.getByRole("button") as HTMLButtonElement).disabled).toBe(true);
66
+ });
67
+
68
+ it("shows the spinner only on the active button that is submitting", () => {
69
+ const { rerender } = render(
70
+ <DecisionButton
71
+ label="Approve"
72
+ variant="primary"
73
+ onClick={() => {}}
74
+ isSubmitting
75
+ isActive
76
+ />,
77
+ );
78
+ expect(screen.getByRole("button").querySelector("svg.animate-spin")).toBeTruthy();
79
+
80
+ // In-flight, but a DIFFERENT button is the active one — no spinner here.
81
+ rerender(
82
+ <DecisionButton
83
+ label="Approve"
84
+ variant="primary"
85
+ onClick={() => {}}
86
+ isSubmitting
87
+ isActive={false}
88
+ />,
89
+ );
90
+ expect(screen.getByRole("button").querySelector("svg.animate-spin")).toBeNull();
91
+ });
92
+
93
+ it("forwards className and the data-cursor-target hook", () => {
94
+ render(
95
+ <DecisionButton
96
+ label="Approve all"
97
+ variant="ghost"
98
+ onClick={() => {}}
99
+ className="ml-auto"
100
+ cursorTarget="approve-all-button"
101
+ />,
102
+ );
103
+ const btn = screen.getByRole("button");
104
+ expect(btn.className).toContain("ml-auto");
105
+ expect(btn.getAttribute("data-cursor-target")).toBe("approve-all-button");
106
+ });
107
+ });