@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,210 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { render, screen, cleanup, act, fireEvent } from "@testing-library/react";
3
+ import { create } from "@bufbuild/protobuf";
4
+ import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
5
+ import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
6
+ import {
7
+ ExecutionArtifactKind,
8
+ ExecutionPhase,
9
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
10
+ import { ARTIFACT_DOCUMENT_ENTRY_ID } from "../artifact-document";
11
+ import { artifactKey, type SessionArtifactEntry } from "../useSessionArtifacts";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Wiring-contract test for the Artifacts facet's open/pin gestures. The
15
+ // `onOpenArtifact` (single-click preview) and `onActivateArtifact` (double-click
16
+ // pin) callbacks must thread from SessionViewer, through the intermediate
17
+ // SessionPanelRegion, into useSessionRailViews' <ArtifactsTab>, and — when
18
+ // invoked — land as an artifact virtual-document tab on the editors store.
19
+ // WorkspaceSurface renders as a prop-capturing probe; the store + panel-region
20
+ // wire between them is the subject under test. Tab MECHANICS (preview-slot
21
+ // reuse, pin promotion) live in workspace-editors-store.test.ts and are not
22
+ // re-tested here.
23
+ // ---------------------------------------------------------------------------
24
+
25
+ type CapturedProps = Record<string, unknown>;
26
+
27
+ const threadProps: CapturedProps[] = [];
28
+ vi.mock("../../execution/MessageThread", () => ({
29
+ MessageThread: (props: CapturedProps) => {
30
+ threadProps.push(props);
31
+ return <div data-testid="thread-probe" />;
32
+ },
33
+ }));
34
+
35
+ const surfaceProps: CapturedProps[] = [];
36
+ vi.mock("../../workspace/WorkspaceSurface", () => ({
37
+ WorkspaceSurface: (props: CapturedProps) => {
38
+ surfaceProps.push(props);
39
+ return <div data-testid="surface-probe" />;
40
+ },
41
+ }));
42
+
43
+ vi.mock("../../composer", async (importOriginal) => {
44
+ const actual = await importOriginal<typeof import("../../composer")>();
45
+ return {
46
+ ...actual,
47
+ SessionComposer: () => <div data-testid="composer-probe" />,
48
+ };
49
+ });
50
+
51
+ const artifact = create(ExecutionArtifactSchema, {
52
+ name: "agent.yaml",
53
+ kind: ExecutionArtifactKind.FILE,
54
+ sizeBytes: 512n,
55
+ sandboxPath: ".stigmer/agent.yaml",
56
+ storageKey: "artifacts/aex_1/agent.yaml",
57
+ });
58
+
59
+ const artifactExecution = create(AgentExecutionSchema, {
60
+ metadata: { id: "aex_1" },
61
+ status: {
62
+ phase: ExecutionPhase.EXECUTION_COMPLETED,
63
+ artifacts: [artifact],
64
+ },
65
+ });
66
+
67
+ const entry: SessionArtifactEntry = {
68
+ artifact,
69
+ executionId: "aex_1",
70
+ isTerminal: true,
71
+ hasNameCollision: false,
72
+ };
73
+
74
+ const stubWorkspace = {
75
+ entries: [],
76
+ hasEntries: false,
77
+ toInput: vi.fn().mockReturnValue([]),
78
+ addGitRepo: vi.fn(),
79
+ addLocalPath: vi.fn(),
80
+ remove: vi.fn(),
81
+ clear: vi.fn(),
82
+ clearLocal: vi.fn(),
83
+ };
84
+
85
+ const stubConv = {
86
+ session: { spec: {} },
87
+ isLoading: false,
88
+ loadError: null,
89
+ completedExecutions: [artifactExecution],
90
+ activeStreamExecution: null,
91
+ pendingUserMessage: null,
92
+ workspaceEntries: [],
93
+ fileChangeSets: [],
94
+ submitFileDecision: vi.fn(),
95
+ submittingFileDecisionKeys: new Set<string>(),
96
+ fileDecisionErrors: new Map<string, Error>(),
97
+ sendError: null,
98
+ stopError: null,
99
+ streamError: null,
100
+ reconnectStream: vi.fn(),
101
+ submittingApprovalIds: new Set<string>(),
102
+ approvalErrors: new Map<string, Error>(),
103
+ isStoppable: false,
104
+ isStopping: false,
105
+ isSending: false,
106
+ canSendFollowUp: true,
107
+ isReconnecting: false,
108
+ connectTimedOut: false,
109
+ isSlow: false,
110
+ stop: vi.fn(),
111
+ retryLastSend: vi.fn(),
112
+ };
113
+
114
+ const stubSessionPageFlow = {
115
+ conv: stubConv,
116
+ harness: "native" as const,
117
+ executionTarget: undefined,
118
+ model: [undefined, vi.fn()] as const,
119
+ interactionMode: ["agent" as const, vi.fn()] as const,
120
+ agentRef: { org: "acme", slug: "support-bot" },
121
+ setAgentRef: vi.fn(),
122
+ resolution: null,
123
+ setResolution: vi.fn(),
124
+ isDefaultAgent: false,
125
+ mcpServerUsages: [],
126
+ setMcpServerUsages: vi.fn(),
127
+ skillRefs: [],
128
+ setSkillRefs: vi.fn(),
129
+ workspace: stubWorkspace,
130
+ sessionVariables: { entries: [], isEmpty: true, clear: vi.fn() },
131
+ autoApproveAll: false,
132
+ setAutoApproveAll: vi.fn(),
133
+ handleSubmit: vi.fn(),
134
+ submitError: null as Error | null,
135
+ displayExecution: artifactExecution,
136
+ allExecutions: [artifactExecution],
137
+ sandboxWorkspaceRoot: "/home/daytona/workspace",
138
+ };
139
+ vi.mock("../useSessionPageFlow", () => ({
140
+ useSessionPageFlow: () => stubSessionPageFlow,
141
+ }));
142
+
143
+ vi.mock("../../hooks", () => ({
144
+ useStigmer: () => ({
145
+ agentExecution: {
146
+ uploadAttachment: vi.fn(),
147
+ getArtifactContent: vi.fn(),
148
+ },
149
+ }),
150
+ }));
151
+
152
+ import { SessionViewer } from "../SessionViewer";
153
+
154
+ /** The latest editors array the (mocked) WorkspaceSurface received. */
155
+ function latestEditors(): ReadonlyArray<{
156
+ entryId: string;
157
+ path: string;
158
+ preview: boolean;
159
+ }> {
160
+ return (surfaceProps.at(-1)?.editors ?? []) as never;
161
+ }
162
+
163
+ /** Read a callback off the rail-view <ArtifactsTab> element the surface received. */
164
+ function artifactsTabCallback(
165
+ name: "onOpenArtifact" | "onActivateArtifact",
166
+ ): (entry: SessionArtifactEntry) => void {
167
+ const views = surfaceProps.at(-1)?.extraViews as
168
+ | ReadonlyArray<{ id: string; content: { props: CapturedProps } }>
169
+ | undefined;
170
+ const artifactsView = views?.find((v) => v.id === "artifacts");
171
+ if (!artifactsView) throw new Error("Artifacts rail view not present");
172
+ return artifactsView.content.props[name] as (e: SessionArtifactEntry) => void;
173
+ }
174
+
175
+ beforeEach(() => {
176
+ threadProps.length = 0;
177
+ surfaceProps.length = 0;
178
+ });
179
+
180
+ afterEach(() => {
181
+ cleanup();
182
+ vi.clearAllMocks();
183
+ });
184
+
185
+ describe("SessionViewer — Artifacts facet open/pin wiring", () => {
186
+ it("threads onOpenArtifact + onActivateArtifact into the Artifacts rail view", () => {
187
+ render(<SessionViewer sessionId="ses_1" org="acme" />);
188
+ fireEvent.click(screen.getByRole("button", { name: "Show panel" }));
189
+
190
+ expect(typeof artifactsTabCallback("onOpenArtifact")).toBe("function");
191
+ expect(typeof artifactsTabCallback("onActivateArtifact")).toBe("function");
192
+ });
193
+
194
+ it("opening an artifact lands a PREVIEW tab; activating it pins the tab", () => {
195
+ render(<SessionViewer sessionId="ses_1" org="acme" />);
196
+ fireEvent.click(screen.getByRole("button", { name: "Show panel" }));
197
+
198
+ // Single-click preview → one preview tab keyed by the artifact identity.
199
+ act(() => artifactsTabCallback("onOpenArtifact")(entry));
200
+ expect(latestEditors()).toEqual([
201
+ { entryId: ARTIFACT_DOCUMENT_ENTRY_ID, path: artifactKey(artifact), preview: true },
202
+ ]);
203
+
204
+ // Double-click pin → the same tab, now persistent.
205
+ act(() => artifactsTabCallback("onActivateArtifact")(entry));
206
+ expect(latestEditors()).toEqual([
207
+ { entryId: ARTIFACT_DOCUMENT_ENTRY_ID, path: artifactKey(artifact), preview: false },
208
+ ]);
209
+ });
210
+ });
@@ -0,0 +1,233 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { render, cleanup, act } from "@testing-library/react";
3
+ import { forwardRef, useImperativeHandle } from "react";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Wiring-contract test for edit-and-resubmit (stigmer/stigmer#181): the
7
+ // onEditMessage SessionViewer hands to MessageThread stops the in-flight
8
+ // turn, prefills the composer, and enters editing mode; submitting while
9
+ // editing attaches the superseded execution's id to the submit context;
10
+ // cancelling drops the link so the next send appends normally. MessageThread
11
+ // and SessionComposer render as prop-capturing probes; the viewer's editing
12
+ // state machine is the subject under test.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ type CapturedProps = Record<string, unknown>;
16
+
17
+ const threadProps: CapturedProps[] = [];
18
+ vi.mock("../../execution/MessageThread", () => ({
19
+ MessageThread: (props: CapturedProps) => {
20
+ threadProps.push(props);
21
+ return <div data-testid="thread-probe" />;
22
+ },
23
+ }));
24
+
25
+ vi.mock("../../workspace/WorkspaceSurface", () => ({
26
+ WorkspaceSurface: () => <div data-testid="surface-probe" />,
27
+ }));
28
+
29
+ const composerProps: CapturedProps[] = [];
30
+ const composerSetMessage = vi.fn();
31
+ const composerFocus = vi.fn();
32
+ vi.mock("../../composer", async (importOriginal) => {
33
+ const actual = await importOriginal<typeof import("../../composer")>();
34
+ const ComposerProbe = forwardRef<unknown, CapturedProps>(
35
+ function ComposerProbe(props, ref) {
36
+ useImperativeHandle(ref, () => ({
37
+ setMessage: composerSetMessage,
38
+ focus: composerFocus,
39
+ submit: vi.fn(),
40
+ }));
41
+ composerProps.push(props);
42
+ return <div data-testid="composer-probe" />;
43
+ },
44
+ );
45
+ return { ...actual, SessionComposer: ComposerProbe };
46
+ });
47
+
48
+ const stubConv = {
49
+ session: { spec: {} },
50
+ isLoading: false,
51
+ loadError: null,
52
+ completedExecutions: [],
53
+ activeStreamExecution: { metadata: { id: "exec-old" } },
54
+ pendingUserMessage: null,
55
+ workspaceEntries: [],
56
+ fileChangeSets: [],
57
+ submitFileDecision: vi.fn(),
58
+ submittingFileDecisionKeys: new Set<string>(),
59
+ fileDecisionErrors: new Map<string, Error>(),
60
+ sendError: null,
61
+ stopError: null,
62
+ streamError: null,
63
+ reconnectStream: vi.fn(),
64
+ submittingApprovalIds: new Set<string>(),
65
+ approvalErrors: new Map<string, Error>(),
66
+ isStoppable: true,
67
+ isStopping: false,
68
+ isSending: false,
69
+ canSendFollowUp: false,
70
+ isReconnecting: false,
71
+ connectTimedOut: false,
72
+ isSlow: false,
73
+ stop: vi.fn().mockResolvedValue(undefined),
74
+ retryLastSend: vi.fn(),
75
+ };
76
+
77
+ const stubSessionPageFlow = {
78
+ conv: stubConv,
79
+ harness: "native" as const,
80
+ executionTarget: undefined,
81
+ model: [undefined, vi.fn()] as const,
82
+ interactionMode: ["agent" as const, vi.fn()] as const,
83
+ agentRef: { org: "acme", slug: "support-bot" },
84
+ setAgentRef: vi.fn(),
85
+ resolution: null,
86
+ setResolution: vi.fn(),
87
+ isDefaultAgent: false,
88
+ mcpServerUsages: [],
89
+ setMcpServerUsages: vi.fn(),
90
+ skillRefs: [],
91
+ setSkillRefs: vi.fn(),
92
+ workspace: {
93
+ entries: [],
94
+ hasEntries: false,
95
+ toInput: vi.fn().mockReturnValue([]),
96
+ addGitRepo: vi.fn(),
97
+ addLocalPath: vi.fn(),
98
+ remove: vi.fn(),
99
+ clear: vi.fn(),
100
+ clearLocal: vi.fn(),
101
+ },
102
+ sessionVariables: { entries: [], isEmpty: true, clear: vi.fn() },
103
+ autoApproveAll: false,
104
+ setAutoApproveAll: vi.fn(),
105
+ handleSubmit: vi.fn(),
106
+ submitError: null as Error | null,
107
+ displayExecution: null,
108
+ allExecutions: [],
109
+ sandboxWorkspaceRoot: "/home/daytona/workspace",
110
+ };
111
+ vi.mock("../useSessionPageFlow", () => ({
112
+ useSessionPageFlow: () => stubSessionPageFlow,
113
+ }));
114
+
115
+ vi.mock("../../hooks", () => ({
116
+ useStigmer: () => ({
117
+ agentExecution: {
118
+ uploadAttachment: vi.fn(),
119
+ getArtifactContent: vi.fn(),
120
+ },
121
+ }),
122
+ }));
123
+
124
+ import { SessionViewer } from "../SessionViewer";
125
+
126
+ /** The onEditMessage handler threaded down to the (mocked) MessageThread. */
127
+ function capturedOnEditMessage(): ((text: string) => void) | undefined {
128
+ return threadProps.at(-1)?.onEditMessage as
129
+ | ((text: string) => void)
130
+ | undefined;
131
+ }
132
+
133
+ /** The latest props the (mocked) SessionComposer received. */
134
+ function latestComposerProps(): CapturedProps {
135
+ return composerProps.at(-1) ?? {};
136
+ }
137
+
138
+ function submitViaComposer(message: string): void {
139
+ const onSubmit = latestComposerProps().onSubmit as (
140
+ m: string,
141
+ model?: string,
142
+ context?: Record<string, unknown>,
143
+ ) => void;
144
+ onSubmit(message, undefined, { interactionMode: "agent" });
145
+ }
146
+
147
+ beforeEach(() => {
148
+ threadProps.length = 0;
149
+ composerProps.length = 0;
150
+ });
151
+
152
+ afterEach(() => {
153
+ cleanup();
154
+ vi.clearAllMocks();
155
+ });
156
+
157
+ describe("SessionViewer — edit-and-resubmit wiring", () => {
158
+ it("wires onEditMessage to the thread while the turn is stoppable", () => {
159
+ render(<SessionViewer sessionId="ses_1" org="acme" />);
160
+ expect(typeof capturedOnEditMessage()).toBe("function");
161
+ expect(latestComposerProps().isEditing).toBe(false);
162
+ });
163
+
164
+ it("Edit stops the turn, prefills the composer, and enters editing mode", () => {
165
+ render(<SessionViewer sessionId="ses_1" org="acme" />);
166
+
167
+ act(() => {
168
+ capturedOnEditMessage()!("original message");
169
+ });
170
+
171
+ expect(stubConv.stop).toHaveBeenCalledTimes(1);
172
+ expect(composerSetMessage).toHaveBeenCalledWith("original message");
173
+ expect(composerFocus).toHaveBeenCalledTimes(1);
174
+ expect(latestComposerProps().isEditing).toBe(true);
175
+ });
176
+
177
+ it("submitting while editing attaches the supersede link and exits editing", () => {
178
+ render(<SessionViewer sessionId="ses_1" org="acme" />);
179
+
180
+ act(() => {
181
+ capturedOnEditMessage()!("original message");
182
+ });
183
+ act(() => {
184
+ submitViaComposer("corrected message");
185
+ });
186
+
187
+ expect(stubSessionPageFlow.handleSubmit).toHaveBeenCalledTimes(1);
188
+ const [message, , context] =
189
+ stubSessionPageFlow.handleSubmit.mock.calls[0];
190
+ expect(message).toBe("corrected message");
191
+ expect(context).toMatchObject({
192
+ supersedesExecutionId: "exec-old",
193
+ // Composer-provided context fields survive the merge.
194
+ interactionMode: "agent",
195
+ });
196
+ expect(latestComposerProps().isEditing).toBe(false);
197
+ });
198
+
199
+ it("cancelling the edit clears the composer and drops the supersede link", () => {
200
+ render(<SessionViewer sessionId="ses_1" org="acme" />);
201
+
202
+ act(() => {
203
+ capturedOnEditMessage()!("original message");
204
+ });
205
+ expect(latestComposerProps().isEditing).toBe(true);
206
+
207
+ act(() => {
208
+ (latestComposerProps().onCancelEdit as () => void)();
209
+ });
210
+ expect(latestComposerProps().isEditing).toBe(false);
211
+ expect(composerSetMessage).toHaveBeenLastCalledWith("");
212
+
213
+ // A subsequent unrelated send must NOT carry the supersede link.
214
+ act(() => {
215
+ submitViaComposer("unrelated new message");
216
+ });
217
+ const [, , context] = stubSessionPageFlow.handleSubmit.mock.calls[0];
218
+ expect(context).not.toHaveProperty("supersedesExecutionId");
219
+ });
220
+
221
+ it("ordinary sends pass the composer context through untouched", () => {
222
+ render(<SessionViewer sessionId="ses_1" org="acme" />);
223
+
224
+ act(() => {
225
+ submitViaComposer("plain message");
226
+ });
227
+
228
+ const [message, , context] =
229
+ stubSessionPageFlow.handleSubmit.mock.calls[0];
230
+ expect(message).toBe("plain message");
231
+ expect(context).toEqual({ interactionMode: "agent" });
232
+ });
233
+ });
@@ -3,6 +3,8 @@ import { render, screen, cleanup, fireEvent } from "@testing-library/react";
3
3
  import { create } from "@bufbuild/protobuf";
4
4
  import {
5
5
  CapturedFileChangeSchema,
6
+ FileChangeProgressSchema,
7
+ FileChangeProgressEntrySchema,
6
8
  FileChangeSetSchema,
7
9
  } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
8
10
  import { FileContentSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
@@ -70,6 +72,31 @@ function pendingChangeSet(id: string) {
70
72
  });
71
73
  }
72
74
 
75
+ function progressSnapshot() {
76
+ return create(FileChangeProgressSchema, {
77
+ changeSetId: "cs-1",
78
+ filesChanged: 2,
79
+ linesAdded: 4,
80
+ linesRemoved: 1,
81
+ entries: [
82
+ create(FileChangeProgressEntrySchema, {
83
+ pathAfter: "a.ts",
84
+ kind: FileChangeKind.ADD,
85
+ linesAdded: 4,
86
+ linesRemoved: 0,
87
+ }),
88
+ create(FileChangeProgressEntrySchema, {
89
+ pathBefore: "b.ts",
90
+ pathAfter: "b.ts",
91
+ kind: FileChangeKind.MODIFY,
92
+ linesAdded: 0,
93
+ linesRemoved: 1,
94
+ }),
95
+ ],
96
+ capturedAt: "2026-07-05T00:00:00Z",
97
+ });
98
+ }
99
+
73
100
  const stubConv = {
74
101
  session: { spec: {} },
75
102
  isLoading: false,
@@ -92,6 +119,7 @@ const stubConv = {
92
119
  submitApproval: vi.fn(),
93
120
  submittingApprovalIds: new Set<string>(),
94
121
  fileChangeSets: [] as ReturnType<typeof pendingChangeSet>[],
122
+ fileChangeProgress: undefined as ReturnType<typeof progressSnapshot> | undefined,
95
123
  submitFileDecision: vi.fn(),
96
124
  submittingFileDecisionKeys: new Set<string>(),
97
125
  fileDecisionErrors: new Map<string, Error>(),
@@ -144,6 +172,7 @@ import { SessionViewer } from "../SessionViewer";
144
172
  beforeEach(() => {
145
173
  threadProps.length = 0;
146
174
  stubConv.fileChangeSets = [];
175
+ stubConv.fileChangeProgress = undefined;
147
176
  stubConv.submitFileDecision = vi.fn();
148
177
  });
149
178
 
@@ -189,6 +218,34 @@ describe("SessionViewer — file-review dock wiring", () => {
189
218
  );
190
219
  });
191
220
 
221
+ it("renders no progress bar when no turn is capturing", () => {
222
+ render(<SessionViewer sessionId="ses_1" org="acme" />);
223
+ expect(
224
+ document.querySelector('[data-cursor-target="file-change-progress-bar"]'),
225
+ ).toBeNull();
226
+ });
227
+
228
+ it("shows the mid-run progress strip above the composer while a turn is capturing", () => {
229
+ // Progress is present (CAPTURING) and the dock is empty — the two are
230
+ // mutually exclusive per turn, so the strip stands in for the dock.
231
+ stubConv.fileChangeProgress = progressSnapshot();
232
+ render(<SessionViewer sessionId="ses_1" org="acme" />);
233
+
234
+ const bar = document.querySelector('[data-cursor-target="file-change-progress-bar"]');
235
+ expect(bar).toBeTruthy();
236
+ expect(screen.getByText(/2 files changing/)).toBeTruthy();
237
+ expect(
238
+ document.querySelector('[data-cursor-target="file-review-dock"]'),
239
+ ).toBeNull();
240
+
241
+ // Pinned in the fixed strip, ordered before the composer input.
242
+ const composer = screen.getByTestId("composer-probe");
243
+ expect(bar!.parentElement).toBe(composer.parentElement);
244
+ expect(
245
+ bar!.compareDocumentPosition(composer) & Node.DOCUMENT_POSITION_FOLLOWING,
246
+ ).toBeTruthy();
247
+ });
248
+
192
249
  it("gives the thread records-only file-review props (no decision callbacks)", () => {
193
250
  render(<SessionViewer sessionId="ses_1" org="acme" />);
194
251
 
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { renderHook, cleanup } from "@testing-library/react";
3
+ import { create } from "@bufbuild/protobuf";
4
+ import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
5
+ import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
6
+ import {
7
+ ExecutionArtifactKind,
8
+ ExecutionPhase,
9
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
10
+ import { useSessionArtifacts, artifactKey } from "../useSessionArtifacts";
11
+
12
+ function artifact(opts: { name: string; sandboxPath?: string }) {
13
+ return create(ExecutionArtifactSchema, {
14
+ name: opts.name,
15
+ kind: ExecutionArtifactKind.FILE,
16
+ sizeBytes: 8n,
17
+ ...(opts.sandboxPath ? { sandboxPath: opts.sandboxPath } : {}),
18
+ storageKey: `artifacts/aex/${opts.name}`,
19
+ });
20
+ }
21
+
22
+ afterEach(cleanup);
23
+
24
+ describe("artifactKey", () => {
25
+ it("uses sandbox_path when present (its filesystem identity)", () => {
26
+ expect(artifactKey(artifact({ name: "a.md", sandboxPath: "/w/dir/a.md" }))).toBe(
27
+ "/w/dir/a.md",
28
+ );
29
+ });
30
+
31
+ it("falls back to name for artifacts without a sandbox path", () => {
32
+ expect(artifactKey(artifact({ name: "a.md" }))).toBe("a.md");
33
+ });
34
+
35
+ it("is the key useSessionArtifacts dedups on (same key → latest wins)", () => {
36
+ const older = create(AgentExecutionSchema, {
37
+ metadata: { id: "aex_1" },
38
+ status: {
39
+ phase: ExecutionPhase.EXECUTION_COMPLETED,
40
+ artifacts: [artifact({ name: "a.md", sandboxPath: "/w/a.md" })],
41
+ },
42
+ });
43
+ const newer = create(AgentExecutionSchema, {
44
+ metadata: { id: "aex_2" },
45
+ status: {
46
+ phase: ExecutionPhase.EXECUTION_COMPLETED,
47
+ artifacts: [artifact({ name: "a.md", sandboxPath: "/w/a.md" })],
48
+ },
49
+ });
50
+
51
+ const { result } = renderHook(() => useSessionArtifacts([older, newer]));
52
+ expect(result.current.artifacts).toHaveLength(1);
53
+ // The later execution's version wins for a shared key.
54
+ expect(result.current.artifacts[0].executionId).toBe("aex_2");
55
+ expect(artifactKey(result.current.artifacts[0].artifact)).toBe("/w/a.md");
56
+ });
57
+ });