@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
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
 
3
- import { memo, useState } from "react";
3
+ import { memo, useState, type ReactNode } from "react";
4
4
  import { cn } from "@stigmer/theme";
5
5
  import type { ExecutionArtifact } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
6
6
  import { ArtifactPreviewModal } from "./ArtifactPreviewModal.js";
7
7
  import { formatArtifactSize } from "./artifact-utils.js";
8
+ import { useArtifactCopy } from "./useArtifactCopy.js";
8
9
  import { useArtifactDownload } from "./useArtifactDownload.js";
9
10
  import { useBuildFromPlanHotkey } from "./use-build-from-plan-hotkey.js";
10
11
 
@@ -21,20 +22,20 @@ export interface PlanArtifactCardProps {
21
22
  */
22
23
  readonly title?: string;
23
24
  /**
24
- * Organization slug. Required for the "Open full" modal (the shared
25
+ * Organization slug. Required for the open-in-modal fallback (the shared
25
26
  * {@link ArtifactPreviewModal} uses it for its detection/apply pipeline and
26
- * to fetch the content). When omitted, "Open full" is hidden — the prominent
27
- * "Build from plan" action and "Download" remain.
27
+ * to fetch the content). When omitted, that fallback is hidden — the
28
+ * prominent "Build" action and the download icon remain.
28
29
  */
29
30
  readonly org?: string;
30
- /** Called when the user clicks "Build from plan". Hidden when omitted. */
31
+ /** Called when the user clicks "Build". Hidden when omitted. */
31
32
  readonly onImplement?: () => void;
32
33
  /**
33
34
  * Opens the plan in the session panel's plan document tab — the
34
35
  * side-by-side review/refine surface. When provided it REPLACES the
35
- * modal-based "Open full" secondary with "Open plan" (one review
36
- * affordance, not two). When omitted (hosts without a session panel) the
37
- * modal remains.
36
+ * modal-based open fallback (one review affordance, not two). When
37
+ * omitted (hosts without a session panel) the same "Open plan" icon
38
+ * opens the preview modal instead.
38
39
  */
39
40
  readonly onOpenPlan?: () => void;
40
41
  /** Disables the primary CTA (e.g., while an execution is active). */
@@ -52,9 +53,15 @@ export interface PlanArtifactCardProps {
52
53
  * the panel carries the document.
53
54
  *
54
55
  * Anatomy: a document-style identity block (plan icon, the plan's title, a
55
- * `plan.md · size` meta line) with the actions trailing — **Open plan** and
56
- * **Download** as subdued secondaries and one prominent, themeable primary,
57
- * **Build from plan** — so the next step is unmistakable.
56
+ * `<name> · size` meta line) with the actions trailing — open, copy, and
57
+ * download as icon-only secondaries (labelled via `aria-label`/`title`) and
58
+ * one prominent, themeable primary, **Build** — so the next step is
59
+ * unmistakable.
60
+ *
61
+ * Action-label rule (shared with `PlanStreamingCard`, which keeps its
62
+ * labelled "Open plan"): a lone secondary keeps its text label; a row of
63
+ * secondaries competing with a primary collapses to icons, so the primary
64
+ * carries the card's one visible verb.
58
65
  *
59
66
  * Only the LATEST plan's card carries `onImplement`; a superseded plan's card
60
67
  * is review-only (its `onOpenPlan` opens the document read-only), so a stale
@@ -76,6 +83,7 @@ export const PlanArtifactCard = memo(function PlanArtifactCard({
76
83
  }: PlanArtifactCardProps) {
77
84
  const [previewOpen, setPreviewOpen] = useState(false);
78
85
  const handleKeyDown = useBuildFromPlanHotkey(onImplement, disabled);
86
+ const { copy, isCopying, copied } = useArtifactCopy(executionId);
79
87
  const { download, isDownloading } = useArtifactDownload(executionId);
80
88
 
81
89
  return (
@@ -103,53 +111,41 @@ export const PlanArtifactCard = memo(function PlanArtifactCard({
103
111
  </div>
104
112
  </div>
105
113
 
106
- <div className="ml-auto flex flex-wrap items-center gap-x-3 gap-y-1.5">
114
+ <div className="ml-auto flex flex-wrap items-center gap-x-1.5 gap-y-1.5">
107
115
  {onOpenPlan ? (
108
- <button
109
- type="button"
116
+ <IconAction
117
+ label="Open plan"
110
118
  onClick={onOpenPlan}
111
- className={cn(
112
- "inline-flex items-center gap-1 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground",
113
- FOCUS_RING,
114
- )}
115
- >
116
- <ExpandIcon />
117
- Open plan
118
- </button>
119
+ icon={<EyeIcon />}
120
+ />
119
121
  ) : (
120
122
  org && (
121
- <button
122
- type="button"
123
+ <IconAction
124
+ label="Open plan"
123
125
  onClick={() => setPreviewOpen(true)}
124
- className={cn(
125
- "inline-flex items-center gap-1 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground",
126
- FOCUS_RING,
127
- )}
128
- >
129
- <ExpandIcon />
130
- Open full
131
- </button>
126
+ icon={<EyeIcon />}
127
+ />
132
128
  )
133
129
  )}
134
- <button
135
- type="button"
130
+ <IconAction
131
+ label={copied ? "Copied" : isCopying ? "Copying…" : "Copy plan"}
132
+ onClick={() => copy(artifact.storageKey)}
133
+ disabled={isCopying}
134
+ icon={copied ? <CheckIcon /> : <CopyIcon />}
135
+ />
136
+ <IconAction
137
+ label={isDownloading ? "Preparing…" : `Download ${artifact.name}`}
136
138
  onClick={() => download(artifact.storageKey, artifact.name)}
137
139
  disabled={isDownloading}
138
- className={cn(
139
- "inline-flex items-center gap-1 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground disabled:opacity-50",
140
- FOCUS_RING,
141
- )}
142
- >
143
- <DownloadIcon />
144
- {isDownloading ? "Preparing…" : "Download"}
145
- </button>
140
+ icon={<DownloadIcon />}
141
+ />
146
142
  {onImplement && (
147
143
  <button
148
144
  type="button"
149
145
  disabled={disabled}
150
146
  onClick={onImplement}
151
147
  className={cn(
152
- "inline-flex items-center gap-1.5 rounded-md px-3 py-1.5",
148
+ "ml-1.5 inline-flex items-center gap-1.5 rounded-md px-3 py-1.5",
153
149
  "text-xs font-medium transition-colors",
154
150
  "bg-primary text-primary-foreground hover:bg-primary-hover",
155
151
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
@@ -157,13 +153,18 @@ export const PlanArtifactCard = memo(function PlanArtifactCard({
157
153
  )}
158
154
  >
159
155
  <ImplementIcon />
160
- {buildPending ? "Starting build…" : "Build from plan"}
156
+ {buildPending ? "Starting build…" : "Build"}
161
157
  </button>
162
158
  )}
163
159
  </div>
164
160
 
165
- {/* "Open full" reuses the shared artifact preview popup — the single place
166
- that fetches/renders plan content (with Copy + Source/Rendered). */}
161
+ <div role="status" aria-live="polite" aria-atomic="true" className="sr-only">
162
+ {copied && "Plan copied to clipboard"}
163
+ </div>
164
+
165
+ {/* The modal open fallback reuses the shared artifact preview popup —
166
+ the single place that fetches/renders plan content (with Copy +
167
+ Source/Rendered). */}
167
168
  {org && previewOpen && (
168
169
  <ArtifactPreviewModal
169
170
  artifact={artifact}
@@ -182,6 +183,41 @@ export const PlanArtifactCard = memo(function PlanArtifactCard({
182
183
  const FOCUS_RING =
183
184
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:rounded-sm";
184
185
 
186
+ /**
187
+ * An icon-only secondary action. The accessible name doubles as the hover
188
+ * tooltip (native `title` — the codebase's tooltip pattern), and the padded
189
+ * hit area keeps the target comfortable despite the compact glyph.
190
+ */
191
+ function IconAction({
192
+ label,
193
+ onClick,
194
+ icon,
195
+ disabled,
196
+ }: {
197
+ readonly label: string;
198
+ readonly onClick: () => void;
199
+ readonly icon: ReactNode;
200
+ readonly disabled?: boolean;
201
+ }) {
202
+ return (
203
+ <button
204
+ type="button"
205
+ aria-label={label}
206
+ title={label}
207
+ onClick={onClick}
208
+ disabled={disabled}
209
+ className={cn(
210
+ "inline-flex items-center rounded-md p-1.5 text-muted-foreground",
211
+ "transition-colors hover:bg-muted hover:text-foreground",
212
+ "disabled:pointer-events-none disabled:opacity-50",
213
+ FOCUS_RING,
214
+ )}
215
+ >
216
+ {icon}
217
+ </button>
218
+ );
219
+ }
220
+
185
221
  function PlanIcon() {
186
222
  return (
187
223
  <svg
@@ -223,8 +259,8 @@ function ImplementIcon() {
223
259
  function DownloadIcon() {
224
260
  return (
225
261
  <svg
226
- width="10"
227
- height="10"
262
+ width="13"
263
+ height="13"
228
264
  viewBox="0 0 12 12"
229
265
  fill="none"
230
266
  stroke="currentColor"
@@ -241,21 +277,63 @@ function DownloadIcon() {
241
277
  );
242
278
  }
243
279
 
244
- function ExpandIcon() {
280
+ function CopyIcon() {
281
+ return (
282
+ <svg
283
+ width="13"
284
+ height="13"
285
+ viewBox="0 0 12 12"
286
+ fill="none"
287
+ stroke="currentColor"
288
+ strokeWidth="1.2"
289
+ strokeLinecap="round"
290
+ strokeLinejoin="round"
291
+ className="shrink-0"
292
+ aria-hidden="true"
293
+ >
294
+ <rect x="4" y="4" width="6.5" height="6.5" rx="1" />
295
+ <path d="M8 4V2.5C8 1.95 7.55 1.5 7 1.5H2.5C1.95 1.5 1.5 1.95 1.5 2.5V7C1.5 7.55 1.95 8 2.5 8H4" />
296
+ </svg>
297
+ );
298
+ }
299
+
300
+ /** A check glyph — transient confirmation that the copy succeeded. */
301
+ function CheckIcon() {
245
302
  return (
246
303
  <svg
247
- width="10"
248
- height="10"
304
+ width="13"
305
+ height="13"
249
306
  viewBox="0 0 12 12"
250
307
  fill="none"
251
308
  stroke="currentColor"
252
309
  strokeWidth="1.5"
253
310
  strokeLinecap="round"
254
311
  strokeLinejoin="round"
312
+ className="shrink-0 text-success"
313
+ aria-hidden="true"
314
+ >
315
+ <path d="M2 6.5L4.5 9L10 3" />
316
+ </svg>
317
+ );
318
+ }
319
+
320
+ /** An eye glyph — "view/open the plan to read it". */
321
+ function EyeIcon() {
322
+ return (
323
+ <svg
324
+ width="13"
325
+ height="13"
326
+ viewBox="0 0 16 16"
327
+ fill="none"
328
+ stroke="currentColor"
329
+ strokeWidth="1.5"
330
+ strokeLinecap="round"
331
+ strokeLinejoin="round"
255
332
  className="shrink-0"
256
333
  aria-hidden="true"
257
334
  >
258
- <path d="M7 1.5h3.5V5M10.5 1.5L6.5 5.5M5 10.5H1.5V7M1.5 10.5l4-4" />
335
+ <path d="M1.5 8S4 3.5 8 3.5s6.5 4.5 6.5 4.5-2.5 4.5-6.5 4.5S1.5 8 1.5 8Z" />
336
+ <circle cx="8" cy="8" r="1.75" />
259
337
  </svg>
260
338
  );
261
339
  }
@@ -6,7 +6,7 @@ import { useBuildFromPlanHotkey } from "./use-build-from-plan-hotkey.js";
6
6
 
7
7
  /** Props for {@link PlanCompletionCard}. */
8
8
  export interface PlanCompletionCardProps {
9
- /** Called when the user clicks the "Build from plan" button. */
9
+ /** Called when the user clicks the "Build" button. */
10
10
  readonly onImplement?: () => void;
11
11
  /** Disables the CTA button (e.g., while an execution is active). */
12
12
  readonly disabled?: boolean;
@@ -20,9 +20,9 @@ export interface PlanCompletionCardProps {
20
20
  * a plan whose upload failed). When a plan artifact exists, the richer
21
21
  * `PlanArtifactCard` is shown instead.
22
22
  *
23
- * Mirrors `PlanArtifactCard`'s hierarchy — one prominent, themeable "Build from
24
- * plan" primary action with the same card-scoped `Cmd/Ctrl+Enter` accelerator —
25
- * so the two cards feel like the same affordance. The consumer wires the action
23
+ * Mirrors `PlanArtifactCard`'s hierarchy — one prominent, themeable "Build"
24
+ * primary action with the same card-scoped `Cmd/Ctrl+Enter` accelerator — so
25
+ * the two cards feel like the same affordance. The consumer wires the action
26
26
  * to switch the interaction mode to Agent and submit the implement turn.
27
27
  *
28
28
  * Renders nothing when `onImplement` is not provided, so the card is fully
@@ -67,7 +67,7 @@ export const PlanCompletionCard = memo(function PlanCompletionCard({
67
67
  )}
68
68
  >
69
69
  <ImplementIcon />
70
- Build from plan
70
+ Build
71
71
  </button>
72
72
  </div>
73
73
  );
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { memo } from "react";
4
4
  import { cn } from "@stigmer/theme";
5
- import { PLAN_ARTIFACT_NAME } from "../library/detect-plan-artifact.js";
6
5
  import { formatArtifactSize } from "./artifact-utils.js";
7
6
 
8
7
  /** Props for {@link PlanStreamingCard}. */
@@ -34,8 +33,13 @@ export interface PlanStreamingCardProps {
34
33
  * The live counterpart of {@link PlanArtifactCard} — a streaming Plan turn's
35
34
  * compact stand-in in the thread while the plan document renders live in the
36
35
  * session panel's plan tab. Same anatomy as the completed card (icon, title,
37
- * `plan.md · size` meta, trailing actions) so the completion handoff reads as
38
- * the same card settling, not a new element appearing.
36
+ * a `Writing… · size` meta, trailing actions) so the completion handoff reads
37
+ * as the same card settling, not a new element appearing.
38
+ *
39
+ * The meta line deliberately omits a filename: the artifact's name is derived
40
+ * from the plan's title, which is still streaming in, so a name shown here
41
+ * would change under the user when the plan settles. The completed card
42
+ * introduces the final `<slug>_<id>.plan.md` name once it exists.
39
43
  *
40
44
  * Deliberately action-light: "Open plan" is the only affordance. Download and
41
45
  * Build require the published artifact, which does not exist until the turn
@@ -72,8 +76,6 @@ export const PlanStreamingCard = memo(function PlanStreamingCard({
72
76
  {title ?? "Writing plan…"}
73
77
  </div>
74
78
  <div className="text-[0.65rem] text-muted-foreground-faint">
75
- {PLAN_ARTIFACT_NAME}
76
- <span aria-hidden="true"> · </span>
77
79
  <span role="status">Writing…</span>
78
80
  <span aria-hidden="true"> · </span>
79
81
  <span className="tabular-nums">{formatArtifactSize(sizeBytes)}</span>
@@ -89,7 +91,7 @@ export const PlanStreamingCard = memo(function PlanStreamingCard({
89
91
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:rounded-sm",
90
92
  )}
91
93
  >
92
- <ExpandIcon />
94
+ <EyeIcon />
93
95
  Open plan
94
96
  </button>
95
97
  )}
@@ -117,12 +119,13 @@ function PlanIcon({ className }: { readonly className?: string }) {
117
119
  );
118
120
  }
119
121
 
120
- function ExpandIcon() {
122
+ /** An eye glyph — "view/open the plan to read it" (matches PlanArtifactCard). */
123
+ function EyeIcon() {
121
124
  return (
122
125
  <svg
123
- width="10"
124
- height="10"
125
- viewBox="0 0 12 12"
126
+ width="11"
127
+ height="11"
128
+ viewBox="0 0 16 16"
126
129
  fill="none"
127
130
  stroke="currentColor"
128
131
  strokeWidth="1.5"
@@ -131,7 +134,8 @@ function ExpandIcon() {
131
134
  className="shrink-0"
132
135
  aria-hidden="true"
133
136
  >
134
- <path d="M7 1.5h3.5V5M10.5 1.5L6.5 5.5M5 10.5H1.5V7M1.5 10.5l4-4" />
137
+ <path d="M1.5 8S4 3.5 8 3.5s6.5 4.5 6.5 4.5-2.5 4.5-6.5 4.5S1.5 8 1.5 8Z" />
138
+ <circle cx="8" cy="8" r="1.75" />
135
139
  </svg>
136
140
  );
137
141
  }
@@ -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
+ });
@@ -26,8 +26,8 @@ function withStigmer(children: ReactNode) {
26
26
 
27
27
  afterEach(cleanup);
28
28
 
29
- describe("ArtifactPreviewContent — Build from plan action", () => {
30
- it("renders a 'Build from plan' button only when onImplement is provided", () => {
29
+ describe("ArtifactPreviewContent — Build action", () => {
30
+ it("renders a 'Build' button only when onImplement is provided", () => {
31
31
  const { rerender } = render(
32
32
  withStigmer(
33
33
  <ArtifactPreviewContent
@@ -39,7 +39,7 @@ describe("ArtifactPreviewContent — Build from plan action", () => {
39
39
  />,
40
40
  ),
41
41
  );
42
- expect(screen.queryByText("Build from plan")).toBeNull();
42
+ expect(screen.queryByText("Build")).toBeNull();
43
43
 
44
44
  rerender(
45
45
  withStigmer(
@@ -53,10 +53,10 @@ describe("ArtifactPreviewContent — Build from plan action", () => {
53
53
  />,
54
54
  ),
55
55
  );
56
- expect(screen.getByText("Build from plan")).toBeTruthy();
56
+ expect(screen.getByText("Build")).toBeTruthy();
57
57
  });
58
58
 
59
- it("calls onImplement then closes the modal when 'Build from plan' is clicked", () => {
59
+ it("calls onImplement then closes the modal when 'Build' is clicked", () => {
60
60
  const calls: string[] = [];
61
61
  const onImplement = vi.fn(() => calls.push("implement"));
62
62
  const onClose = vi.fn(() => calls.push("close"));
@@ -74,7 +74,7 @@ describe("ArtifactPreviewContent — Build from plan action", () => {
74
74
  ),
75
75
  );
76
76
 
77
- fireEvent.click(screen.getByText("Build from plan"));
77
+ fireEvent.click(screen.getByText("Build"));
78
78
 
79
79
  expect(onImplement).toHaveBeenCalledTimes(1);
80
80
  expect(onClose).toHaveBeenCalledTimes(1);