@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
@@ -0,0 +1,165 @@
1
+ "use client";
2
+
3
+ import type { ExecutionArtifact } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
4
+ import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
5
+ import { cn } from "@stigmer/theme";
6
+ import { FileTypeIcon, FolderTypeIcon } from "../internal/file-icons/index.js";
7
+ import { useArtifactDownload } from "./useArtifactDownload.js";
8
+ import { formatArtifactSize } from "./artifact-utils.js";
9
+
10
+ /** Props for {@link ArtifactRow}. */
11
+ export interface ArtifactRowProps {
12
+ /** The execution artifact to render. */
13
+ readonly artifact: ExecutionArtifact;
14
+ /** ID of the execution that produced this artifact — used to mint its download URL. */
15
+ readonly executionId: string;
16
+ /**
17
+ * When `true`, another artifact in the list shares this artifact's display
18
+ * `name` but has a different `sandbox_path`; the row renders the parent
19
+ * directory as a muted subtitle to disambiguate. Typically supplied from
20
+ * {@link SessionArtifactEntry.hasNameCollision}.
21
+ */
22
+ readonly hasNameCollision?: boolean;
23
+ /** Single click / Enter / Space: open the artifact (a preview tab, or a modal). */
24
+ readonly onOpen: () => void;
25
+ /**
26
+ * Double click: promote the artifact to a persistent (pinned) tab. Mirrors the
27
+ * file tree's `onOpenFile` (preview) / `onActivateFile` (pin) split — the
28
+ * leading single click of the double click has already opened the preview, so
29
+ * this promotes it. Omit in panel-less hosts (e.g. the modal-only widget):
30
+ * with no handler the row binds no `onDoubleClick`, so a double click is just
31
+ * two harmless single clicks, exactly as `FileTreeNode` behaves.
32
+ */
33
+ readonly onActivate?: () => void;
34
+ /** Additional CSS classes for the row's `<li>`. */
35
+ readonly className?: string;
36
+ }
37
+
38
+ /**
39
+ * One dense, VS Code-style artifact row: a full-width open button (shared
40
+ * file-type icon + name + optional collision subtitle + size) with a sibling,
41
+ * hover/focus-revealed Download button.
42
+ *
43
+ * The open target and the Download control are SIBLINGS, never nested — a
44
+ * `<button>` inside a `<button>` is an axe `nested-interactive` (WCAG 4.1.2)
45
+ * violation. This mirrors `ExplorerRoot`'s header + remove-control pattern.
46
+ *
47
+ * Deliberately execution-pure and presentational: it takes only the artifact,
48
+ * its `executionId`, and open/activate closures — no session-domain types and
49
+ * no content fetch (resource detection and Apply/Push live in the opened
50
+ * document/modal, not the list). The two hosts — the session panel's
51
+ * `ArtifactsTab` facet and the panel-less {@link ArtifactsWidget} — map
52
+ * their entries onto these props and own the behavior wiring.
53
+ *
54
+ * @see ArtifactsTab — session-panel facet (rows open editor-pane document tabs)
55
+ * @see ArtifactsWidget — panel-less embeddable (rows open the preview modal)
56
+ */
57
+ export function ArtifactRow({
58
+ artifact,
59
+ executionId,
60
+ hasNameCollision = false,
61
+ onOpen,
62
+ onActivate,
63
+ className,
64
+ }: ArtifactRowProps) {
65
+ const isDirectory = artifact.kind === ExecutionArtifactKind.DIRECTORY;
66
+ const { download, isDownloading } = useArtifactDownload(executionId);
67
+
68
+ const parentDir =
69
+ hasNameCollision && artifact.sandboxPath
70
+ ? parentDirectory(artifact.sandboxPath)
71
+ : null;
72
+
73
+ return (
74
+ <li className={cn("group flex items-stretch", className)}>
75
+ <button
76
+ type="button"
77
+ onClick={onOpen}
78
+ onDoubleClick={onActivate}
79
+ title={artifact.sandboxPath || artifact.name}
80
+ className={cn(
81
+ "flex min-w-0 flex-1 items-center gap-2 px-2 py-1 text-left text-xs text-muted-foreground transition-colors",
82
+ "hover:bg-muted hover:text-foreground",
83
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
84
+ )}
85
+ >
86
+ <span className="shrink-0 text-muted-foreground">
87
+ {isDirectory ? (
88
+ <FolderTypeIcon open={false} />
89
+ ) : (
90
+ <FileTypeIcon fileName={artifact.name} />
91
+ )}
92
+ </span>
93
+ <span className="min-w-0 flex-1 truncate text-foreground">
94
+ {artifact.name}
95
+ {isDirectory && "/"}
96
+ {parentDir && (
97
+ <span className="ml-1.5 text-[0.65rem] text-muted-foreground">
98
+ {parentDir}
99
+ </span>
100
+ )}
101
+ </span>
102
+ <span className="shrink-0 tabular-nums text-[0.65rem] text-muted-foreground-faint">
103
+ {formatArtifactSize(artifact.sizeBytes)}
104
+ </span>
105
+ </button>
106
+ <button
107
+ type="button"
108
+ onClick={() => download(artifact.storageKey, artifact.name)}
109
+ disabled={isDownloading}
110
+ aria-label={
111
+ isDownloading
112
+ ? `Preparing ${artifact.name}`
113
+ : `Download ${artifact.name}`
114
+ }
115
+ title={isDirectory ? "Download ZIP" : "Download"}
116
+ className={cn(
117
+ "flex shrink-0 items-center px-2 text-muted-foreground opacity-0 transition-opacity",
118
+ "group-hover:opacity-100 focus-visible:opacity-100",
119
+ "hover:text-foreground disabled:opacity-50",
120
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
121
+ )}
122
+ >
123
+ <DownloadIcon />
124
+ </button>
125
+ </li>
126
+ );
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Helpers
131
+ // ---------------------------------------------------------------------------
132
+
133
+ /**
134
+ * Extracts a human-readable parent directory label from a sandbox path.
135
+ * Given `/workspace/configs/agent.yaml` returns `configs/`.
136
+ * Returns `null` when the path has no meaningful parent segment.
137
+ */
138
+ function parentDirectory(sandboxPath: string): string | null {
139
+ const lastSlash = sandboxPath.lastIndexOf("/");
140
+ if (lastSlash <= 0) return null;
141
+ const parent = sandboxPath.slice(0, lastSlash);
142
+ const segment = parent.slice(parent.lastIndexOf("/") + 1);
143
+ return segment ? `${segment}/` : null;
144
+ }
145
+
146
+ function DownloadIcon() {
147
+ return (
148
+ <svg
149
+ width="12"
150
+ height="12"
151
+ viewBox="0 0 12 12"
152
+ fill="none"
153
+ stroke="currentColor"
154
+ strokeWidth="1.5"
155
+ strokeLinecap="round"
156
+ strokeLinejoin="round"
157
+ className="shrink-0"
158
+ aria-hidden="true"
159
+ >
160
+ <path d="M6 1.5V8.5" />
161
+ <path d="M3 6L6 9L9 6" />
162
+ <path d="M2 10.5H10" />
163
+ </svg>
164
+ );
165
+ }
@@ -5,9 +5,10 @@ import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexe
5
5
  import { cn } from "@stigmer/theme";
6
6
  import {
7
7
  useSessionArtifacts,
8
+ artifactKey,
8
9
  type SessionArtifactEntry,
9
10
  } from "../session/useSessionArtifacts.js";
10
- import { ArtifactCard } from "./ArtifactCard.js";
11
+ import { ArtifactRow } from "./ArtifactRow.js";
11
12
  import { ArtifactPreviewModal } from "./ArtifactPreviewModal.js";
12
13
  import type { ApplyResourceResult } from "../library/useApplyResource.js";
13
14
 
@@ -45,19 +46,16 @@ export interface ArtifactsWidgetProps {
45
46
  * a file-explorer-like view of the conversation's output — no
46
47
  * execution/turn concepts are exposed.
47
48
  *
48
- * Composes {@link ArtifactCard} (summary + detection badges) with
49
- * {@link ArtifactPreviewModal} (full content review + Apply/Push CTA).
50
- * The card's "Preview" action opens the modal; the modal is the sole
51
- * location for Apply/Push actions (review-before-apply pattern).
49
+ * The panel-less counterpart of the session panel's Artifacts facet: it
50
+ * renders the same dense {@link ArtifactRow} list, but having no editor pane
51
+ * to open document tabs into a click opens the {@link ArtifactPreviewModal}
52
+ * (the review-before-apply popup, and the sole home of the Apply/Push CTA).
53
+ * This is the embeddable path a platform builder drops into their own sidebar.
52
54
  *
53
55
  * Returns `null` when the executions list is empty or no execution
54
56
  * has artifacts, matching the conditional-render pattern of
55
57
  * {@link ExecutionProgress} and {@link UsageWidget}.
56
58
  *
57
- * Renders without card chrome — each {@link ArtifactCard} provides its
58
- * own border and padding. The consumer controls the container styling
59
- * (or places the widget directly in a flex/grid layout).
60
- *
61
59
  * All visual properties flow through `--stgm-*` tokens.
62
60
  *
63
61
  * @example
@@ -74,7 +72,7 @@ export interface ArtifactsWidgetProps {
74
72
  * />
75
73
  * ```
76
74
  *
77
- * @see {@link ArtifactCard} — compact summary card per artifact
75
+ * @see {@link ArtifactRow} — the dense row rendered per artifact
78
76
  * @see {@link ArtifactPreviewModal} — full preview with Apply/Push CTA
79
77
  * @see {@link useSessionArtifacts} — headless session-level artifact aggregation hook
80
78
  * @see {@link useExecutionArtifacts} — headless single-execution artifact extraction hook
@@ -88,27 +86,22 @@ export function ArtifactsWidget({
88
86
  const { artifacts, hasArtifacts, artifactCount } =
89
87
  useSessionArtifacts(executions);
90
88
 
91
- // Store the dedup key (sandboxPath or name) instead of a snapshot of
92
- // the full entry. The actual entry is derived from the live artifacts
93
- // list on every render, so the preview modal always reflects the
94
- // latest artifact version even when a newer execution publishes an
95
- // updated artifact for the same path while the modal is open.
89
+ // Store the artifact identity (not a snapshot of the full entry). The entry is
90
+ // re-derived from the live artifacts list on every render, so the modal always
91
+ // reflects the latest artifact version even when a newer execution publishes
92
+ // an updated artifact for the same path while the modal is open.
96
93
  const [previewKey, setPreviewKey] = useState<string | null>(null);
97
94
 
98
95
  const previewEntry = useMemo<SessionArtifactEntry | null>(
99
96
  () =>
100
97
  previewKey !== null
101
- ? artifacts.find(
102
- (e) =>
103
- (e.artifact.sandboxPath || e.artifact.name) === previewKey,
104
- ) ?? null
98
+ ? artifacts.find((e) => artifactKey(e.artifact) === previewKey) ?? null
105
99
  : null,
106
100
  [previewKey, artifacts],
107
101
  );
108
102
 
109
- const handlePreview = useCallback(
110
- (entry: SessionArtifactEntry) =>
111
- setPreviewKey(entry.artifact.sandboxPath || entry.artifact.name),
103
+ const handleOpen = useCallback(
104
+ (entry: SessionArtifactEntry) => setPreviewKey(artifactKey(entry.artifact)),
112
105
  [],
113
106
  );
114
107
 
@@ -123,19 +116,17 @@ export function ArtifactsWidget({
123
116
  </span>
124
117
  </div>
125
118
 
126
- <div role="list" className="space-y-2">
119
+ <ul role="list" className="flex flex-col">
127
120
  {artifacts.map((entry) => (
128
- <div key={entry.artifact.storageKey} role="listitem">
129
- <ArtifactCard
130
- artifact={entry.artifact}
131
- executionId={entry.executionId}
132
- org={org}
133
- hasNameCollision={entry.hasNameCollision}
134
- onPreview={() => handlePreview(entry)}
135
- />
136
- </div>
121
+ <ArtifactRow
122
+ key={artifactKey(entry.artifact)}
123
+ artifact={entry.artifact}
124
+ executionId={entry.executionId}
125
+ hasNameCollision={entry.hasNameCollision}
126
+ onOpen={() => handleOpen(entry)}
127
+ />
137
128
  ))}
138
- </div>
129
+ </ul>
139
130
 
140
131
  {previewEntry && (
141
132
  <ArtifactPreviewModal
@@ -0,0 +1,121 @@
1
+ "use client";
2
+
3
+ import { memo, useState } from "react";
4
+ import type { FileChangeProgress } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
5
+ import { cn } from "@stigmer/theme";
6
+ import { FileKindBadge, FileLineStats } from "./FileReviewAtoms.js";
7
+ import { FilePathLink } from "./FilePathLink.js";
8
+
9
+ /** Props for {@link FileChangeProgressBar}. */
10
+ export interface FileChangeProgressBarProps {
11
+ /**
12
+ * The transient mid-run progress snapshot for the active turn (DD-32). Feed
13
+ * this from `useSessionConversation().fileChangeProgress`. Renders nothing when
14
+ * undefined or when no files have changed yet — the server clears it once the
15
+ * turn's change set leaves CAPTURING, so the bar disappears exactly when the
16
+ * decision surface ({@link FileReviewDock}) takes over.
17
+ */
18
+ readonly progress: FileChangeProgress | undefined;
19
+ /** Additional CSS class names for the root container. */
20
+ readonly className?: string;
21
+ }
22
+
23
+ /**
24
+ * The live "N files changed so far" strip for a turn that is still running.
25
+ *
26
+ * WHY IT EXISTS. The transcript streams per-edit tool rows, but a change made by
27
+ * a shell command (a `sed`, a build step) has no tool row, and there is no
28
+ * running net-of-many-edits rollup until the turn ends. This bar fills that gap:
29
+ * a compact, NON-INTERACTIVE count of the workspace delta accumulating during
30
+ * the turn, pinned above the composer where the {@link FileReviewDock} will later
31
+ * appear. The two are mutually exclusive per turn (progress shows while CAPTURING;
32
+ * the dock shows once AWAITING_REVIEW), so the bar hands off cleanly to the dock.
33
+ *
34
+ * NEVER A DECISION SURFACE. Progress is non-authoritative and carries no file
35
+ * bodies — it is never reviewable or decidable. The reviewed diff is the
36
+ * turn-boundary candidate ({@link FileReviewDock} / the stamped transcript rows).
37
+ * A secret-like path is surfaced by name with its counts withheld (zeroed), so
38
+ * `FileLineStats` renders nothing for it.
39
+ *
40
+ * A11Y. Only the summary count is an `aria-live` region — the per-file list is
41
+ * not — so a screen reader hears "3 files changing" as the count moves, not a
42
+ * per-file barrage on every ~2s refresh.
43
+ *
44
+ * Purely presentational (headless-first, DD-003): the data lives in
45
+ * `useSessionConversation().fileChangeProgress`. `SessionViewer` mounts it above
46
+ * the dock by default.
47
+ *
48
+ * @example
49
+ * ```tsx
50
+ * const conv = useSessionConversation(sessionId);
51
+ * <FileChangeProgressBar progress={conv.fileChangeProgress} />
52
+ * ```
53
+ */
54
+ export const FileChangeProgressBar = memo(function FileChangeProgressBar({
55
+ progress,
56
+ className,
57
+ }: FileChangeProgressBarProps) {
58
+ const [expanded, setExpanded] = useState(false);
59
+
60
+ if (!progress || progress.filesChanged === 0) return null;
61
+
62
+ const { filesChanged, linesAdded, linesRemoved, entries } = progress;
63
+ const hiddenCount = filesChanged - entries.length;
64
+
65
+ return (
66
+ <div
67
+ role="region"
68
+ aria-label="File changes in progress"
69
+ data-cursor-target="file-change-progress-bar"
70
+ className={cn("border-t border-border-muted px-4 py-2", className)}
71
+ >
72
+ <div className="flex items-center gap-2">
73
+ <button
74
+ type="button"
75
+ onClick={() => setExpanded((prev) => !prev)}
76
+ aria-expanded={expanded}
77
+ className="flex min-w-0 flex-1 items-center gap-2 text-left text-xs text-muted-foreground hover:text-foreground"
78
+ >
79
+ <span
80
+ aria-hidden="true"
81
+ className={cn("shrink-0 transition-transform", expanded && "rotate-90")}
82
+ >
83
+
84
+ </span>
85
+ {/* Only the count is live: a screen reader hears the aggregate move,
86
+ not a per-file announcement on every refresh. */}
87
+ <span aria-live="polite" className="min-w-0 truncate">
88
+ {filesChanged} {filesChanged === 1 ? "file" : "files"} changing…
89
+ </span>
90
+ </button>
91
+ <FileLineStats linesAdded={linesAdded} linesRemoved={linesRemoved} />
92
+ </div>
93
+ {expanded && (
94
+ <ul className="mt-2 max-h-[30vh] space-y-1 overflow-y-auto">
95
+ {entries.map((entry) => {
96
+ const path = entry.pathAfter || entry.pathBefore;
97
+ return (
98
+ <li key={path} className="flex items-center gap-2">
99
+ <FileKindBadge kind={entry.kind} />
100
+ <FilePathLink
101
+ path={path}
102
+ dirDisplay="dim"
103
+ className="min-w-0 flex-1 text-xs"
104
+ />
105
+ <FileLineStats
106
+ linesAdded={entry.linesAdded}
107
+ linesRemoved={entry.linesRemoved}
108
+ />
109
+ </li>
110
+ );
111
+ })}
112
+ {hiddenCount > 0 && (
113
+ <li className="text-xs text-muted-foreground-faint">
114
+ … and {hiddenCount} more
115
+ </li>
116
+ )}
117
+ </ul>
118
+ )}
119
+ </div>
120
+ );
121
+ });
@@ -167,7 +167,13 @@ export function FileChangeDiff({
167
167
  if (change.captureLevel === FileChangeCaptureLevel.HUNK_ONLY) {
168
168
  return { additions: change.linesAdded, deletions: change.linesRemoved };
169
169
  }
170
- return countHunks(hunks);
170
+ // Whole-file: count the rendered hunks (what the user actually sees). When
171
+ // there are none to count — the body is offloaded/loading/truncated — fall
172
+ // back to the capture-time counts, which are computed with the same diff
173
+ // algorithm and so agree with the hunks whenever both exist.
174
+ const computed = countHunks(hunks);
175
+ if (computed.additions > 0 || computed.deletions > 0) return computed;
176
+ return { additions: change.linesAdded, deletions: change.linesRemoved };
171
177
  }, [change.captureLevel, change.linesAdded, change.linesRemoved, hunks]);
172
178
 
173
179
  const showStatsRow = showStats && (stats.additions > 0 || stats.deletions > 0);
@@ -343,8 +349,11 @@ function countHunks(hunks: readonly DiffHunk[]): {
343
349
  * Projects a {@link FileChange} into a {@link FileDiffEntry} for the file list.
344
350
  *
345
351
  * Counts: hunk-only uses the runner's authoritative numbers; whole-file inline
346
- * is derived from the content; an offloaded whole-file side has no synchronous
347
- * content, so its count stays 0 until the file is opened and fetched.
352
+ * is derived from the content (identical to the runner's capture-time counts by
353
+ * construction same diff algorithm). A whole-file side with no synchronous
354
+ * content (offloaded / binary) falls back to the runner's capture-time
355
+ * `linesAdded`/`linesRemoved` — zero when no count exists, which the list
356
+ * renders as no stat.
348
357
  */
349
358
  function toFileDiffEntry(change: FileChange): FileDiffEntry {
350
359
  const changeType =
@@ -370,7 +379,12 @@ function toFileDiffEntry(change: FileChange): FileDiffEntry {
370
379
  return { path: change.path, changeType, additions, deletions };
371
380
  }
372
381
 
373
- return { path: change.path, changeType, additions: 0, deletions: 0 };
382
+ return {
383
+ path: change.path,
384
+ changeType,
385
+ additions: change.linesAdded,
386
+ deletions: change.linesRemoved,
387
+ };
374
388
  }
375
389
 
376
390
  /** Inline text of a side, or null when absent/offloaded/binary. */
@@ -0,0 +1,89 @@
1
+ "use client";
2
+
3
+ import { FileChangeKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
4
+ import { cn } from "@stigmer/theme";
5
+
6
+ /**
7
+ * Shared presentational atoms for the file-review surfaces — the single source
8
+ * for the `+N −M` stat and the change-kind marker, so the review card, the
9
+ * compact list, and the mid-run progress bar can never drift in how they render
10
+ * a file's magnitude or kind.
11
+ */
12
+
13
+ /**
14
+ * The `+N −M` stat for one file (or a whole set), in the same visual vocabulary
15
+ * as the tool-row diff stats. Renders nothing when both counts are zero — the
16
+ * honest signal that no count exists (binary or secret-withheld changes, records
17
+ * captured before counts were stamped) — rather than a misleading `+0 −0`.
18
+ */
19
+ export function FileLineStats({
20
+ linesAdded,
21
+ linesRemoved,
22
+ }: {
23
+ readonly linesAdded: number;
24
+ readonly linesRemoved: number;
25
+ }) {
26
+ if (linesAdded === 0 && linesRemoved === 0) return null;
27
+ return (
28
+ <span
29
+ className="shrink-0 text-xs tabular-nums"
30
+ aria-label={`${linesAdded} added, ${linesRemoved} removed`}
31
+ data-cursor-target="file-review-line-stats"
32
+ >
33
+ <span className="text-diff-added-fg">+{linesAdded}</span>{" "}
34
+ <span className="text-diff-removed-fg">-{linesRemoved}</span>
35
+ </span>
36
+ );
37
+ }
38
+
39
+ /** The kind letter, tone, and a11y name for each wire {@link FileChangeKind}. */
40
+ const KIND_BADGE: Partial<
41
+ Record<FileChangeKind, { letter: string; colorClass: string; label: string }>
42
+ > = {
43
+ [FileChangeKind.ADD]: {
44
+ letter: "A",
45
+ colorClass: "text-diff-added-fg",
46
+ label: "added",
47
+ },
48
+ [FileChangeKind.MODIFY]: {
49
+ letter: "M",
50
+ colorClass: "text-diff-hunk-header-fg",
51
+ label: "modified",
52
+ },
53
+ [FileChangeKind.DELETE]: {
54
+ letter: "D",
55
+ colorClass: "text-diff-removed-fg",
56
+ label: "deleted",
57
+ },
58
+ [FileChangeKind.RENAME]: {
59
+ letter: "R",
60
+ colorClass: "text-diff-hunk-header-fg",
61
+ label: "renamed",
62
+ },
63
+ // A binary change is a modification whose diff is not text; the letter stays
64
+ // M and the binary-ness is told by the row's "Keep anyway" + reason note.
65
+ [FileChangeKind.BINARY_CHANGE]: {
66
+ letter: "M",
67
+ colorClass: "text-diff-hunk-header-fg",
68
+ label: "modified (binary)",
69
+ },
70
+ };
71
+
72
+ /**
73
+ * The single-letter change-kind marker — the same M/A/D visual vocabulary as
74
+ * the version-history `DiffFileList`, extended with R for captured renames.
75
+ * The letter itself is the non-color channel; the full word is exposed to
76
+ * assistive tech. An UNSPECIFIED kind renders nothing rather than guessing.
77
+ */
78
+ export function FileKindBadge({ kind }: { kind: FileChangeKind }) {
79
+ const badge = KIND_BADGE[kind];
80
+ if (!badge) return null;
81
+ return (
82
+ <span
83
+ className={cn("shrink-0 font-mono text-[10px] font-bold", badge.colorClass)}
84
+ aria-label={badge.label}
85
+ >
86
+ {badge.letter}
87
+ </span>
88
+ );
89
+ }