@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
@@ -7,6 +7,7 @@ import {
7
7
  AgentExecutionStatusSchema,
8
8
  type AgentExecution,
9
9
  } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
10
+ import { AgentExecutionSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
10
11
  import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
11
12
  import { SessionSchema, type Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
12
13
  import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
@@ -25,7 +26,11 @@ function makeSession(id: string): Session {
25
26
  return session;
26
27
  }
27
28
 
28
- function makeExecution(id: string, phase: ExecutionPhase): AgentExecution {
29
+ function makeExecution(
30
+ id: string,
31
+ phase: ExecutionPhase,
32
+ opts?: { supersedes?: string },
33
+ ): AgentExecution {
29
34
  const exec = create(AgentExecutionSchema);
30
35
  const metadata = create(ApiResourceMetadataSchema);
31
36
  metadata.id = id;
@@ -33,6 +38,11 @@ function makeExecution(id: string, phase: ExecutionPhase): AgentExecution {
33
38
  const status = create(AgentExecutionStatusSchema);
34
39
  status.phase = phase;
35
40
  exec.status = status;
41
+ if (opts?.supersedes) {
42
+ const spec = create(AgentExecutionSpecSchema);
43
+ spec.supersedesExecutionId = opts.supersedes;
44
+ exec.spec = spec;
45
+ }
36
46
  return exec;
37
47
  }
38
48
 
@@ -312,6 +322,50 @@ describe("useSessionConversation", () => {
312
322
  });
313
323
  });
314
324
 
325
+ it("sets NO optimistic pendingUserMessage for a buildFromPlan send (the thread hides the turn)", async () => {
326
+ methods.listBySession.mockResolvedValue({ entries: [] });
327
+ const newExec = makeExecution("new-exec", ExecutionPhase.EXECUTION_PENDING);
328
+ newExec.metadata!.id = "new-exec";
329
+ methods.executionCreate.mockResolvedValue(newExec);
330
+
331
+ const { result } = renderHook(
332
+ () => useSessionConversation("session-1", "org"),
333
+ { wrapper: createWrapper(mockStigmer) },
334
+ );
335
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
336
+
337
+ await act(async () => {
338
+ await result.current.sendFollowUp("Build from plan", {
339
+ buildFromPlan: true,
340
+ });
341
+ });
342
+
343
+ expect(result.current.pendingUserMessage).toBeNull();
344
+ expect(methods.executionCreate).toHaveBeenCalledWith(
345
+ expect.objectContaining({ message: "Build from plan" }),
346
+ );
347
+ });
348
+
349
+ it("sets pendingUserMessage when a buildFromPlan send FAILS (failure must be visible)", async () => {
350
+ methods.listBySession.mockResolvedValue({ entries: [] });
351
+ methods.executionCreate.mockRejectedValue(new Error("create boom"));
352
+
353
+ const { result } = renderHook(
354
+ () => useSessionConversation("session-1", "org"),
355
+ { wrapper: createWrapper(mockStigmer) },
356
+ );
357
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
358
+
359
+ await act(async () => {
360
+ await result.current.sendFollowUp("Build from plan", {
361
+ buildFromPlan: true,
362
+ });
363
+ });
364
+
365
+ expect(result.current.sendError?.message).toBe("create boom");
366
+ expect(result.current.pendingUserMessage).toBe("Build from plan");
367
+ });
368
+
315
369
  it("preserves the message and surfaces sendError when the send fails", async () => {
316
370
  methods.listBySession.mockResolvedValue({ entries: [] });
317
371
  methods.executionCreate.mockRejectedValue(new Error("create boom"));
@@ -505,6 +559,167 @@ describe("useSessionConversation", () => {
505
559
  });
506
560
  });
507
561
 
562
+ describe("useSessionConversation — supersede filtering (edit-and-resubmit)", () => {
563
+ let methods: MockMethods;
564
+ let mockStigmer: Stigmer;
565
+ let stream: ReturnType<typeof createControllableStream<AgentExecution>>;
566
+
567
+ beforeEach(() => {
568
+ stream = createControllableStream<AgentExecution>();
569
+ methods = {
570
+ sessionGet: vi.fn().mockResolvedValue(makeSession("session-1")),
571
+ listBySession: vi.fn().mockResolvedValue({ entries: [] }),
572
+ executionCreate: vi.fn(),
573
+ subscribe: vi.fn().mockReturnValue(stream.generator),
574
+ submitApproval: vi.fn().mockResolvedValue({}),
575
+ };
576
+ mockStigmer = createMockStigmer(methods);
577
+ });
578
+
579
+ it("hides a superseded execution from completedExecutions", async () => {
580
+ const cancelled = makeExecution("e1", ExecutionPhase.EXECUTION_CANCELLED);
581
+ const successor = makeExecution("e2", ExecutionPhase.EXECUTION_COMPLETED, {
582
+ supersedes: "e1",
583
+ });
584
+ methods.listBySession.mockResolvedValue({
585
+ entries: [cancelled, successor],
586
+ });
587
+
588
+ const { result } = renderHook(
589
+ () => useSessionConversation("session-1", "org"),
590
+ { wrapper: createWrapper(mockStigmer) },
591
+ );
592
+
593
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
594
+ expect(result.current.completedExecutions).toHaveLength(1);
595
+ expect(result.current.completedExecutions[0]).toBe(successor);
596
+ });
597
+
598
+ it("hides every link of a chained supersede (A <- B <- C)", async () => {
599
+ const a = makeExecution("e1", ExecutionPhase.EXECUTION_CANCELLED);
600
+ const b = makeExecution("e2", ExecutionPhase.EXECUTION_CANCELLED, {
601
+ supersedes: "e1",
602
+ });
603
+ const c = makeExecution("e3", ExecutionPhase.EXECUTION_COMPLETED, {
604
+ supersedes: "e2",
605
+ });
606
+ methods.listBySession.mockResolvedValue({ entries: [a, b, c] });
607
+
608
+ const { result } = renderHook(
609
+ () => useSessionConversation("session-1", "org"),
610
+ { wrapper: createWrapper(mockStigmer) },
611
+ );
612
+
613
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
614
+ expect(result.current.completedExecutions).toHaveLength(1);
615
+ expect(result.current.completedExecutions[0]).toBe(c);
616
+ });
617
+
618
+ it("filters via the streaming successor's link before the list refetch delivers it", async () => {
619
+ // The stopped turn is still the only listed execution; its successor
620
+ // exists only as the live stream — the supersede link must be read off
621
+ // the stream copy for the old turn to disappear immediately.
622
+ const stopped = makeExecution("e1", ExecutionPhase.EXECUTION_IN_PROGRESS);
623
+ methods.listBySession.mockResolvedValue({ entries: [stopped] });
624
+
625
+ const successorStream = createControllableStream<AgentExecution>();
626
+ methods.subscribe.mockReturnValue(successorStream.generator);
627
+ methods.executionCreate.mockResolvedValue(
628
+ makeExecution("e2", ExecutionPhase.EXECUTION_PENDING),
629
+ );
630
+
631
+ const { result } = renderHook(
632
+ () => useSessionConversation("session-1", "org"),
633
+ { wrapper: createWrapper(mockStigmer) },
634
+ );
635
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
636
+
637
+ await act(async () => {
638
+ await result.current.sendFollowUp("corrected message", {
639
+ supersedesExecutionId: "e1",
640
+ });
641
+ });
642
+
643
+ act(() => {
644
+ successorStream.push(
645
+ makeExecution("e2", ExecutionPhase.EXECUTION_IN_PROGRESS, {
646
+ supersedes: "e1",
647
+ }),
648
+ );
649
+ });
650
+
651
+ await waitFor(() => {
652
+ expect(result.current.completedExecutions).toHaveLength(0);
653
+ });
654
+ });
655
+
656
+ it("treats a dangling supersede link as a no-op", async () => {
657
+ const completed = makeExecution("e1", ExecutionPhase.EXECUTION_COMPLETED);
658
+ const successor = makeExecution("e2", ExecutionPhase.EXECUTION_COMPLETED, {
659
+ supersedes: "does-not-exist",
660
+ });
661
+ methods.listBySession.mockResolvedValue({
662
+ entries: [completed, successor],
663
+ });
664
+
665
+ const { result } = renderHook(
666
+ () => useSessionConversation("session-1", "org"),
667
+ { wrapper: createWrapper(mockStigmer) },
668
+ );
669
+
670
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
671
+ expect(result.current.completedExecutions).toHaveLength(2);
672
+ });
673
+
674
+ it("sendFollowUp forwards supersedesExecutionId to execution creation", async () => {
675
+ methods.listBySession.mockResolvedValue({ entries: [] });
676
+ methods.executionCreate.mockResolvedValue(
677
+ makeExecution("e2", ExecutionPhase.EXECUTION_PENDING),
678
+ );
679
+
680
+ const { result } = renderHook(
681
+ () => useSessionConversation("session-1", "org"),
682
+ { wrapper: createWrapper(mockStigmer) },
683
+ );
684
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
685
+
686
+ await act(async () => {
687
+ await result.current.sendFollowUp("corrected message", {
688
+ supersedesExecutionId: "e1",
689
+ });
690
+ });
691
+
692
+ expect(methods.executionCreate).toHaveBeenCalledWith(
693
+ expect.objectContaining({
694
+ message: "corrected message",
695
+ supersedesExecutionId: "e1",
696
+ }),
697
+ );
698
+ });
699
+
700
+ it("keeps active-id resolution on the raw list when the superseded turn is still winding down", async () => {
701
+ // e1 was cancelled but has not reached a terminal phase yet; e2 (its
702
+ // successor) is the active turn. The display filter hides e1, but the
703
+ // active execution must still resolve to e2 from the UNfiltered list.
704
+ const windingDown = makeExecution("e1", ExecutionPhase.EXECUTION_IN_PROGRESS);
705
+ const successor = makeExecution("e2", ExecutionPhase.EXECUTION_IN_PROGRESS, {
706
+ supersedes: "e1",
707
+ });
708
+ methods.listBySession.mockResolvedValue({
709
+ entries: [windingDown, successor],
710
+ });
711
+
712
+ const { result } = renderHook(
713
+ () => useSessionConversation("session-1", "org"),
714
+ { wrapper: createWrapper(mockStigmer) },
715
+ );
716
+
717
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
718
+ expect(result.current.activeStreamExecution?.metadata?.id).toBe("e2");
719
+ expect(result.current.completedExecutions).toHaveLength(0);
720
+ });
721
+ });
722
+
508
723
  describe("useSessionConversation — local runner worker lifecycle", () => {
509
724
  let methods: MockMethods;
510
725
  let mockStigmer: Stigmer;
@@ -1,8 +1,15 @@
1
1
  import { describe, it, expect, afterEach } from "vitest";
2
2
  import { renderHook, act, cleanup } from "@testing-library/react";
3
- import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
3
+ import { create } from "@bufbuild/protobuf";
4
+ import {
5
+ ExecutionArtifactKind,
6
+ ExecutionPhase,
7
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
8
+ import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
4
9
  import { useSessionPanel, type UseSessionPanelOptions } from "../useSessionPanel";
5
10
  import { PLAN_DOCUMENT_ENTRY_ID, PLAN_DOCUMENT_PATH } from "../plan-document";
11
+ import { ARTIFACT_DOCUMENT_ENTRY_ID } from "../artifact-document";
12
+ import { artifactKey } from "../useSessionArtifacts";
6
13
  import type { SelectedThreadItem } from "../../internal/store/selection-store";
7
14
 
8
15
  // ---------------------------------------------------------------------------
@@ -255,6 +262,68 @@ describe("useSessionPanel — plan document tab", () => {
255
262
  });
256
263
  });
257
264
 
265
+ describe("useSessionPanel — artifact document tabs", () => {
266
+ function artifact(sandboxPath: string, name: string) {
267
+ return create(ExecutionArtifactSchema, {
268
+ name,
269
+ kind: ExecutionArtifactKind.FILE,
270
+ sandboxPath,
271
+ storageKey: `artifacts/aex_1/${name}`,
272
+ });
273
+ }
274
+
275
+ it("openArtifact opens the panel with a PREVIEW tab keyed by artifactKey", () => {
276
+ const { result } = renderPanel();
277
+ const a = artifact(".stigmer/notes.md", "notes.md");
278
+ act(() => result.current.openArtifact(a));
279
+
280
+ expect(result.current.isOpen).toBe(true);
281
+ expect(result.current.editorsStore.getSnapshot().editors).toEqual([
282
+ { entryId: ARTIFACT_DOCUMENT_ENTRY_ID, path: artifactKey(a), preview: true },
283
+ ]);
284
+ });
285
+
286
+ it("reuses the single preview slot as different artifacts are browsed", () => {
287
+ const { result } = renderPanel();
288
+ act(() => result.current.openArtifact(artifact(".stigmer/a.md", "a.md")));
289
+ act(() => result.current.openArtifact(artifact(".stigmer/b.md", "b.md")));
290
+
291
+ // VS Code preview semantics: single-click browsing reuses one tab.
292
+ expect(result.current.editorsStore.getSnapshot().editors).toEqual([
293
+ { entryId: ARTIFACT_DOCUMENT_ENTRY_ID, path: ".stigmer/b.md", preview: true },
294
+ ]);
295
+ });
296
+
297
+ it("does not evict a pinned plan tab when an artifact opens", () => {
298
+ const { result } = renderPanel();
299
+ act(() => result.current.openPlanDocument());
300
+ act(() => result.current.openArtifact(artifact(".stigmer/a.md", "a.md")));
301
+
302
+ expect(result.current.editorsStore.getSnapshot().editors).toEqual([
303
+ { entryId: PLAN_DOCUMENT_ENTRY_ID, path: PLAN_DOCUMENT_PATH, preview: false },
304
+ { entryId: ARTIFACT_DOCUMENT_ENTRY_ID, path: ".stigmer/a.md", preview: true },
305
+ ]);
306
+ });
307
+
308
+ it("pinArtifact promotes an open artifact preview to a persistent tab", () => {
309
+ const { result } = renderPanel();
310
+ const a = artifact(".stigmer/notes.md", "notes.md");
311
+ // The single-click open leaves a preview tab; the double-click pins it.
312
+ act(() => result.current.openArtifact(a));
313
+ act(() => result.current.pinArtifact(a));
314
+
315
+ expect(result.current.editorsStore.getSnapshot().editors).toEqual([
316
+ { entryId: ARTIFACT_DOCUMENT_ENTRY_ID, path: artifactKey(a), preview: false },
317
+ ]);
318
+ });
319
+
320
+ it("pinArtifact is a no-op when the artifact is not open (mirrors the store's generic pin)", () => {
321
+ const { result } = renderPanel();
322
+ act(() => result.current.pinArtifact(artifact(".stigmer/x.md", "x.md")));
323
+ expect(result.current.editorsStore.getSnapshot().editors).toEqual([]);
324
+ });
325
+ });
326
+
258
327
  describe("useSessionPanel — defaultView (home view)", () => {
259
328
  it("seeds the view to the provided defaultView (launcher homes on Config)", () => {
260
329
  const { result } = renderPanel({ defaultView: "configure" });
@@ -0,0 +1,23 @@
1
+ // Identity of the session's artifact document tabs in the workspace surface's
2
+ // editor group.
3
+ //
4
+ // Kept in its own module (not in ArtifactDocument.tsx) so behavior-only
5
+ // consumers — useSessionPanel opens an artifact tab, SessionViewer routes to it
6
+ // — depend on the identity without pulling the component (and its content
7
+ // renderer) into their module graph (DD-003 headless-first). Mirrors
8
+ // plan-document.ts.
9
+
10
+ import { virtualEntryId } from "../internal/store/index.js";
11
+
12
+ /**
13
+ * Virtual entry id of the artifact document FAMILY. Unlike the plan (one tab
14
+ * per session), artifacts are many: every opened artifact shares this one
15
+ * entry id and is distinguished by its {@link artifactKey}-derived `path`, so
16
+ * `editorKey(ARTIFACT_DOCUMENT_ENTRY_ID, path)` yields one tab per artifact —
17
+ * the VS Code "each file is a tab" model. The NUL-namespaced virtual id can
18
+ * never alias a real workspace entry, and its distinct `"artifact"` kind can
19
+ * never collide with the plan tab's `"plan"` kind.
20
+ *
21
+ * @see artifactKey — the `path` component (an artifact's stable identity)
22
+ */
23
+ export const ARTIFACT_DOCUMENT_ENTRY_ID = virtualEntryId("artifact");
@@ -4,10 +4,11 @@ import { useCallback, useState } from "react";
4
4
  import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
5
5
  import {
6
6
  useSessionArtifacts,
7
+ artifactKey,
7
8
  type SessionArtifactEntry,
8
9
  } from "../useSessionArtifacts.js";
9
- import { ArtifactCard } from "../../execution/ArtifactCard.js";
10
10
  import { ArtifactPreviewModal } from "../../execution/ArtifactPreviewModal.js";
11
+ import { ArtifactRow } from "../../execution/ArtifactRow.js";
11
12
  import { isPlanArtifact } from "../../library/detect-plan-artifact.js";
12
13
  import type { ApplyResourceResult } from "../../library/useApplyResource.js";
13
14
 
@@ -29,14 +30,39 @@ export interface ArtifactsTabProps {
29
30
  * every artifact.
30
31
  */
31
32
  readonly onOpenPlan?: (executionId: string) => void;
33
+ /**
34
+ * Open an artifact as an editor-pane document (the session panel's
35
+ * artifact-family virtual-document tab — the VS Code "each file is a tab"
36
+ * model). When provided, clicking a non-plan artifact routes here instead of
37
+ * the preview modal. When omitted (panel-less hosts) the modal remains the
38
+ * open mechanism. Plan artifacts always defer to {@link onOpenPlan} first.
39
+ */
40
+ readonly onOpenArtifact?: (entry: SessionArtifactEntry) => void;
41
+ /**
42
+ * Pin a non-plan artifact's document tab — the double-click half of the
43
+ * open/activate split (mirrors the file tree's `onOpenFile`/`onActivateFile`).
44
+ * When provided, each row binds a double-click that promotes the preview the
45
+ * single click opened. Omit in panel-less/modal hosts: with no handler the
46
+ * row binds no double-click, so a double-click is just two harmless single
47
+ * clicks. Plans always route to {@link onOpenPlan} (their tab is already
48
+ * pinned), so double-click == single-click for a plan.
49
+ */
50
+ readonly onActivateArtifact?: (entry: SessionArtifactEntry) => void;
32
51
  }
33
52
 
34
53
  /**
35
54
  * Artifacts facet for the session panel (a `useSessionRailViews` rail view).
36
55
  *
37
- * Wraps `useSessionArtifacts` `ArtifactCard` list with
38
- * `ArtifactPreviewModal` the same content as `ArtifactsWidget`
39
- * without the section heading.
56
+ * A VS Code-style dense file list of the session's outputs: one compact
57
+ * {@link ArtifactRow} per artifact (shared file-type icon + name + size, with a
58
+ * hover/focus Download). Clicking a row opens the artifact — a `plan.md` in the
59
+ * plan document tab ({@link onOpenPlan}), any other artifact in an editor-pane
60
+ * document ({@link onOpenArtifact}), or, for panel-less hosts that inject
61
+ * neither, the {@link ArtifactPreviewModal} popup fallback.
62
+ *
63
+ * Deliberately metadata-only: rows do NOT fetch content for resource detection
64
+ * (that would be one request per artifact on open) — detection and the
65
+ * Apply/Push action live in the opened document/modal instead.
40
66
  */
41
67
  export function ArtifactsTab({
42
68
  executions,
@@ -44,20 +70,41 @@ export function ArtifactsTab({
44
70
  onApplied,
45
71
  onImplementPlan,
46
72
  onOpenPlan,
73
+ onOpenArtifact,
74
+ onActivateArtifact,
47
75
  }: ArtifactsTabProps) {
48
76
  const { artifacts, hasArtifacts } = useSessionArtifacts(executions);
49
77
  const [previewEntry, setPreviewEntry] = useState<SessionArtifactEntry | null>(null);
50
78
 
51
- const handlePreview = useCallback(
79
+ const handleOpen = useCallback(
52
80
  (entry: SessionArtifactEntry) => {
53
- // The plan opens as a document (the panel's plan tab), never a popup.
81
+ // A plan is a first-class document — it opens in the dedicated plan tab,
82
+ // never as a generic artifact or a popup.
54
83
  if (onOpenPlan && isPlanArtifact(entry.artifact)) {
55
84
  onOpenPlan(entry.executionId);
56
85
  return;
57
86
  }
87
+ // Editor-pane document when the host injected it; otherwise the modal.
88
+ if (onOpenArtifact) {
89
+ onOpenArtifact(entry);
90
+ return;
91
+ }
58
92
  setPreviewEntry(entry);
59
93
  },
60
- [onOpenPlan],
94
+ [onOpenPlan, onOpenArtifact],
95
+ );
96
+
97
+ const handleActivate = useCallback(
98
+ (entry: SessionArtifactEntry) => {
99
+ // A plan's tab is always pinned, so double-click matches single-click:
100
+ // route to the plan tab, never pin a generic artifact.
101
+ if (onOpenPlan && isPlanArtifact(entry.artifact)) {
102
+ onOpenPlan(entry.executionId);
103
+ return;
104
+ }
105
+ onActivateArtifact?.(entry);
106
+ },
107
+ [onOpenPlan, onActivateArtifact],
61
108
  );
62
109
 
63
110
  const handleClosePreview = useCallback(() => {
@@ -76,19 +123,20 @@ export function ArtifactsTab({
76
123
 
77
124
  return (
78
125
  <>
79
- <div role="list" className="space-y-2">
126
+ <ul role="list" className="flex flex-col">
80
127
  {artifacts.map((entry) => (
81
- <div key={entry.artifact.sandboxPath || entry.artifact.name} role="listitem">
82
- <ArtifactCard
83
- artifact={entry.artifact}
84
- executionId={entry.executionId}
85
- org={org}
86
- hasNameCollision={entry.hasNameCollision}
87
- onPreview={() => handlePreview(entry)}
88
- />
89
- </div>
128
+ <ArtifactRow
129
+ key={artifactKey(entry.artifact)}
130
+ artifact={entry.artifact}
131
+ executionId={entry.executionId}
132
+ hasNameCollision={entry.hasNameCollision}
133
+ onOpen={() => handleOpen(entry)}
134
+ onActivate={
135
+ onActivateArtifact ? () => handleActivate(entry) : undefined
136
+ }
137
+ />
90
138
  ))}
91
- </div>
139
+ </ul>
92
140
 
93
141
  {previewEntry && (
94
142
  <ArtifactPreviewModal