@stigmer/react 3.1.3 → 3.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/composer/SessionComposer.d.ts +31 -0
  2. package/composer/SessionComposer.d.ts.map +1 -1
  3. package/composer/SessionComposer.js +12 -4
  4. package/composer/SessionComposer.js.map +1 -1
  5. package/execution/ArtifactApplyButton.d.ts +25 -0
  6. package/execution/ArtifactApplyButton.d.ts.map +1 -0
  7. package/execution/ArtifactApplyButton.js +21 -0
  8. package/execution/ArtifactApplyButton.js.map +1 -0
  9. package/execution/ArtifactContentBody.d.ts +36 -0
  10. package/execution/ArtifactContentBody.d.ts.map +1 -0
  11. package/execution/ArtifactContentBody.js +62 -0
  12. package/execution/ArtifactContentBody.js.map +1 -0
  13. package/execution/ArtifactDocument.d.ts +48 -0
  14. package/execution/ArtifactDocument.d.ts.map +1 -0
  15. package/execution/ArtifactDocument.js +89 -0
  16. package/execution/ArtifactDocument.js.map +1 -0
  17. package/execution/ArtifactPreviewModal.d.ts +4 -4
  18. package/execution/ArtifactPreviewModal.d.ts.map +1 -1
  19. package/execution/ArtifactPreviewModal.js +15 -142
  20. package/execution/ArtifactPreviewModal.js.map +1 -1
  21. package/execution/ArtifactRow.d.ts +49 -0
  22. package/execution/ArtifactRow.d.ts.map +1 -0
  23. package/execution/ArtifactRow.js +56 -0
  24. package/execution/ArtifactRow.js.map +1 -0
  25. package/execution/ArtifactsWidget.d.ts +6 -9
  26. package/execution/ArtifactsWidget.d.ts.map +1 -1
  27. package/execution/ArtifactsWidget.js +15 -19
  28. package/execution/ArtifactsWidget.js.map +1 -1
  29. package/execution/FileReviewContext.d.ts +15 -1
  30. package/execution/FileReviewContext.d.ts.map +1 -1
  31. package/execution/FileReviewContext.js +20 -1
  32. package/execution/FileReviewContext.js.map +1 -1
  33. package/execution/MessageThread.d.ts +65 -2
  34. package/execution/MessageThread.d.ts.map +1 -1
  35. package/execution/MessageThread.js +42 -23
  36. package/execution/MessageThread.js.map +1 -1
  37. package/execution/TodoCard.d.ts +8 -0
  38. package/execution/TodoCard.d.ts.map +1 -1
  39. package/execution/TodoCard.js +6 -4
  40. package/execution/TodoCard.js.map +1 -1
  41. package/execution/TodoList.d.ts +20 -1
  42. package/execution/TodoList.d.ts.map +1 -1
  43. package/execution/TodoList.js +9 -6
  44. package/execution/TodoList.js.map +1 -1
  45. package/execution/ToolCallDetail.d.ts +3 -2
  46. package/execution/ToolCallDetail.d.ts.map +1 -1
  47. package/execution/ToolCallDetail.js +49 -16
  48. package/execution/ToolCallDetail.js.map +1 -1
  49. package/execution/file-review-status.d.ts +15 -0
  50. package/execution/file-review-status.d.ts.map +1 -1
  51. package/execution/file-review-status.js +22 -0
  52. package/execution/file-review-status.js.map +1 -1
  53. package/execution/index.d.ts +13 -7
  54. package/execution/index.d.ts.map +1 -1
  55. package/execution/index.js +7 -4
  56. package/execution/index.js.map +1 -1
  57. package/execution/useArtifactContent.d.ts +6 -1
  58. package/execution/useArtifactContent.d.ts.map +1 -1
  59. package/execution/useArtifactContent.js +7 -2
  60. package/execution/useArtifactContent.js.map +1 -1
  61. package/execution/useArtifactInspection.d.ts +85 -0
  62. package/execution/useArtifactInspection.d.ts.map +1 -0
  63. package/execution/useArtifactInspection.js +143 -0
  64. package/execution/useArtifactInspection.js.map +1 -0
  65. package/execution/useCreateAgentExecution.d.ts +11 -0
  66. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  67. package/execution/useCreateAgentExecution.js +1 -0
  68. package/execution/useCreateAgentExecution.js.map +1 -1
  69. package/github/useGitHubFileReader.d.ts +4 -2
  70. package/github/useGitHubFileReader.d.ts.map +1 -1
  71. package/github/useGitHubFileReader.js +16 -3
  72. package/github/useGitHubFileReader.js.map +1 -1
  73. package/github/useGitHubTreeLister.d.ts +2 -1
  74. package/github/useGitHubTreeLister.d.ts.map +1 -1
  75. package/github/useGitHubTreeLister.js +7 -3
  76. package/github/useGitHubTreeLister.js.map +1 -1
  77. package/index.d.ts +4 -4
  78. package/index.d.ts.map +1 -1
  79. package/index.js +3 -3
  80. package/index.js.map +1 -1
  81. package/internal/MermaidDiagram.d.ts +47 -0
  82. package/internal/MermaidDiagram.d.ts.map +1 -0
  83. package/internal/MermaidDiagram.js +118 -0
  84. package/internal/MermaidDiagram.js.map +1 -0
  85. package/internal/VirtualizedThread.d.ts +3 -2
  86. package/internal/VirtualizedThread.d.ts.map +1 -1
  87. package/internal/VirtualizedThread.js +3 -2
  88. package/internal/VirtualizedThread.js.map +1 -1
  89. package/internal/markdown-components.d.ts.map +1 -1
  90. package/internal/markdown-components.js +34 -0
  91. package/internal/markdown-components.js.map +1 -1
  92. package/internal/mermaid-loader.d.ts +26 -0
  93. package/internal/mermaid-loader.d.ts.map +1 -0
  94. package/internal/mermaid-loader.js +34 -0
  95. package/internal/mermaid-loader.js.map +1 -0
  96. package/internal/mermaid-theme-css.d.ts +41 -0
  97. package/internal/mermaid-theme-css.d.ts.map +1 -0
  98. package/internal/mermaid-theme-css.js +179 -0
  99. package/internal/mermaid-theme-css.js.map +1 -0
  100. package/internal/slug.d.ts +9 -5
  101. package/internal/slug.d.ts.map +1 -1
  102. package/internal/slug.js +10 -6
  103. package/internal/slug.js.map +1 -1
  104. package/package.json +5 -4
  105. package/session/SessionViewer.d.ts +11 -1
  106. package/session/SessionViewer.d.ts.map +1 -1
  107. package/session/SessionViewer.js +101 -21
  108. package/session/SessionViewer.js.map +1 -1
  109. package/session/artifact-document.d.ts +13 -0
  110. package/session/artifact-document.d.ts.map +1 -0
  111. package/session/artifact-document.js +22 -0
  112. package/session/artifact-document.js.map +1 -0
  113. package/session/facets/ArtifactsTab.d.ts +30 -4
  114. package/session/facets/ArtifactsTab.d.ts.map +1 -1
  115. package/session/facets/ArtifactsTab.js +32 -10
  116. package/session/facets/ArtifactsTab.js.map +1 -1
  117. package/session/index.d.ts +3 -1
  118. package/session/index.d.ts.map +1 -1
  119. package/session/index.js +5 -1
  120. package/session/index.js.map +1 -1
  121. package/session/useSessionArtifacts.d.ts +17 -0
  122. package/session/useSessionArtifacts.d.ts.map +1 -1
  123. package/session/useSessionArtifacts.js +16 -5
  124. package/session/useSessionArtifacts.js.map +1 -1
  125. package/session/useSessionConversation.d.ts +17 -1
  126. package/session/useSessionConversation.d.ts.map +1 -1
  127. package/session/useSessionConversation.js +32 -4
  128. package/session/useSessionConversation.js.map +1 -1
  129. package/session/useSessionPageFlow.d.ts.map +1 -1
  130. package/session/useSessionPageFlow.js +1 -0
  131. package/session/useSessionPageFlow.js.map +1 -1
  132. package/session/useSessionPanel.d.ts +20 -0
  133. package/session/useSessionPanel.d.ts.map +1 -1
  134. package/session/useSessionPanel.js +13 -0
  135. package/session/useSessionPanel.js.map +1 -1
  136. package/session/useSessionRailViews.d.ts +13 -1
  137. package/session/useSessionRailViews.d.ts.map +1 -1
  138. package/session/useSessionRailViews.js +5 -3
  139. package/session/useSessionRailViews.js.map +1 -1
  140. package/session/useWorkspaceReadRefs.d.ts +45 -0
  141. package/session/useWorkspaceReadRefs.d.ts.map +1 -0
  142. package/session/useWorkspaceReadRefs.js +104 -0
  143. package/session/useWorkspaceReadRefs.js.map +1 -0
  144. package/src/composer/SessionComposer.tsx +69 -1
  145. package/src/composer/__tests__/SessionComposer-editing.test.tsx +128 -0
  146. package/src/execution/ArtifactApplyButton.tsx +79 -0
  147. package/src/execution/ArtifactContentBody.tsx +255 -0
  148. package/src/execution/ArtifactDocument.tsx +344 -0
  149. package/src/execution/ArtifactPreviewModal.tsx +39 -405
  150. package/src/execution/ArtifactRow.tsx +165 -0
  151. package/src/execution/ArtifactsWidget.tsx +24 -33
  152. package/src/execution/FileReviewContext.ts +27 -1
  153. package/src/execution/MessageThread.tsx +110 -22
  154. package/src/execution/TodoCard.tsx +20 -4
  155. package/src/execution/TodoList.tsx +26 -4
  156. package/src/execution/ToolCallDetail.tsx +69 -16
  157. package/src/execution/__tests__/ArtifactContentBody.test.tsx +131 -0
  158. package/src/execution/__tests__/ArtifactDocument.test.tsx +100 -0
  159. package/src/execution/__tests__/ArtifactRow.test.tsx +139 -0
  160. package/src/execution/__tests__/ArtifactsWidget.test.tsx +78 -0
  161. package/src/execution/__tests__/ToolCallDetail.test.tsx +188 -2
  162. package/src/execution/__tests__/file-review-status.test.ts +47 -0
  163. package/src/execution/__tests__/message-entry.test.tsx +50 -2
  164. package/src/execution/__tests__/message-thread-slots.test.tsx +463 -0
  165. package/src/execution/__tests__/useArtifactInspection.test.tsx +151 -0
  166. package/src/execution/__tests__/useCreateAgentExecution.test.tsx +35 -0
  167. package/src/execution/file-review-status.ts +24 -0
  168. package/src/execution/index.ts +27 -5
  169. package/src/execution/useArtifactContent.ts +7 -0
  170. package/src/execution/useArtifactInspection.ts +252 -0
  171. package/src/execution/useCreateAgentExecution.ts +12 -0
  172. package/src/github/__tests__/useGitHubFileReader.test.ts +35 -2
  173. package/src/github/__tests__/useGitHubTreeLister.test.ts +14 -0
  174. package/src/github/useGitHubFileReader.ts +19 -6
  175. package/src/github/useGitHubTreeLister.ts +7 -3
  176. package/src/index.ts +15 -2
  177. package/src/internal/MermaidDiagram.tsx +164 -0
  178. package/src/internal/VirtualizedThread.tsx +5 -1
  179. package/src/internal/__tests__/a11y/mermaid-theme.a11y.test.tsx +122 -0
  180. package/src/internal/__tests__/markdown-components.test.tsx +93 -1
  181. package/src/internal/__tests__/mermaid-diagram.test.tsx +282 -0
  182. package/src/internal/markdown-components.tsx +36 -1
  183. package/src/internal/mermaid-loader.ts +44 -0
  184. package/src/internal/mermaid-theme-css.ts +178 -0
  185. package/src/internal/slug.ts +10 -6
  186. package/src/session/SessionViewer.tsx +204 -35
  187. package/src/session/__tests__/SessionViewer-artifactTabs.test.tsx +210 -0
  188. package/src/session/__tests__/SessionViewer-editResubmit.test.tsx +233 -0
  189. package/src/session/__tests__/useSessionArtifacts.test.ts +57 -0
  190. package/src/session/__tests__/useSessionConversation.test.tsx +172 -1
  191. package/src/session/__tests__/useSessionPanel.test.ts +70 -1
  192. package/src/session/__tests__/useWorkspaceReadRefs.test.ts +245 -0
  193. package/src/session/artifact-document.ts +23 -0
  194. package/src/session/facets/ArtifactsTab.tsx +66 -18
  195. package/src/session/facets/__tests__/ArtifactsTab.test.tsx +111 -47
  196. package/src/session/index.ts +7 -1
  197. package/src/session/useSessionArtifacts.ts +16 -5
  198. package/src/session/useSessionConversation.ts +48 -6
  199. package/src/session/useSessionPageFlow.ts +1 -0
  200. package/src/session/useSessionPanel.ts +41 -0
  201. package/src/session/useSessionRailViews.tsx +21 -1
  202. package/src/session/useWorkspaceReadRefs.ts +118 -0
  203. package/src/workspace/FileViewer.tsx +144 -38
  204. package/src/workspace/WorkspaceFileReader.ts +21 -0
  205. package/src/workspace/__tests__/FileViewer.test.tsx +138 -3
  206. package/src/workspace/__tests__/useWorkspaceFileContent.test.tsx +20 -0
  207. package/src/workspace/__tests__/workspaceListingCache.test.ts +114 -0
  208. package/src/workspace/index.ts +4 -1
  209. package/src/workspace/useWorkspaceEntries.ts +11 -0
  210. package/src/workspace/useWorkspaceFileContent.ts +4 -1
  211. package/src/workspace/useWorkspaceFileSearch.ts +8 -3
  212. package/src/workspace/useWorkspaceFiles.ts +3 -2
  213. package/src/workspace/workspaceListingCache.ts +21 -7
  214. package/styles.css +1 -1
  215. package/workspace/FileViewer.d.ts +1 -1
  216. package/workspace/FileViewer.d.ts.map +1 -1
  217. package/workspace/FileViewer.js +55 -5
  218. package/workspace/FileViewer.js.map +1 -1
  219. package/workspace/WorkspaceFileReader.d.ts +17 -0
  220. package/workspace/WorkspaceFileReader.d.ts.map +1 -1
  221. package/workspace/WorkspaceFileReader.js +18 -0
  222. package/workspace/WorkspaceFileReader.js.map +1 -1
  223. package/workspace/index.d.ts +1 -1
  224. package/workspace/index.d.ts.map +1 -1
  225. package/workspace/index.js +1 -1
  226. package/workspace/index.js.map +1 -1
  227. package/workspace/useWorkspaceEntries.d.ts +11 -0
  228. package/workspace/useWorkspaceEntries.d.ts.map +1 -1
  229. package/workspace/useWorkspaceEntries.js.map +1 -1
  230. package/workspace/useWorkspaceFileContent.d.ts.map +1 -1
  231. package/workspace/useWorkspaceFileContent.js +4 -1
  232. package/workspace/useWorkspaceFileContent.js.map +1 -1
  233. package/workspace/useWorkspaceFileSearch.d.ts.map +1 -1
  234. package/workspace/useWorkspaceFileSearch.js +8 -3
  235. package/workspace/useWorkspaceFileSearch.js.map +1 -1
  236. package/workspace/useWorkspaceFiles.d.ts +2 -1
  237. package/workspace/useWorkspaceFiles.d.ts.map +1 -1
  238. package/workspace/useWorkspaceFiles.js +3 -2
  239. package/workspace/useWorkspaceFiles.js.map +1 -1
  240. package/workspace/workspaceListingCache.d.ts +1 -1
  241. package/workspace/workspaceListingCache.d.ts.map +1 -1
  242. package/workspace/workspaceListingCache.js +20 -7
  243. package/workspace/workspaceListingCache.js.map +1 -1
  244. package/execution/ArtifactCard.d.ts +0 -81
  245. package/execution/ArtifactCard.d.ts.map +0 -1
  246. package/execution/ArtifactCard.js +0 -130
  247. package/execution/ArtifactCard.js.map +0 -1
  248. package/src/execution/ArtifactCard.tsx +0 -320
@@ -17,14 +17,14 @@ import {
17
17
  WorkspaceSurface,
18
18
  type SurfaceVirtualDocument,
19
19
  } from "../workspace/WorkspaceSurface.js";
20
- import type { InteractionModeOption, SessionComposerHandle } from "../composer/index.js";
20
+ import type { InteractionModeOption, SessionComposerHandle, SessionComposerSubmitContext } from "../composer/index.js";
21
21
  import type { ApplyResourceResult } from "../library/useApplyResource.js";
22
22
  import { ResizableSplit } from "../internal/ResizableSplit.js";
23
23
  import { SelectionStore } from "../internal/store/selection-store.js";
24
24
  import { useWorkspaceEditors, isVirtualEntryId } from "../internal/store/index.js";
25
25
  import { ThreadSelectionContext } from "../execution/ThreadSelectionContext.js";
26
26
  import { useSelectedThreadItem } from "../execution/useThreadSelection.js";
27
- import { MessageThread } from "../execution/MessageThread.js";
27
+ import { MessageThread, type MessageThreadSlots } from "../execution/MessageThread.js";
28
28
  import { FileChangeProgressBar } from "../execution/FileChangeProgressBar.js";
29
29
  import { FileReviewDock } from "../execution/FileReviewDock.js";
30
30
  import {
@@ -47,11 +47,18 @@ import { usePlanDraft, planDraftKey, type PlanDraftController } from "./usePlanD
47
47
  import { PlanEditor } from "./PlanEditor.js";
48
48
  import { PlanStreamingDocument } from "./PlanStreamingDocument.js";
49
49
  import { PLAN_DOCUMENT_ENTRY_ID, PLAN_DOCUMENT_PATH } from "./plan-document.js";
50
+ import { ARTIFACT_DOCUMENT_ENTRY_ID } from "./artifact-document.js";
51
+ import { ArtifactDocument } from "../execution/ArtifactDocument.js";
50
52
  import { useSessionRailViews } from "./useSessionRailViews.js";
51
53
  import { useSessionPanel, type SessionPanelController } from "./useSessionPanel.js";
52
54
  import { SessionPanelChip } from "./SessionPanelChip.js";
53
55
  import { useSessionWriteBacks } from "./useSessionWriteBacks.js";
54
- import { useSessionArtifacts } from "./useSessionArtifacts.js";
56
+ import { useWorkspaceReadRefs } from "./useWorkspaceReadRefs.js";
57
+ import {
58
+ useSessionArtifacts,
59
+ artifactKey,
60
+ type SessionArtifactEntry,
61
+ } from "./useSessionArtifacts.js";
55
62
  import type { SetupTabProps } from "./facets/SetupTab.js";
56
63
  import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
57
64
  import type { RuntimeEnvProvider } from "./runtime-env.js";
@@ -222,6 +229,15 @@ export interface SessionViewerProps {
222
229
  readonly accessSlot?: ReactNode;
223
230
  /** Called after a resource is applied from the Artifacts tab. */
224
231
  readonly onApplied?: (result: ApplyResourceResult) => void;
232
+ /**
233
+ * Component overrides for the conversation thread's chrome (message
234
+ * bubbles, approval card, to-dos card, plan cards, thinking indicator) —
235
+ * forwarded to the inner {@link MessageThread}. See
236
+ * {@link MessageThreadSlots}. Define the object and its components at
237
+ * module level (or memoize) so thread rows keep their streaming
238
+ * re-render guarantees.
239
+ */
240
+ readonly threadSlots?: MessageThreadSlots;
225
241
  /** Additional CSS classes for the root container. */
226
242
  readonly className?: string;
227
243
  }
@@ -283,6 +299,7 @@ export function SessionViewer({
283
299
  headerActions,
284
300
  accessSlot,
285
301
  onApplied,
302
+ threadSlots,
286
303
  className,
287
304
  }: SessionViewerProps) {
288
305
  const flow = useSessionPageFlow({ sessionId, org, getRuntimeEnv });
@@ -455,6 +472,28 @@ export function SessionViewer({
455
472
  [sessionPlan, panel.openPlanDocument],
456
473
  );
457
474
 
475
+ // "Open artifact" (an Artifacts-facet row) → an editor-pane document tab.
476
+ // Writing the editors store re-renders only the panel subtree (which
477
+ // subscribes), never this streaming column. Plan artifacts are routed to the
478
+ // plan tab upstream (in ArtifactsTab), so they never reach here.
479
+ const handleOpenArtifact = useCallback(
480
+ (entry: SessionArtifactEntry) => {
481
+ panel.openArtifact(entry.artifact);
482
+ },
483
+ [panel.openArtifact],
484
+ );
485
+
486
+ // Double-click an artifact row → pin its tab (the leading single click already
487
+ // opened the preview via `handleOpenArtifact`). Mirrors the file tree's
488
+ // preview/pin split; plan artifacts are routed to the plan tab upstream (in
489
+ // ArtifactsTab) and never reach here.
490
+ const handleActivateArtifact = useCallback(
491
+ (entry: SessionArtifactEntry) => {
492
+ panel.pinArtifact(entry.artifact);
493
+ },
494
+ [panel.pinArtifact],
495
+ );
496
+
458
497
  // Open a transcript tool-call file path in the panel's read-only editor.
459
498
  // Resolves the (possibly absolute / subdir-prefixed) path to a
460
499
  // repo/root-relative selection the editor can fetch; on a definite hit it
@@ -544,6 +583,7 @@ export function SessionViewer({
544
583
  onDismissPlanAttachFailed={() => setPlanAttachFailed(false)}
545
584
  onFilePathClick={handleTranscriptFilePathClick}
546
585
  isEndUser={isEndUser}
586
+ threadSlots={threadSlots}
547
587
  />
548
588
  }
549
589
  secondary={
@@ -561,6 +601,8 @@ export function SessionViewer({
561
601
  planDraft={planDraft}
562
602
  openPlanExecutionId={openPlanExecutionId}
563
603
  onOpenPlan={handleOpenPlan}
604
+ onOpenArtifact={handleOpenArtifact}
605
+ onActivateArtifact={handleActivateArtifact}
564
606
  enableLocal={enableLocal}
565
607
  onBrowseLocalFolder={onBrowseLocalFolder}
566
608
  workspaceFileLister={workspaceFileLister}
@@ -607,6 +649,7 @@ interface ConversationColumnProps {
607
649
  */
608
650
  readonly onFilePathClick: (path: string) => boolean;
609
651
  readonly isEndUser: boolean;
652
+ readonly threadSlots?: MessageThreadSlots;
610
653
  }
611
654
 
612
655
  const ConversationColumn = memo(function ConversationColumn({
@@ -628,6 +671,7 @@ const ConversationColumn = memo(function ConversationColumn({
628
671
  onDismissPlanAttachFailed,
629
672
  onFilePathClick,
630
673
  isEndUser,
674
+ threadSlots,
631
675
  }: ConversationColumnProps) {
632
676
  const { conv } = flow;
633
677
  // Approval failures are NOT folded into this banner: they now surface in-card,
@@ -652,18 +696,55 @@ const ConversationColumn = memo(function ConversationColumn({
652
696
  void conv.stop();
653
697
  }, [conv.stop]);
654
698
 
655
- // Edit-and-resubmit: stop the in-flight turn and pre-fill the composer with
656
- // the original text. The append-only execution log can't be rewritten, so
657
- // the user reviews the prefilled message and resubmits it as a NEW execution
658
- // through the normal Send pipeline. The cancelled turn stays in history with
659
- // its phase badge an honest record rather than a silent edit.
699
+ // Edit-and-resubmit: stop the in-flight turn, pre-fill the composer with
700
+ // the original text, and remember which execution is being edited. The
701
+ // append-only execution log is never rewritten submitting while editing
702
+ // creates a NEW execution that carries `supersedesExecutionId`, and the
703
+ // conversation read model hides the superseded turn so the edited message
704
+ // replaces the original in place (Cursor-style, stigmer/stigmer#181).
705
+ //
706
+ // The editing state is explicit (banner + cancel in the composer) so the
707
+ // supersede link only attaches when the user actually resubmits the edit.
708
+ // Cancelling drops the link — a subsequent unrelated message appends
709
+ // normally without hiding any history.
710
+ const [editingExecutionId, setEditingExecutionId] = useState<string | null>(
711
+ null,
712
+ );
713
+ const activeExecutionId = conv.activeStreamExecution?.metadata?.id ?? null;
714
+
660
715
  const handleEditMessage = useCallback(
661
716
  (text: string) => {
662
717
  void conv.stop();
718
+ setEditingExecutionId(activeExecutionId);
663
719
  composerRef.current?.setMessage(text);
664
720
  composerRef.current?.focus();
665
721
  },
666
- [conv.stop, composerRef],
722
+ [conv.stop, activeExecutionId, composerRef],
723
+ );
724
+
725
+ const handleCancelEdit = useCallback(() => {
726
+ setEditingExecutionId(null);
727
+ composerRef.current?.setMessage("");
728
+ }, [composerRef]);
729
+
730
+ // Submit wrapper that attaches the supersede link while editing, then
731
+ // exits editing mode. Ordinary (non-editing) submits pass through as-is.
732
+ const handleComposerSubmit = useCallback(
733
+ (
734
+ message: string,
735
+ modelName?: string,
736
+ context?: SessionComposerSubmitContext,
737
+ ) => {
738
+ void flow.handleSubmit(
739
+ message,
740
+ modelName,
741
+ editingExecutionId
742
+ ? { ...context, supersedesExecutionId: editingExecutionId }
743
+ : context,
744
+ );
745
+ setEditingExecutionId(null);
746
+ },
747
+ [flow.handleSubmit, editingExecutionId],
667
748
  );
668
749
 
669
750
  // The composer-docked FileReviewDock sits OUTSIDE MessageThread, so the
@@ -703,6 +784,7 @@ const ConversationColumn = memo(function ConversationColumn({
703
784
  planActionsDisabled={!conv.canSendFollowUp || isBuildingFromPlan}
704
785
  planBuildPending={isBuildingFromPlan}
705
786
  contentColumn="center"
787
+ slots={threadSlots}
706
788
  className="flex-1"
707
789
  />
708
790
  <div className={CONVERSATION_COLUMN_CLASS}>
@@ -743,11 +825,13 @@ const ConversationColumn = memo(function ConversationColumn({
743
825
  </FilePathContext.Provider>
744
826
  <SessionComposer
745
827
  ref={composerRef}
746
- onSubmit={flow.handleSubmit}
828
+ onSubmit={handleComposerSubmit}
747
829
  isSubmitting={conv.isSending}
748
830
  disabled={!conv.canSendFollowUp}
749
831
  onStop={conv.isStoppable ? handleStop : undefined}
750
832
  isStopping={conv.isStopping}
833
+ isEditing={editingExecutionId != null}
834
+ onCancelEdit={handleCancelEdit}
751
835
  org={org}
752
836
  harness={flow.harness}
753
837
  defaultModelId={modelId}
@@ -815,6 +899,10 @@ interface SessionPanelRegionProps {
815
899
  readonly openPlanExecutionId: string | null;
816
900
  /** Opens a plan in the plan tab (routed to the Artifacts facet's plan.md). */
817
901
  readonly onOpenPlan: (executionId: string) => void;
902
+ /** Opens an artifact as an editor-pane document (routed to the Artifacts facet). */
903
+ readonly onOpenArtifact: (entry: SessionArtifactEntry) => void;
904
+ /** Pins an artifact's document tab — the double-click half of the open/pin split. */
905
+ readonly onActivateArtifact: (entry: SessionArtifactEntry) => void;
818
906
  readonly enableLocal: boolean;
819
907
  readonly onBrowseLocalFolder?: () => Promise<string | null>;
820
908
  readonly workspaceFileLister?: WorkspaceFileLister;
@@ -836,6 +924,8 @@ function SessionPanelRegion({
836
924
  planDraft,
837
925
  openPlanExecutionId,
838
926
  onOpenPlan,
927
+ onOpenArtifact,
928
+ onActivateArtifact,
839
929
  enableLocal,
840
930
  onBrowseLocalFolder,
841
931
  workspaceFileLister,
@@ -847,6 +937,9 @@ function SessionPanelRegion({
847
937
  const { editors, activeKey, activeFile, reveal } = useWorkspaceEditors(
848
938
  panel.editorsStore,
849
939
  );
940
+ // Session artifacts, deduped by identity — the source both the Artifacts
941
+ // rail-view badge and the open artifact document tabs resolve against.
942
+ const { artifacts: sessionArtifacts } = useSessionArtifacts(flow.allExecutions);
850
943
 
851
944
  // Honor a pending jump-to-line reveal only while it targets the active editor
852
945
  // (an EditorReveal is structurally a RevealTarget). Switching tabs changes
@@ -902,30 +995,28 @@ function SessionPanelRegion({
902
995
  // tab and its turn then ended without publishing, with no earlier plan to
903
996
  // fall back to.
904
997
  const openPlanIsLatest = openPlan?.executionId === sessionPlan?.executionId;
905
- const virtualDocuments = useMemo<readonly SurfaceVirtualDocument[]>(() => {
906
- return [
907
- {
908
- entryId: PLAN_DOCUMENT_ENTRY_ID,
909
- path: PLAN_DOCUMENT_PATH,
910
- content: streamingPlan ? (
911
- <PlanStreamingDocument
912
- key={`streaming-${streamingPlan.executionId}`}
913
- displayText={streamingPlan.displayText}
914
- />
915
- ) : openPlan ? (
916
- <PlanEditor
917
- key={planDraftKey(openPlan)}
918
- plan={openPlan}
919
- draft={openPlanIsLatest ? planDraft : undefined}
920
- onBuildFromPlan={openPlanIsLatest ? onImplementPlan : undefined}
921
- buildDisabled={implementPlanDisabled}
922
- readOnly={!openPlanIsLatest}
923
- />
924
- ) : (
925
- <PlanUnavailableNotice />
926
- ),
927
- },
928
- ];
998
+ const planVirtualDocument = useMemo<SurfaceVirtualDocument>(() => {
999
+ return {
1000
+ entryId: PLAN_DOCUMENT_ENTRY_ID,
1001
+ path: PLAN_DOCUMENT_PATH,
1002
+ content: streamingPlan ? (
1003
+ <PlanStreamingDocument
1004
+ key={`streaming-${streamingPlan.executionId}`}
1005
+ displayText={streamingPlan.displayText}
1006
+ />
1007
+ ) : openPlan ? (
1008
+ <PlanEditor
1009
+ key={planDraftKey(openPlan)}
1010
+ plan={openPlan}
1011
+ draft={openPlanIsLatest ? planDraft : undefined}
1012
+ onBuildFromPlan={openPlanIsLatest ? onImplementPlan : undefined}
1013
+ buildDisabled={implementPlanDisabled}
1014
+ readOnly={!openPlanIsLatest}
1015
+ />
1016
+ ) : (
1017
+ <PlanUnavailableNotice />
1018
+ ),
1019
+ };
929
1020
  }, [
930
1021
  streamingPlan,
931
1022
  openPlan,
@@ -935,6 +1026,50 @@ function SessionPanelRegion({
935
1026
  implementPlanDisabled,
936
1027
  ]);
937
1028
 
1029
+ // The open artifact document tabs (the artifact virtual-document FAMILY).
1030
+ // Built in a SEPARATE memo from the plan doc so artifact-tab churn never
1031
+ // recreates the plan doc (and vice versa). Bounded to what is actually open:
1032
+ // one document per editor whose entry id is the artifact family's, resolved
1033
+ // back to its `SessionArtifactEntry` by the same `artifactKey` used to open
1034
+ // it (single source of truth for artifact identity). Only the ACTIVE doc's
1035
+ // `content` is mounted by the surface, so building these elements is cheap;
1036
+ // keying on `contentHash` remounts a tab whose artifact was overwritten by a
1037
+ // later execution. A tab whose artifact no longer exists degrades to an
1038
+ // honest notice rather than vanishing.
1039
+ const artifactByKey = useMemo(
1040
+ () =>
1041
+ new Map(sessionArtifacts.map((entry) => [artifactKey(entry.artifact), entry])),
1042
+ [sessionArtifacts],
1043
+ );
1044
+ const artifactVirtualDocuments = useMemo<readonly SurfaceVirtualDocument[]>(() => {
1045
+ return editors
1046
+ .filter((editor) => editor.entryId === ARTIFACT_DOCUMENT_ENTRY_ID)
1047
+ .map((editor) => {
1048
+ const entry = artifactByKey.get(editor.path);
1049
+ return {
1050
+ entryId: ARTIFACT_DOCUMENT_ENTRY_ID,
1051
+ path: editor.path,
1052
+ content: entry ? (
1053
+ <ArtifactDocument
1054
+ key={entry.artifact.contentHash || editor.path}
1055
+ artifact={entry.artifact}
1056
+ executionId={entry.executionId}
1057
+ org={org}
1058
+ isTerminal={entry.isTerminal}
1059
+ onApplied={onApplied}
1060
+ />
1061
+ ) : (
1062
+ <ArtifactUnavailableNotice />
1063
+ ),
1064
+ };
1065
+ });
1066
+ }, [editors, artifactByKey, org, onApplied]);
1067
+
1068
+ const virtualDocuments = useMemo<readonly SurfaceVirtualDocument[]>(
1069
+ () => [planVirtualDocument, ...artifactVirtualDocuments],
1070
+ [planVirtualDocument, artifactVirtualDocuments],
1071
+ );
1072
+
938
1073
  const handleRemoveAgent = useCallback(() => {
939
1074
  flow.setAgentRef(null);
940
1075
  flow.setResolution(null);
@@ -1001,6 +1136,8 @@ function SessionPanelRegion({
1001
1136
  onApplied,
1002
1137
  onImplementPlan,
1003
1138
  onOpenPlan,
1139
+ onOpenArtifact,
1140
+ onActivateArtifact,
1004
1141
  });
1005
1142
 
1006
1143
  // Explorer-footer folder attach (desktop only — needs the native picker).
@@ -1011,9 +1148,19 @@ function SessionPanelRegion({
1011
1148
  if (path) flow.workspace.addLocalPath(path);
1012
1149
  }, [onBrowseLocalFolder, flow.workspace.addLocalPath]);
1013
1150
 
1151
+ // Entries decorated with per-entry read refs (the latest write-back commit
1152
+ // SHA), so the surface's read-side capabilities — file viewer, tree listing,
1153
+ // name search — resolve at the ref the agent's changes actually live on. A
1154
+ // read-side projection only: `flow.workspace.entries` stays undecorated for
1155
+ // setup and session input.
1156
+ const surfaceEntries = useWorkspaceReadRefs(
1157
+ flow.allExecutions,
1158
+ flow.workspace.entries,
1159
+ );
1160
+
1014
1161
  return (
1015
1162
  <WorkspaceSurface
1016
- entries={flow.workspace.entries}
1163
+ entries={surfaceEntries}
1017
1164
  lister={workspaceFileLister}
1018
1165
  reader={workspaceFileReader}
1019
1166
  searcher={workspaceContentSearcher}
@@ -1125,6 +1272,28 @@ function PlanUnavailableNotice() {
1125
1272
  );
1126
1273
  }
1127
1274
 
1275
+ /**
1276
+ * Shown in an artifact document tab whose artifact is no longer among the
1277
+ * session's outputs (an edge only reachable if the executions backing the tab
1278
+ * changed underneath it). The tab stays closable; the content is honest rather
1279
+ * than blank.
1280
+ */
1281
+ function ArtifactUnavailableNotice() {
1282
+ return (
1283
+ <div
1284
+ role="status"
1285
+ className="mx-auto flex w-full max-w-3xl flex-col items-center gap-1 px-4 py-8 text-center"
1286
+ >
1287
+ <p className="text-xs font-medium text-foreground">
1288
+ This artifact is no longer available.
1289
+ </p>
1290
+ <p className="text-xs text-muted-foreground">
1291
+ It may have been replaced by a newer version of this session.
1292
+ </p>
1293
+ </div>
1294
+ );
1295
+ }
1296
+
1128
1297
  /**
1129
1298
  * Non-blocking notice for a build that fell back to message-only implement
1130
1299
  * (the approved plan.md could not be uploaded/attached). The build itself
@@ -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
+ });