@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.
- package/composer/SessionComposer.d.ts +31 -0
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +12 -4
- package/composer/SessionComposer.js.map +1 -1
- package/execution/ArtifactApplyButton.d.ts +25 -0
- package/execution/ArtifactApplyButton.d.ts.map +1 -0
- package/execution/ArtifactApplyButton.js +21 -0
- package/execution/ArtifactApplyButton.js.map +1 -0
- package/execution/ArtifactContentBody.d.ts +36 -0
- package/execution/ArtifactContentBody.d.ts.map +1 -0
- package/execution/ArtifactContentBody.js +62 -0
- package/execution/ArtifactContentBody.js.map +1 -0
- package/execution/ArtifactDocument.d.ts +48 -0
- package/execution/ArtifactDocument.d.ts.map +1 -0
- package/execution/ArtifactDocument.js +89 -0
- package/execution/ArtifactDocument.js.map +1 -0
- package/execution/ArtifactPreviewModal.d.ts +4 -4
- package/execution/ArtifactPreviewModal.d.ts.map +1 -1
- package/execution/ArtifactPreviewModal.js +15 -142
- package/execution/ArtifactPreviewModal.js.map +1 -1
- package/execution/ArtifactRow.d.ts +49 -0
- package/execution/ArtifactRow.d.ts.map +1 -0
- package/execution/ArtifactRow.js +56 -0
- package/execution/ArtifactRow.js.map +1 -0
- package/execution/ArtifactsWidget.d.ts +6 -9
- package/execution/ArtifactsWidget.d.ts.map +1 -1
- package/execution/ArtifactsWidget.js +15 -19
- package/execution/ArtifactsWidget.js.map +1 -1
- package/execution/FileReviewContext.d.ts +15 -1
- package/execution/FileReviewContext.d.ts.map +1 -1
- package/execution/FileReviewContext.js +20 -1
- package/execution/FileReviewContext.js.map +1 -1
- package/execution/MessageThread.d.ts +65 -2
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +42 -23
- package/execution/MessageThread.js.map +1 -1
- package/execution/TodoCard.d.ts +8 -0
- package/execution/TodoCard.d.ts.map +1 -1
- package/execution/TodoCard.js +6 -4
- package/execution/TodoCard.js.map +1 -1
- package/execution/TodoList.d.ts +20 -1
- package/execution/TodoList.d.ts.map +1 -1
- package/execution/TodoList.js +9 -6
- package/execution/TodoList.js.map +1 -1
- package/execution/ToolCallDetail.d.ts +3 -2
- package/execution/ToolCallDetail.d.ts.map +1 -1
- package/execution/ToolCallDetail.js +49 -16
- package/execution/ToolCallDetail.js.map +1 -1
- package/execution/file-review-status.d.ts +15 -0
- package/execution/file-review-status.d.ts.map +1 -1
- package/execution/file-review-status.js +22 -0
- package/execution/file-review-status.js.map +1 -1
- package/execution/index.d.ts +13 -7
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +7 -4
- package/execution/index.js.map +1 -1
- package/execution/useArtifactContent.d.ts +6 -1
- package/execution/useArtifactContent.d.ts.map +1 -1
- package/execution/useArtifactContent.js +7 -2
- package/execution/useArtifactContent.js.map +1 -1
- package/execution/useArtifactInspection.d.ts +85 -0
- package/execution/useArtifactInspection.d.ts.map +1 -0
- package/execution/useArtifactInspection.js +143 -0
- package/execution/useArtifactInspection.js.map +1 -0
- package/execution/useCreateAgentExecution.d.ts +11 -0
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js +1 -0
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/github/useGitHubFileReader.d.ts +4 -2
- package/github/useGitHubFileReader.d.ts.map +1 -1
- package/github/useGitHubFileReader.js +16 -3
- package/github/useGitHubFileReader.js.map +1 -1
- package/github/useGitHubTreeLister.d.ts +2 -1
- package/github/useGitHubTreeLister.d.ts.map +1 -1
- package/github/useGitHubTreeLister.js +7 -3
- package/github/useGitHubTreeLister.js.map +1 -1
- package/index.d.ts +4 -4
- package/index.d.ts.map +1 -1
- package/index.js +3 -3
- package/index.js.map +1 -1
- package/internal/MermaidDiagram.d.ts +47 -0
- package/internal/MermaidDiagram.d.ts.map +1 -0
- package/internal/MermaidDiagram.js +118 -0
- package/internal/MermaidDiagram.js.map +1 -0
- package/internal/VirtualizedThread.d.ts +3 -2
- package/internal/VirtualizedThread.d.ts.map +1 -1
- package/internal/VirtualizedThread.js +3 -2
- package/internal/VirtualizedThread.js.map +1 -1
- package/internal/markdown-components.d.ts.map +1 -1
- package/internal/markdown-components.js +34 -0
- package/internal/markdown-components.js.map +1 -1
- package/internal/mermaid-loader.d.ts +26 -0
- package/internal/mermaid-loader.d.ts.map +1 -0
- package/internal/mermaid-loader.js +34 -0
- package/internal/mermaid-loader.js.map +1 -0
- package/internal/mermaid-theme-css.d.ts +41 -0
- package/internal/mermaid-theme-css.d.ts.map +1 -0
- package/internal/mermaid-theme-css.js +179 -0
- package/internal/mermaid-theme-css.js.map +1 -0
- package/internal/slug.d.ts +9 -5
- package/internal/slug.d.ts.map +1 -1
- package/internal/slug.js +10 -6
- package/internal/slug.js.map +1 -1
- package/package.json +5 -4
- package/session/SessionViewer.d.ts +11 -1
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +101 -21
- package/session/SessionViewer.js.map +1 -1
- package/session/artifact-document.d.ts +13 -0
- package/session/artifact-document.d.ts.map +1 -0
- package/session/artifact-document.js +22 -0
- package/session/artifact-document.js.map +1 -0
- package/session/facets/ArtifactsTab.d.ts +30 -4
- package/session/facets/ArtifactsTab.d.ts.map +1 -1
- package/session/facets/ArtifactsTab.js +32 -10
- package/session/facets/ArtifactsTab.js.map +1 -1
- package/session/index.d.ts +3 -1
- package/session/index.d.ts.map +1 -1
- package/session/index.js +5 -1
- package/session/index.js.map +1 -1
- package/session/useSessionArtifacts.d.ts +17 -0
- package/session/useSessionArtifacts.d.ts.map +1 -1
- package/session/useSessionArtifacts.js +16 -5
- package/session/useSessionArtifacts.js.map +1 -1
- package/session/useSessionConversation.d.ts +17 -1
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +32 -4
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +1 -0
- package/session/useSessionPageFlow.js.map +1 -1
- package/session/useSessionPanel.d.ts +20 -0
- package/session/useSessionPanel.d.ts.map +1 -1
- package/session/useSessionPanel.js +13 -0
- package/session/useSessionPanel.js.map +1 -1
- package/session/useSessionRailViews.d.ts +13 -1
- package/session/useSessionRailViews.d.ts.map +1 -1
- package/session/useSessionRailViews.js +5 -3
- package/session/useSessionRailViews.js.map +1 -1
- package/session/useWorkspaceReadRefs.d.ts +45 -0
- package/session/useWorkspaceReadRefs.d.ts.map +1 -0
- package/session/useWorkspaceReadRefs.js +104 -0
- package/session/useWorkspaceReadRefs.js.map +1 -0
- package/src/composer/SessionComposer.tsx +69 -1
- package/src/composer/__tests__/SessionComposer-editing.test.tsx +128 -0
- package/src/execution/ArtifactApplyButton.tsx +79 -0
- package/src/execution/ArtifactContentBody.tsx +255 -0
- package/src/execution/ArtifactDocument.tsx +344 -0
- package/src/execution/ArtifactPreviewModal.tsx +39 -405
- package/src/execution/ArtifactRow.tsx +165 -0
- package/src/execution/ArtifactsWidget.tsx +24 -33
- package/src/execution/FileReviewContext.ts +27 -1
- package/src/execution/MessageThread.tsx +110 -22
- package/src/execution/TodoCard.tsx +20 -4
- package/src/execution/TodoList.tsx +26 -4
- package/src/execution/ToolCallDetail.tsx +69 -16
- package/src/execution/__tests__/ArtifactContentBody.test.tsx +131 -0
- package/src/execution/__tests__/ArtifactDocument.test.tsx +100 -0
- package/src/execution/__tests__/ArtifactRow.test.tsx +139 -0
- package/src/execution/__tests__/ArtifactsWidget.test.tsx +78 -0
- package/src/execution/__tests__/ToolCallDetail.test.tsx +188 -2
- package/src/execution/__tests__/file-review-status.test.ts +47 -0
- package/src/execution/__tests__/message-entry.test.tsx +50 -2
- package/src/execution/__tests__/message-thread-slots.test.tsx +463 -0
- package/src/execution/__tests__/useArtifactInspection.test.tsx +151 -0
- package/src/execution/__tests__/useCreateAgentExecution.test.tsx +35 -0
- package/src/execution/file-review-status.ts +24 -0
- package/src/execution/index.ts +27 -5
- package/src/execution/useArtifactContent.ts +7 -0
- package/src/execution/useArtifactInspection.ts +252 -0
- package/src/execution/useCreateAgentExecution.ts +12 -0
- package/src/github/__tests__/useGitHubFileReader.test.ts +35 -2
- package/src/github/__tests__/useGitHubTreeLister.test.ts +14 -0
- package/src/github/useGitHubFileReader.ts +19 -6
- package/src/github/useGitHubTreeLister.ts +7 -3
- package/src/index.ts +15 -2
- package/src/internal/MermaidDiagram.tsx +164 -0
- package/src/internal/VirtualizedThread.tsx +5 -1
- package/src/internal/__tests__/a11y/mermaid-theme.a11y.test.tsx +122 -0
- package/src/internal/__tests__/markdown-components.test.tsx +93 -1
- package/src/internal/__tests__/mermaid-diagram.test.tsx +282 -0
- package/src/internal/markdown-components.tsx +36 -1
- package/src/internal/mermaid-loader.ts +44 -0
- package/src/internal/mermaid-theme-css.ts +178 -0
- package/src/internal/slug.ts +10 -6
- package/src/session/SessionViewer.tsx +204 -35
- package/src/session/__tests__/SessionViewer-artifactTabs.test.tsx +210 -0
- package/src/session/__tests__/SessionViewer-editResubmit.test.tsx +233 -0
- package/src/session/__tests__/useSessionArtifacts.test.ts +57 -0
- package/src/session/__tests__/useSessionConversation.test.tsx +172 -1
- package/src/session/__tests__/useSessionPanel.test.ts +70 -1
- package/src/session/__tests__/useWorkspaceReadRefs.test.ts +245 -0
- package/src/session/artifact-document.ts +23 -0
- package/src/session/facets/ArtifactsTab.tsx +66 -18
- package/src/session/facets/__tests__/ArtifactsTab.test.tsx +111 -47
- package/src/session/index.ts +7 -1
- package/src/session/useSessionArtifacts.ts +16 -5
- package/src/session/useSessionConversation.ts +48 -6
- package/src/session/useSessionPageFlow.ts +1 -0
- package/src/session/useSessionPanel.ts +41 -0
- package/src/session/useSessionRailViews.tsx +21 -1
- package/src/session/useWorkspaceReadRefs.ts +118 -0
- package/src/workspace/FileViewer.tsx +144 -38
- package/src/workspace/WorkspaceFileReader.ts +21 -0
- package/src/workspace/__tests__/FileViewer.test.tsx +138 -3
- package/src/workspace/__tests__/useWorkspaceFileContent.test.tsx +20 -0
- package/src/workspace/__tests__/workspaceListingCache.test.ts +114 -0
- package/src/workspace/index.ts +4 -1
- package/src/workspace/useWorkspaceEntries.ts +11 -0
- package/src/workspace/useWorkspaceFileContent.ts +4 -1
- package/src/workspace/useWorkspaceFileSearch.ts +8 -3
- package/src/workspace/useWorkspaceFiles.ts +3 -2
- package/src/workspace/workspaceListingCache.ts +21 -7
- package/styles.css +1 -1
- package/workspace/FileViewer.d.ts +1 -1
- package/workspace/FileViewer.d.ts.map +1 -1
- package/workspace/FileViewer.js +55 -5
- package/workspace/FileViewer.js.map +1 -1
- package/workspace/WorkspaceFileReader.d.ts +17 -0
- package/workspace/WorkspaceFileReader.d.ts.map +1 -1
- package/workspace/WorkspaceFileReader.js +18 -0
- package/workspace/WorkspaceFileReader.js.map +1 -1
- package/workspace/index.d.ts +1 -1
- package/workspace/index.d.ts.map +1 -1
- package/workspace/index.js +1 -1
- package/workspace/index.js.map +1 -1
- package/workspace/useWorkspaceEntries.d.ts +11 -0
- package/workspace/useWorkspaceEntries.d.ts.map +1 -1
- package/workspace/useWorkspaceEntries.js.map +1 -1
- package/workspace/useWorkspaceFileContent.d.ts.map +1 -1
- package/workspace/useWorkspaceFileContent.js +4 -1
- package/workspace/useWorkspaceFileContent.js.map +1 -1
- package/workspace/useWorkspaceFileSearch.d.ts.map +1 -1
- package/workspace/useWorkspaceFileSearch.js +8 -3
- package/workspace/useWorkspaceFileSearch.js.map +1 -1
- package/workspace/useWorkspaceFiles.d.ts +2 -1
- package/workspace/useWorkspaceFiles.d.ts.map +1 -1
- package/workspace/useWorkspaceFiles.js +3 -2
- package/workspace/useWorkspaceFiles.js.map +1 -1
- package/workspace/workspaceListingCache.d.ts +1 -1
- package/workspace/workspaceListingCache.d.ts.map +1 -1
- package/workspace/workspaceListingCache.js +20 -7
- package/workspace/workspaceListingCache.js.map +1 -1
- package/execution/ArtifactCard.d.ts +0 -81
- package/execution/ArtifactCard.d.ts.map +0 -1
- package/execution/ArtifactCard.js +0 -130
- package/execution/ArtifactCard.js.map +0 -1
- 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",
|