@stigmer/react 3.1.3 → 3.1.5

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 (248) hide show
  1. package/composer/SessionComposer.d.ts +31 -0
  2. package/composer/SessionComposer.d.ts.map +1 -1
  3. package/composer/SessionComposer.js +12 -4
  4. package/composer/SessionComposer.js.map +1 -1
  5. package/execution/ArtifactApplyButton.d.ts +25 -0
  6. package/execution/ArtifactApplyButton.d.ts.map +1 -0
  7. package/execution/ArtifactApplyButton.js +21 -0
  8. package/execution/ArtifactApplyButton.js.map +1 -0
  9. package/execution/ArtifactContentBody.d.ts +36 -0
  10. package/execution/ArtifactContentBody.d.ts.map +1 -0
  11. package/execution/ArtifactContentBody.js +62 -0
  12. package/execution/ArtifactContentBody.js.map +1 -0
  13. package/execution/ArtifactDocument.d.ts +48 -0
  14. package/execution/ArtifactDocument.d.ts.map +1 -0
  15. package/execution/ArtifactDocument.js +89 -0
  16. package/execution/ArtifactDocument.js.map +1 -0
  17. package/execution/ArtifactPreviewModal.d.ts +4 -4
  18. package/execution/ArtifactPreviewModal.d.ts.map +1 -1
  19. package/execution/ArtifactPreviewModal.js +15 -142
  20. package/execution/ArtifactPreviewModal.js.map +1 -1
  21. package/execution/ArtifactRow.d.ts +49 -0
  22. package/execution/ArtifactRow.d.ts.map +1 -0
  23. package/execution/ArtifactRow.js +56 -0
  24. package/execution/ArtifactRow.js.map +1 -0
  25. package/execution/ArtifactsWidget.d.ts +6 -9
  26. package/execution/ArtifactsWidget.d.ts.map +1 -1
  27. package/execution/ArtifactsWidget.js +15 -19
  28. package/execution/ArtifactsWidget.js.map +1 -1
  29. package/execution/FileReviewContext.d.ts +15 -1
  30. package/execution/FileReviewContext.d.ts.map +1 -1
  31. package/execution/FileReviewContext.js +20 -1
  32. package/execution/FileReviewContext.js.map +1 -1
  33. package/execution/MessageThread.d.ts +65 -2
  34. package/execution/MessageThread.d.ts.map +1 -1
  35. package/execution/MessageThread.js +42 -23
  36. package/execution/MessageThread.js.map +1 -1
  37. package/execution/TodoCard.d.ts +8 -0
  38. package/execution/TodoCard.d.ts.map +1 -1
  39. package/execution/TodoCard.js +6 -4
  40. package/execution/TodoCard.js.map +1 -1
  41. package/execution/TodoList.d.ts +20 -1
  42. package/execution/TodoList.d.ts.map +1 -1
  43. package/execution/TodoList.js +9 -6
  44. package/execution/TodoList.js.map +1 -1
  45. package/execution/ToolCallDetail.d.ts +3 -2
  46. package/execution/ToolCallDetail.d.ts.map +1 -1
  47. package/execution/ToolCallDetail.js +49 -16
  48. package/execution/ToolCallDetail.js.map +1 -1
  49. package/execution/file-review-status.d.ts +15 -0
  50. package/execution/file-review-status.d.ts.map +1 -1
  51. package/execution/file-review-status.js +22 -0
  52. package/execution/file-review-status.js.map +1 -1
  53. package/execution/index.d.ts +13 -7
  54. package/execution/index.d.ts.map +1 -1
  55. package/execution/index.js +7 -4
  56. package/execution/index.js.map +1 -1
  57. package/execution/useArtifactContent.d.ts +6 -1
  58. package/execution/useArtifactContent.d.ts.map +1 -1
  59. package/execution/useArtifactContent.js +7 -2
  60. package/execution/useArtifactContent.js.map +1 -1
  61. package/execution/useArtifactInspection.d.ts +85 -0
  62. package/execution/useArtifactInspection.d.ts.map +1 -0
  63. package/execution/useArtifactInspection.js +143 -0
  64. package/execution/useArtifactInspection.js.map +1 -0
  65. package/execution/useCreateAgentExecution.d.ts +11 -0
  66. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  67. package/execution/useCreateAgentExecution.js +1 -0
  68. package/execution/useCreateAgentExecution.js.map +1 -1
  69. package/github/useGitHubFileReader.d.ts +4 -2
  70. package/github/useGitHubFileReader.d.ts.map +1 -1
  71. package/github/useGitHubFileReader.js +16 -3
  72. package/github/useGitHubFileReader.js.map +1 -1
  73. package/github/useGitHubTreeLister.d.ts +2 -1
  74. package/github/useGitHubTreeLister.d.ts.map +1 -1
  75. package/github/useGitHubTreeLister.js +7 -3
  76. package/github/useGitHubTreeLister.js.map +1 -1
  77. package/index.d.ts +4 -4
  78. package/index.d.ts.map +1 -1
  79. package/index.js +3 -3
  80. package/index.js.map +1 -1
  81. package/internal/MermaidDiagram.d.ts +47 -0
  82. package/internal/MermaidDiagram.d.ts.map +1 -0
  83. package/internal/MermaidDiagram.js +118 -0
  84. package/internal/MermaidDiagram.js.map +1 -0
  85. package/internal/VirtualizedThread.d.ts +3 -2
  86. package/internal/VirtualizedThread.d.ts.map +1 -1
  87. package/internal/VirtualizedThread.js +3 -2
  88. package/internal/VirtualizedThread.js.map +1 -1
  89. package/internal/markdown-components.d.ts.map +1 -1
  90. package/internal/markdown-components.js +34 -0
  91. package/internal/markdown-components.js.map +1 -1
  92. package/internal/mermaid-loader.d.ts +26 -0
  93. package/internal/mermaid-loader.d.ts.map +1 -0
  94. package/internal/mermaid-loader.js +34 -0
  95. package/internal/mermaid-loader.js.map +1 -0
  96. package/internal/mermaid-theme-css.d.ts +41 -0
  97. package/internal/mermaid-theme-css.d.ts.map +1 -0
  98. package/internal/mermaid-theme-css.js +179 -0
  99. package/internal/mermaid-theme-css.js.map +1 -0
  100. package/internal/slug.d.ts +9 -5
  101. package/internal/slug.d.ts.map +1 -1
  102. package/internal/slug.js +10 -6
  103. package/internal/slug.js.map +1 -1
  104. package/package.json +5 -4
  105. package/session/SessionViewer.d.ts +11 -1
  106. package/session/SessionViewer.d.ts.map +1 -1
  107. package/session/SessionViewer.js +101 -21
  108. package/session/SessionViewer.js.map +1 -1
  109. package/session/artifact-document.d.ts +13 -0
  110. package/session/artifact-document.d.ts.map +1 -0
  111. package/session/artifact-document.js +22 -0
  112. package/session/artifact-document.js.map +1 -0
  113. package/session/facets/ArtifactsTab.d.ts +30 -4
  114. package/session/facets/ArtifactsTab.d.ts.map +1 -1
  115. package/session/facets/ArtifactsTab.js +32 -10
  116. package/session/facets/ArtifactsTab.js.map +1 -1
  117. package/session/index.d.ts +3 -1
  118. package/session/index.d.ts.map +1 -1
  119. package/session/index.js +5 -1
  120. package/session/index.js.map +1 -1
  121. package/session/useSessionArtifacts.d.ts +17 -0
  122. package/session/useSessionArtifacts.d.ts.map +1 -1
  123. package/session/useSessionArtifacts.js +16 -5
  124. package/session/useSessionArtifacts.js.map +1 -1
  125. package/session/useSessionConversation.d.ts +17 -1
  126. package/session/useSessionConversation.d.ts.map +1 -1
  127. package/session/useSessionConversation.js +32 -4
  128. package/session/useSessionConversation.js.map +1 -1
  129. package/session/useSessionPageFlow.d.ts.map +1 -1
  130. package/session/useSessionPageFlow.js +1 -0
  131. package/session/useSessionPageFlow.js.map +1 -1
  132. package/session/useSessionPanel.d.ts +20 -0
  133. package/session/useSessionPanel.d.ts.map +1 -1
  134. package/session/useSessionPanel.js +13 -0
  135. package/session/useSessionPanel.js.map +1 -1
  136. package/session/useSessionRailViews.d.ts +13 -1
  137. package/session/useSessionRailViews.d.ts.map +1 -1
  138. package/session/useSessionRailViews.js +5 -3
  139. package/session/useSessionRailViews.js.map +1 -1
  140. package/session/useWorkspaceReadRefs.d.ts +45 -0
  141. package/session/useWorkspaceReadRefs.d.ts.map +1 -0
  142. package/session/useWorkspaceReadRefs.js +104 -0
  143. package/session/useWorkspaceReadRefs.js.map +1 -0
  144. package/src/composer/SessionComposer.tsx +69 -1
  145. package/src/composer/__tests__/SessionComposer-editing.test.tsx +128 -0
  146. package/src/execution/ArtifactApplyButton.tsx +79 -0
  147. package/src/execution/ArtifactContentBody.tsx +255 -0
  148. package/src/execution/ArtifactDocument.tsx +344 -0
  149. package/src/execution/ArtifactPreviewModal.tsx +39 -405
  150. package/src/execution/ArtifactRow.tsx +165 -0
  151. package/src/execution/ArtifactsWidget.tsx +24 -33
  152. package/src/execution/FileReviewContext.ts +27 -1
  153. package/src/execution/MessageThread.tsx +110 -22
  154. package/src/execution/TodoCard.tsx +20 -4
  155. package/src/execution/TodoList.tsx +26 -4
  156. package/src/execution/ToolCallDetail.tsx +69 -16
  157. package/src/execution/__tests__/ArtifactContentBody.test.tsx +131 -0
  158. package/src/execution/__tests__/ArtifactDocument.test.tsx +100 -0
  159. package/src/execution/__tests__/ArtifactRow.test.tsx +139 -0
  160. package/src/execution/__tests__/ArtifactsWidget.test.tsx +78 -0
  161. package/src/execution/__tests__/ToolCallDetail.test.tsx +188 -2
  162. package/src/execution/__tests__/file-review-status.test.ts +47 -0
  163. package/src/execution/__tests__/message-entry.test.tsx +50 -2
  164. package/src/execution/__tests__/message-thread-slots.test.tsx +463 -0
  165. package/src/execution/__tests__/useArtifactInspection.test.tsx +151 -0
  166. package/src/execution/__tests__/useCreateAgentExecution.test.tsx +35 -0
  167. package/src/execution/file-review-status.ts +24 -0
  168. package/src/execution/index.ts +27 -5
  169. package/src/execution/useArtifactContent.ts +7 -0
  170. package/src/execution/useArtifactInspection.ts +252 -0
  171. package/src/execution/useCreateAgentExecution.ts +12 -0
  172. package/src/github/__tests__/useGitHubFileReader.test.ts +35 -2
  173. package/src/github/__tests__/useGitHubTreeLister.test.ts +14 -0
  174. package/src/github/useGitHubFileReader.ts +19 -6
  175. package/src/github/useGitHubTreeLister.ts +7 -3
  176. package/src/index.ts +15 -2
  177. package/src/internal/MermaidDiagram.tsx +164 -0
  178. package/src/internal/VirtualizedThread.tsx +5 -1
  179. package/src/internal/__tests__/a11y/mermaid-theme.a11y.test.tsx +122 -0
  180. package/src/internal/__tests__/markdown-components.test.tsx +93 -1
  181. package/src/internal/__tests__/mermaid-diagram.test.tsx +282 -0
  182. package/src/internal/markdown-components.tsx +36 -1
  183. package/src/internal/mermaid-loader.ts +44 -0
  184. package/src/internal/mermaid-theme-css.ts +178 -0
  185. package/src/internal/slug.ts +10 -6
  186. package/src/session/SessionViewer.tsx +204 -35
  187. package/src/session/__tests__/SessionViewer-artifactTabs.test.tsx +210 -0
  188. package/src/session/__tests__/SessionViewer-editResubmit.test.tsx +233 -0
  189. package/src/session/__tests__/useSessionArtifacts.test.ts +57 -0
  190. package/src/session/__tests__/useSessionConversation.test.tsx +172 -1
  191. package/src/session/__tests__/useSessionPanel.test.ts +70 -1
  192. package/src/session/__tests__/useWorkspaceReadRefs.test.ts +245 -0
  193. package/src/session/artifact-document.ts +23 -0
  194. package/src/session/facets/ArtifactsTab.tsx +66 -18
  195. package/src/session/facets/__tests__/ArtifactsTab.test.tsx +111 -47
  196. package/src/session/index.ts +7 -1
  197. package/src/session/useSessionArtifacts.ts +16 -5
  198. package/src/session/useSessionConversation.ts +48 -6
  199. package/src/session/useSessionPageFlow.ts +1 -0
  200. package/src/session/useSessionPanel.ts +41 -0
  201. package/src/session/useSessionRailViews.tsx +21 -1
  202. package/src/session/useWorkspaceReadRefs.ts +118 -0
  203. package/src/workspace/FileViewer.tsx +144 -38
  204. package/src/workspace/WorkspaceFileReader.ts +21 -0
  205. package/src/workspace/__tests__/FileViewer.test.tsx +138 -3
  206. package/src/workspace/__tests__/useWorkspaceFileContent.test.tsx +20 -0
  207. package/src/workspace/__tests__/workspaceListingCache.test.ts +114 -0
  208. package/src/workspace/index.ts +4 -1
  209. package/src/workspace/useWorkspaceEntries.ts +11 -0
  210. package/src/workspace/useWorkspaceFileContent.ts +4 -1
  211. package/src/workspace/useWorkspaceFileSearch.ts +8 -3
  212. package/src/workspace/useWorkspaceFiles.ts +3 -2
  213. package/src/workspace/workspaceListingCache.ts +21 -7
  214. package/styles.css +1 -1
  215. package/workspace/FileViewer.d.ts +1 -1
  216. package/workspace/FileViewer.d.ts.map +1 -1
  217. package/workspace/FileViewer.js +55 -5
  218. package/workspace/FileViewer.js.map +1 -1
  219. package/workspace/WorkspaceFileReader.d.ts +17 -0
  220. package/workspace/WorkspaceFileReader.d.ts.map +1 -1
  221. package/workspace/WorkspaceFileReader.js +18 -0
  222. package/workspace/WorkspaceFileReader.js.map +1 -1
  223. package/workspace/index.d.ts +1 -1
  224. package/workspace/index.d.ts.map +1 -1
  225. package/workspace/index.js +1 -1
  226. package/workspace/index.js.map +1 -1
  227. package/workspace/useWorkspaceEntries.d.ts +11 -0
  228. package/workspace/useWorkspaceEntries.d.ts.map +1 -1
  229. package/workspace/useWorkspaceEntries.js.map +1 -1
  230. package/workspace/useWorkspaceFileContent.d.ts.map +1 -1
  231. package/workspace/useWorkspaceFileContent.js +4 -1
  232. package/workspace/useWorkspaceFileContent.js.map +1 -1
  233. package/workspace/useWorkspaceFileSearch.d.ts.map +1 -1
  234. package/workspace/useWorkspaceFileSearch.js +8 -3
  235. package/workspace/useWorkspaceFileSearch.js.map +1 -1
  236. package/workspace/useWorkspaceFiles.d.ts +2 -1
  237. package/workspace/useWorkspaceFiles.d.ts.map +1 -1
  238. package/workspace/useWorkspaceFiles.js +3 -2
  239. package/workspace/useWorkspaceFiles.js.map +1 -1
  240. package/workspace/workspaceListingCache.d.ts +1 -1
  241. package/workspace/workspaceListingCache.d.ts.map +1 -1
  242. package/workspace/workspaceListingCache.js +20 -7
  243. package/workspace/workspaceListingCache.js.map +1 -1
  244. package/execution/ArtifactCard.d.ts +0 -81
  245. package/execution/ArtifactCard.d.ts.map +0 -1
  246. package/execution/ArtifactCard.js +0 -130
  247. package/execution/ArtifactCard.js.map +0 -1
  248. package/src/execution/ArtifactCard.tsx +0 -320
@@ -1,15 +1,47 @@
1
- import { describe, it, expect, afterEach } from "vitest";
1
+ import { describe, it, expect, afterEach, beforeEach, vi } from "vitest";
2
2
  import { render, cleanup } from "@testing-library/react";
3
3
  import { create, type JsonObject } from "@bufbuild/protobuf";
4
4
  import {
5
5
  ToolCallSchema,
6
6
  type ToolCall,
7
7
  } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
8
+ import {
9
+ CapturedFileChangeSchema,
10
+ FileChangeSetSchema,
11
+ type FileChangeSet,
12
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
8
13
  import {
9
14
  ApprovalPolicySource,
15
+ FileChangeKind,
16
+ FileReviewBlockReason,
17
+ ToolCallStatus,
10
18
  ToolKind,
11
19
  } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
12
- import { ToolCallDetail } from "../ToolCallDetail";
20
+ import type { UseFileChangeContentReturn } from "../useFileChangeContent";
21
+
22
+ // Mock the diff-body content resolver (the seam FileChangesView.test.tsx also
23
+ // mocks): FileChangeDiff calls it unconditionally, and its real implementation
24
+ // needs a Stigmer client for offloaded bodies — irrelevant to these tests.
25
+ let mockContent: UseFileChangeContentReturn;
26
+
27
+ vi.mock("../useFileChangeContent", () => ({
28
+ useFileChangeContent: () => mockContent,
29
+ }));
30
+
31
+ const { ToolCallDetail } = await import("../ToolCallDetail");
32
+ const { FileReviewContext } = await import("../FileReviewContext");
33
+
34
+ beforeEach(() => {
35
+ mockContent = {
36
+ beforeText: "alpha\n",
37
+ afterText: "captured-after\n",
38
+ isBinary: false,
39
+ isLoading: false,
40
+ error: null,
41
+ isTruncated: false,
42
+ downloadUrl: null,
43
+ };
44
+ });
13
45
 
14
46
  afterEach(cleanup);
15
47
 
@@ -21,6 +53,9 @@ function makeToolCall(opts: {
21
53
  approvalPolicySource?: ApprovalPolicySource;
22
54
  startedAt?: string;
23
55
  completedAt?: string;
56
+ status?: ToolCallStatus;
57
+ error?: string;
58
+ fileChangeSetId?: string;
24
59
  }): ToolCall {
25
60
  return create(ToolCallSchema, {
26
61
  id: opts.name,
@@ -32,6 +67,9 @@ function makeToolCall(opts: {
32
67
  opts.approvalPolicySource ?? ApprovalPolicySource.UNSPECIFIED,
33
68
  startedAt: opts.startedAt ?? "",
34
69
  completedAt: opts.completedAt ?? "",
70
+ status: opts.status ?? ToolCallStatus.TOOL_CALL_COMPLETED,
71
+ error: opts.error ?? "",
72
+ fileChangeSetId: opts.fileChangeSetId ?? "",
35
73
  });
36
74
  }
37
75
 
@@ -193,3 +231,151 @@ describe("ToolCallDetail — edit body has no duplicate stats", () => {
193
231
  expect(container.textContent).not.toContain("+1");
194
232
  });
195
233
  });
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Write body — captured diff on a stamped row, single-bounded fallback
237
+ // ---------------------------------------------------------------------------
238
+
239
+ describe("ToolCallDetail — write body", () => {
240
+ const SET_ID = "aex-1:0";
241
+ // BoundedContent's collapsed pixel clamp (PREVIEW_MAX_HEIGHT). Its presence/
242
+ // absence is how these tests pin WHICH truncation mechanism bounds the body —
243
+ // jsdom has no layout, so the reveal control itself only appears for the
244
+ // line-count mechanism (CollapsibleCode), never for the pixel clamp.
245
+ const CLAMP = ".max-h-48";
246
+
247
+ /** A change set capturing one reviewable modify of `path`. */
248
+ function capturedSet(
249
+ path = "notes.md",
250
+ overrides?: { diffComplete?: boolean; blockedReason?: FileReviewBlockReason },
251
+ ): FileChangeSet {
252
+ return create(FileChangeSetSchema, {
253
+ id: SET_ID,
254
+ changes: [
255
+ create(CapturedFileChangeSchema, {
256
+ id: `${SET_ID}:${path}`,
257
+ pathBefore: path,
258
+ pathAfter: path,
259
+ kind: FileChangeKind.MODIFY,
260
+ diffComplete: overrides?.diffComplete ?? true,
261
+ blockedReason:
262
+ overrides?.blockedReason ?? FileReviewBlockReason.UNSPECIFIED,
263
+ }),
264
+ ],
265
+ });
266
+ }
267
+
268
+ function writeCall(opts?: Partial<Parameters<typeof makeToolCall>[0]>): ToolCall {
269
+ return makeToolCall({
270
+ name: "write_file",
271
+ toolKind: ToolKind.FILE_WRITE,
272
+ args: { file_path: "notes.md", contents: "proposed body\n" },
273
+ result: "Successfully wrote to 'notes.md'",
274
+ ...opts,
275
+ });
276
+ }
277
+
278
+ function renderWithSet(toolCall: ToolCall, set: FileChangeSet | null) {
279
+ const changeSetsById = new Map(set ? [[SET_ID, set]] : []);
280
+ return render(
281
+ <FileReviewContext.Provider value={{ changeSetsById }}>
282
+ <ToolCallDetail toolCall={toolCall} />
283
+ </FileReviewContext.Provider>,
284
+ );
285
+ }
286
+
287
+ it("renders the captured net diff (not the Content block) on a stamped row", () => {
288
+ const { container } = renderWithSet(
289
+ writeCall({ fileChangeSetId: SET_ID }),
290
+ capturedSet(),
291
+ );
292
+ // The captured before/after renders as a real diff table...
293
+ expect(container.querySelector('[data-cursor-target="file-diff"]')).not.toBeNull();
294
+ expect(container.textContent).toContain("captured-after");
295
+ // ...bounded by the shared pixel clamp, with the +N -M stats the write
296
+ // row's header does not carry, and without the fallback's raw args body.
297
+ // (The diff table itself carries a hidden a11y column header named
298
+ // "Content", so the discriminator is the body text, not the label.)
299
+ expect(container.querySelector(CLAMP)).not.toBeNull();
300
+ expect(container.textContent).toContain("+1");
301
+ expect(container.textContent).not.toContain("proposed body");
302
+ });
303
+
304
+ it("renders exactly one reveal control for the content fallback (double-expander regression)", () => {
305
+ const longBody = Array.from({ length: 55 }, (_, i) => `line ${i + 1}`).join("\n");
306
+ const { container, getAllByRole } = render(
307
+ <ToolCallDetail
308
+ toolCall={writeCall({ args: { file_path: "notes.md", contents: longBody } })}
309
+ />,
310
+ );
311
+ // The file view self-truncates (line-count mechanism, one control)...
312
+ const reveals = getAllByRole("button").filter((b) =>
313
+ /^Show /.test(b.textContent ?? ""),
314
+ );
315
+ expect(reveals).toHaveLength(1);
316
+ expect(reveals[0].textContent).toContain("Show all 55 lines");
317
+ // ...and is never ALSO wrapped in BoundedContent's pixel clamp.
318
+ expect(container.querySelector(CLAMP)).toBeNull();
319
+ });
320
+
321
+ it("falls back to the Content block for every unresolvable capture", () => {
322
+ const cases: Array<[string, ToolCall, FileChangeSet | null]> = [
323
+ ["unstamped row", writeCall(), capturedSet()],
324
+ ["set missing from context", writeCall({ fileChangeSetId: SET_ID }), null],
325
+ [
326
+ "path not in set",
327
+ writeCall({ fileChangeSetId: SET_ID }),
328
+ capturedSet("other.md"),
329
+ ],
330
+ [
331
+ "non-reviewable (secret-withheld) change",
332
+ writeCall({ fileChangeSetId: SET_ID }),
333
+ capturedSet("notes.md", {
334
+ diffComplete: false,
335
+ blockedReason: FileReviewBlockReason.SECRET_WITHHELD,
336
+ }),
337
+ ],
338
+ ];
339
+ for (const [label, toolCall, set] of cases) {
340
+ const { container } = renderWithSet(toolCall, set);
341
+ expect(container.textContent, label).toContain("Content");
342
+ expect(container.textContent, label).toContain("proposed body");
343
+ expect(
344
+ container.querySelector('[data-cursor-target="file-diff"]'),
345
+ label,
346
+ ).toBeNull();
347
+ cleanup();
348
+ }
349
+ });
350
+
351
+ it("keeps a FAILED write's error view inside the pixel clamp (bounding-rule lock)", () => {
352
+ const { container } = render(
353
+ <ToolCallDetail
354
+ toolCall={writeCall({
355
+ status: ToolCallStatus.TOOL_CALL_FAILED,
356
+ error: "EACCES: permission denied, open 'notes.md'",
357
+ })}
358
+ />,
359
+ );
360
+ expect(container.textContent).toContain("permission denied");
361
+ // The error <pre> has no internal truncation, so BoundedContent bounds it.
362
+ expect(container.querySelector(CLAMP)).not.toBeNull();
363
+ });
364
+
365
+ it("leaves a stamped EDIT row on its per-call args diff, never the captured net diff", () => {
366
+ const edit = makeToolCall({
367
+ name: "edit_file",
368
+ toolKind: ToolKind.FILE_EDIT,
369
+ args: { file_path: "notes.md", old_string: "alpha", new_string: "edit-args-b" },
370
+ result: "Successfully replaced 1 occurrence in 'notes.md'",
371
+ fileChangeSetId: SET_ID,
372
+ });
373
+ const { container } = renderWithSet(edit, capturedSet());
374
+ // The per-call diff from the row's own args renders...
375
+ expect(container.textContent).toContain("edit-args-b");
376
+ // ...not the change set's captured net content...
377
+ expect(container.textContent).not.toContain("captured-after");
378
+ // ...still inside the shared pixel clamp (unchanged behavior).
379
+ expect(container.querySelector(CLAMP)).not.toBeNull();
380
+ });
381
+ });
@@ -24,6 +24,7 @@ import {
24
24
  changeSetReviewability,
25
25
  deriveEffectiveVerdicts,
26
26
  fileReviewability,
27
+ fileReviewRowChange,
27
28
  fileReviewRowState,
28
29
  } from "../file-review-status";
29
30
 
@@ -291,6 +292,52 @@ describe("changeForRowPath", () => {
291
292
  });
292
293
  });
293
294
 
295
+ describe("fileReviewRowChange", () => {
296
+ it("returns null with no set or path", () => {
297
+ const set = reviewSet({
298
+ status: FileChangeSetStatus.AWAITING_REVIEW,
299
+ changes: [fileChange("f1", "a.ts")],
300
+ });
301
+ expect(fileReviewRowChange(undefined, "a.ts")).toBeNull();
302
+ expect(fileReviewRowChange(set, null)).toBeNull();
303
+ expect(fileReviewRowChange(set, "")).toBeNull();
304
+ });
305
+
306
+ it("resolves a reviewable change by row path (same matcher as the badge)", () => {
307
+ const set = reviewSet({
308
+ status: FileChangeSetStatus.AWAITING_REVIEW,
309
+ changes: [fileChange("f1", "src/a.ts")],
310
+ });
311
+ expect(fileReviewRowChange(set, "src/a.ts")?.id).toBe("f1");
312
+ expect(fileReviewRowChange(set, "/home/user/ws/src/a.ts")?.id).toBe("f1");
313
+ });
314
+
315
+ it("returns null for a path absent from the set", () => {
316
+ const set = reviewSet({
317
+ status: FileChangeSetStatus.AWAITING_REVIEW,
318
+ changes: [fileChange("f1", "a.ts")],
319
+ });
320
+ expect(fileReviewRowChange(set, "gone.ts")).toBeNull();
321
+ });
322
+
323
+ it("returns null for a non-reviewable change (honest degradation)", () => {
324
+ const set = reviewSet({
325
+ status: FileChangeSetStatus.AWAITING_REVIEW,
326
+ changes: [
327
+ create(CapturedFileChangeSchema, {
328
+ id: "f1",
329
+ pathBefore: "secret.env",
330
+ pathAfter: "secret.env",
331
+ kind: FileChangeKind.MODIFY,
332
+ diffComplete: false,
333
+ blockedReason: FileReviewBlockReason.SECRET_WITHHELD,
334
+ }),
335
+ ],
336
+ });
337
+ expect(fileReviewRowChange(set, "secret.env")).toBeNull();
338
+ });
339
+ });
340
+
294
341
  describe("fileReviewRowState", () => {
295
342
  it("returns null with no set (not yet projected / unknown id)", () => {
296
343
  expect(fileReviewRowState(undefined, "a.ts")).toBeNull();
@@ -1,9 +1,22 @@
1
- import { describe, it, expect, afterEach } from "vitest";
2
- import { render, cleanup } from "@testing-library/react";
1
+ import { describe, it, expect, afterEach, vi } from "vitest";
2
+ import { render, cleanup, waitFor } from "@testing-library/react";
3
3
  import { create } from "@bufbuild/protobuf";
4
4
  import { AgentMessageSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
5
5
  import { MessageType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
6
6
  import { MessageEntry } from "../MessageEntry";
7
+ import type { MermaidModule } from "../../internal/mermaid-loader";
8
+
9
+ // Stub only the library loader: the real MermaidDiagram + markdown overrides
10
+ // run end-to-end through Streamdown, so these tests cover the full chat path
11
+ // from fenced source to rendered diagram.
12
+ vi.mock("../../internal/mermaid-loader", () => ({
13
+ loadMermaid: vi.fn().mockResolvedValue({
14
+ initialize: vi.fn(),
15
+ render: vi
16
+ .fn()
17
+ .mockResolvedValue({ svg: "<svg data-testid='mermaid-svg'></svg>" }),
18
+ } as unknown as MermaidModule),
19
+ }));
7
20
 
8
21
  afterEach(cleanup);
9
22
 
@@ -169,6 +182,41 @@ describe("MessageEntry — AI messages (Streamdown)", () => {
169
182
  article!.querySelectorAll('span[class*="hljs-"]').length,
170
183
  ).toBeGreaterThan(0);
171
184
  });
185
+
186
+ it("renders a completed ```mermaid fence as a diagram, not a code block", async () => {
187
+ const msg = makeMessage(
188
+ MessageType.MESSAGE_AI,
189
+ "Here is the flow:\n\n```mermaid\nflowchart LR\n A --> B\n```",
190
+ );
191
+ const { container } = render(<MessageEntry message={msg} />);
192
+
193
+ const article = queryArticle(container, "AI response");
194
+ await waitFor(() =>
195
+ expect(
196
+ article!.querySelector('[role="img"][aria-label="Mermaid diagram"]'),
197
+ ).not.toBeNull(),
198
+ );
199
+ expect(article!.querySelector("[data-testid='mermaid-svg']")).not.toBeNull();
200
+ // The fenced source is no longer shown as a code block.
201
+ expect(article!.querySelector("pre")).toBeNull();
202
+ });
203
+
204
+ it("keeps an unclosed ```mermaid fence as a code block while streaming", () => {
205
+ const msg = makeMessage(
206
+ MessageType.MESSAGE_AI,
207
+ "```mermaid\nflowchart LR\n A --> B",
208
+ { isStreaming: true },
209
+ );
210
+ const { container } = render(<MessageEntry message={msg} />);
211
+
212
+ const article = queryArticle(container, "AI response");
213
+ // Mid-stream the source stays a plain code block — no diagram, and no
214
+ // half-parsed error flashing.
215
+ expect(
216
+ article!.querySelector('[role="img"][aria-label="Mermaid diagram"]'),
217
+ ).toBeNull();
218
+ expect(article!.textContent).toContain("flowchart LR");
219
+ });
172
220
  });
173
221
 
174
222
  // ---------------------------------------------------------------------------