@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
@@ -0,0 +1,463 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { render, screen, cleanup, fireEvent } from "@testing-library/react";
3
+ import React, { memo } from "react";
4
+ import { create } from "@bufbuild/protobuf";
5
+ import {
6
+ AgentExecutionSchema,
7
+ AgentExecutionStatusSchema,
8
+ type AgentExecution,
9
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
10
+ import {
11
+ AgentExecutionSpecSchema,
12
+ ExecutionConfigSchema,
13
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
14
+ import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
15
+ import { AgentMessageSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
16
+ import { PendingApprovalSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
17
+ import { TodoItemSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/todo_pb";
18
+ import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
19
+ import {
20
+ ApprovalAction,
21
+ ExecutionArtifactKind,
22
+ ExecutionPhase,
23
+ InteractionMode,
24
+ MessageType,
25
+ TodoStatus,
26
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
27
+
28
+ import { MessageThread, type MessageThreadSlots } from "../MessageThread";
29
+ import type { MessageEntryProps } from "../MessageEntry";
30
+ import type { ApprovalCardProps } from "../ApprovalCard";
31
+ import type { TodoCardProps } from "../TodoCard";
32
+ import type { TodoRowProps } from "../TodoList";
33
+ import type { SetupProgressProps } from "../SetupProgress";
34
+ import type { PlanCompletionCardProps } from "../PlanCompletionCard";
35
+ import type { PlanArtifactCardProps } from "../PlanArtifactCard";
36
+ import type { PlanStreamingCardProps } from "../PlanStreamingCard";
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Mock react-virtuoso (same pattern as virtualized-thread.test.tsx) so the
40
+ // virtualized path renders its items synchronously in happy-dom.
41
+ // ---------------------------------------------------------------------------
42
+
43
+ vi.mock("react-virtuoso", () => ({
44
+ Virtuoso: React.forwardRef(function MockVirtuoso(
45
+ props: Record<string, unknown>,
46
+ ref: React.Ref<unknown>,
47
+ ) {
48
+ const data = props.data as { key: string }[];
49
+ const itemContent = props.itemContent as (
50
+ index: number,
51
+ item: unknown,
52
+ ) => React.ReactNode;
53
+ React.useImperativeHandle(ref, () => ({ scrollToIndex: vi.fn() }));
54
+ return (
55
+ <div data-testid="virtuoso-scroller">
56
+ {data.map((item, i) => (
57
+ <div key={item.key}>{itemContent(i, item)}</div>
58
+ ))}
59
+ </div>
60
+ );
61
+ }),
62
+ }));
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // DOM stubs — useAutoScroll depends on browser APIs not available in happy-dom
66
+ // ---------------------------------------------------------------------------
67
+
68
+ beforeEach(() => {
69
+ vi.stubGlobal(
70
+ "IntersectionObserver",
71
+ vi.fn(() => ({
72
+ observe: vi.fn(),
73
+ unobserve: vi.fn(),
74
+ disconnect: vi.fn(),
75
+ takeRecords: vi.fn(() => []),
76
+ root: null,
77
+ rootMargin: "",
78
+ thresholds: [0],
79
+ })),
80
+ );
81
+ vi.stubGlobal(
82
+ "ResizeObserver",
83
+ vi.fn(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn() })),
84
+ );
85
+ vi.stubGlobal(
86
+ "requestAnimationFrame",
87
+ vi.fn((cb: FrameRequestCallback) => {
88
+ cb(performance.now());
89
+ return 1;
90
+ }),
91
+ );
92
+ vi.stubGlobal("cancelAnimationFrame", vi.fn());
93
+ });
94
+
95
+ afterEach(() => {
96
+ cleanup();
97
+ vi.restoreAllMocks();
98
+ });
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // Fixtures
102
+ // ---------------------------------------------------------------------------
103
+
104
+ function makeExecution(opts: {
105
+ id: string;
106
+ specMessage?: string;
107
+ phase?: ExecutionPhase;
108
+ interactionMode?: InteractionMode;
109
+ aiContent?: string;
110
+ todos?: Record<string, { content: string; status: TodoStatus }>;
111
+ pendingApprovalToolCallId?: string;
112
+ planArtifactName?: string;
113
+ }): AgentExecution {
114
+ const exec = create(AgentExecutionSchema);
115
+
116
+ const meta = create(ApiResourceMetadataSchema);
117
+ meta.id = opts.id;
118
+ exec.metadata = meta;
119
+
120
+ const spec = create(AgentExecutionSpecSchema);
121
+ spec.message = opts.specMessage ?? "Hello";
122
+ if (opts.interactionMode !== undefined) {
123
+ const config = create(ExecutionConfigSchema);
124
+ config.interactionMode = opts.interactionMode;
125
+ spec.executionConfig = config;
126
+ }
127
+ exec.spec = spec;
128
+
129
+ const status = create(AgentExecutionStatusSchema);
130
+ status.phase = opts.phase ?? ExecutionPhase.EXECUTION_COMPLETED;
131
+
132
+ const aiMsg = create(AgentMessageSchema);
133
+ aiMsg.type = MessageType.MESSAGE_AI;
134
+ aiMsg.content = opts.aiContent ?? "Hi there!";
135
+ status.messages = [aiMsg];
136
+
137
+ if (opts.todos) {
138
+ for (const [id, todo] of Object.entries(opts.todos)) {
139
+ status.todos[id] = create(TodoItemSchema, {
140
+ id,
141
+ content: todo.content,
142
+ status: todo.status,
143
+ });
144
+ }
145
+ }
146
+
147
+ if (opts.pendingApprovalToolCallId) {
148
+ status.pendingApprovals = [
149
+ create(PendingApprovalSchema, {
150
+ toolCallId: opts.pendingApprovalToolCallId,
151
+ toolName: "shell_exec",
152
+ argsPreview: "{}",
153
+ }),
154
+ ];
155
+ }
156
+
157
+ if (opts.planArtifactName) {
158
+ status.artifacts = [
159
+ create(ExecutionArtifactSchema, {
160
+ name: opts.planArtifactName,
161
+ kind: ExecutionArtifactKind.FILE,
162
+ }),
163
+ ];
164
+ }
165
+
166
+ exec.status = status;
167
+ return exec;
168
+ }
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // Slot overrides — each slot renders in place of its built-in
172
+ // ---------------------------------------------------------------------------
173
+
174
+ describe("MessageThread slots", () => {
175
+ it("renders the MessageEntry slot for every message, including the failed-send bubble", () => {
176
+ const CustomEntry = ({ message }: MessageEntryProps) => (
177
+ <div data-testid="custom-entry">{message.content}</div>
178
+ );
179
+ const slots: MessageThreadSlots = { MessageEntry: CustomEntry };
180
+
181
+ render(
182
+ <MessageThread
183
+ executions={[makeExecution({ id: "e1", specMessage: "prompt text" })]}
184
+ pendingUserMessage="failed follow-up"
185
+ pendingMessageFailed
186
+ onRetrySend={vi.fn()}
187
+ slots={slots}
188
+ />,
189
+ );
190
+
191
+ const entries = screen.getAllByTestId("custom-entry");
192
+ // Synthesized human bubble + AI message + the failed pending bubble.
193
+ expect(entries.length).toBe(3);
194
+ expect(entries.map((e) => e.textContent)).toContain("failed follow-up");
195
+ // The built-in bubble chrome must not render.
196
+ expect(screen.queryByRole("article", { name: "User message" })).toBeNull();
197
+ // The failed-send Retry affordance stays built-in around the slot.
198
+ expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy();
199
+ });
200
+
201
+ it("renders the ApprovalCard slot with the row's stabilized onSubmit wiring intact", () => {
202
+ const onApprovalSubmit = vi.fn();
203
+ const CustomApproval = ({ pendingApproval, onSubmit }: ApprovalCardProps) => (
204
+ <button
205
+ type="button"
206
+ data-testid="custom-approval"
207
+ onClick={() => onSubmit(ApprovalAction.APPROVE)}
208
+ >
209
+ approve {pendingApproval.toolName}
210
+ </button>
211
+ );
212
+
213
+ render(
214
+ <MessageThread
215
+ executions={[
216
+ makeExecution({
217
+ id: "e1",
218
+ phase: ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL,
219
+ pendingApprovalToolCallId: "tc-1",
220
+ }),
221
+ ]}
222
+ onApprovalSubmit={onApprovalSubmit}
223
+ slots={{ ApprovalCard: CustomApproval }}
224
+ />,
225
+ );
226
+
227
+ fireEvent.click(screen.getByTestId("custom-approval"));
228
+ // The memoized ApprovalCardRow still injects the toolCallId — the slot's
229
+ // onSubmit contract is identical to the built-in's.
230
+ expect(onApprovalSubmit).toHaveBeenCalledWith(
231
+ "tc-1",
232
+ ApprovalAction.APPROVE,
233
+ undefined,
234
+ );
235
+ });
236
+
237
+ it("renders the TodoCard slot with the TodoRow slot forwarded through its props", () => {
238
+ const forwarded: Array<TodoRowProps["item"]["content"]> = [];
239
+ const CustomRow = ({ item }: TodoRowProps) => {
240
+ forwarded.push(item.content);
241
+ return <li data-testid="custom-row">{item.content}</li>;
242
+ };
243
+ const CustomCard = ({ todos, TodoRow }: TodoCardProps) => (
244
+ <div data-testid="custom-todo-card">
245
+ <ul>
246
+ {Object.values(todos).map((item) =>
247
+ TodoRow ? <TodoRow key={item.id} item={item} /> : null,
248
+ )}
249
+ </ul>
250
+ </div>
251
+ );
252
+
253
+ render(
254
+ <MessageThread
255
+ executions={[
256
+ makeExecution({
257
+ id: "e1",
258
+ todos: { t1: { content: "write tests", status: TodoStatus.TODO_IN_PROGRESS } },
259
+ }),
260
+ ]}
261
+ slots={{ TodoCard: CustomCard, TodoRow: CustomRow }}
262
+ />,
263
+ );
264
+
265
+ expect(screen.getByTestId("custom-todo-card")).toBeTruthy();
266
+ expect(forwarded).toEqual(["write tests"]);
267
+ });
268
+
269
+ it("renders the TodoRow slot inside the built-in TodoCard when only TodoRow is overridden", () => {
270
+ const CustomRow = ({ item }: TodoRowProps) => (
271
+ <li data-testid="custom-row">ROW: {item.content}</li>
272
+ );
273
+
274
+ render(
275
+ <MessageThread
276
+ executions={[
277
+ makeExecution({
278
+ id: "e1",
279
+ todos: { t1: { content: "write tests", status: TodoStatus.TODO_IN_PROGRESS } },
280
+ }),
281
+ ]}
282
+ slots={{ TodoRow: CustomRow }}
283
+ />,
284
+ );
285
+
286
+ // The built-in card chrome (region + disclosure) hosts the custom row —
287
+ // auto-disclosure opens the card because an item is in progress.
288
+ expect(screen.getByRole("region", { name: "Agent to-dos" })).toBeTruthy();
289
+ expect(screen.getByTestId("custom-row").textContent).toBe("ROW: write tests");
290
+ });
291
+
292
+ it("renders the SetupProgress slot for the pre-first-token window", () => {
293
+ const CustomSetup = ({ isAwaitingResponse }: SetupProgressProps) => (
294
+ <div data-testid="custom-setup">{isAwaitingResponse ? "waiting" : "setup"}</div>
295
+ );
296
+
297
+ const exec = makeExecution({
298
+ id: "e-live",
299
+ phase: ExecutionPhase.EXECUTION_IN_PROGRESS,
300
+ });
301
+ // No renderable response yet: the setup-progress item is emitted.
302
+ exec.status!.messages = [];
303
+
304
+ render(
305
+ <MessageThread
306
+ executions={[]}
307
+ activeStreamExecution={exec}
308
+ slots={{ SetupProgress: CustomSetup }}
309
+ />,
310
+ );
311
+
312
+ expect(screen.getByTestId("custom-setup").textContent).toBe("waiting");
313
+ });
314
+
315
+ it("renders the PlanCompletionCard slot for an artifact-less completed Plan turn", () => {
316
+ const CustomCompletion = ({ onImplement }: PlanCompletionCardProps) => (
317
+ <button type="button" data-testid="custom-completion" onClick={onImplement}>
318
+ custom build
319
+ </button>
320
+ );
321
+ const onBuildFromPlan = vi.fn();
322
+
323
+ render(
324
+ <MessageThread
325
+ executions={[
326
+ makeExecution({
327
+ id: "e-plan",
328
+ phase: ExecutionPhase.EXECUTION_COMPLETED,
329
+ interactionMode: InteractionMode.PLAN,
330
+ }),
331
+ ]}
332
+ onBuildFromPlan={onBuildFromPlan}
333
+ slots={{ PlanCompletionCard: CustomCompletion }}
334
+ />,
335
+ );
336
+
337
+ fireEvent.click(screen.getByTestId("custom-completion"));
338
+ expect(onBuildFromPlan).toHaveBeenCalledOnce();
339
+ });
340
+
341
+ it("renders the PlanArtifactCard slot for a completed Plan turn with a published artifact", () => {
342
+ const CustomArtifact = ({ artifact, title }: PlanArtifactCardProps) => (
343
+ <div data-testid="custom-artifact">
344
+ {title ?? "Plan"}: {artifact.name}
345
+ </div>
346
+ );
347
+
348
+ render(
349
+ <MessageThread
350
+ executions={[
351
+ makeExecution({
352
+ id: "e-plan",
353
+ phase: ExecutionPhase.EXECUTION_COMPLETED,
354
+ interactionMode: InteractionMode.PLAN,
355
+ aiContent: "# Rollout Plan\n\n1. First step",
356
+ planArtifactName: "rollout_abc123.plan.md",
357
+ }),
358
+ ]}
359
+ onBuildFromPlan={vi.fn()}
360
+ slots={{ PlanArtifactCard: CustomArtifact }}
361
+ />,
362
+ );
363
+
364
+ expect(screen.getByTestId("custom-artifact").textContent).toBe(
365
+ "Rollout Plan: rollout_abc123.plan.md",
366
+ );
367
+ });
368
+
369
+ it("renders the PlanStreamingCard slot while a plan is being written", () => {
370
+ const CustomStreaming = ({ title }: PlanStreamingCardProps) => (
371
+ <div data-testid="custom-streaming">writing: {title}</div>
372
+ );
373
+
374
+ render(
375
+ <MessageThread
376
+ executions={[]}
377
+ activeStreamExecution={makeExecution({
378
+ id: "e-live",
379
+ phase: ExecutionPhase.EXECUTION_IN_PROGRESS,
380
+ interactionMode: InteractionMode.PLAN,
381
+ aiContent: "# Rollout Plan\n\n1. First step",
382
+ })}
383
+ onOpenPlan={vi.fn()}
384
+ slots={{ PlanStreamingCard: CustomStreaming }}
385
+ />,
386
+ );
387
+
388
+ expect(screen.getByTestId("custom-streaming").textContent).toBe(
389
+ "writing: Rollout Plan",
390
+ );
391
+ });
392
+
393
+ // -------------------------------------------------------------------------
394
+ // Backward compatibility and memo guarantees
395
+ // -------------------------------------------------------------------------
396
+
397
+ it("renders every built-in unchanged when slots is omitted", () => {
398
+ render(
399
+ <MessageThread
400
+ executions={[
401
+ makeExecution({
402
+ id: "e1",
403
+ phase: ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL,
404
+ todos: { t1: { content: "write tests", status: TodoStatus.TODO_IN_PROGRESS } },
405
+ pendingApprovalToolCallId: "tc-1",
406
+ }),
407
+ ]}
408
+ onApprovalSubmit={vi.fn()}
409
+ />,
410
+ );
411
+
412
+ expect(screen.getByRole("article", { name: "User message" })).toBeTruthy();
413
+ expect(screen.getByRole("region", { name: "Agent to-dos" })).toBeTruthy();
414
+ expect(
415
+ screen.getByRole("alert", { name: "Approval required for shell_exec" }),
416
+ ).toBeTruthy();
417
+ });
418
+
419
+ it("renders slot overrides identically under the virtualized path", async () => {
420
+ const CustomEntry = ({ message }: MessageEntryProps) => (
421
+ <div data-testid="custom-entry">{message.content}</div>
422
+ );
423
+
424
+ render(
425
+ <MessageThread
426
+ executions={[makeExecution({ id: "e1", specMessage: "prompt text" })]}
427
+ virtualized
428
+ slots={{ MessageEntry: CustomEntry }}
429
+ />,
430
+ );
431
+
432
+ // The lazy VirtualizedThread resolves asynchronously.
433
+ await vi.waitFor(() => {
434
+ expect(screen.getByTestId("virtuoso-scroller")).toBeTruthy();
435
+ });
436
+ const entries = screen.getAllByTestId("custom-entry");
437
+ expect(entries.length).toBe(2); // synthesized human bubble + AI message
438
+ expect(screen.queryByRole("article", { name: "User message" })).toBeNull();
439
+ });
440
+
441
+ it("does not re-render a memoized slot when the thread re-renders with identical data", () => {
442
+ const renderCount = vi.fn();
443
+ const CustomCard = memo(function CustomCard({ todos }: TodoCardProps) {
444
+ renderCount();
445
+ return <div data-testid="custom-todo-card">{Object.keys(todos).length} todos</div>;
446
+ });
447
+ const slots: MessageThreadSlots = { TodoCard: CustomCard };
448
+
449
+ const exec = makeExecution({
450
+ id: "e1",
451
+ todos: { t1: { content: "write tests", status: TodoStatus.TODO_PENDING } },
452
+ });
453
+
454
+ const { rerender } = render(
455
+ <MessageThread executions={[exec]} slots={slots} />,
456
+ );
457
+ rerender(<MessageThread executions={[exec]} slots={slots} />);
458
+
459
+ // Same execution reference, same slots object: the renderer must not
460
+ // manufacture fresh props that defeat the slot's React.memo (DD-010).
461
+ expect(renderCount).toHaveBeenCalledTimes(1);
462
+ });
463
+ });
@@ -0,0 +1,151 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { renderHook, act, waitFor } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import { create } from "@bufbuild/protobuf";
5
+ import type { Stigmer } from "@stigmer/sdk";
6
+ import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
7
+ import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
8
+ import { StigmerContext } from "../../context";
9
+ import { useArtifactInspection } from "../useArtifactInspection";
10
+
11
+ function fileArtifact(name: string) {
12
+ return create(ExecutionArtifactSchema, {
13
+ name,
14
+ kind: ExecutionArtifactKind.FILE,
15
+ sizeBytes: 64n,
16
+ sandboxPath: `.stigmer/${name}`,
17
+ storageKey: `artifacts/aex_1/${name}`,
18
+ contentHash: "hash-1",
19
+ });
20
+ }
21
+
22
+ function dirArtifact(name: string) {
23
+ return create(ExecutionArtifactSchema, {
24
+ name,
25
+ kind: ExecutionArtifactKind.DIRECTORY,
26
+ sizeBytes: 512n,
27
+ sandboxPath: `.stigmer/${name}`,
28
+ storageKey: `artifacts/aex_1/${name}`,
29
+ entries: ["SKILL.md", "run.sh"],
30
+ });
31
+ }
32
+
33
+ function contentResult(text: string) {
34
+ return {
35
+ content: new TextEncoder().encode(text),
36
+ contentType: "text/plain",
37
+ truncated: false,
38
+ };
39
+ }
40
+
41
+ function wrapperFor(stigmer: Stigmer) {
42
+ return ({ children }: { children: ReactNode }) => (
43
+ <StigmerContext.Provider value={stigmer}>{children}</StigmerContext.Provider>
44
+ );
45
+ }
46
+
47
+ const writeText = vi.fn().mockResolvedValue(undefined);
48
+
49
+ beforeEach(() => {
50
+ writeText.mockClear();
51
+ vi.stubGlobal("navigator", { clipboard: { writeText } });
52
+ });
53
+
54
+ const AGENT_YAML = [
55
+ "apiVersion: agentic.stigmer.ai/v1",
56
+ "kind: Agent",
57
+ "metadata:",
58
+ " name: my-agent",
59
+ "spec: {}",
60
+ "",
61
+ ].join("\n");
62
+
63
+ describe("useArtifactInspection — content + copy", () => {
64
+ it("fetches text content and copies it to the clipboard on demand", async () => {
65
+ const getArtifactContent = vi.fn().mockResolvedValue(contentResult("hello world"));
66
+ const stigmer = { agentExecution: { getArtifactContent } } as unknown as Stigmer;
67
+
68
+ const { result } = renderHook(
69
+ () => useArtifactInspection(fileArtifact("notes.txt"), "aex_1", "acme"),
70
+ { wrapper: wrapperFor(stigmer) },
71
+ );
72
+
73
+ await waitFor(() => expect(result.current.content).toBe("hello world"));
74
+ expect(result.current.isDirectory).toBe(false);
75
+ expect(result.current.isDetected).toBe(false);
76
+ expect(result.current.ctaLabel).toBeNull();
77
+
78
+ act(() => {
79
+ result.current.copy();
80
+ });
81
+ expect(writeText).toHaveBeenCalledWith("hello world");
82
+ await waitFor(() => expect(result.current.copied).toBe(true));
83
+ });
84
+
85
+ it("does not fetch content for a directory artifact", () => {
86
+ const getArtifactContent = vi.fn().mockReturnValue(new Promise(() => {}));
87
+ const stigmer = { agentExecution: { getArtifactContent } } as unknown as Stigmer;
88
+
89
+ const { result } = renderHook(
90
+ () => useArtifactInspection(dirArtifact("skill-pack"), "aex_1", "acme"),
91
+ { wrapper: wrapperFor(stigmer) },
92
+ );
93
+
94
+ expect(result.current.isDirectory).toBe(true);
95
+ // Directory content is not fetched via the text-content RPC (skill
96
+ // detection uses its own entry-scoped fetch).
97
+ expect(
98
+ getArtifactContent.mock.calls.every(
99
+ (call) => call[0]?.entryPath !== undefined,
100
+ ),
101
+ ).toBe(true);
102
+ });
103
+ });
104
+
105
+ describe("useArtifactInspection — detection + apply", () => {
106
+ it("labels a detected Agent YAML and offers an Apply CTA", async () => {
107
+ const getArtifactContent = vi.fn().mockResolvedValue(contentResult(AGENT_YAML));
108
+ const stigmer = { agentExecution: { getArtifactContent } } as unknown as Stigmer;
109
+
110
+ const { result } = renderHook(
111
+ () => useArtifactInspection(fileArtifact("agent.yaml"), "aex_1", "acme"),
112
+ { wrapper: wrapperFor(stigmer) },
113
+ );
114
+
115
+ await waitFor(() => expect(result.current.isDetected).toBe(true));
116
+ expect(result.current.detectionLabel).toBe("Agent detected");
117
+ expect(result.current.ctaLabel).toBe("Apply to acme");
118
+ });
119
+
120
+ it("applies a detected Agent and fires onApplied with the result", async () => {
121
+ const getArtifactContent = vi.fn().mockResolvedValue(contentResult(AGENT_YAML));
122
+ const apply = vi.fn().mockResolvedValue({
123
+ metadata: { name: "my-agent", org: "acme", slug: "my-agent" },
124
+ });
125
+ const stigmer = {
126
+ agentExecution: { getArtifactContent },
127
+ agent: { apply },
128
+ } as unknown as Stigmer;
129
+ const onApplied = vi.fn();
130
+
131
+ const { result } = renderHook(
132
+ () =>
133
+ useArtifactInspection(fileArtifact("agent.yaml"), "aex_1", "acme", {
134
+ onApplied,
135
+ }),
136
+ { wrapper: wrapperFor(stigmer) },
137
+ );
138
+
139
+ await waitFor(() => expect(result.current.isDetected).toBe(true));
140
+
141
+ await act(async () => {
142
+ await result.current.apply();
143
+ });
144
+
145
+ expect(apply).toHaveBeenCalledTimes(1);
146
+ expect(apply.mock.calls[0][0].name).toBe("my-agent");
147
+ await waitFor(() => expect(result.current.applyResult?.kind).toBe("Agent"));
148
+ expect(onApplied).toHaveBeenCalledTimes(1);
149
+ expect(onApplied.mock.calls[0][0].name).toBe("my-agent");
150
+ });
151
+ });
@@ -83,3 +83,38 @@ describe("useCreateAgentExecution — executionConfig mapping", () => {
83
83
  expect(mockCreate.mock.calls[0][0].executionConfig).toBeUndefined();
84
84
  });
85
85
  });
86
+
87
+ describe("useCreateAgentExecution — supersede link (edit-and-resubmit)", () => {
88
+ it("maps supersedesExecutionId into the create call", async () => {
89
+ const { result } = renderHook(() => useCreateAgentExecution(), {
90
+ wrapper: createWrapper(makeMockClient()),
91
+ });
92
+
93
+ await act(async () => {
94
+ await result.current.create({
95
+ org: "acme",
96
+ sessionId: "ses-1",
97
+ message: "corrected message",
98
+ supersedesExecutionId: "aex-old",
99
+ });
100
+ });
101
+
102
+ expect(mockCreate.mock.calls[0][0].supersedesExecutionId).toBe("aex-old");
103
+ });
104
+
105
+ it("leaves supersedesExecutionId undefined for ordinary sends", async () => {
106
+ const { result } = renderHook(() => useCreateAgentExecution(), {
107
+ wrapper: createWrapper(makeMockClient()),
108
+ });
109
+
110
+ await act(async () => {
111
+ await result.current.create({
112
+ org: "acme",
113
+ sessionId: "ses-1",
114
+ message: "Hello",
115
+ });
116
+ });
117
+
118
+ expect(mockCreate.mock.calls[0][0].supersedesExecutionId).toBeUndefined();
119
+ });
120
+ });
@@ -177,6 +177,30 @@ export function changeForRowPath(
177
177
  return null;
178
178
  }
179
179
 
180
+ /**
181
+ * The captured change a stamped transcript row should render as its inline
182
+ * diff, or `null` to keep the row's own content view. The row-diff sibling of
183
+ * {@link fileReviewRowState}: both resolve through {@link changeForRowPath},
184
+ * so the badge and the diff can never disagree about which file a row refers
185
+ * to. What renders is the set's captured NET change for the file — the same
186
+ * artifact the badge's verdict applies to.
187
+ *
188
+ * `null` follows the badge's honest-degradation discipline, plus one rule of
189
+ * its own: a non-`reviewable` change ({@link fileReviewability} — binary,
190
+ * secret-withheld, size-elided) resolves to `null` so the row falls back to
191
+ * showing the tool's own args content rather than a diff that cannot honestly
192
+ * render.
193
+ */
194
+ export function fileReviewRowChange(
195
+ set: FileChangeSet | undefined,
196
+ rowPath: string | null,
197
+ ): CapturedFileChange | null {
198
+ if (!set || !rowPath) return null;
199
+ const change = changeForRowPath(set, rowPath);
200
+ if (!change) return null;
201
+ return fileReviewability(change).kind === "reviewable" ? change : null;
202
+ }
203
+
180
204
  /**
181
205
  * The review state a stamped transcript row should badge — how this file edit
182
206
  * stands in its change set's review lifecycle. `null` means "show no badge",