@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,109 @@
1
+ import { createContext, useCallback, useContext } from "react";
2
+ import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
3
+ import type { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
4
+
5
+ /**
6
+ * Context value that carries the run's pending approvals to the tool rows
7
+ * that own them, so an approval gate can render *inline* on the exact tool
8
+ * call it blocks (parent thread and nested sub-agents alike) instead of as a
9
+ * detached card at the bottom of the thread.
10
+ *
11
+ * Provided by {@link MessageThread} (or a platform builder's custom wrapper),
12
+ * beside {@link FilePathContext} / `SandboxContext`. When no provider is
13
+ * present — or the consumer did not supply an `onApprovalSubmit` handler — the
14
+ * map is empty and {@link useApproval} returns `null`, so the inline UI is a
15
+ * no-op (backward compatible).
16
+ *
17
+ * The map is keyed by `tool_call_id` and rebuilt only when the underlying
18
+ * `pendingApprovals` list changes (not per streaming frame), so subscribing
19
+ * tool rows re-render on approval events, not on every snapshot — preserving
20
+ * the thread's streaming re-render isolation (DD-009 / DD-010).
21
+ */
22
+ export interface ApprovalContextValue {
23
+ /** Unresolved approvals, keyed by the `tool_call_id` they gate. */
24
+ readonly approvalsByToolCallId: ReadonlyMap<string, PendingApproval>;
25
+ /**
26
+ * Submits a decision for a gated tool call. Wired to the same handler the
27
+ * bottom {@link ApprovalCard} backstop uses; when absent, no inline approval
28
+ * UI is shown.
29
+ */
30
+ readonly onSubmit?: (
31
+ toolCallId: string,
32
+ action: ApprovalAction,
33
+ comment?: string,
34
+ ) => void;
35
+ /** Tool-call ids whose decision RPC is in flight, for per-card spinners. */
36
+ readonly submittingIds: ReadonlySet<string>;
37
+ /**
38
+ * Per-tool-call failures, keyed by `tool_call_id` (the parallel of
39
+ * {@link submittingIds}). A failed decision is surfaced in-card, beside the
40
+ * exact gate that failed, so the inline row can name *which* action did not
41
+ * take and why. Empty when no provider supplies it.
42
+ */
43
+ readonly errorsByToolCallId: ReadonlyMap<string, Error>;
44
+ }
45
+
46
+ const DEFAULT_VALUE: ApprovalContextValue = {
47
+ approvalsByToolCallId: new Map(),
48
+ submittingIds: new Set(),
49
+ errorsByToolCallId: new Map(),
50
+ };
51
+
52
+ /** Context that supplies a run's pending approvals to the tool rows they gate. */
53
+ export const ApprovalContext =
54
+ createContext<ApprovalContextValue>(DEFAULT_VALUE);
55
+
56
+ /**
57
+ * Resolved inline-approval state for a single tool call. Its shape is a superset
58
+ * of the props {@link ApprovalCardBody} reads, so a consumer can spread it
59
+ * directly: `<ApprovalCardBody {...approval} />`.
60
+ */
61
+ export interface UseApprovalResult {
62
+ /** The pending approval gating this tool call. */
63
+ readonly pendingApproval: PendingApproval;
64
+ /** Submits a decision for this tool call (id already bound). */
65
+ readonly onSubmit: (action: ApprovalAction, comment?: string) => void;
66
+ /** True while this tool call's decision RPC is in flight. */
67
+ readonly isSubmitting: boolean;
68
+ /** This gate's last failed decision, or `null` — surfaced in-card beside it. */
69
+ readonly error: Error | null;
70
+ }
71
+
72
+ /**
73
+ * Returns the inline-approval state for a tool call, or `null` when the call
74
+ * is not currently gated (no matching approval, or no submit handler wired).
75
+ *
76
+ * The returned `onSubmit` is pre-bound to `toolCallId` and stable across
77
+ * renders, so it can flow straight into a memoized approval body.
78
+ *
79
+ * @example
80
+ * ```tsx
81
+ * const approval = useApproval(toolCall.id);
82
+ * if (approval) {
83
+ * return <ApprovalCardBody {...approval} />;
84
+ * }
85
+ * ```
86
+ */
87
+ export function useApproval(toolCallId: string): UseApprovalResult | null {
88
+ const { approvalsByToolCallId, onSubmit, submittingIds, errorsByToolCallId } =
89
+ useContext(ApprovalContext);
90
+
91
+ const handleSubmit = useCallback(
92
+ (action: ApprovalAction, comment?: string) => {
93
+ onSubmit?.(toolCallId, action, comment);
94
+ },
95
+ [onSubmit, toolCallId],
96
+ );
97
+
98
+ const pendingApproval = toolCallId
99
+ ? approvalsByToolCallId.get(toolCallId)
100
+ : undefined;
101
+ if (!pendingApproval || !onSubmit) return null;
102
+
103
+ return {
104
+ pendingApproval,
105
+ onSubmit: handleSubmit,
106
+ isSubmitting: submittingIds.has(toolCallId),
107
+ error: errorsByToolCallId.get(toolCallId) ?? null,
108
+ };
109
+ }
@@ -4,6 +4,7 @@ import type { ExecutionArtifact } from "@stigmer/protos/ai/stigmer/agentic/agent
4
4
  import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
5
5
  import { cn } from "@stigmer/theme";
6
6
  import { useArtifactContent } from "./useArtifactContent";
7
+ import { useArtifactDownload } from "./useArtifactDownload";
7
8
  import { isTextArtifact, formatArtifactSize } from "./artifact-utils";
8
9
  import { useDetectStigmerResource } from "../library/useDetectStigmerResource";
9
10
  import { useDetectSkillPackage } from "../library/useDetectSkillPackage";
@@ -148,6 +149,8 @@ export function ArtifactCard({
148
149
 
149
150
  const canPreview = !!onPreview && (isTextArtifact(artifact) || isDirectory);
150
151
 
152
+ const { download, isDownloading } = useArtifactDownload(executionId);
153
+
151
154
  const parentDir =
152
155
  hasNameCollision && artifact.sandboxPath
153
156
  ? parentDirectory(artifact.sandboxPath)
@@ -211,17 +214,18 @@ export function ArtifactCard({
211
214
  Preview
212
215
  </button>
213
216
  )}
214
- <a
215
- href={artifact.downloadUrl}
216
- download
217
+ <button
218
+ type="button"
219
+ onClick={() => download(artifact.storageKey, artifact.name)}
220
+ disabled={isDownloading}
217
221
  className={cn(
218
- "inline-flex items-center gap-1 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground",
222
+ "inline-flex items-center gap-1 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground disabled:opacity-50",
219
223
  FOCUS_RING_CLASSES,
220
224
  )}
221
225
  >
222
226
  <DownloadIcon />
223
- {isDirectory ? "Download ZIP" : "Download"}
224
- </a>
227
+ {isDownloading ? "Preparing…" : isDirectory ? "Download ZIP" : "Download"}
228
+ </button>
225
229
  </div>
226
230
 
227
231
  </div>
@@ -5,6 +5,7 @@ import type { ExecutionArtifact } from "@stigmer/protos/ai/stigmer/agentic/agent
5
5
  import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
6
6
  import { cn } from "@stigmer/theme";
7
7
  import { useArtifactContent } from "./useArtifactContent";
8
+ import { useArtifactDownload } from "./useArtifactDownload";
8
9
  import { isTextArtifact, formatArtifactSize } from "./artifact-utils";
9
10
  import { ArtifactContentRenderer } from "./ArtifactContentRenderer";
10
11
  import { useDetectStigmerResource } from "../library/useDetectStigmerResource";
@@ -269,6 +270,7 @@ export function ArtifactPreviewContent({
269
270
 
270
271
  <ActionBar
271
272
  artifact={artifact}
273
+ executionId={executionId}
272
274
  isDirectory={isDirectory}
273
275
  hasContent={content !== null}
274
276
  copied={copied}
@@ -622,6 +624,7 @@ function EntryIcon({ name }: { readonly name: string }) {
622
624
 
623
625
  function ActionBar({
624
626
  artifact,
627
+ executionId,
625
628
  isDirectory,
626
629
  hasContent,
627
630
  copied,
@@ -636,6 +639,7 @@ function ActionBar({
636
639
  onImplement,
637
640
  }: {
638
641
  readonly artifact: ExecutionArtifact;
642
+ readonly executionId: string;
639
643
  readonly isDirectory: boolean;
640
644
  readonly hasContent: boolean;
641
645
  readonly copied: boolean;
@@ -649,6 +653,7 @@ function ActionBar({
649
653
  readonly onApply: () => void;
650
654
  readonly onImplement?: () => void;
651
655
  }) {
656
+ const { download, isDownloading } = useArtifactDownload(executionId);
652
657
  return (
653
658
  <div className="flex items-center justify-between gap-3 border-t border-border px-4 py-3">
654
659
  <div className="flex items-center gap-3">
@@ -670,17 +675,18 @@ function ActionBar({
670
675
  </button>
671
676
  )}
672
677
 
673
- <a
674
- href={artifact.downloadUrl}
675
- download
678
+ <button
679
+ type="button"
680
+ onClick={() => download(artifact.storageKey, artifact.name)}
681
+ disabled={isDownloading}
676
682
  className={cn(
677
- "inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground",
683
+ "inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground disabled:opacity-50",
678
684
  FOCUS_RING_CLASSES,
679
685
  )}
680
686
  >
681
687
  <DownloadIcon />
682
- {isDirectory ? "Download ZIP" : "Download"}
683
- </a>
688
+ {isDownloading ? "Preparing…" : isDirectory ? "Download ZIP" : "Download"}
689
+ </button>
684
690
  </div>
685
691
 
686
692
  <div className="flex shrink-0 items-center gap-3">
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { cn } from "@stigmer/theme";
4
+
5
+ /**
6
+ * Which empty file-change state to describe.
7
+ *
8
+ * - `"empty-create"` — a new file proven to have no content (the approval gate's
9
+ * proto `FileChange` carries `changeType === CREATE` with an empty body).
10
+ * - `"create"` — a new file IS being written, but its proposed content could not
11
+ * be captured for preview (the rare content-less FILE_WRITE gate, e.g. a
12
+ * no-stream denial synthesis). Distinguished from `empty-create` because the
13
+ * file is not proven empty — only proven to be a write/create.
14
+ * - `"no-preview"` — the change carries no renderable diff/content AND cannot be
15
+ * proven to be a create (e.g. a path-only resume placeholder for an edit).
16
+ * Never claims emptiness or creation it cannot prove.
17
+ */
18
+ export type EmptyChangeKind = "empty-create" | "create" | "no-preview";
19
+
20
+ const MESSAGE: Record<EmptyChangeKind, string> = {
21
+ "empty-create": "New empty file",
22
+ create: "New file — preview unavailable",
23
+ "no-preview": "No preview available for this change",
24
+ };
25
+
26
+ /** Props for {@link EmptyChangeNotice}. */
27
+ export interface EmptyChangeNoticeProps {
28
+ /** The empty-change state to describe. */
29
+ readonly kind: EmptyChangeKind;
30
+ /** Additional CSS class names for the root container. */
31
+ readonly className?: string;
32
+ }
33
+
34
+ /**
35
+ * A quiet, neutral notice rendered in place of a diff when a file change has
36
+ * nothing to show — a newly created empty file, or a change whose content is
37
+ * genuinely unavailable.
38
+ *
39
+ * It is the single owner of both messages so the approval gate and the
40
+ * post-execution detail can never drift apart in wording. The two cases are
41
+ * deliberately distinct: an honest "new empty file" requires proof (the gate's
42
+ * proto change type), while everything else degrades to the non-committal
43
+ * "no preview available" rather than misrepresenting an uncaptured change as
44
+ * empty.
45
+ *
46
+ * Styled to match the `Notice` used elsewhere in the change views (neutral
47
+ * border + muted surface, tokens only — DD-005).
48
+ */
49
+ export function EmptyChangeNotice({ kind, className }: EmptyChangeNoticeProps) {
50
+ return (
51
+ <div
52
+ role="status"
53
+ data-cursor-target="empty-change-notice"
54
+ className={cn(
55
+ "rounded-md border border-border bg-muted-subtle px-3 py-2 text-xs text-muted-foreground",
56
+ className,
57
+ )}
58
+ >
59
+ {MESSAGE[kind]}
60
+ </div>
61
+ );
62
+ }
@@ -0,0 +1,383 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import type { FileChange } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
5
+ import {
6
+ FileChangeCaptureLevel,
7
+ FileChangeType,
8
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
9
+ import { cn } from "@stigmer/theme";
10
+ import { BoundedContent } from "../internal/BoundedContent";
11
+ import { computeDiff } from "../version-history/computeDiff";
12
+ import { DiffViewer } from "../version-history/DiffViewer";
13
+ import { DiffFileList } from "../version-history/DiffFileList";
14
+ import { DiffSummary } from "../version-history/DiffSummary";
15
+ import { UnifiedDiffView } from "../version-history/UnifiedDiffView";
16
+ import type { DiffHunk, FileDiffEntry } from "../version-history/types";
17
+ import { FilePathLink } from "./FilePathLink";
18
+ import { EmptyChangeNotice } from "./EmptyChangeNotice";
19
+ import { useFileChangeContent } from "./useFileChangeContent";
20
+
21
+ /** Props for {@link FileChangesView}. */
22
+ export interface FileChangesViewProps {
23
+ /**
24
+ * Net file changes to render, typically from {@link useSessionFileChanges}.
25
+ * One entry per changed file.
26
+ */
27
+ readonly changes: readonly FileChange[];
28
+ /** Additional CSS classes for the root container. */
29
+ readonly className?: string;
30
+ }
31
+
32
+ /**
33
+ * Consolidated, GitHub-style view of the files an agent changed: a summary
34
+ * bar, a file selector (when more than one file changed), and the unified diff
35
+ * for the selected file.
36
+ *
37
+ * Renders the runner's authoritative {@link FileChange} captures directly
38
+ * (proto-first). Whole-file captures (native) become a true before/after diff;
39
+ * hunk-only captures (Cursor today) render their unified diff; offloaded bodies
40
+ * are fetched lazily only when their file is opened (see {@link FileChangeDiff}).
41
+ *
42
+ * Zero Console dependencies — safe for platform builder embedding.
43
+ *
44
+ * @example
45
+ * ```tsx
46
+ * const { fileChanges } = useSessionFileChanges(allExecutions);
47
+ * <FileChangesView changes={fileChanges} />
48
+ * ```
49
+ */
50
+ export function FileChangesView({ changes, className }: FileChangesViewProps) {
51
+ const entries = useMemo<readonly FileDiffEntry[]>(
52
+ () => changes.map(toFileDiffEntry),
53
+ [changes],
54
+ );
55
+
56
+ const [selectedPath, setSelectedPath] = useState<string | null>(
57
+ changes[0]?.path ?? null,
58
+ );
59
+
60
+ const selected =
61
+ changes.find((c) => c.path === selectedPath) ?? changes[0] ?? null;
62
+
63
+ const totals = useMemo(
64
+ () =>
65
+ entries.reduce(
66
+ (acc, e) => ({
67
+ additions: acc.additions + e.additions,
68
+ deletions: acc.deletions + e.deletions,
69
+ }),
70
+ { additions: 0, deletions: 0 },
71
+ ),
72
+ [entries],
73
+ );
74
+
75
+ if (changes.length === 0 || !selected) return null;
76
+
77
+ return (
78
+ <div className={cn("flex flex-col gap-3", className)}>
79
+ <DiffSummary
80
+ fileCount={changes.length}
81
+ additions={totals.additions}
82
+ deletions={totals.deletions}
83
+ />
84
+
85
+ {changes.length > 1 && (
86
+ <div className="overflow-hidden rounded-lg border border-border">
87
+ <DiffFileList
88
+ files={entries}
89
+ selectedPath={selected.path}
90
+ onSelect={setSelectedPath}
91
+ />
92
+ </div>
93
+ )}
94
+
95
+ <FileChangeDiff key={selected.path} change={selected} />
96
+ </div>
97
+ );
98
+ }
99
+
100
+ /** Props for {@link FileChangeDiff}. */
101
+ export interface FileChangeDiffProps {
102
+ /** The single file change to render. */
103
+ readonly change: FileChange;
104
+ /** Additional CSS classes for the root container. */
105
+ readonly className?: string;
106
+ /**
107
+ * Optional class applied to the scrolling diff body (the `DiffViewer` table or
108
+ * unified-diff `<pre>`). A raw height cap with no reveal control; prefer
109
+ * {@link bounded} for the consistent clamp-and-reveal used across tool cards.
110
+ */
111
+ readonly bodyClassName?: string;
112
+ /**
113
+ * When `true`, the diff body is wrapped in the shared {@link BoundedContent}
114
+ * primitive: clamped to the standard preview height with a bottom fade and an
115
+ * in-place "Show more" / "Show less", identical to the always-visible
116
+ * edit/write diff body in the timeline. Defaults to `false` so the dedicated
117
+ * Changes view ({@link FileChangesView}) renders the full diff; the approval
118
+ * gate opts in so a large change cannot push the decision buttons off-screen
119
+ * while still letting the reviewer expand the whole diff in place.
120
+ */
121
+ readonly bounded?: boolean;
122
+ /**
123
+ * Whether to render the filename header (the `FilePathLink` + any rename
124
+ * prefix). Defaults to `true`. Set to `false` where an ancestor already names
125
+ * the file — the approval gate header, or a tool-call row — so the diff is not
126
+ * captioned with a path the user just read; the `+N -M` stats still render.
127
+ */
128
+ readonly showFileName?: boolean;
129
+ /**
130
+ * Whether to render the `+N -M` summary. Defaults to `true`. Set to `false`
131
+ * where an ancestor row already shows the result summary (a tool-call row) so
132
+ * the body does not repeat the counts. Orthogonal to {@link showFileName}: the
133
+ * approval gate suppresses the name but keeps the stats.
134
+ */
135
+ readonly showStats?: boolean;
136
+ }
137
+
138
+ /**
139
+ * Renders the diff for a single {@link FileChange}, adapting to how completely
140
+ * the runner captured it:
141
+ *
142
+ * - **Whole-file** (native): a true before/after diff via {@link DiffViewer}.
143
+ * Offloaded bodies are fetched lazily; loading, error, and too-large
144
+ * (server-truncated) states are surfaced honestly.
145
+ * - **Hunk-only** (Cursor today): the captured unified diff, labeled as such.
146
+ * - **Binary**: a "binary file changed" notice instead of a text diff.
147
+ *
148
+ * Importable on its own by platform builders composing custom change UIs.
149
+ */
150
+ export function FileChangeDiff({
151
+ change,
152
+ className,
153
+ bodyClassName,
154
+ bounded = false,
155
+ showFileName = true,
156
+ showStats = true,
157
+ }: FileChangeDiffProps) {
158
+ const { beforeText, afterText, isBinary, isLoading, error, isTruncated, downloadUrl } =
159
+ useFileChangeContent(change);
160
+
161
+ const hunks = useMemo<readonly DiffHunk[]>(() => {
162
+ if (beforeText === null || afterText === null) return [];
163
+ return computeDiff(beforeText, afterText);
164
+ }, [beforeText, afterText]);
165
+
166
+ const stats = useMemo(() => {
167
+ if (change.captureLevel === FileChangeCaptureLevel.HUNK_ONLY) {
168
+ return { additions: change.linesAdded, deletions: change.linesRemoved };
169
+ }
170
+ return countHunks(hunks);
171
+ }, [change.captureLevel, change.linesAdded, change.linesRemoved, hunks]);
172
+
173
+ const showStatsRow = showStats && (stats.additions > 0 || stats.deletions > 0);
174
+
175
+ return (
176
+ <div
177
+ className={cn("flex flex-col gap-1.5", className)}
178
+ data-cursor-target="file-diff"
179
+ >
180
+ {(showFileName || showStatsRow) && (
181
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
182
+ {showFileName &&
183
+ change.changeType === FileChangeType.RENAME &&
184
+ change.renameFrom && (
185
+ <span className="min-w-0 truncate font-mono text-muted-foreground-faint">
186
+ {change.renameFrom} →
187
+ </span>
188
+ )}
189
+ {showFileName && (
190
+ <FilePathLink path={change.path} dirDisplay="dim" className="text-xs" />
191
+ )}
192
+ {showStatsRow && (
193
+ <span className="shrink-0 tabular-nums">
194
+ <span className="text-diff-added-fg">+{stats.additions}</span>{" "}
195
+ <span className="text-diff-removed-fg">-{stats.deletions}</span>
196
+ </span>
197
+ )}
198
+ </div>
199
+ )}
200
+
201
+ <FileChangeBody
202
+ change={change}
203
+ hunks={hunks}
204
+ isBinary={isBinary}
205
+ isLoading={isLoading}
206
+ error={error}
207
+ isTruncated={isTruncated}
208
+ downloadUrl={downloadUrl}
209
+ bodyClassName={bodyClassName}
210
+ bounded={bounded}
211
+ />
212
+ </div>
213
+ );
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // Body — the capture-level / fetch-state switch
218
+ // ---------------------------------------------------------------------------
219
+
220
+ function FileChangeBody({
221
+ change,
222
+ hunks,
223
+ isBinary,
224
+ isLoading,
225
+ error,
226
+ isTruncated,
227
+ downloadUrl,
228
+ bodyClassName,
229
+ bounded,
230
+ }: {
231
+ change: FileChange;
232
+ hunks: readonly DiffHunk[];
233
+ isBinary: boolean;
234
+ isLoading: boolean;
235
+ error: Error | null;
236
+ isTruncated: boolean;
237
+ downloadUrl: string | null;
238
+ bodyClassName?: string;
239
+ bounded: boolean;
240
+ }) {
241
+ // A CREATE with no renderable diff is a genuinely empty new file; anything
242
+ // else with no content degrades to the non-committal "no preview" (we never
243
+ // claim emptiness we cannot prove from the capture).
244
+ const emptyKind =
245
+ change.changeType === FileChangeType.CREATE ? "empty-create" : "no-preview";
246
+
247
+ // When bounded, the actual diff renderers (and only those — short notices need
248
+ // no clamp) are wrapped in the shared BoundedContent budget, so the gate's
249
+ // diff matches the timeline edit/write diff and never crowds out the buttons.
250
+ const bound = (node: React.ReactNode) =>
251
+ bounded ? (
252
+ <BoundedContent cursorTarget="file-diff-expand">{node}</BoundedContent>
253
+ ) : (
254
+ node
255
+ );
256
+
257
+ if (isBinary) {
258
+ return <Notice>Binary file changed.</Notice>;
259
+ }
260
+
261
+ if (change.captureLevel === FileChangeCaptureLevel.HUNK_ONLY) {
262
+ return change.unifiedDiff ? (
263
+ bound(<UnifiedDiffView patch={change.unifiedDiff} className={bodyClassName} />)
264
+ ) : (
265
+ <EmptyChangeNotice kind={emptyKind} className={bodyClassName} />
266
+ );
267
+ }
268
+
269
+ // Whole-file: content may be inline (ready) or offloaded (fetched lazily).
270
+ if (isTruncated) {
271
+ return (
272
+ <Notice>
273
+ This file is too large to diff inline.{" "}
274
+ {downloadUrl && (
275
+ <a
276
+ href={downloadUrl}
277
+ target="_blank"
278
+ rel="noreferrer"
279
+ className="font-medium text-primary underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
280
+ >
281
+ Download the full file
282
+ </a>
283
+ )}
284
+ </Notice>
285
+ );
286
+ }
287
+
288
+ if (isLoading) {
289
+ return <Notice>Loading diff…</Notice>;
290
+ }
291
+
292
+ if (error) {
293
+ return <Notice variant="error">Could not load this file's contents.</Notice>;
294
+ }
295
+
296
+ if (hunks.length === 0) {
297
+ return <EmptyChangeNotice kind={emptyKind} className={bodyClassName} />;
298
+ }
299
+
300
+ return bound(<DiffViewer hunks={hunks} className={bodyClassName} />);
301
+ }
302
+
303
+ function Notice({
304
+ children,
305
+ variant = "muted",
306
+ }: {
307
+ children: React.ReactNode;
308
+ variant?: "muted" | "error";
309
+ }) {
310
+ return (
311
+ <div
312
+ role="status"
313
+ className={cn(
314
+ "rounded-md border border-border bg-muted-subtle px-3 py-2 text-xs",
315
+ variant === "error" ? "text-destructive" : "text-muted-foreground",
316
+ )}
317
+ >
318
+ {children}
319
+ </div>
320
+ );
321
+ }
322
+
323
+ // ---------------------------------------------------------------------------
324
+ // Helpers
325
+ // ---------------------------------------------------------------------------
326
+
327
+ function countHunks(hunks: readonly DiffHunk[]): {
328
+ additions: number;
329
+ deletions: number;
330
+ } {
331
+ let additions = 0;
332
+ let deletions = 0;
333
+ for (const hunk of hunks) {
334
+ for (const line of hunk.lines) {
335
+ if (line.type === "added") additions++;
336
+ else if (line.type === "removed") deletions++;
337
+ }
338
+ }
339
+ return { additions, deletions };
340
+ }
341
+
342
+ /**
343
+ * Projects a {@link FileChange} into a {@link FileDiffEntry} for the file list.
344
+ *
345
+ * Counts: hunk-only uses the runner's authoritative numbers; whole-file inline
346
+ * is derived from the content; an offloaded whole-file side has no synchronous
347
+ * content, so its count stays 0 until the file is opened and fetched.
348
+ */
349
+ function toFileDiffEntry(change: FileChange): FileDiffEntry {
350
+ const changeType =
351
+ change.changeType === FileChangeType.CREATE
352
+ ? "added"
353
+ : change.changeType === FileChangeType.DELETE
354
+ ? "removed"
355
+ : "modified";
356
+
357
+ if (change.captureLevel === FileChangeCaptureLevel.HUNK_ONLY) {
358
+ return {
359
+ path: change.path,
360
+ changeType,
361
+ additions: change.linesAdded,
362
+ deletions: change.linesRemoved,
363
+ };
364
+ }
365
+
366
+ const before = inlineSide(change.before);
367
+ const after = inlineSide(change.after);
368
+ if (before !== null && after !== null) {
369
+ const { additions, deletions } = countHunks(computeDiff(before, after));
370
+ return { path: change.path, changeType, additions, deletions };
371
+ }
372
+
373
+ return { path: change.path, changeType, additions: 0, deletions: 0 };
374
+ }
375
+
376
+ /** Inline text of a side, or null when absent/offloaded/binary. */
377
+ function inlineSide(
378
+ side: FileChange["before"],
379
+ ): string | null {
380
+ if (!side) return "";
381
+ if (side.isBinary) return null;
382
+ return side.body.case === "inline" ? side.body.value : null;
383
+ }