@stigmer/react 3.1.2 → 3.1.4

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 (285) hide show
  1. package/composer/SessionComposer.d.ts +37 -5
  2. package/composer/SessionComposer.d.ts.map +1 -1
  3. package/composer/SessionComposer.js +13 -5
  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 +9 -9
  18. package/execution/ArtifactPreviewModal.d.ts.map +1 -1
  19. package/execution/ArtifactPreviewModal.js +18 -145
  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/FileChangeProgressBar.d.ts +47 -0
  30. package/execution/FileChangeProgressBar.d.ts.map +1 -0
  31. package/execution/FileChangeProgressBar.js +49 -0
  32. package/execution/FileChangeProgressBar.js.map +1 -0
  33. package/execution/FileChangesView.d.ts.map +1 -1
  34. package/execution/FileChangesView.js +19 -4
  35. package/execution/FileChangesView.js.map +1 -1
  36. package/execution/FileReviewAtoms.d.ts +27 -0
  37. package/execution/FileReviewAtoms.d.ts.map +1 -0
  38. package/execution/FileReviewAtoms.js +64 -0
  39. package/execution/FileReviewAtoms.js.map +1 -0
  40. package/execution/FileReviewCard.d.ts +2 -2
  41. package/execution/FileReviewCard.d.ts.map +1 -1
  42. package/execution/FileReviewCard.js +32 -62
  43. package/execution/FileReviewCard.js.map +1 -1
  44. package/execution/MessageEntry.d.ts +0 -9
  45. package/execution/MessageEntry.d.ts.map +1 -1
  46. package/execution/MessageEntry.js +1 -11
  47. package/execution/MessageEntry.js.map +1 -1
  48. package/execution/MessageThread.d.ts +3 -4
  49. package/execution/MessageThread.d.ts.map +1 -1
  50. package/execution/MessageThread.js +25 -11
  51. package/execution/MessageThread.js.map +1 -1
  52. package/execution/PlanArtifactCard.d.ts +16 -10
  53. package/execution/PlanArtifactCard.d.ts.map +1 -1
  54. package/execution/PlanArtifactCard.js +31 -7
  55. package/execution/PlanArtifactCard.js.map +1 -1
  56. package/execution/PlanCompletionCard.d.ts +4 -4
  57. package/execution/PlanCompletionCard.d.ts.map +1 -1
  58. package/execution/PlanCompletionCard.js +4 -4
  59. package/execution/PlanCompletionCard.js.map +1 -1
  60. package/execution/PlanStreamingCard.d.ts +7 -2
  61. package/execution/PlanStreamingCard.d.ts.map +1 -1
  62. package/execution/PlanStreamingCard.js +11 -6
  63. package/execution/PlanStreamingCard.js.map +1 -1
  64. package/execution/index.d.ts +12 -2
  65. package/execution/index.d.ts.map +1 -1
  66. package/execution/index.js +6 -1
  67. package/execution/index.js.map +1 -1
  68. package/execution/use-build-from-plan-hotkey.d.ts +1 -1
  69. package/execution/use-build-from-plan-hotkey.js +1 -1
  70. package/execution/useArtifactContent.d.ts +6 -1
  71. package/execution/useArtifactContent.d.ts.map +1 -1
  72. package/execution/useArtifactContent.js +7 -2
  73. package/execution/useArtifactContent.js.map +1 -1
  74. package/execution/useArtifactCopy.d.ts +51 -0
  75. package/execution/useArtifactCopy.d.ts.map +1 -0
  76. package/execution/useArtifactCopy.js +75 -0
  77. package/execution/useArtifactCopy.js.map +1 -0
  78. package/execution/useArtifactDownload.d.ts.map +1 -1
  79. package/execution/useArtifactDownload.js +17 -5
  80. package/execution/useArtifactDownload.js.map +1 -1
  81. package/execution/useArtifactInspection.d.ts +85 -0
  82. package/execution/useArtifactInspection.d.ts.map +1 -0
  83. package/execution/useArtifactInspection.js +143 -0
  84. package/execution/useArtifactInspection.js.map +1 -0
  85. package/execution/useCreateAgentExecution.d.ts +16 -3
  86. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  87. package/execution/useCreateAgentExecution.js +1 -0
  88. package/execution/useCreateAgentExecution.js.map +1 -1
  89. package/index.d.ts +4 -4
  90. package/index.d.ts.map +1 -1
  91. package/index.js +3 -3
  92. package/index.js.map +1 -1
  93. package/internal/MermaidDiagram.d.ts +47 -0
  94. package/internal/MermaidDiagram.d.ts.map +1 -0
  95. package/internal/MermaidDiagram.js +118 -0
  96. package/internal/MermaidDiagram.js.map +1 -0
  97. package/internal/file-tree/FileTreeNode.d.ts.map +1 -1
  98. package/internal/file-tree/FileTreeNode.js +2 -2
  99. package/internal/file-tree/FileTreeNode.js.map +1 -1
  100. package/internal/markdown-components.d.ts.map +1 -1
  101. package/internal/markdown-components.js +34 -0
  102. package/internal/markdown-components.js.map +1 -1
  103. package/internal/mermaid-loader.d.ts +26 -0
  104. package/internal/mermaid-loader.d.ts.map +1 -0
  105. package/internal/mermaid-loader.js +34 -0
  106. package/internal/mermaid-loader.js.map +1 -0
  107. package/internal/mermaid-theme-css.d.ts +41 -0
  108. package/internal/mermaid-theme-css.d.ts.map +1 -0
  109. package/internal/mermaid-theme-css.js +179 -0
  110. package/internal/mermaid-theme-css.js.map +1 -0
  111. package/internal/slug.d.ts +9 -5
  112. package/internal/slug.d.ts.map +1 -1
  113. package/internal/slug.js +10 -6
  114. package/internal/slug.js.map +1 -1
  115. package/library/detect-plan-artifact.d.ts +27 -9
  116. package/library/detect-plan-artifact.d.ts.map +1 -1
  117. package/library/detect-plan-artifact.js +26 -6
  118. package/library/detect-plan-artifact.js.map +1 -1
  119. package/library/index.d.ts +1 -1
  120. package/library/index.d.ts.map +1 -1
  121. package/library/index.js +1 -1
  122. package/library/index.js.map +1 -1
  123. package/package.json +5 -4
  124. package/session/PlanEditor.js +1 -1
  125. package/session/PlanEditor.js.map +1 -1
  126. package/session/SessionViewer.d.ts.map +1 -1
  127. package/session/SessionViewer.js +122 -30
  128. package/session/SessionViewer.js.map +1 -1
  129. package/session/artifact-document.d.ts +13 -0
  130. package/session/artifact-document.d.ts.map +1 -0
  131. package/session/artifact-document.js +22 -0
  132. package/session/artifact-document.js.map +1 -0
  133. package/session/facets/ArtifactsTab.d.ts +30 -4
  134. package/session/facets/ArtifactsTab.d.ts.map +1 -1
  135. package/session/facets/ArtifactsTab.js +32 -10
  136. package/session/facets/ArtifactsTab.js.map +1 -1
  137. package/session/index.d.ts +2 -1
  138. package/session/index.d.ts.map +1 -1
  139. package/session/index.js +4 -1
  140. package/session/index.js.map +1 -1
  141. package/session/plan-document.d.ts +14 -2
  142. package/session/plan-document.d.ts.map +1 -1
  143. package/session/plan-document.js +14 -3
  144. package/session/plan-document.js.map +1 -1
  145. package/session/usePlanDraft.d.ts +1 -1
  146. package/session/useSessionArtifacts.d.ts +17 -0
  147. package/session/useSessionArtifacts.d.ts.map +1 -1
  148. package/session/useSessionArtifacts.js +16 -5
  149. package/session/useSessionArtifacts.js.map +1 -1
  150. package/session/useSessionConversation.d.ts +26 -3
  151. package/session/useSessionConversation.d.ts.map +1 -1
  152. package/session/useSessionConversation.js +51 -6
  153. package/session/useSessionConversation.js.map +1 -1
  154. package/session/useSessionPageFlow.d.ts.map +1 -1
  155. package/session/useSessionPageFlow.js +1 -0
  156. package/session/useSessionPageFlow.js.map +1 -1
  157. package/session/useSessionPanel.d.ts +20 -0
  158. package/session/useSessionPanel.d.ts.map +1 -1
  159. package/session/useSessionPanel.js +13 -0
  160. package/session/useSessionPanel.js.map +1 -1
  161. package/session/useSessionRailViews.d.ts +13 -1
  162. package/session/useSessionRailViews.d.ts.map +1 -1
  163. package/session/useSessionRailViews.js +5 -3
  164. package/session/useSessionRailViews.js.map +1 -1
  165. package/src/composer/SessionComposer.tsx +76 -7
  166. package/src/composer/__tests__/SessionComposer-editing.test.tsx +128 -0
  167. package/src/execution/ArtifactApplyButton.tsx +79 -0
  168. package/src/execution/ArtifactContentBody.tsx +255 -0
  169. package/src/execution/ArtifactDocument.tsx +344 -0
  170. package/src/execution/ArtifactPreviewModal.tsx +49 -415
  171. package/src/execution/ArtifactRow.tsx +165 -0
  172. package/src/execution/ArtifactsWidget.tsx +24 -33
  173. package/src/execution/FileChangeProgressBar.tsx +121 -0
  174. package/src/execution/FileChangesView.tsx +18 -4
  175. package/src/execution/FileReviewAtoms.tsx +89 -0
  176. package/src/execution/FileReviewCard.tsx +57 -90
  177. package/src/execution/MessageEntry.tsx +0 -62
  178. package/src/execution/MessageThread.tsx +29 -15
  179. package/src/execution/PlanArtifactCard.tsx +130 -52
  180. package/src/execution/PlanCompletionCard.tsx +5 -5
  181. package/src/execution/PlanStreamingCard.tsx +15 -11
  182. package/src/execution/__tests__/ArtifactContentBody.test.tsx +131 -0
  183. package/src/execution/__tests__/ArtifactDocument.test.tsx +100 -0
  184. package/src/execution/__tests__/ArtifactPreviewModal.test.tsx +6 -6
  185. package/src/execution/__tests__/ArtifactRow.test.tsx +139 -0
  186. package/src/execution/__tests__/ArtifactsWidget.test.tsx +78 -0
  187. package/src/execution/__tests__/FileChangeProgressBar.test.tsx +125 -0
  188. package/src/execution/__tests__/FileReviewCard.test.tsx +66 -7
  189. package/src/execution/__tests__/MessageThread.test.tsx +1 -1
  190. package/src/execution/__tests__/PlanArtifactCard.test.tsx +52 -18
  191. package/src/execution/__tests__/PlanCompletionCard.test.tsx +2 -2
  192. package/src/execution/__tests__/PlanStreamingCard.test.tsx +11 -4
  193. package/src/execution/__tests__/buildThreadItems-plan-completion.test.ts +104 -8
  194. package/src/execution/__tests__/message-entry.test.tsx +56 -39
  195. package/src/execution/__tests__/useArtifactCopy.test.tsx +90 -0
  196. package/src/execution/__tests__/useArtifactDownload.test.tsx +2 -0
  197. package/src/execution/__tests__/useArtifactInspection.test.tsx +151 -0
  198. package/src/execution/__tests__/useCreateAgentExecution.test.tsx +35 -0
  199. package/src/execution/index.ts +20 -2
  200. package/src/execution/use-build-from-plan-hotkey.ts +1 -1
  201. package/src/execution/useArtifactContent.ts +7 -0
  202. package/src/execution/useArtifactCopy.ts +109 -0
  203. package/src/execution/useArtifactDownload.ts +17 -5
  204. package/src/execution/useArtifactInspection.ts +252 -0
  205. package/src/execution/useCreateAgentExecution.ts +17 -3
  206. package/src/index.ts +17 -2
  207. package/src/internal/MermaidDiagram.tsx +164 -0
  208. package/src/internal/__tests__/a11y/mermaid-theme.a11y.test.tsx +122 -0
  209. package/src/internal/__tests__/markdown-components.test.tsx +93 -1
  210. package/src/internal/__tests__/mermaid-diagram.test.tsx +282 -0
  211. package/src/internal/file-tree/FileTreeNode.tsx +13 -2
  212. package/src/internal/markdown-components.tsx +36 -1
  213. package/src/internal/mermaid-loader.ts +44 -0
  214. package/src/internal/mermaid-theme-css.ts +178 -0
  215. package/src/internal/slug.ts +10 -6
  216. package/src/library/__tests__/detect-plan-artifact.test.ts +21 -0
  217. package/src/library/detect-plan-artifact.ts +32 -10
  218. package/src/library/index.ts +2 -0
  219. package/src/session/PlanEditor.tsx +1 -1
  220. package/src/session/SessionViewer.tsx +227 -51
  221. package/src/session/__tests__/PlanEditor.test.tsx +4 -4
  222. package/src/session/__tests__/PlanStreamingDocument.test.tsx +1 -1
  223. package/src/session/__tests__/SessionViewer-artifactTabs.test.tsx +210 -0
  224. package/src/session/__tests__/SessionViewer-editResubmit.test.tsx +233 -0
  225. package/src/session/__tests__/SessionViewer-fileReviewDock.test.tsx +57 -0
  226. package/src/session/__tests__/useSessionArtifacts.test.ts +57 -0
  227. package/src/session/__tests__/useSessionConversation.test.tsx +216 -1
  228. package/src/session/__tests__/useSessionPanel.test.ts +70 -1
  229. package/src/session/artifact-document.ts +23 -0
  230. package/src/session/facets/ArtifactsTab.tsx +66 -18
  231. package/src/session/facets/__tests__/ArtifactsTab.test.tsx +111 -47
  232. package/src/session/index.ts +5 -1
  233. package/src/session/plan-document.ts +14 -3
  234. package/src/session/usePlanDraft.ts +1 -1
  235. package/src/session/useSessionArtifacts.ts +16 -5
  236. package/src/session/useSessionConversation.ts +80 -10
  237. package/src/session/useSessionPageFlow.ts +1 -0
  238. package/src/session/useSessionPanel.ts +41 -0
  239. package/src/session/useSessionRailViews.tsx +21 -1
  240. package/src/workspace/EditorTabs.tsx +38 -9
  241. package/src/workspace/ExplorerTree.tsx +10 -4
  242. package/src/workspace/FileViewer.tsx +27 -6
  243. package/src/workspace/WorkspaceContentSearch.tsx +44 -20
  244. package/src/workspace/WorkspaceEditor.tsx +19 -16
  245. package/src/workspace/WorkspaceFileSearch.tsx +51 -28
  246. package/src/workspace/WorkspaceSurface.tsx +106 -48
  247. package/src/workspace/__tests__/EditorTabs.test.tsx +4 -2
  248. package/src/workspace/__tests__/ExplorerTree.test.tsx +52 -0
  249. package/src/workspace/__tests__/FileViewer.test.tsx +18 -0
  250. package/src/workspace/__tests__/WorkspaceContentSearch.test.tsx +38 -5
  251. package/src/workspace/__tests__/WorkspaceEditor-initialPanel.test.tsx +19 -0
  252. package/src/workspace/__tests__/WorkspaceFileSearch.test.tsx +27 -6
  253. package/src/workspace/__tests__/WorkspaceSurface.test.tsx +70 -0
  254. package/src/workspace/__tests__/a11y/harness.tsx +140 -0
  255. package/src/workspace/__tests__/a11y/mock-capabilities.ts +138 -0
  256. package/src/workspace/__tests__/a11y/workspace-editor.a11y.test.tsx +65 -0
  257. package/src/workspace/__tests__/a11y/workspace-explorer-search.a11y.test.tsx +112 -0
  258. package/src/workspace/__tests__/a11y/workspace-viewer-tabs.a11y.test.tsx +69 -0
  259. package/styles.css +1 -1
  260. package/workspace/EditorTabs.d.ts +21 -3
  261. package/workspace/EditorTabs.d.ts.map +1 -1
  262. package/workspace/EditorTabs.js +18 -6
  263. package/workspace/EditorTabs.js.map +1 -1
  264. package/workspace/ExplorerTree.js +8 -4
  265. package/workspace/ExplorerTree.js.map +1 -1
  266. package/workspace/FileViewer.d.ts.map +1 -1
  267. package/workspace/FileViewer.js +19 -8
  268. package/workspace/FileViewer.js.map +1 -1
  269. package/workspace/WorkspaceContentSearch.d.ts.map +1 -1
  270. package/workspace/WorkspaceContentSearch.js +23 -2
  271. package/workspace/WorkspaceContentSearch.js.map +1 -1
  272. package/workspace/WorkspaceEditor.d.ts.map +1 -1
  273. package/workspace/WorkspaceEditor.js +8 -10
  274. package/workspace/WorkspaceEditor.js.map +1 -1
  275. package/workspace/WorkspaceFileSearch.d.ts.map +1 -1
  276. package/workspace/WorkspaceFileSearch.js +23 -3
  277. package/workspace/WorkspaceFileSearch.js.map +1 -1
  278. package/workspace/WorkspaceSurface.d.ts.map +1 -1
  279. package/workspace/WorkspaceSurface.js +54 -18
  280. package/workspace/WorkspaceSurface.js.map +1 -1
  281. package/execution/ArtifactCard.d.ts +0 -81
  282. package/execution/ArtifactCard.d.ts.map +0 -1
  283. package/execution/ArtifactCard.js +0 -130
  284. package/execution/ArtifactCard.js.map +0 -1
  285. package/src/execution/ArtifactCard.tsx +0 -320
@@ -5,7 +5,7 @@ import { PlanStreamingCard } from "../PlanStreamingCard";
5
5
  afterEach(cleanup);
6
6
 
7
7
  describe("PlanStreamingCard — live plan stand-in", () => {
8
- it("is a busy region with the streaming title, filename, and live size", () => {
8
+ it("is a busy region with the streaming title, status, and live size", () => {
9
9
  render(
10
10
  <PlanStreamingCard
11
11
  title="Refactor the auth flow"
@@ -17,11 +17,18 @@ describe("PlanStreamingCard — live plan stand-in", () => {
17
17
  const region = screen.getByRole("region", { name: "Plan being written" });
18
18
  expect(region.getAttribute("aria-busy")).toBe("true");
19
19
  expect(region.textContent).toContain("Refactor the auth flow");
20
- expect(region.textContent).toContain("plan.md");
21
20
  expect(region.textContent).toContain("Writing…");
22
21
  expect(region.textContent).toContain("2.0 KB");
23
22
  });
24
23
 
24
+ it("shows no filename mid-stream (the final name is not known yet)", () => {
25
+ render(
26
+ <PlanStreamingCard title="Refactor the auth flow" sizeBytes={2048} onOpenPlan={vi.fn()} />,
27
+ );
28
+ const region = screen.getByRole("region", { name: "Plan being written" });
29
+ expect(region.textContent).not.toContain("plan.md");
30
+ });
31
+
25
32
  it("falls back to 'Writing plan…' until the title has streamed in", () => {
26
33
  render(<PlanStreamingCard sizeBytes={12} onOpenPlan={vi.fn()} />);
27
34
  expect(screen.getByText("Writing plan…")).toBeTruthy();
@@ -37,8 +44,8 @@ describe("PlanStreamingCard — live plan stand-in", () => {
37
44
 
38
45
  it("offers no Download or Build actions — nothing final exists yet", () => {
39
46
  render(<PlanStreamingCard sizeBytes={64} onOpenPlan={vi.fn()} />);
40
- expect(screen.queryByText("Download")).toBeNull();
41
- expect(screen.queryByText("Build from plan")).toBeNull();
47
+ expect(screen.queryByRole("button", { name: /download/i })).toBeNull();
48
+ expect(screen.queryByText("Build")).toBeNull();
42
49
  });
43
50
 
44
51
  it("renders without an action when onOpenPlan is absent", () => {
@@ -638,7 +638,7 @@ describe("buildThreadItems plan-writing (live streaming collapse)", () => {
638
638
  });
639
639
  });
640
640
 
641
- describe("buildThreadItems build-from-plan prompt", () => {
641
+ describe("buildThreadItems build-from-plan prompt suppression", () => {
642
642
  function promptItem(items: readonly ThreadItem[], execId: string) {
643
643
  return items.find(
644
644
  (i): i is Extract<ThreadItem, { kind: "message" }> =>
@@ -646,7 +646,7 @@ describe("buildThreadItems build-from-plan prompt", () => {
646
646
  );
647
647
  }
648
648
 
649
- it("stamps isBuildFromPlan on the prompt of a build turn", () => {
649
+ it("synthesizes NO prompt item for a build turn (the label is machine-written, not user prose)", () => {
650
650
  const exec = makeExecution({
651
651
  id: "exec-build",
652
652
  specMessage: "Build from plan",
@@ -656,14 +656,17 @@ describe("buildThreadItems build-from-plan prompt", () => {
656
656
  });
657
657
 
658
658
  const items = buildThreadItems([exec], null, null, false, undefined);
659
- const prompt = promptItem(items, "exec-build");
660
659
 
661
- expect(prompt).toBeDefined();
662
- expect(prompt!.isBuildFromPlan).toBe(true);
663
- expect(prompt!.message.content).toBe("Build from plan");
660
+ expect(promptItem(items, "exec-build")).toBeUndefined();
661
+ // The turn's agent output still renders — only the fake prompt is gone.
662
+ expect(
663
+ items.some(
664
+ (i) => i.kind === "message" && i.message.content === "Implementing…",
665
+ ),
666
+ ).toBe(true);
664
667
  });
665
668
 
666
- it("leaves the flag unset on an ordinary prompt (renders as a normal bubble)", () => {
669
+ it("still synthesizes the prompt for an ordinary turn", () => {
667
670
  const exec = makeExecution({
668
671
  id: "exec-normal",
669
672
  interactionMode: InteractionMode.AGENT,
@@ -674,6 +677,99 @@ describe("buildThreadItems build-from-plan prompt", () => {
674
677
  const prompt = promptItem(items, "exec-normal");
675
678
 
676
679
  expect(prompt).toBeDefined();
677
- expect(prompt!.isBuildFromPlan).toBeUndefined();
680
+ expect(prompt!.message.content).toBe("test message");
681
+ });
682
+
683
+ it("offers no retryMessage on a failed build turn's error item (a plain resend would drop the flag)", () => {
684
+ const exec = makeExecution({
685
+ id: "exec-build-fail",
686
+ specMessage: "Build from plan",
687
+ phase: ExecutionPhase.EXECUTION_FAILED,
688
+ interactionMode: InteractionMode.AGENT,
689
+ buildFromPlan: true,
690
+ });
691
+ exec.status!.error = "runner crashed";
692
+
693
+ const items = buildThreadItems([exec], null, null, false, undefined);
694
+ const errorItem = items.find(
695
+ (i): i is Extract<ThreadItem, { kind: "execution-error" }> =>
696
+ i.kind === "execution-error",
697
+ );
698
+
699
+ expect(errorItem).toBeDefined();
700
+ expect(errorItem!.retryMessage).toBeUndefined();
701
+ });
702
+
703
+ it("keeps retryMessage on an ordinary failed turn", () => {
704
+ const exec = makeExecution({
705
+ id: "exec-fail",
706
+ specMessage: "do the thing",
707
+ phase: ExecutionPhase.EXECUTION_FAILED,
708
+ interactionMode: InteractionMode.AGENT,
709
+ });
710
+ exec.status!.error = "runner crashed";
711
+
712
+ const items = buildThreadItems([exec], null, null, false, undefined);
713
+ const errorItem = items.find(
714
+ (i): i is Extract<ThreadItem, { kind: "execution-error" }> =>
715
+ i.kind === "execution-error",
716
+ );
717
+
718
+ expect(errorItem!.retryMessage).toBe("do the thing");
719
+ });
720
+ });
721
+
722
+ describe("buildThreadItems pending-bubble tail guard", () => {
723
+ function pendingItem(items: readonly ThreadItem[]) {
724
+ return items.find(
725
+ (i): i is Extract<ThreadItem, { kind: "message" }> =>
726
+ i.kind === "message" && i.key === "pending-user-turn",
727
+ );
728
+ }
729
+
730
+ it("suppresses the pending bubble once the same turn has streamed in (succeeded send)", () => {
731
+ const exec = makeExecution({
732
+ id: "exec-1",
733
+ specMessage: "hello there",
734
+ interactionMode: InteractionMode.AGENT,
735
+ });
736
+
737
+ const items = buildThreadItems(
738
+ [exec],
739
+ null,
740
+ "hello there",
741
+ false,
742
+ undefined,
743
+ );
744
+
745
+ expect(pendingItem(items)).toBeUndefined();
746
+ });
747
+
748
+ it("renders a FAILED pending message even when an older execution carries identical text", () => {
749
+ // Two build attempts of the same plan share the message "Build from
750
+ // plan". A failed second send created no execution, so the first build
751
+ // turn must never swallow the failed bubble — the failure would
752
+ // otherwise render nothing at all (build turns synthesize no prompt).
753
+ const firstBuild = makeExecution({
754
+ id: "exec-build-1",
755
+ specMessage: "Build from plan",
756
+ interactionMode: InteractionMode.AGENT,
757
+ buildFromPlan: true,
758
+ });
759
+
760
+ const items = buildThreadItems(
761
+ [firstBuild],
762
+ null,
763
+ "Build from plan",
764
+ false,
765
+ undefined,
766
+ undefined,
767
+ true, // pendingMessageFailed
768
+ );
769
+
770
+ const failed = pendingItem(items);
771
+ expect(failed).toBeDefined();
772
+ expect(failed!.isFailed).toBe(true);
773
+ expect(failed!.message.content).toBe("Build from plan");
678
774
  });
679
775
  });
@@ -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
  // ---------------------------------------------------------------------------
@@ -195,51 +243,20 @@ describe("MessageEntry — Human messages", () => {
195
243
  });
196
244
 
197
245
  // ---------------------------------------------------------------------------
198
- // Build-from-plan chipthe flagged human turn renders as a compact pill
246
+ // Build-from-plan turnsno special human-message treatment
199
247
  // ---------------------------------------------------------------------------
200
248
 
201
- describe("MessageEntry — Build-from-plan chip", () => {
202
- it("renders a compact chip instead of a prompt bubble when flagged", () => {
203
- const msg = makeMessage(MessageType.MESSAGE_HUMAN, "Build from plan");
204
- const { container } = render(
205
- <MessageEntry message={msg} isBuildFromPlan />,
206
- );
207
-
208
- const chip = queryArticle(container, "Build from plan");
209
- expect(chip).not.toBeNull();
210
- expect(chip!.textContent).toContain("Build from plan");
211
- // The ordinary prompt bubble must not render alongside the chip.
212
- expect(queryArticle(container, "User message")).toBeNull();
213
- });
214
-
215
- it("keeps the ordinary bubble when the flag is absent", () => {
249
+ describe("MessageEntry — Build-from-plan turns", () => {
250
+ it("renders any human message as the ordinary prompt bubble (the thread hides build turns before they reach MessageEntry)", () => {
251
+ // The retired chip treatment lived here; today buildThreadItems skips
252
+ // synthesizing the prompt item for a build turn entirely, so a human
253
+ // message that DOES reach MessageEntry is always ordinary prose.
216
254
  const msg = makeMessage(MessageType.MESSAGE_HUMAN, "Build from plan");
217
255
  const { container } = render(<MessageEntry message={msg} />);
218
256
 
219
257
  expect(queryArticle(container, "User message")).not.toBeNull();
220
258
  expect(queryArticle(container, "Build from plan")).toBeNull();
221
259
  });
222
-
223
- it("never offers Edit on the chip (the label is not user prose)", () => {
224
- const msg = makeMessage(MessageType.MESSAGE_HUMAN, "Build from plan");
225
- const { container } = render(
226
- <MessageEntry message={msg} isBuildFromPlan onEdit={() => {}} />,
227
- );
228
-
229
- expect(
230
- container.querySelector('[aria-label="Edit message"]'),
231
- ).toBeNull();
232
- });
233
-
234
- it("ignores the flag on non-human messages", () => {
235
- const msg = makeMessage(MessageType.MESSAGE_AI, "Implementing…");
236
- const { container } = render(
237
- <MessageEntry message={msg} isBuildFromPlan />,
238
- );
239
-
240
- expect(queryArticle(container, "AI response")).not.toBeNull();
241
- expect(queryArticle(container, "Build from plan")).toBeNull();
242
- });
243
260
  });
244
261
 
245
262
  // ---------------------------------------------------------------------------
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { renderHook, act, waitFor } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import type { Stigmer } from "@stigmer/sdk";
5
+ import { StigmerContext } from "../../context";
6
+ import { useArtifactCopy } from "../useArtifactCopy";
7
+
8
+ function wrapperFor(stigmer: Stigmer) {
9
+ return ({ children }: { children: ReactNode }) => (
10
+ <StigmerContext.Provider value={stigmer}>{children}</StigmerContext.Provider>
11
+ );
12
+ }
13
+
14
+ function makeStigmer(getArtifactContent = vi.fn()) {
15
+ return { agentExecution: { getArtifactContent } } as unknown as Stigmer;
16
+ }
17
+
18
+ const writeText = vi.fn().mockResolvedValue(undefined);
19
+
20
+ beforeEach(() => {
21
+ writeText.mockClear();
22
+ vi.stubGlobal("navigator", { clipboard: { writeText } });
23
+ });
24
+
25
+ describe("useArtifactCopy", () => {
26
+ it("fetches content at click time and writes decoded text to the clipboard", async () => {
27
+ const bytes = new TextEncoder().encode("# Plan\n\nsteps");
28
+ const fn = vi.fn().mockResolvedValue({ content: bytes });
29
+ const { result } = renderHook(() => useArtifactCopy("aex_1"), {
30
+ wrapper: wrapperFor(makeStigmer(fn)),
31
+ });
32
+
33
+ expect(result.current.copied).toBe(false);
34
+ await act(async () => {
35
+ await result.current.copy("artifacts/aex_1/plan.md");
36
+ });
37
+
38
+ expect(fn).toHaveBeenCalledTimes(1);
39
+ const req = fn.mock.calls[0][0];
40
+ expect(req.executionId).toBe("aex_1");
41
+ expect(req.storageKey).toBe("artifacts/aex_1/plan.md");
42
+ expect(writeText).toHaveBeenCalledWith("# Plan\n\nsteps");
43
+ expect(result.current.copied).toBe(true);
44
+ expect(result.current.error).toBeNull();
45
+ });
46
+
47
+ it("resets the copied flag after the feedback window", async () => {
48
+ vi.useFakeTimers();
49
+ const bytes = new TextEncoder().encode("x");
50
+ const fn = vi.fn().mockResolvedValue({ content: bytes });
51
+ const { result } = renderHook(() => useArtifactCopy("aex_1"), {
52
+ wrapper: wrapperFor(makeStigmer(fn)),
53
+ });
54
+
55
+ await act(async () => {
56
+ await result.current.copy("artifacts/aex_1/plan.md");
57
+ });
58
+ expect(result.current.copied).toBe(true);
59
+
60
+ act(() => {
61
+ vi.advanceTimersByTime(2000);
62
+ });
63
+ expect(result.current.copied).toBe(false);
64
+ vi.useRealTimers();
65
+ });
66
+
67
+ it("is a no-op when the execution id is unknown", async () => {
68
+ const fn = vi.fn();
69
+ const { result } = renderHook(() => useArtifactCopy(null), {
70
+ wrapper: wrapperFor(makeStigmer(fn)),
71
+ });
72
+ await act(async () => {
73
+ await result.current.copy("artifacts/x/plan.md");
74
+ });
75
+ expect(fn).not.toHaveBeenCalled();
76
+ expect(writeText).not.toHaveBeenCalled();
77
+ });
78
+
79
+ it("surfaces an error when the content RPC fails", async () => {
80
+ const fn = vi.fn().mockRejectedValue(new Error("boom"));
81
+ const { result } = renderHook(() => useArtifactCopy("aex_1"), {
82
+ wrapper: wrapperFor(makeStigmer(fn)),
83
+ });
84
+ await act(async () => {
85
+ await result.current.copy("artifacts/aex_1/plan.md");
86
+ });
87
+ await waitFor(() => expect(result.current.error?.message).toBe("boom"));
88
+ expect(result.current.copied).toBe(false);
89
+ });
90
+ });
@@ -38,6 +38,8 @@ describe("useArtifactDownload", () => {
38
38
  const req = fn.mock.calls[0][0];
39
39
  expect(req.executionId).toBe("aex_1");
40
40
  expect(req.storageKey).toBe("artifacts/aex_1/file.bin");
41
+ // Save-to-disk actions force an attachment disposition on the minted URL.
42
+ expect(req.asAttachment).toBe(true);
41
43
  expect(result.current.isDownloading).toBe(false);
42
44
  expect(result.current.error).toBeNull();
43
45
  });
@@ -0,0 +1,151 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { renderHook, act, waitFor } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import { create } from "@bufbuild/protobuf";
5
+ import type { Stigmer } from "@stigmer/sdk";
6
+ import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
7
+ import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
8
+ import { StigmerContext } from "../../context";
9
+ import { useArtifactInspection } from "../useArtifactInspection";
10
+
11
+ function fileArtifact(name: string) {
12
+ return create(ExecutionArtifactSchema, {
13
+ name,
14
+ kind: ExecutionArtifactKind.FILE,
15
+ sizeBytes: 64n,
16
+ sandboxPath: `.stigmer/${name}`,
17
+ storageKey: `artifacts/aex_1/${name}`,
18
+ contentHash: "hash-1",
19
+ });
20
+ }
21
+
22
+ function dirArtifact(name: string) {
23
+ return create(ExecutionArtifactSchema, {
24
+ name,
25
+ kind: ExecutionArtifactKind.DIRECTORY,
26
+ sizeBytes: 512n,
27
+ sandboxPath: `.stigmer/${name}`,
28
+ storageKey: `artifacts/aex_1/${name}`,
29
+ entries: ["SKILL.md", "run.sh"],
30
+ });
31
+ }
32
+
33
+ function contentResult(text: string) {
34
+ return {
35
+ content: new TextEncoder().encode(text),
36
+ contentType: "text/plain",
37
+ truncated: false,
38
+ };
39
+ }
40
+
41
+ function wrapperFor(stigmer: Stigmer) {
42
+ return ({ children }: { children: ReactNode }) => (
43
+ <StigmerContext.Provider value={stigmer}>{children}</StigmerContext.Provider>
44
+ );
45
+ }
46
+
47
+ const writeText = vi.fn().mockResolvedValue(undefined);
48
+
49
+ beforeEach(() => {
50
+ writeText.mockClear();
51
+ vi.stubGlobal("navigator", { clipboard: { writeText } });
52
+ });
53
+
54
+ const AGENT_YAML = [
55
+ "apiVersion: agentic.stigmer.ai/v1",
56
+ "kind: Agent",
57
+ "metadata:",
58
+ " name: my-agent",
59
+ "spec: {}",
60
+ "",
61
+ ].join("\n");
62
+
63
+ describe("useArtifactInspection — content + copy", () => {
64
+ it("fetches text content and copies it to the clipboard on demand", async () => {
65
+ const getArtifactContent = vi.fn().mockResolvedValue(contentResult("hello world"));
66
+ const stigmer = { agentExecution: { getArtifactContent } } as unknown as Stigmer;
67
+
68
+ const { result } = renderHook(
69
+ () => useArtifactInspection(fileArtifact("notes.txt"), "aex_1", "acme"),
70
+ { wrapper: wrapperFor(stigmer) },
71
+ );
72
+
73
+ await waitFor(() => expect(result.current.content).toBe("hello world"));
74
+ expect(result.current.isDirectory).toBe(false);
75
+ expect(result.current.isDetected).toBe(false);
76
+ expect(result.current.ctaLabel).toBeNull();
77
+
78
+ act(() => {
79
+ result.current.copy();
80
+ });
81
+ expect(writeText).toHaveBeenCalledWith("hello world");
82
+ await waitFor(() => expect(result.current.copied).toBe(true));
83
+ });
84
+
85
+ it("does not fetch content for a directory artifact", () => {
86
+ const getArtifactContent = vi.fn().mockReturnValue(new Promise(() => {}));
87
+ const stigmer = { agentExecution: { getArtifactContent } } as unknown as Stigmer;
88
+
89
+ const { result } = renderHook(
90
+ () => useArtifactInspection(dirArtifact("skill-pack"), "aex_1", "acme"),
91
+ { wrapper: wrapperFor(stigmer) },
92
+ );
93
+
94
+ expect(result.current.isDirectory).toBe(true);
95
+ // Directory content is not fetched via the text-content RPC (skill
96
+ // detection uses its own entry-scoped fetch).
97
+ expect(
98
+ getArtifactContent.mock.calls.every(
99
+ (call) => call[0]?.entryPath !== undefined,
100
+ ),
101
+ ).toBe(true);
102
+ });
103
+ });
104
+
105
+ describe("useArtifactInspection — detection + apply", () => {
106
+ it("labels a detected Agent YAML and offers an Apply CTA", async () => {
107
+ const getArtifactContent = vi.fn().mockResolvedValue(contentResult(AGENT_YAML));
108
+ const stigmer = { agentExecution: { getArtifactContent } } as unknown as Stigmer;
109
+
110
+ const { result } = renderHook(
111
+ () => useArtifactInspection(fileArtifact("agent.yaml"), "aex_1", "acme"),
112
+ { wrapper: wrapperFor(stigmer) },
113
+ );
114
+
115
+ await waitFor(() => expect(result.current.isDetected).toBe(true));
116
+ expect(result.current.detectionLabel).toBe("Agent detected");
117
+ expect(result.current.ctaLabel).toBe("Apply to acme");
118
+ });
119
+
120
+ it("applies a detected Agent and fires onApplied with the result", async () => {
121
+ const getArtifactContent = vi.fn().mockResolvedValue(contentResult(AGENT_YAML));
122
+ const apply = vi.fn().mockResolvedValue({
123
+ metadata: { name: "my-agent", org: "acme", slug: "my-agent" },
124
+ });
125
+ const stigmer = {
126
+ agentExecution: { getArtifactContent },
127
+ agent: { apply },
128
+ } as unknown as Stigmer;
129
+ const onApplied = vi.fn();
130
+
131
+ const { result } = renderHook(
132
+ () =>
133
+ useArtifactInspection(fileArtifact("agent.yaml"), "aex_1", "acme", {
134
+ onApplied,
135
+ }),
136
+ { wrapper: wrapperFor(stigmer) },
137
+ );
138
+
139
+ await waitFor(() => expect(result.current.isDetected).toBe(true));
140
+
141
+ await act(async () => {
142
+ await result.current.apply();
143
+ });
144
+
145
+ expect(apply).toHaveBeenCalledTimes(1);
146
+ expect(apply.mock.calls[0][0].name).toBe("my-agent");
147
+ await waitFor(() => expect(result.current.applyResult?.kind).toBe("Agent"));
148
+ expect(onApplied).toHaveBeenCalledTimes(1);
149
+ expect(onApplied.mock.calls[0][0].name).toBe("my-agent");
150
+ });
151
+ });
@@ -83,3 +83,38 @@ describe("useCreateAgentExecution — executionConfig mapping", () => {
83
83
  expect(mockCreate.mock.calls[0][0].executionConfig).toBeUndefined();
84
84
  });
85
85
  });
86
+
87
+ describe("useCreateAgentExecution — supersede link (edit-and-resubmit)", () => {
88
+ it("maps supersedesExecutionId into the create call", async () => {
89
+ const { result } = renderHook(() => useCreateAgentExecution(), {
90
+ wrapper: createWrapper(makeMockClient()),
91
+ });
92
+
93
+ await act(async () => {
94
+ await result.current.create({
95
+ org: "acme",
96
+ sessionId: "ses-1",
97
+ message: "corrected message",
98
+ supersedesExecutionId: "aex-old",
99
+ });
100
+ });
101
+
102
+ expect(mockCreate.mock.calls[0][0].supersedesExecutionId).toBe("aex-old");
103
+ });
104
+
105
+ it("leaves supersedesExecutionId undefined for ordinary sends", async () => {
106
+ const { result } = renderHook(() => useCreateAgentExecution(), {
107
+ wrapper: createWrapper(makeMockClient()),
108
+ });
109
+
110
+ await act(async () => {
111
+ await result.current.create({
112
+ org: "acme",
113
+ sessionId: "ses-1",
114
+ message: "Hello",
115
+ });
116
+ });
117
+
118
+ expect(mockCreate.mock.calls[0][0].supersedesExecutionId).toBeUndefined();
119
+ });
120
+ });
@@ -62,6 +62,9 @@ export type {
62
62
  export { useArtifactDownload } from "./useArtifactDownload.js";
63
63
  export type { UseArtifactDownloadReturn } from "./useArtifactDownload.js";
64
64
 
65
+ export { useArtifactCopy } from "./useArtifactCopy.js";
66
+ export type { UseArtifactCopyReturn } from "./useArtifactCopy.js";
67
+
65
68
  export { useToolOutputContent } from "./useToolOutputContent.js";
66
69
  export type {
67
70
  UseToolOutputContentReturn,
@@ -230,18 +233,33 @@ export type { FileReviewCardProps } from "./FileReviewCard.js";
230
233
  export { FileReviewDock } from "./FileReviewDock.js";
231
234
  export type { FileReviewDockProps } from "./FileReviewDock.js";
232
235
 
236
+ export { FileChangeProgressBar } from "./FileChangeProgressBar.js";
237
+ export type { FileChangeProgressBarProps } from "./FileChangeProgressBar.js";
238
+
233
239
  export { ApprovalContext, useApproval } from "./ApprovalContext.js";
234
240
  export type { ApprovalContextValue, UseApprovalResult } from "./ApprovalContext.js";
235
241
 
236
242
  export { FileReviewContext, useFileReviewRowState } from "./FileReviewContext.js";
237
243
  export type { FileReviewContextValue } from "./FileReviewContext.js";
238
244
 
239
- export { ArtifactCard } from "./ArtifactCard.js";
240
- export type { ArtifactCardProps } from "./ArtifactCard.js";
245
+ export { ArtifactRow } from "./ArtifactRow.js";
246
+ export type { ArtifactRowProps } from "./ArtifactRow.js";
241
247
 
242
248
  export { ArtifactContentRenderer } from "./ArtifactContentRenderer.js";
243
249
  export type { ArtifactContentRendererProps } from "./ArtifactContentRenderer.js";
244
250
 
251
+ export { ArtifactContentBody } from "./ArtifactContentBody.js";
252
+ export type { ArtifactContentBodyProps } from "./ArtifactContentBody.js";
253
+
254
+ export { ArtifactDocument } from "./ArtifactDocument.js";
255
+ export type { ArtifactDocumentProps } from "./ArtifactDocument.js";
256
+
257
+ export { useArtifactInspection } from "./useArtifactInspection.js";
258
+ export type {
259
+ ArtifactInspection,
260
+ UseArtifactInspectionOptions,
261
+ } from "./useArtifactInspection.js";
262
+
245
263
  export { ArtifactPreviewContent, ArtifactPreviewModal } from "./ArtifactPreviewModal.js";
246
264
  export type { ArtifactPreviewContentProps, ArtifactPreviewModalProps } from "./ArtifactPreviewModal.js";
247
265
 
@@ -3,7 +3,7 @@
3
3
  import { useCallback, type KeyboardEvent } from "react";
4
4
 
5
5
  /**
6
- * Card-scoped `Cmd/Ctrl+Enter` accelerator for the "Build from plan" action.
6
+ * Card-scoped `Cmd/Ctrl+Enter` accelerator for a plan card's "Build" action.
7
7
  *
8
8
  * Returns an `onKeyDown` handler for a plan card's root element. Because it is
9
9
  * attached to the card (not `window`), it fires only when the keystroke