@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,9 +1,14 @@
1
1
  "use client";
2
2
 
3
+ import { useMemo } from "react";
4
+ import type { CapturedFileChange } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
3
5
  import type { ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
6
+ import { toDisplayFileChange } from "@stigmer/sdk";
4
7
  import type { ToolResultView } from "@stigmer/sdk";
5
8
  import { cn } from "@stigmer/theme";
6
9
  import { BoundedContent } from "../internal/BoundedContent.js";
10
+ import { FileChangeDiff } from "./FileChangesView.js";
11
+ import { useFileReviewRowChange } from "./FileReviewContext.js";
7
12
  import { McpToolDetail } from "./McpToolDetail.js";
8
13
  import { ToolArgsView } from "./ToolArgsView.js";
9
14
  import { ResultView } from "./ResultView.js";
@@ -43,8 +48,9 @@ export interface ToolCallDetailProps {
43
48
  * degrade to readable JSON instead of a raw dump.
44
49
  *
45
50
  * Composition varies by category to avoid redundancy: an edit shows only the
46
- * diff (which already names the file and quantifies the change); a write shows
47
- * its input content; a read shows just the path.
51
+ * diff (which already names the file and quantifies the change); a stamped
52
+ * write shows its change set's captured diff, degrading to the proposed
53
+ * content when no capture resolves; a read shows just the path.
48
54
  *
49
55
  * Importable on its own by platform builders composing custom tool UIs.
50
56
  *
@@ -60,6 +66,16 @@ export function ToolCallDetail({
60
66
  }: ToolCallDetailProps) {
61
67
  const { category, result, primaryArg } = useToolPresentation(toolCall);
62
68
 
69
+ // The captured net change a stamped write row renders as its inline diff
70
+ // (see the write branch below). Resolved here unconditionally (Rules of
71
+ // Hooks); null for every other category's rows — they are never stamped —
72
+ // and for every honest-degradation case, so this costs nothing outside the
73
+ // stamped-write path.
74
+ const capturedChange = useFileReviewRowChange(
75
+ toolCall.fileChangeSetId,
76
+ primaryArg,
77
+ );
78
+
63
79
  return (
64
80
  <div className={cn("space-y-2 text-xs", className)}>
65
81
  <CategoryDetail
@@ -68,6 +84,7 @@ export function ToolCallDetail({
68
84
  result={result}
69
85
  primaryArg={primaryArg}
70
86
  primaryArgTruncated={primaryArgTruncated}
87
+ capturedChange={capturedChange}
71
88
  />
72
89
  </div>
73
90
  );
@@ -90,12 +107,14 @@ function CategoryDetail({
90
107
  result,
91
108
  primaryArg,
92
109
  primaryArgTruncated,
110
+ capturedChange,
93
111
  }: {
94
112
  toolCall: ToolCall;
95
113
  category: ToolCategory;
96
114
  result: ToolResultView;
97
115
  primaryArg: string | null;
98
116
  primaryArgTruncated: boolean;
117
+ capturedChange: CapturedFileChange | null;
99
118
  }) {
100
119
  const args = <ArgsSection toolCall={toolCall} />;
101
120
 
@@ -128,27 +147,50 @@ function CategoryDetail({
128
147
  );
129
148
 
130
149
  case "edit":
131
- case "write":
150
+ case "write": {
132
151
  // The diff already names the file and quantifies the change, so it stands
133
- // alone — showing the written content as an argument would duplicate it. A
134
- // write whose capture is unavailable degrades to a `file` result here
135
- // (ResultView shows the content), so the input is never lost. The owning
136
- // row already names the file, so the body suppresses the path.
152
+ // alone — showing the written content as an argument would duplicate it.
153
+ // The owning row already names the file, so the body suppresses the path.
137
154
  //
138
- // The diff is the one tool body with no internal truncation of its own, so
139
- // it is bounded by the shared BoundedContent budget (the same clamp the
140
- // approval gate diff uses) a large or still-streaming edit shows a
141
- // consistent, scannable window with one in-place reveal, never the whole
142
- // file. Every other category's body self-truncates (terminal / text via
143
- // CollapsiblePre), so only edit/write wrap here never double-bound.
155
+ // An EDIT's diff is reconstructed per-call from its own args (old/new
156
+ // strings, or the Cursor envelope's unified diff). A whole-file WRITE has
157
+ // no per-call before-image in its args, so a stamped write renders the
158
+ // captured NET change from its change set instead the same artifact the
159
+ // row's review badge refers to, resolved through the same path matcher so
160
+ // the two can never disagree. An unresolvable write (still streaming
161
+ // before the turn-boundary capture, no capture mode, legacy session, or a
162
+ // non-reviewable change) keeps the honest fallback: the proposed content
163
+ // from the args, as a `file` view.
164
+ if (category === "write" && capturedChange) {
165
+ return (
166
+ <>
167
+ <ProvenanceNote toolCall={toolCall} />
168
+ <CapturedRowDiff change={capturedChange} />
169
+ </>
170
+ );
171
+ }
172
+ // Bounding is keyed on the VIEW, not the category: BoundedContent is for
173
+ // bodies with no internal truncation of their own — the diff and a FAILED
174
+ // call's error `<pre>` — giving a large or still-streaming edit the same
175
+ // scannable clamp the approval gate uses. The `file` view self-truncates
176
+ // (CollapsibleCode's line cap), so wrapping it would stack two reveal
177
+ // controls on one block — exactly what BoundedContent's contract forbids.
178
+ const body = (
179
+ <ResultView view={result} showFileName={false} showStats={false} />
180
+ );
144
181
  return (
145
182
  <>
146
183
  <ProvenanceNote toolCall={toolCall} />
147
- <BoundedContent cursorTarget="tool-detail-expand">
148
- <ResultView view={result} showFileName={false} showStats={false} />
149
- </BoundedContent>
184
+ {result.type === "file" ? (
185
+ body
186
+ ) : (
187
+ <BoundedContent cursorTarget="tool-detail-expand">
188
+ {body}
189
+ </BoundedContent>
190
+ )}
150
191
  </>
151
192
  );
193
+ }
152
194
 
153
195
  case "read":
154
196
  case "delete":
@@ -245,6 +287,17 @@ function ThinkToolDetail({ toolCall }: { toolCall: ToolCall }) {
245
287
  // Shared sub-components
246
288
  // ---------------------------------------------------------------------------
247
289
 
290
+ // A stamped write row's inline diff: the captured change projected onto the
291
+ // display FileChange and rendered through the same bounded FileChangeDiff the
292
+ // review card's body uses — offloaded-body fetch, binary notices, and
293
+ // truncation states included. The file name is suppressed (the owning row
294
+ // header names it); the `+N −M` stats stay because a write row's header
295
+ // summary carries none.
296
+ function CapturedRowDiff({ change }: { change: CapturedFileChange }) {
297
+ const adapted = useMemo(() => toDisplayFileChange(change), [change]);
298
+ return <FileChangeDiff change={adapted} bounded showFileName={false} />;
299
+ }
300
+
248
301
  // The detail body's only metadata. Duration and the MCP slug live in the owning
249
302
  // row header (ToolCallItem / ApprovalCardHeader), so the body never restates
250
303
  // them; it surfaces the authorization provenance the runner stamped onto the
@@ -0,0 +1,131 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { render, screen, cleanup } from "@testing-library/react";
3
+ import { create } from "@bufbuild/protobuf";
4
+ import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
5
+ import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
6
+ import { ArtifactContentBody } from "../ArtifactContentBody";
7
+ import type { SkillPackageDetection } from "../../library/detect-skill-package";
8
+
9
+ const NOT_SKILL: SkillPackageDetection = { detected: false };
10
+
11
+ function fileArtifact(name: string) {
12
+ return create(ExecutionArtifactSchema, {
13
+ name,
14
+ kind: ExecutionArtifactKind.FILE,
15
+ sizeBytes: 64n,
16
+ });
17
+ }
18
+
19
+ function dirArtifact(name: string, entries: string[]) {
20
+ return create(ExecutionArtifactSchema, {
21
+ name,
22
+ kind: ExecutionArtifactKind.DIRECTORY,
23
+ sizeBytes: 512n,
24
+ entries,
25
+ });
26
+ }
27
+
28
+ afterEach(cleanup);
29
+
30
+ describe("ArtifactContentBody — file states", () => {
31
+ it("renders a loading skeleton", () => {
32
+ render(
33
+ <ArtifactContentBody
34
+ artifact={fileArtifact("notes.txt")}
35
+ content={null}
36
+ contentType={null}
37
+ isLoading
38
+ error={null}
39
+ isTruncated={false}
40
+ skillDetection={NOT_SKILL}
41
+ />,
42
+ );
43
+ expect(screen.getByLabelText("Loading content")).toBeTruthy();
44
+ });
45
+
46
+ it("renders an error message", () => {
47
+ render(
48
+ <ArtifactContentBody
49
+ artifact={fileArtifact("notes.txt")}
50
+ content={null}
51
+ contentType={null}
52
+ isLoading={false}
53
+ error={new Error("boom")}
54
+ isTruncated={false}
55
+ skillDetection={NOT_SKILL}
56
+ />,
57
+ );
58
+ expect(screen.getByText("boom")).toBeTruthy();
59
+ });
60
+
61
+ it("renders the binary / unavailable state when content is null and settled", () => {
62
+ render(
63
+ <ArtifactContentBody
64
+ artifact={fileArtifact("image.bin")}
65
+ content={null}
66
+ contentType={null}
67
+ isLoading={false}
68
+ error={null}
69
+ isTruncated={false}
70
+ skillDetection={NOT_SKILL}
71
+ />,
72
+ );
73
+ expect(screen.getByText(/Content not available for preview/)).toBeTruthy();
74
+ });
75
+
76
+ it("renders text content", () => {
77
+ render(
78
+ <ArtifactContentBody
79
+ artifact={fileArtifact("notes.txt")}
80
+ content={"hello from the artifact"}
81
+ contentType={"text/plain"}
82
+ isLoading={false}
83
+ error={null}
84
+ isTruncated={false}
85
+ skillDetection={NOT_SKILL}
86
+ />,
87
+ );
88
+ expect(screen.getByText(/hello from the artifact/)).toBeTruthy();
89
+ });
90
+ });
91
+
92
+ describe("ArtifactContentBody — directory state", () => {
93
+ it("lists directory entries", () => {
94
+ render(
95
+ <ArtifactContentBody
96
+ artifact={dirArtifact("pack", ["SKILL.md", "run.sh"])}
97
+ content={null}
98
+ contentType={null}
99
+ isLoading={false}
100
+ error={null}
101
+ isTruncated={false}
102
+ skillDetection={NOT_SKILL}
103
+ />,
104
+ );
105
+ expect(screen.getByText("Files (2)")).toBeTruthy();
106
+ expect(screen.getByText("SKILL.md")).toBeTruthy();
107
+ expect(screen.getByText("run.sh")).toBeTruthy();
108
+ });
109
+
110
+ it("shows the skill banner when a skill package is detected", () => {
111
+ const detection = {
112
+ detected: true,
113
+ skillName: "My Skill",
114
+ skillDescription: "does things",
115
+ fileCount: 2,
116
+ } as unknown as SkillPackageDetection;
117
+ render(
118
+ <ArtifactContentBody
119
+ artifact={dirArtifact("pack", ["SKILL.md"])}
120
+ content={null}
121
+ contentType={null}
122
+ isLoading={false}
123
+ error={null}
124
+ isTruncated={false}
125
+ skillDetection={detection}
126
+ />,
127
+ );
128
+ expect(screen.getByText("My Skill")).toBeTruthy();
129
+ expect(screen.getByText("does things")).toBeTruthy();
130
+ });
131
+ });
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { render, screen, cleanup, waitFor } from "@testing-library/react";
3
+ import { create } from "@bufbuild/protobuf";
4
+ import type { Stigmer } from "@stigmer/sdk";
5
+ import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
6
+ import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
7
+ import { StigmerContext } from "../../context";
8
+ import { ArtifactDocument } from "../ArtifactDocument";
9
+
10
+ function fileArtifact(name: string) {
11
+ return create(ExecutionArtifactSchema, {
12
+ name,
13
+ kind: ExecutionArtifactKind.FILE,
14
+ sizeBytes: 64n,
15
+ storageKey: `artifacts/aex_1/${name}`,
16
+ contentHash: "hash-1",
17
+ });
18
+ }
19
+
20
+ function dirArtifact(name: string) {
21
+ return create(ExecutionArtifactSchema, {
22
+ name,
23
+ kind: ExecutionArtifactKind.DIRECTORY,
24
+ sizeBytes: 512n,
25
+ storageKey: `artifacts/aex_1/${name}`,
26
+ entries: ["a.txt", "b.txt"],
27
+ });
28
+ }
29
+
30
+ function stigmerReturning(text: string): Stigmer {
31
+ return {
32
+ agentExecution: {
33
+ getArtifactContent: vi.fn().mockResolvedValue({
34
+ content: new TextEncoder().encode(text),
35
+ contentType: "text/plain",
36
+ truncated: false,
37
+ }),
38
+ },
39
+ } as unknown as Stigmer;
40
+ }
41
+
42
+ function renderDoc(stigmer: Stigmer, artifact = fileArtifact("notes.txt")) {
43
+ return render(
44
+ <StigmerContext.Provider value={stigmer}>
45
+ <ArtifactDocument
46
+ artifact={artifact}
47
+ executionId="aex_1"
48
+ org="acme"
49
+ isTerminal
50
+ />
51
+ </StigmerContext.Provider>,
52
+ );
53
+ }
54
+
55
+ const AGENT_YAML = [
56
+ "apiVersion: agentic.stigmer.ai/v1",
57
+ "kind: Agent",
58
+ "metadata:",
59
+ " name: my-agent",
60
+ "spec: {}",
61
+ "",
62
+ ].join("\n");
63
+
64
+ afterEach(cleanup);
65
+
66
+ describe("ArtifactDocument", () => {
67
+ it("renders the toolbar (name + Download) and the fetched content", async () => {
68
+ renderDoc(stigmerReturning("hello document"));
69
+
70
+ expect(screen.getByRole("article", { name: "Artifact notes.txt" })).toBeTruthy();
71
+ expect(screen.getByText("notes.txt")).toBeTruthy();
72
+ expect(screen.getByText("Download")).toBeTruthy();
73
+ await waitFor(() => expect(screen.getByText(/hello document/)).toBeTruthy());
74
+ expect(screen.getByText("Copy")).toBeTruthy();
75
+ });
76
+
77
+ it("surfaces resource detection and an Apply CTA for a detected Agent", async () => {
78
+ renderDoc(stigmerReturning(AGENT_YAML), fileArtifact("agent.yaml"));
79
+
80
+ await waitFor(() => expect(screen.getByText("Agent detected")).toBeTruthy());
81
+ expect(screen.getByText("Apply to acme")).toBeTruthy();
82
+ });
83
+
84
+ it("renders a directory artifact with a Download ZIP action and no Copy", () => {
85
+ render(
86
+ <StigmerContext.Provider value={stigmerReturning("")}>
87
+ <ArtifactDocument
88
+ artifact={dirArtifact("pack")}
89
+ executionId="aex_1"
90
+ org="acme"
91
+ isTerminal
92
+ />
93
+ </StigmerContext.Provider>,
94
+ );
95
+
96
+ expect(screen.getByText("Download ZIP")).toBeTruthy();
97
+ expect(screen.queryByText("Copy")).toBeNull();
98
+ expect(screen.getByText("Files (2)")).toBeTruthy();
99
+ });
100
+ });
@@ -0,0 +1,139 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { render, screen, fireEvent, cleanup } from "@testing-library/react";
3
+ import { create } from "@bufbuild/protobuf";
4
+ import type { Stigmer } from "@stigmer/sdk";
5
+ import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
6
+ import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
7
+ import { StigmerContext } from "../../context";
8
+ import { ArtifactRow } from "../ArtifactRow";
9
+
10
+ function fileArtifact(name: string, sandboxPath = `.stigmer/${name}`) {
11
+ return create(ExecutionArtifactSchema, {
12
+ name,
13
+ kind: ExecutionArtifactKind.FILE,
14
+ sizeBytes: 2048n,
15
+ sandboxPath,
16
+ storageKey: `artifacts/aex_1/${name}`,
17
+ });
18
+ }
19
+
20
+ function dirArtifact(name: string) {
21
+ return create(ExecutionArtifactSchema, {
22
+ name,
23
+ kind: ExecutionArtifactKind.DIRECTORY,
24
+ sizeBytes: 4096n,
25
+ sandboxPath: `.stigmer/${name}`,
26
+ storageKey: `artifacts/aex_1/${name}`,
27
+ });
28
+ }
29
+
30
+ const getArtifactDownloadUrl = vi.fn().mockResolvedValue({ downloadUrl: "" });
31
+
32
+ function createStigmerMock(): Stigmer {
33
+ return {
34
+ agentExecution: { getArtifactDownloadUrl },
35
+ } as unknown as Stigmer;
36
+ }
37
+
38
+ function renderRow(props: Partial<Parameters<typeof ArtifactRow>[0]> = {}) {
39
+ const merged = {
40
+ artifact: fileArtifact("notes.md"),
41
+ executionId: "aex_1",
42
+ onOpen: vi.fn(),
43
+ ...props,
44
+ } as Parameters<typeof ArtifactRow>[0];
45
+ return render(
46
+ <StigmerContext.Provider value={createStigmerMock()}>
47
+ <ul>
48
+ <ArtifactRow {...merged} />
49
+ </ul>
50
+ </StigmerContext.Provider>,
51
+ );
52
+ }
53
+
54
+ afterEach(() => {
55
+ cleanup();
56
+ getArtifactDownloadUrl.mockClear();
57
+ });
58
+
59
+ describe("ArtifactRow — presentation", () => {
60
+ it("renders a file-type icon, the name, and the formatted size", () => {
61
+ renderRow({ artifact: fileArtifact("notes.md") });
62
+ const open = screen.getByText("notes.md").closest("button")!;
63
+ expect(open.querySelector("svg")).toBeTruthy();
64
+ expect(screen.getByText("2.0 KB")).toBeTruthy();
65
+ });
66
+
67
+ it("appends a trailing slash for a directory artifact", () => {
68
+ renderRow({ artifact: dirArtifact("return-policy") });
69
+ // The name span's own text is "return-policy" + "/".
70
+ expect(screen.getByText("return-policy/")).toBeTruthy();
71
+ });
72
+
73
+ it("shows the parent-directory subtitle only on a name collision", () => {
74
+ const artifact = fileArtifact("agent.yaml", "/workspace/configs/agent.yaml");
75
+ const { rerender } = renderRow({ artifact, hasNameCollision: true });
76
+ expect(screen.getByText("configs/")).toBeTruthy();
77
+
78
+ rerender(
79
+ <StigmerContext.Provider value={createStigmerMock()}>
80
+ <ul>
81
+ <ArtifactRow artifact={artifact} executionId="aex_1" onOpen={vi.fn()} />
82
+ </ul>
83
+ </StigmerContext.Provider>,
84
+ );
85
+ expect(screen.queryByText("configs/")).toBeNull();
86
+ });
87
+ });
88
+
89
+ describe("ArtifactRow — nested-interactive avoidance (DD-22)", () => {
90
+ it("renders the Download control as a SIBLING of the open button, never nested", () => {
91
+ renderRow({ artifact: fileArtifact("notes.md") });
92
+ const open = screen.getByText("notes.md").closest("button")!;
93
+ const download = screen.getByLabelText("Download notes.md");
94
+ expect(download.tagName).toBe("BUTTON");
95
+ // A <button> inside a <button> is a WCAG 4.1.2 nested-interactive violation.
96
+ expect(open.contains(download)).toBe(false);
97
+ });
98
+ });
99
+
100
+ describe("ArtifactRow — open / activate interaction (file-tree parity)", () => {
101
+ it("fires onOpen on a single click of the open target", () => {
102
+ const onOpen = vi.fn();
103
+ renderRow({ onOpen });
104
+ fireEvent.click(screen.getByText("notes.md"));
105
+ expect(onOpen).toHaveBeenCalledTimes(1);
106
+ });
107
+
108
+ it("fires onActivate on a double-click when provided", () => {
109
+ const onActivate = vi.fn();
110
+ renderRow({ onActivate });
111
+ fireEvent.doubleClick(screen.getByText("notes.md"));
112
+ expect(onActivate).toHaveBeenCalledTimes(1);
113
+ });
114
+
115
+ it("binds no double-click handler when onActivate is omitted (harmless)", () => {
116
+ const onOpen = vi.fn();
117
+ renderRow({ onOpen });
118
+ // With no onActivate, a dblclick fires nothing (onOpen only responds to click).
119
+ fireEvent.doubleClick(screen.getByText("notes.md"));
120
+ expect(onOpen).not.toHaveBeenCalled();
121
+ });
122
+ });
123
+
124
+ describe("ArtifactRow — download", () => {
125
+ it("mints a download URL for the artifact's storage key on Download click", () => {
126
+ renderRow({ artifact: fileArtifact("notes.md") });
127
+ fireEvent.click(screen.getByLabelText("Download notes.md"));
128
+ expect(getArtifactDownloadUrl).toHaveBeenCalledTimes(1);
129
+ expect(getArtifactDownloadUrl.mock.calls[0][0]).toMatchObject({
130
+ executionId: "aex_1",
131
+ storageKey: "artifacts/aex_1/notes.md",
132
+ });
133
+ });
134
+
135
+ it("labels the Download control 'Download ZIP' for a directory", () => {
136
+ renderRow({ artifact: dirArtifact("return-policy") });
137
+ expect(screen.getByTitle("Download ZIP")).toBeTruthy();
138
+ });
139
+ });
@@ -0,0 +1,78 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { render, screen, fireEvent, cleanup } from "@testing-library/react";
3
+ import { create } from "@bufbuild/protobuf";
4
+ import type { Stigmer } from "@stigmer/sdk";
5
+ import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
6
+ import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
7
+ import {
8
+ ExecutionArtifactKind,
9
+ ExecutionPhase,
10
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
11
+ import { StigmerContext } from "../../context";
12
+ import { ArtifactsWidget } from "../ArtifactsWidget";
13
+
14
+ function artifact(name: string) {
15
+ return create(ExecutionArtifactSchema, {
16
+ name,
17
+ kind: ExecutionArtifactKind.FILE,
18
+ sizeBytes: 1024n,
19
+ sandboxPath: `.stigmer/${name}`,
20
+ storageKey: `artifacts/aex_1/${name}`,
21
+ });
22
+ }
23
+
24
+ const execution = create(AgentExecutionSchema, {
25
+ metadata: { id: "aex_1" },
26
+ status: {
27
+ phase: ExecutionPhase.EXECUTION_COMPLETED,
28
+ artifacts: [artifact("agent.yaml"), artifact("notes.md")],
29
+ },
30
+ });
31
+
32
+ /** Modal content fetch — keep pending so nothing rejects during the test. */
33
+ function createStigmerMock(): Stigmer {
34
+ return {
35
+ agentExecution: {
36
+ getArtifactContent: vi.fn().mockReturnValue(new Promise(() => {})),
37
+ getArtifactDownloadUrl: vi.fn().mockResolvedValue({ downloadUrl: "" }),
38
+ },
39
+ } as unknown as Stigmer;
40
+ }
41
+
42
+ function renderWidget(executions = [execution]) {
43
+ return render(
44
+ <StigmerContext.Provider value={createStigmerMock()}>
45
+ <ArtifactsWidget executions={executions} org="acme" />
46
+ </StigmerContext.Provider>,
47
+ );
48
+ }
49
+
50
+ afterEach(cleanup);
51
+
52
+ describe("ArtifactsWidget — dense list (panel-less)", () => {
53
+ it("renders nothing when there are no artifacts", () => {
54
+ const { container } = renderWidget([]);
55
+ expect(container.firstChild).toBeNull();
56
+ });
57
+
58
+ it("renders a heading, count, and one dense row per artifact (no ArtifactCard chrome)", () => {
59
+ renderWidget();
60
+ expect(screen.getByRole("heading", { name: "Artifacts" })).toBeTruthy();
61
+ // Count badge reflects the two deduped artifacts.
62
+ expect(screen.getByText("2")).toBeTruthy();
63
+ expect(screen.getAllByRole("listitem")).toHaveLength(2);
64
+ expect(screen.getByText("agent.yaml")).toBeTruthy();
65
+ expect(screen.getByText("notes.md")).toBeTruthy();
66
+ // The retired ArtifactCard rendered a "Preview" button and role="article".
67
+ expect(screen.queryByText("Preview")).toBeNull();
68
+ expect(screen.queryByRole("article")).toBeNull();
69
+ expect(screen.getByLabelText("Download agent.yaml")).toBeTruthy();
70
+ });
71
+
72
+ it("opens the preview modal (the panel-less fallback) on a row click", () => {
73
+ renderWidget();
74
+ expect(document.querySelector("dialog")).toBeNull();
75
+ fireEvent.click(screen.getByText("notes.md"));
76
+ expect(document.querySelector("dialog")).toBeTruthy();
77
+ });
78
+ });