@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
@@ -117,6 +117,15 @@ export interface SendFollowUpOptions {
117
117
  * @see {@link CreateAgentExecutionInput.workspaceFileRefs}
118
118
  */
119
119
  readonly workspaceFileRefs?: string[];
120
+ /**
121
+ * ID of the execution this follow-up supersedes (edit-and-resubmit).
122
+ *
123
+ * The conversation read model hides the superseded execution so the
124
+ * edited message replaces the original in place.
125
+ *
126
+ * @see {@link CreateAgentExecutionInput.supersedesExecutionId}
127
+ */
128
+ readonly supersedesExecutionId?: string;
120
129
  }
121
130
 
122
131
  /**
@@ -129,7 +138,14 @@ export interface SendFollowUpOptions {
129
138
  export interface UseSessionConversationReturn {
130
139
  /** The session object, or null while loading. */
131
140
  readonly session: Session | null;
132
- /** Executions in terminal phases, in chronological order. */
141
+ /**
142
+ * Executions in terminal phases, in chronological order.
143
+ *
144
+ * Excludes turns replaced via edit-and-resubmit (an execution whose id
145
+ * appears as another execution's `supersedesExecutionId`) — the edited
146
+ * message stands in the original's place. Superseded records remain in
147
+ * execution history surfaces; only the conversation view hides them.
148
+ */
133
149
  readonly completedExecutions: readonly AgentExecution[];
134
150
  /** Currently streaming execution (stream or fetch fallback), or null. */
135
151
  readonly activeStreamExecution: AgentExecution | null;
@@ -491,12 +507,37 @@ export function useSessionConversation(
491
507
  }
492
508
  }, [activeExecutionId, stream.phase, refetch]);
493
509
 
510
+ // Executions replaced via edit-and-resubmit. The successor carries
511
+ // `spec.supersedes_execution_id`; hiding the superseded turn makes the
512
+ // edited message read as a single corrected exchange (in-place replace).
513
+ // The link must also be read off the live stream copy — right after a
514
+ // resubmit, the successor streams before the list refetch delivers it.
515
+ // Display-only: the raw `executions` list (and therefore active-id
516
+ // resolution above) is never filtered.
517
+ //
518
+ // Dep is the scalar link, NOT stream.execution: the stream object changes
519
+ // reference every frame, and rebuilding the Set per frame would hand
520
+ // MessageThread a fresh executions array mid-stream, defeating its
521
+ // memoization (DD-010).
522
+ const streamSupersededId =
523
+ stream.execution?.spec?.supersedesExecutionId || null;
524
+ const supersededIds = useMemo(() => {
525
+ const ids = new Set<string>();
526
+ for (const e of executions) {
527
+ const superseded = e.spec?.supersedesExecutionId;
528
+ if (superseded) ids.add(superseded);
529
+ }
530
+ if (streamSupersededId) ids.add(streamSupersededId);
531
+ return ids;
532
+ }, [executions, streamSupersededId]);
533
+
494
534
  const completedExecutions = useMemo(() => {
495
- if (!activeExecutionId) return executions;
496
- return executions.filter(
497
- (e) => (e.metadata?.id ?? "") !== activeExecutionId,
498
- );
499
- }, [executions, activeExecutionId]);
535
+ return executions.filter((e) => {
536
+ const id = e.metadata?.id ?? "";
537
+ if (activeExecutionId && id === activeExecutionId) return false;
538
+ return !supersededIds.has(id);
539
+ });
540
+ }, [executions, activeExecutionId, supersededIds]);
500
541
 
501
542
  const fetchedActiveExecution = useMemo(() => {
502
543
  if (!activeExecutionId) return null;
@@ -604,6 +645,7 @@ export function useSessionConversation(
604
645
  buildFromPlan: options?.buildFromPlan,
605
646
  autoApproveAll: options?.autoApproveAll,
606
647
  workspaceFileRefs: options?.workspaceFileRefs,
648
+ supersedesExecutionId: options?.supersedesExecutionId,
607
649
  });
608
650
  setPendingExecutionId(result.executionId);
609
651
  refetch();
@@ -410,6 +410,7 @@ export function useSessionPageFlow(
410
410
  // not from the composer (the pre-arm toggle was removed).
411
411
  autoApproveAll: autoApproveAll || undefined,
412
412
  workspaceFileRefs: context?.workspaceFileRefs,
413
+ supersedesExecutionId: context?.supersedesExecutionId,
413
414
  });
414
415
 
415
416
  sessionVariables.clear();
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import type { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
5
+ import type { ExecutionArtifact } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
5
6
  import { isTerminalPhase } from "../execution/execution-phases.js";
6
7
  import type { SelectedThreadItem } from "../internal/store/selection-store.js";
7
8
  import {
@@ -10,6 +11,8 @@ import {
10
11
  type WorkspaceEditorsStore,
11
12
  } from "../internal/store/index.js";
12
13
  import { PLAN_DOCUMENT_ENTRY_ID, PLAN_DOCUMENT_PATH } from "./plan-document.js";
14
+ import { ARTIFACT_DOCUMENT_ENTRY_ID } from "./artifact-document.js";
15
+ import { artifactKey } from "./useSessionArtifacts.js";
13
16
 
14
17
  /** Options for {@link useSessionPanel}. */
15
18
  export interface UseSessionPanelOptions {
@@ -118,6 +121,25 @@ export interface SessionPanelController {
118
121
  * both when a plan STARTS STREAMING and when it publishes (see `planKey`).
119
122
  */
120
123
  readonly openPlanDocument: () => void;
124
+ /**
125
+ * Open (or focus) an artifact as an editor-pane document tab and expand the
126
+ * panel. Uses the PREVIEW slot — like files and content-search hits, casual
127
+ * artifact browsing reuses one tab (single-click), and double-clicking the
128
+ * tab pins it. Distinct from `openPlanDocument`, which pins its single tab: a
129
+ * plan is the turn's deliverable, artifacts are browsable outputs. The
130
+ * artifact's {@link artifactKey} is the tab identity, shared with the
131
+ * `SurfaceVirtualDocument` the viewer builds for the open tab.
132
+ */
133
+ readonly openArtifact: (artifact: ExecutionArtifact) => void;
134
+ /**
135
+ * Pin an artifact's tab (clear its preview state) — the double-click half of
136
+ * the open/activate split, the encapsulated sibling of {@link openArtifact}.
137
+ * Delegates to the editors store's generic `pin`, exactly as the file tree
138
+ * pins via `pinEditor`: the leading single-click of the double-click has
139
+ * already opened the preview tab, so this promotes it to a persistent tab. A
140
+ * no-op if the artifact is not open.
141
+ */
142
+ readonly pinArtifact: (artifact: ExecutionArtifact) => void;
121
143
  /**
122
144
  * Report the current thread-item selection. Called from an effect inside the
123
145
  * panel subtree (the level that subscribes to the selection store), keeping
@@ -224,6 +246,21 @@ export function useSessionPanel({
224
246
  [editorsStore],
225
247
  );
226
248
 
249
+ const openArtifact = useCallback(
250
+ (artifact: ExecutionArtifact) => {
251
+ editorsStore.openPreview(ARTIFACT_DOCUMENT_ENTRY_ID, artifactKey(artifact));
252
+ setIsOpen(true);
253
+ },
254
+ [editorsStore],
255
+ );
256
+
257
+ const pinArtifact = useCallback(
258
+ (artifact: ExecutionArtifact) => {
259
+ editorsStore.pin(ARTIFACT_DOCUMENT_ENTRY_ID, artifactKey(artifact));
260
+ },
261
+ [editorsStore],
262
+ );
263
+
227
264
  const activateEditor = useCallback(
228
265
  (entryId: string, path: string) => editorsStore.activate(entryId, path),
229
266
  [editorsStore],
@@ -268,6 +305,8 @@ export function useSessionPanel({
268
305
  pinEditor,
269
306
  closeEditor,
270
307
  openPlanDocument,
308
+ openArtifact,
309
+ pinArtifact,
271
310
  notifySelection,
272
311
  }),
273
312
  [
@@ -282,6 +321,8 @@ export function useSessionPanel({
282
321
  pinEditor,
283
322
  closeEditor,
284
323
  openPlanDocument,
324
+ openArtifact,
325
+ pinArtifact,
285
326
  notifySelection,
286
327
  ],
287
328
  );
@@ -6,7 +6,10 @@ import type { SurfaceRailView } from "../workspace/WorkspaceSurface.js";
6
6
  import type { SelectedThreadItem } from "../internal/store/selection-store.js";
7
7
  import type { ApplyResourceResult } from "../library/useApplyResource.js";
8
8
  import { useSessionWriteBacks } from "./useSessionWriteBacks.js";
9
- import { useSessionArtifacts } from "./useSessionArtifacts.js";
9
+ import {
10
+ useSessionArtifacts,
11
+ type SessionArtifactEntry,
12
+ } from "./useSessionArtifacts.js";
10
13
  import { SetupTab, type SetupTabProps } from "./facets/SetupTab.js";
11
14
  import { ChangesTab } from "./facets/ChangesTab.js";
12
15
  import { ArtifactsTab } from "./facets/ArtifactsTab.js";
@@ -33,6 +36,17 @@ export interface UseSessionRailViewsOptions {
33
36
  * of the preview modal.
34
37
  */
35
38
  readonly onOpenPlan?: (executionId: string) => void;
39
+ /**
40
+ * Open a (non-plan) artifact as an editor-pane document tab. Routed to the
41
+ * Artifacts facet so clicking an artifact opens it in the editor area (VS
42
+ * Code "each file is a tab") instead of the preview modal.
43
+ */
44
+ readonly onOpenArtifact?: (entry: SessionArtifactEntry) => void;
45
+ /**
46
+ * Pin a (non-plan) artifact's document tab — the double-click half of the
47
+ * Artifacts facet's open/activate split. Routed through to the facet's rows.
48
+ */
49
+ readonly onActivateArtifact?: (entry: SessionArtifactEntry) => void;
36
50
  /**
37
51
  * Whether to offer the execution-derived facets (Changes / Artifacts /
38
52
  * Usage). The launcher passes `false` — before a session exists there are no
@@ -60,6 +74,8 @@ export function useSessionRailViews({
60
74
  onApplied,
61
75
  onImplementPlan,
62
76
  onOpenPlan,
77
+ onOpenArtifact,
78
+ onActivateArtifact,
63
79
  includeExecutionFacets = true,
64
80
  }: UseSessionRailViewsOptions): readonly SurfaceRailView[] {
65
81
  const { hasWriteBacks, writeBackCount } = useSessionWriteBacks(allExecutions);
@@ -101,6 +117,8 @@ export function useSessionRailViews({
101
117
  onApplied={onApplied}
102
118
  onImplementPlan={onImplementPlan}
103
119
  onOpenPlan={onOpenPlan}
120
+ onOpenArtifact={onOpenArtifact}
121
+ onActivateArtifact={onActivateArtifact}
104
122
  />
105
123
  ),
106
124
  });
@@ -136,6 +154,8 @@ export function useSessionRailViews({
136
154
  onApplied,
137
155
  onImplementPlan,
138
156
  onOpenPlan,
157
+ onOpenArtifact,
158
+ onActivateArtifact,
139
159
  selectedItem,
140
160
  ]);
141
161
  }
@@ -0,0 +1,118 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
5
+ import type { WorkspaceEntry } from "../workspace/useWorkspaceEntries.js";
6
+
7
+ /**
8
+ * Pure derivation hook that projects the session's workspace write-backs onto
9
+ * its workspace entries as per-entry read refs.
10
+ *
11
+ * The agent's file changes land on a write-back branch (`stigmer/<id>`), not
12
+ * the branch the session was configured with — so read-side surfaces (file
13
+ * viewer, tree listing, name search) pointed at the configured branch would
14
+ * 404 on agent-created files and silently show stale content for modified
15
+ * ones. This hook returns the entries with `readRef` set to the latest
16
+ * write-back commit SHA, which those surfaces resolve in preference to
17
+ * `gitBranch`. A commit SHA (not the branch name) so reads are immutable-ref
18
+ * exact, immune to branch eventual-consistency.
19
+ *
20
+ * **Ref selection:** the latest write-back with a non-empty `commitSha` wins
21
+ * per workspace entry, across all executions in chronological order. FAILED
22
+ * write-back records carry no SHA and never regress the ref.
23
+ *
24
+ * **Entry matching** mirrors the runner's `WriteBackCoordinator.resolveEntry`:
25
+ * single-entry sessions provision with an empty `workspaceEntryName`, so when
26
+ * the session has exactly one git entry every write-back applies to it;
27
+ * multi-entry sessions match strictly by name.
28
+ *
29
+ * **Reference stability (DD-010):** the decorated array is memoized on the
30
+ * entries array and a value signature of the derived refs — streaming frames
31
+ * that don't change any write-back SHA return the identical array, so
32
+ * downstream listing/content effects never churn.
33
+ *
34
+ * @param executions - All executions for the session, in chronological order
35
+ * (same input contract as {@link useSessionWriteBacks}).
36
+ * @param entries - The session's workspace entries. Returned as-is (same
37
+ * reference) when no entry derives a ref.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * const surfaceEntries = useWorkspaceReadRefs(flow.allExecutions, flow.workspace.entries);
42
+ * <WorkspaceSurface entries={surfaceEntries} ... />
43
+ * ```
44
+ *
45
+ * @see useSessionWriteBacks — the write-back list this projection derives from
46
+ * @see WorkspaceEntry.readRef — the field this hook populates
47
+ */
48
+ export function useWorkspaceReadRefs(
49
+ executions: readonly AgentExecution[],
50
+ entries: readonly WorkspaceEntry[],
51
+ ): readonly WorkspaceEntry[] {
52
+ // Cheap per-render pass; memoizing on `executions` would defeat the point —
53
+ // its identity changes every streaming frame while the SHAs rarely do. The
54
+ // signature collapses those frames into a stable memo key.
55
+ const refByName = collectLatestShas(executions);
56
+ const signature = [...refByName]
57
+ .map(([name, sha]) => `${name}\u0001${sha}`)
58
+ .join("\u0000");
59
+
60
+ return useMemo(() => {
61
+ if (refByName.size === 0) return entries;
62
+
63
+ const gitEntries = entries.filter((entry) => entry.type === "git");
64
+ let decorated = false;
65
+
66
+ const next = entries.map((entry) => {
67
+ const readRef = resolveReadRef(entry, gitEntries.length, refByName);
68
+ if (!readRef || readRef === entry.readRef) return entry;
69
+ decorated = true;
70
+ return { ...entry, readRef };
71
+ });
72
+
73
+ return decorated ? next : entries;
74
+ // `signature` is the value-equality key for `refByName` (rebuilt each
75
+ // render); the map itself must stay out of the deps.
76
+ }, [entries, signature]);
77
+ }
78
+
79
+ /** Latest non-empty write-back commit SHA per workspace entry name. */
80
+ function collectLatestShas(
81
+ executions: readonly AgentExecution[],
82
+ ): Map<string, string> {
83
+ const refByName = new Map<string, string>();
84
+ for (const execution of executions) {
85
+ for (const wb of execution.status?.workspaceWriteBacks ?? []) {
86
+ if (wb.commitSha) refByName.set(wb.workspaceEntryName, wb.commitSha);
87
+ }
88
+ }
89
+ return refByName;
90
+ }
91
+
92
+ /**
93
+ * The write-back SHA for one entry, honoring the runner's single-entry
94
+ * convention: with exactly one git entry, any write-back applies to it
95
+ * (single-entry sessions write back under an empty entry name).
96
+ */
97
+ function resolveReadRef(
98
+ entry: WorkspaceEntry,
99
+ gitEntryCount: number,
100
+ refByName: ReadonlyMap<string, string>,
101
+ ): string | undefined {
102
+ if (entry.type !== "git") return undefined;
103
+
104
+ const byName = refByName.get(entry.name);
105
+ if (byName) return byName;
106
+
107
+ if (gitEntryCount === 1) {
108
+ // Single-entry sessions write back under an empty entry name; a lone
109
+ // unambiguous name is equally safe to claim. Anything else (multiple
110
+ // names against one entry) is malformed status data — decorate nothing
111
+ // rather than guess.
112
+ const unnamed = refByName.get("");
113
+ if (unnamed) return unnamed;
114
+ if (refByName.size === 1) return refByName.values().next().value;
115
+ }
116
+
117
+ return undefined;
118
+ }
@@ -11,14 +11,21 @@ import {
11
11
  } from "react";
12
12
  import { cn } from "@stigmer/theme";
13
13
  import type { FileChange } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
14
- import { FileChangeType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
14
+ import {
15
+ FileChangeCaptureLevel,
16
+ FileChangeType,
17
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
15
18
  import { ArtifactContentRenderer } from "../execution/ArtifactContentRenderer.js";
16
19
  import { FileChangeDiff } from "../execution/FileChangesView.js";
20
+ import { useFileChangeContent } from "../execution/useFileChangeContent.js";
17
21
  import type { RevealTarget } from "../internal/useRevealLine.js";
18
22
  import type { SelectedWorkspaceFile } from "../internal/store/workspace-file-selection-store.js";
19
23
  import { useWorkspaceFileContent } from "./useWorkspaceFileContent.js";
20
24
  import type { WorkspaceEntry } from "./useWorkspaceEntries.js";
21
- import type { WorkspaceFileReader } from "./WorkspaceFileReader.js";
25
+ import {
26
+ WorkspaceFileNotFoundError,
27
+ type WorkspaceFileReader,
28
+ } from "./WorkspaceFileReader.js";
22
29
 
23
30
  /** Line-skeleton widths, mirroring `FileContentStateView` in `ArtifactPreviewModal`. */
24
31
  const SKELETON_LINE_WIDTHS = [85, 72, 90, 65, 78, 88, 70, 82] as const;
@@ -218,22 +225,16 @@ export const FileViewer = forwardRef<FileViewerHandle, FileViewerProps>(
218
225
  <FileChangeDiff change={change} showFileName={false} showStats />
219
226
  </div>
220
227
  ) : (
221
- <>
222
- {change && (
223
- <p className="border-b border-border-muted px-4 py-1.5 text-[0.65rem] text-muted-foreground">
224
- Live — may differ from the reviewed change.
225
- </p>
226
- )}
227
- <FileViewerBody
228
- basename={basename}
229
- content={content}
230
- isLoading={isLoading}
231
- isUnsupported={isUnsupported}
232
- error={error}
233
- onRetry={refetch}
234
- reveal={reveal}
235
- />
236
- </>
228
+ <FileViewerBody
229
+ basename={basename}
230
+ content={content}
231
+ isLoading={isLoading}
232
+ isUnsupported={isUnsupported}
233
+ error={error}
234
+ onRetry={refetch}
235
+ reveal={reveal}
236
+ change={change}
237
+ />
237
238
  )}
238
239
  </div>
239
240
  </div>
@@ -328,6 +329,23 @@ function ViewerModeToggle({
328
329
  // Body — exactly one state, mirroring FileContentStateView + workspace extras
329
330
  // ---------------------------------------------------------------------------
330
331
 
332
+ /**
333
+ * Whether the session captured this change's full after-content — the
334
+ * precondition for serving it when the live substrate can't (a cloud file not
335
+ * yet pushed to any readable ref). A DELETE has no after side, and a
336
+ * HUNK_ONLY capture carries only the diff, never the whole file.
337
+ */
338
+ function hasCapturedAfterContent(
339
+ change: FileChange | undefined,
340
+ ): change is FileChange {
341
+ return (
342
+ change !== undefined &&
343
+ change.changeType !== FileChangeType.DELETE &&
344
+ change.captureLevel !== FileChangeCaptureLevel.HUNK_ONLY &&
345
+ change.after !== undefined
346
+ );
347
+ }
348
+
331
349
  function FileViewerBody({
332
350
  basename,
333
351
  content,
@@ -336,6 +354,7 @@ function FileViewerBody({
336
354
  error,
337
355
  onRetry,
338
356
  reveal,
357
+ change,
339
358
  }: {
340
359
  readonly basename: string;
341
360
  readonly content: ReturnType<typeof useWorkspaceFileContent>["content"];
@@ -344,7 +363,20 @@ function FileViewerBody({
344
363
  readonly error: Error | null;
345
364
  readonly onRetry: () => void;
346
365
  readonly reveal?: RevealTarget;
366
+ readonly change?: FileChange;
347
367
  }) {
368
+ // A live read that can't be served (unsupported substrate, or the file
369
+ // isn't at the read ref yet) degrades to the session's captured
370
+ // after-content when the change carries it — the same bytes the Diff view
371
+ // renders, so nothing is fabricated. Other errors keep the retry path.
372
+ const isNotFound = error instanceof WorkspaceFileNotFoundError;
373
+ const canFallBackToCaptured =
374
+ (isUnsupported || isNotFound) && hasCapturedAfterContent(change);
375
+
376
+ if (canFallBackToCaptured) {
377
+ return <CapturedFileContent change={change} basename={basename} reveal={reveal} />;
378
+ }
379
+
348
380
  // Unsupported comes first: a missing reader means there is nothing to load.
349
381
  if (isUnsupported) {
350
382
  return (
@@ -355,21 +387,25 @@ function FileViewerBody({
355
387
  }
356
388
 
357
389
  if (isLoading) {
390
+ return <LoadingSkeleton />;
391
+ }
392
+
393
+ if (isNotFound) {
394
+ // Expected while the agent's write-back is still syncing to the ref —
395
+ // a calm notice (DD-006), not a failure.
358
396
  return (
359
- <div
360
- role="status"
361
- className="space-y-2 p-4"
362
- aria-busy="true"
363
- aria-label="Loading file"
364
- >
365
- {SKELETON_LINE_WIDTHS.map((width, i) => (
366
- <div
367
- key={i}
368
- className="h-4 animate-pulse rounded bg-muted"
369
- style={{ width: `${width}%` }}
370
- aria-hidden="true"
371
- />
372
- ))}
397
+ <div className="flex flex-col items-center justify-center gap-2 p-8 text-center">
398
+ <p className="text-xs text-muted-foreground">
399
+ This file isn&rsquo;t available in the workspace source yet. The
400
+ agent&rsquo;s latest changes may still be syncing.
401
+ </p>
402
+ <button
403
+ type="button"
404
+ onClick={onRetry}
405
+ className="text-xs font-medium text-foreground underline underline-offset-2 hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:rounded-sm"
406
+ >
407
+ Check again
408
+ </button>
373
409
  </div>
374
410
  );
375
411
  }
@@ -417,12 +453,82 @@ function FileViewerBody({
417
453
  }
418
454
 
419
455
  return (
420
- <ArtifactContentRenderer
421
- content={content.text}
422
- fileName={basename}
423
- isTruncated={content.truncated}
424
- reveal={reveal}
425
- />
456
+ <>
457
+ {change && (
458
+ <p className="border-b border-border-muted px-4 py-1.5 text-[0.65rem] text-muted-foreground">
459
+ Live — may differ from the reviewed change.
460
+ </p>
461
+ )}
462
+ <ArtifactContentRenderer
463
+ content={content.text}
464
+ fileName={basename}
465
+ isTruncated={content.truncated}
466
+ reveal={reveal}
467
+ />
468
+ </>
469
+ );
470
+ }
471
+
472
+ /**
473
+ * The captured-content fallback: renders a change's after-side text (the
474
+ * bytes the session already holds — inline or offloaded to artifact storage)
475
+ * when no live substrate can serve the file. Mounted only when eligible, so
476
+ * its content fetch never runs for live-served files.
477
+ */
478
+ function CapturedFileContent({
479
+ change,
480
+ basename,
481
+ reveal,
482
+ }: {
483
+ readonly change: FileChange;
484
+ readonly basename: string;
485
+ readonly reveal?: RevealTarget;
486
+ }) {
487
+ const { afterText, isBinary, isLoading, error } = useFileChangeContent(change);
488
+
489
+ if (isLoading) {
490
+ return <LoadingSkeleton />;
491
+ }
492
+
493
+ if (isBinary) {
494
+ return <MessageState>Binary file — not shown.</MessageState>;
495
+ }
496
+
497
+ if (error || afterText === null) {
498
+ return <MessageState>Content not available for preview.</MessageState>;
499
+ }
500
+
501
+ return (
502
+ <>
503
+ <p className="border-b border-border-muted px-4 py-1.5 text-[0.65rem] text-muted-foreground">
504
+ As of the agent&rsquo;s last change — not yet in the workspace source.
505
+ </p>
506
+ <ArtifactContentRenderer
507
+ content={afterText}
508
+ fileName={basename}
509
+ reveal={reveal}
510
+ />
511
+ </>
512
+ );
513
+ }
514
+
515
+ function LoadingSkeleton() {
516
+ return (
517
+ <div
518
+ role="status"
519
+ className="space-y-2 p-4"
520
+ aria-busy="true"
521
+ aria-label="Loading file"
522
+ >
523
+ {SKELETON_LINE_WIDTHS.map((width, i) => (
524
+ <div
525
+ key={i}
526
+ className="h-4 animate-pulse rounded bg-muted"
527
+ style={{ width: `${width}%` }}
528
+ aria-hidden="true"
529
+ />
530
+ ))}
531
+ </div>
426
532
  );
427
533
  }
428
534
 
@@ -32,6 +32,25 @@ export interface WorkspaceFileContent {
32
32
  readonly truncated?: boolean;
33
33
  }
34
34
 
35
+ /**
36
+ * Thrown by a {@link WorkspaceFileReader} when the requested path does not
37
+ * exist at the entry's read ref — as opposed to a transport or permission
38
+ * failure.
39
+ *
40
+ * The distinction matters to consumers: a file the agent just created exists
41
+ * in session state before it exists at any readable ref, so "not found" is a
42
+ * recoverable, expected condition (the viewer falls back to the session's
43
+ * captured content), while a network error or 5xx is a genuine failure worth
44
+ * a retry affordance. Platform builders implementing custom readers should
45
+ * throw this class for their substrate's not-found condition.
46
+ */
47
+ export class WorkspaceFileNotFoundError extends Error {
48
+ constructor(path: string) {
49
+ super(`"${path}" doesn't exist at the workspace's current ref.`);
50
+ this.name = "WorkspaceFileNotFoundError";
51
+ }
52
+ }
53
+
35
54
  /**
36
55
  * Platform-injected callback that reads the content of a single workspace file.
37
56
  *
@@ -46,6 +65,8 @@ export interface WorkspaceFileContent {
46
65
  * - **Throws** on a genuine failure (404, network error, 5xx, unreadable file,
47
66
  * a directory path). Consumers catch this into a distinct error state — a
48
67
  * clicked file that 404s is "failed to load," not "unsupported here."
68
+ * A missing path should throw {@link WorkspaceFileNotFoundError} so
69
+ * consumers can distinguish "not there yet" from "broken."
49
70
  *
50
71
  * `path` is repo-relative for git entries and root-relative for local entries —
51
72
  * exactly the strings the matching `WorkspaceFileLister` emits, so a file-tree