@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,233 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { render, cleanup, act } from "@testing-library/react";
|
|
3
|
+
import { forwardRef, useImperativeHandle } from "react";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Wiring-contract test for edit-and-resubmit (stigmer/stigmer#181): the
|
|
7
|
+
// onEditMessage SessionViewer hands to MessageThread stops the in-flight
|
|
8
|
+
// turn, prefills the composer, and enters editing mode; submitting while
|
|
9
|
+
// editing attaches the superseded execution's id to the submit context;
|
|
10
|
+
// cancelling drops the link so the next send appends normally. MessageThread
|
|
11
|
+
// and SessionComposer render as prop-capturing probes; the viewer's editing
|
|
12
|
+
// state machine is the subject under test.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
type CapturedProps = Record<string, unknown>;
|
|
16
|
+
|
|
17
|
+
const threadProps: CapturedProps[] = [];
|
|
18
|
+
vi.mock("../../execution/MessageThread", () => ({
|
|
19
|
+
MessageThread: (props: CapturedProps) => {
|
|
20
|
+
threadProps.push(props);
|
|
21
|
+
return <div data-testid="thread-probe" />;
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("../../workspace/WorkspaceSurface", () => ({
|
|
26
|
+
WorkspaceSurface: () => <div data-testid="surface-probe" />,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const composerProps: CapturedProps[] = [];
|
|
30
|
+
const composerSetMessage = vi.fn();
|
|
31
|
+
const composerFocus = vi.fn();
|
|
32
|
+
vi.mock("../../composer", async (importOriginal) => {
|
|
33
|
+
const actual = await importOriginal<typeof import("../../composer")>();
|
|
34
|
+
const ComposerProbe = forwardRef<unknown, CapturedProps>(
|
|
35
|
+
function ComposerProbe(props, ref) {
|
|
36
|
+
useImperativeHandle(ref, () => ({
|
|
37
|
+
setMessage: composerSetMessage,
|
|
38
|
+
focus: composerFocus,
|
|
39
|
+
submit: vi.fn(),
|
|
40
|
+
}));
|
|
41
|
+
composerProps.push(props);
|
|
42
|
+
return <div data-testid="composer-probe" />;
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
return { ...actual, SessionComposer: ComposerProbe };
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const stubConv = {
|
|
49
|
+
session: { spec: {} },
|
|
50
|
+
isLoading: false,
|
|
51
|
+
loadError: null,
|
|
52
|
+
completedExecutions: [],
|
|
53
|
+
activeStreamExecution: { metadata: { id: "exec-old" } },
|
|
54
|
+
pendingUserMessage: null,
|
|
55
|
+
workspaceEntries: [],
|
|
56
|
+
fileChangeSets: [],
|
|
57
|
+
submitFileDecision: vi.fn(),
|
|
58
|
+
submittingFileDecisionKeys: new Set<string>(),
|
|
59
|
+
fileDecisionErrors: new Map<string, Error>(),
|
|
60
|
+
sendError: null,
|
|
61
|
+
stopError: null,
|
|
62
|
+
streamError: null,
|
|
63
|
+
reconnectStream: vi.fn(),
|
|
64
|
+
submittingApprovalIds: new Set<string>(),
|
|
65
|
+
approvalErrors: new Map<string, Error>(),
|
|
66
|
+
isStoppable: true,
|
|
67
|
+
isStopping: false,
|
|
68
|
+
isSending: false,
|
|
69
|
+
canSendFollowUp: false,
|
|
70
|
+
isReconnecting: false,
|
|
71
|
+
connectTimedOut: false,
|
|
72
|
+
isSlow: false,
|
|
73
|
+
stop: vi.fn().mockResolvedValue(undefined),
|
|
74
|
+
retryLastSend: vi.fn(),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const stubSessionPageFlow = {
|
|
78
|
+
conv: stubConv,
|
|
79
|
+
harness: "native" as const,
|
|
80
|
+
executionTarget: undefined,
|
|
81
|
+
model: [undefined, vi.fn()] as const,
|
|
82
|
+
interactionMode: ["agent" as const, vi.fn()] as const,
|
|
83
|
+
agentRef: { org: "acme", slug: "support-bot" },
|
|
84
|
+
setAgentRef: vi.fn(),
|
|
85
|
+
resolution: null,
|
|
86
|
+
setResolution: vi.fn(),
|
|
87
|
+
isDefaultAgent: false,
|
|
88
|
+
mcpServerUsages: [],
|
|
89
|
+
setMcpServerUsages: vi.fn(),
|
|
90
|
+
skillRefs: [],
|
|
91
|
+
setSkillRefs: vi.fn(),
|
|
92
|
+
workspace: {
|
|
93
|
+
entries: [],
|
|
94
|
+
hasEntries: false,
|
|
95
|
+
toInput: vi.fn().mockReturnValue([]),
|
|
96
|
+
addGitRepo: vi.fn(),
|
|
97
|
+
addLocalPath: vi.fn(),
|
|
98
|
+
remove: vi.fn(),
|
|
99
|
+
clear: vi.fn(),
|
|
100
|
+
clearLocal: vi.fn(),
|
|
101
|
+
},
|
|
102
|
+
sessionVariables: { entries: [], isEmpty: true, clear: vi.fn() },
|
|
103
|
+
autoApproveAll: false,
|
|
104
|
+
setAutoApproveAll: vi.fn(),
|
|
105
|
+
handleSubmit: vi.fn(),
|
|
106
|
+
submitError: null as Error | null,
|
|
107
|
+
displayExecution: null,
|
|
108
|
+
allExecutions: [],
|
|
109
|
+
sandboxWorkspaceRoot: "/home/daytona/workspace",
|
|
110
|
+
};
|
|
111
|
+
vi.mock("../useSessionPageFlow", () => ({
|
|
112
|
+
useSessionPageFlow: () => stubSessionPageFlow,
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
vi.mock("../../hooks", () => ({
|
|
116
|
+
useStigmer: () => ({
|
|
117
|
+
agentExecution: {
|
|
118
|
+
uploadAttachment: vi.fn(),
|
|
119
|
+
getArtifactContent: vi.fn(),
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
import { SessionViewer } from "../SessionViewer";
|
|
125
|
+
|
|
126
|
+
/** The onEditMessage handler threaded down to the (mocked) MessageThread. */
|
|
127
|
+
function capturedOnEditMessage(): ((text: string) => void) | undefined {
|
|
128
|
+
return threadProps.at(-1)?.onEditMessage as
|
|
129
|
+
| ((text: string) => void)
|
|
130
|
+
| undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The latest props the (mocked) SessionComposer received. */
|
|
134
|
+
function latestComposerProps(): CapturedProps {
|
|
135
|
+
return composerProps.at(-1) ?? {};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function submitViaComposer(message: string): void {
|
|
139
|
+
const onSubmit = latestComposerProps().onSubmit as (
|
|
140
|
+
m: string,
|
|
141
|
+
model?: string,
|
|
142
|
+
context?: Record<string, unknown>,
|
|
143
|
+
) => void;
|
|
144
|
+
onSubmit(message, undefined, { interactionMode: "agent" });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
threadProps.length = 0;
|
|
149
|
+
composerProps.length = 0;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
afterEach(() => {
|
|
153
|
+
cleanup();
|
|
154
|
+
vi.clearAllMocks();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("SessionViewer — edit-and-resubmit wiring", () => {
|
|
158
|
+
it("wires onEditMessage to the thread while the turn is stoppable", () => {
|
|
159
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
160
|
+
expect(typeof capturedOnEditMessage()).toBe("function");
|
|
161
|
+
expect(latestComposerProps().isEditing).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("Edit stops the turn, prefills the composer, and enters editing mode", () => {
|
|
165
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
166
|
+
|
|
167
|
+
act(() => {
|
|
168
|
+
capturedOnEditMessage()!("original message");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(stubConv.stop).toHaveBeenCalledTimes(1);
|
|
172
|
+
expect(composerSetMessage).toHaveBeenCalledWith("original message");
|
|
173
|
+
expect(composerFocus).toHaveBeenCalledTimes(1);
|
|
174
|
+
expect(latestComposerProps().isEditing).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("submitting while editing attaches the supersede link and exits editing", () => {
|
|
178
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
179
|
+
|
|
180
|
+
act(() => {
|
|
181
|
+
capturedOnEditMessage()!("original message");
|
|
182
|
+
});
|
|
183
|
+
act(() => {
|
|
184
|
+
submitViaComposer("corrected message");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(stubSessionPageFlow.handleSubmit).toHaveBeenCalledTimes(1);
|
|
188
|
+
const [message, , context] =
|
|
189
|
+
stubSessionPageFlow.handleSubmit.mock.calls[0];
|
|
190
|
+
expect(message).toBe("corrected message");
|
|
191
|
+
expect(context).toMatchObject({
|
|
192
|
+
supersedesExecutionId: "exec-old",
|
|
193
|
+
// Composer-provided context fields survive the merge.
|
|
194
|
+
interactionMode: "agent",
|
|
195
|
+
});
|
|
196
|
+
expect(latestComposerProps().isEditing).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("cancelling the edit clears the composer and drops the supersede link", () => {
|
|
200
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
201
|
+
|
|
202
|
+
act(() => {
|
|
203
|
+
capturedOnEditMessage()!("original message");
|
|
204
|
+
});
|
|
205
|
+
expect(latestComposerProps().isEditing).toBe(true);
|
|
206
|
+
|
|
207
|
+
act(() => {
|
|
208
|
+
(latestComposerProps().onCancelEdit as () => void)();
|
|
209
|
+
});
|
|
210
|
+
expect(latestComposerProps().isEditing).toBe(false);
|
|
211
|
+
expect(composerSetMessage).toHaveBeenLastCalledWith("");
|
|
212
|
+
|
|
213
|
+
// A subsequent unrelated send must NOT carry the supersede link.
|
|
214
|
+
act(() => {
|
|
215
|
+
submitViaComposer("unrelated new message");
|
|
216
|
+
});
|
|
217
|
+
const [, , context] = stubSessionPageFlow.handleSubmit.mock.calls[0];
|
|
218
|
+
expect(context).not.toHaveProperty("supersedesExecutionId");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("ordinary sends pass the composer context through untouched", () => {
|
|
222
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
223
|
+
|
|
224
|
+
act(() => {
|
|
225
|
+
submitViaComposer("plain message");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const [message, , context] =
|
|
229
|
+
stubSessionPageFlow.handleSubmit.mock.calls[0];
|
|
230
|
+
expect(message).toBe("plain message");
|
|
231
|
+
expect(context).toEqual({ interactionMode: "agent" });
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import { renderHook, cleanup } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
5
|
+
import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
6
|
+
import {
|
|
7
|
+
ExecutionArtifactKind,
|
|
8
|
+
ExecutionPhase,
|
|
9
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
10
|
+
import { useSessionArtifacts, artifactKey } from "../useSessionArtifacts";
|
|
11
|
+
|
|
12
|
+
function artifact(opts: { name: string; sandboxPath?: string }) {
|
|
13
|
+
return create(ExecutionArtifactSchema, {
|
|
14
|
+
name: opts.name,
|
|
15
|
+
kind: ExecutionArtifactKind.FILE,
|
|
16
|
+
sizeBytes: 8n,
|
|
17
|
+
...(opts.sandboxPath ? { sandboxPath: opts.sandboxPath } : {}),
|
|
18
|
+
storageKey: `artifacts/aex/${opts.name}`,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
afterEach(cleanup);
|
|
23
|
+
|
|
24
|
+
describe("artifactKey", () => {
|
|
25
|
+
it("uses sandbox_path when present (its filesystem identity)", () => {
|
|
26
|
+
expect(artifactKey(artifact({ name: "a.md", sandboxPath: "/w/dir/a.md" }))).toBe(
|
|
27
|
+
"/w/dir/a.md",
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("falls back to name for artifacts without a sandbox path", () => {
|
|
32
|
+
expect(artifactKey(artifact({ name: "a.md" }))).toBe("a.md");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("is the key useSessionArtifacts dedups on (same key → latest wins)", () => {
|
|
36
|
+
const older = create(AgentExecutionSchema, {
|
|
37
|
+
metadata: { id: "aex_1" },
|
|
38
|
+
status: {
|
|
39
|
+
phase: ExecutionPhase.EXECUTION_COMPLETED,
|
|
40
|
+
artifacts: [artifact({ name: "a.md", sandboxPath: "/w/a.md" })],
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const newer = create(AgentExecutionSchema, {
|
|
44
|
+
metadata: { id: "aex_2" },
|
|
45
|
+
status: {
|
|
46
|
+
phase: ExecutionPhase.EXECUTION_COMPLETED,
|
|
47
|
+
artifacts: [artifact({ name: "a.md", sandboxPath: "/w/a.md" })],
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const { result } = renderHook(() => useSessionArtifacts([older, newer]));
|
|
52
|
+
expect(result.current.artifacts).toHaveLength(1);
|
|
53
|
+
// The later execution's version wins for a shared key.
|
|
54
|
+
expect(result.current.artifacts[0].executionId).toBe("aex_2");
|
|
55
|
+
expect(artifactKey(result.current.artifacts[0].artifact)).toBe("/w/a.md");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
AgentExecutionStatusSchema,
|
|
8
8
|
type AgentExecution,
|
|
9
9
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
10
|
+
import { AgentExecutionSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
|
|
10
11
|
import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
11
12
|
import { SessionSchema, type Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
|
|
12
13
|
import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
|
|
@@ -25,7 +26,11 @@ function makeSession(id: string): Session {
|
|
|
25
26
|
return session;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
function makeExecution(
|
|
29
|
+
function makeExecution(
|
|
30
|
+
id: string,
|
|
31
|
+
phase: ExecutionPhase,
|
|
32
|
+
opts?: { supersedes?: string },
|
|
33
|
+
): AgentExecution {
|
|
29
34
|
const exec = create(AgentExecutionSchema);
|
|
30
35
|
const metadata = create(ApiResourceMetadataSchema);
|
|
31
36
|
metadata.id = id;
|
|
@@ -33,6 +38,11 @@ function makeExecution(id: string, phase: ExecutionPhase): AgentExecution {
|
|
|
33
38
|
const status = create(AgentExecutionStatusSchema);
|
|
34
39
|
status.phase = phase;
|
|
35
40
|
exec.status = status;
|
|
41
|
+
if (opts?.supersedes) {
|
|
42
|
+
const spec = create(AgentExecutionSpecSchema);
|
|
43
|
+
spec.supersedesExecutionId = opts.supersedes;
|
|
44
|
+
exec.spec = spec;
|
|
45
|
+
}
|
|
36
46
|
return exec;
|
|
37
47
|
}
|
|
38
48
|
|
|
@@ -549,6 +559,167 @@ describe("useSessionConversation", () => {
|
|
|
549
559
|
});
|
|
550
560
|
});
|
|
551
561
|
|
|
562
|
+
describe("useSessionConversation — supersede filtering (edit-and-resubmit)", () => {
|
|
563
|
+
let methods: MockMethods;
|
|
564
|
+
let mockStigmer: Stigmer;
|
|
565
|
+
let stream: ReturnType<typeof createControllableStream<AgentExecution>>;
|
|
566
|
+
|
|
567
|
+
beforeEach(() => {
|
|
568
|
+
stream = createControllableStream<AgentExecution>();
|
|
569
|
+
methods = {
|
|
570
|
+
sessionGet: vi.fn().mockResolvedValue(makeSession("session-1")),
|
|
571
|
+
listBySession: vi.fn().mockResolvedValue({ entries: [] }),
|
|
572
|
+
executionCreate: vi.fn(),
|
|
573
|
+
subscribe: vi.fn().mockReturnValue(stream.generator),
|
|
574
|
+
submitApproval: vi.fn().mockResolvedValue({}),
|
|
575
|
+
};
|
|
576
|
+
mockStigmer = createMockStigmer(methods);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it("hides a superseded execution from completedExecutions", async () => {
|
|
580
|
+
const cancelled = makeExecution("e1", ExecutionPhase.EXECUTION_CANCELLED);
|
|
581
|
+
const successor = makeExecution("e2", ExecutionPhase.EXECUTION_COMPLETED, {
|
|
582
|
+
supersedes: "e1",
|
|
583
|
+
});
|
|
584
|
+
methods.listBySession.mockResolvedValue({
|
|
585
|
+
entries: [cancelled, successor],
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
const { result } = renderHook(
|
|
589
|
+
() => useSessionConversation("session-1", "org"),
|
|
590
|
+
{ wrapper: createWrapper(mockStigmer) },
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
594
|
+
expect(result.current.completedExecutions).toHaveLength(1);
|
|
595
|
+
expect(result.current.completedExecutions[0]).toBe(successor);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it("hides every link of a chained supersede (A <- B <- C)", async () => {
|
|
599
|
+
const a = makeExecution("e1", ExecutionPhase.EXECUTION_CANCELLED);
|
|
600
|
+
const b = makeExecution("e2", ExecutionPhase.EXECUTION_CANCELLED, {
|
|
601
|
+
supersedes: "e1",
|
|
602
|
+
});
|
|
603
|
+
const c = makeExecution("e3", ExecutionPhase.EXECUTION_COMPLETED, {
|
|
604
|
+
supersedes: "e2",
|
|
605
|
+
});
|
|
606
|
+
methods.listBySession.mockResolvedValue({ entries: [a, b, c] });
|
|
607
|
+
|
|
608
|
+
const { result } = renderHook(
|
|
609
|
+
() => useSessionConversation("session-1", "org"),
|
|
610
|
+
{ wrapper: createWrapper(mockStigmer) },
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
614
|
+
expect(result.current.completedExecutions).toHaveLength(1);
|
|
615
|
+
expect(result.current.completedExecutions[0]).toBe(c);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it("filters via the streaming successor's link before the list refetch delivers it", async () => {
|
|
619
|
+
// The stopped turn is still the only listed execution; its successor
|
|
620
|
+
// exists only as the live stream — the supersede link must be read off
|
|
621
|
+
// the stream copy for the old turn to disappear immediately.
|
|
622
|
+
const stopped = makeExecution("e1", ExecutionPhase.EXECUTION_IN_PROGRESS);
|
|
623
|
+
methods.listBySession.mockResolvedValue({ entries: [stopped] });
|
|
624
|
+
|
|
625
|
+
const successorStream = createControllableStream<AgentExecution>();
|
|
626
|
+
methods.subscribe.mockReturnValue(successorStream.generator);
|
|
627
|
+
methods.executionCreate.mockResolvedValue(
|
|
628
|
+
makeExecution("e2", ExecutionPhase.EXECUTION_PENDING),
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
const { result } = renderHook(
|
|
632
|
+
() => useSessionConversation("session-1", "org"),
|
|
633
|
+
{ wrapper: createWrapper(mockStigmer) },
|
|
634
|
+
);
|
|
635
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
636
|
+
|
|
637
|
+
await act(async () => {
|
|
638
|
+
await result.current.sendFollowUp("corrected message", {
|
|
639
|
+
supersedesExecutionId: "e1",
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
act(() => {
|
|
644
|
+
successorStream.push(
|
|
645
|
+
makeExecution("e2", ExecutionPhase.EXECUTION_IN_PROGRESS, {
|
|
646
|
+
supersedes: "e1",
|
|
647
|
+
}),
|
|
648
|
+
);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
await waitFor(() => {
|
|
652
|
+
expect(result.current.completedExecutions).toHaveLength(0);
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it("treats a dangling supersede link as a no-op", async () => {
|
|
657
|
+
const completed = makeExecution("e1", ExecutionPhase.EXECUTION_COMPLETED);
|
|
658
|
+
const successor = makeExecution("e2", ExecutionPhase.EXECUTION_COMPLETED, {
|
|
659
|
+
supersedes: "does-not-exist",
|
|
660
|
+
});
|
|
661
|
+
methods.listBySession.mockResolvedValue({
|
|
662
|
+
entries: [completed, successor],
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
const { result } = renderHook(
|
|
666
|
+
() => useSessionConversation("session-1", "org"),
|
|
667
|
+
{ wrapper: createWrapper(mockStigmer) },
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
671
|
+
expect(result.current.completedExecutions).toHaveLength(2);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("sendFollowUp forwards supersedesExecutionId to execution creation", async () => {
|
|
675
|
+
methods.listBySession.mockResolvedValue({ entries: [] });
|
|
676
|
+
methods.executionCreate.mockResolvedValue(
|
|
677
|
+
makeExecution("e2", ExecutionPhase.EXECUTION_PENDING),
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
const { result } = renderHook(
|
|
681
|
+
() => useSessionConversation("session-1", "org"),
|
|
682
|
+
{ wrapper: createWrapper(mockStigmer) },
|
|
683
|
+
);
|
|
684
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
685
|
+
|
|
686
|
+
await act(async () => {
|
|
687
|
+
await result.current.sendFollowUp("corrected message", {
|
|
688
|
+
supersedesExecutionId: "e1",
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
expect(methods.executionCreate).toHaveBeenCalledWith(
|
|
693
|
+
expect.objectContaining({
|
|
694
|
+
message: "corrected message",
|
|
695
|
+
supersedesExecutionId: "e1",
|
|
696
|
+
}),
|
|
697
|
+
);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it("keeps active-id resolution on the raw list when the superseded turn is still winding down", async () => {
|
|
701
|
+
// e1 was cancelled but has not reached a terminal phase yet; e2 (its
|
|
702
|
+
// successor) is the active turn. The display filter hides e1, but the
|
|
703
|
+
// active execution must still resolve to e2 from the UNfiltered list.
|
|
704
|
+
const windingDown = makeExecution("e1", ExecutionPhase.EXECUTION_IN_PROGRESS);
|
|
705
|
+
const successor = makeExecution("e2", ExecutionPhase.EXECUTION_IN_PROGRESS, {
|
|
706
|
+
supersedes: "e1",
|
|
707
|
+
});
|
|
708
|
+
methods.listBySession.mockResolvedValue({
|
|
709
|
+
entries: [windingDown, successor],
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
const { result } = renderHook(
|
|
713
|
+
() => useSessionConversation("session-1", "org"),
|
|
714
|
+
{ wrapper: createWrapper(mockStigmer) },
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
718
|
+
expect(result.current.activeStreamExecution?.metadata?.id).toBe("e2");
|
|
719
|
+
expect(result.current.completedExecutions).toHaveLength(0);
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
|
|
552
723
|
describe("useSessionConversation — local runner worker lifecycle", () => {
|
|
553
724
|
let methods: MockMethods;
|
|
554
725
|
let mockStigmer: Stigmer;
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { describe, it, expect, afterEach } from "vitest";
|
|
2
2
|
import { renderHook, act, cleanup } from "@testing-library/react";
|
|
3
|
-
import {
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import {
|
|
5
|
+
ExecutionArtifactKind,
|
|
6
|
+
ExecutionPhase,
|
|
7
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
8
|
+
import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
4
9
|
import { useSessionPanel, type UseSessionPanelOptions } from "../useSessionPanel";
|
|
5
10
|
import { PLAN_DOCUMENT_ENTRY_ID, PLAN_DOCUMENT_PATH } from "../plan-document";
|
|
11
|
+
import { ARTIFACT_DOCUMENT_ENTRY_ID } from "../artifact-document";
|
|
12
|
+
import { artifactKey } from "../useSessionArtifacts";
|
|
6
13
|
import type { SelectedThreadItem } from "../../internal/store/selection-store";
|
|
7
14
|
|
|
8
15
|
// ---------------------------------------------------------------------------
|
|
@@ -255,6 +262,68 @@ describe("useSessionPanel — plan document tab", () => {
|
|
|
255
262
|
});
|
|
256
263
|
});
|
|
257
264
|
|
|
265
|
+
describe("useSessionPanel — artifact document tabs", () => {
|
|
266
|
+
function artifact(sandboxPath: string, name: string) {
|
|
267
|
+
return create(ExecutionArtifactSchema, {
|
|
268
|
+
name,
|
|
269
|
+
kind: ExecutionArtifactKind.FILE,
|
|
270
|
+
sandboxPath,
|
|
271
|
+
storageKey: `artifacts/aex_1/${name}`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
it("openArtifact opens the panel with a PREVIEW tab keyed by artifactKey", () => {
|
|
276
|
+
const { result } = renderPanel();
|
|
277
|
+
const a = artifact(".stigmer/notes.md", "notes.md");
|
|
278
|
+
act(() => result.current.openArtifact(a));
|
|
279
|
+
|
|
280
|
+
expect(result.current.isOpen).toBe(true);
|
|
281
|
+
expect(result.current.editorsStore.getSnapshot().editors).toEqual([
|
|
282
|
+
{ entryId: ARTIFACT_DOCUMENT_ENTRY_ID, path: artifactKey(a), preview: true },
|
|
283
|
+
]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("reuses the single preview slot as different artifacts are browsed", () => {
|
|
287
|
+
const { result } = renderPanel();
|
|
288
|
+
act(() => result.current.openArtifact(artifact(".stigmer/a.md", "a.md")));
|
|
289
|
+
act(() => result.current.openArtifact(artifact(".stigmer/b.md", "b.md")));
|
|
290
|
+
|
|
291
|
+
// VS Code preview semantics: single-click browsing reuses one tab.
|
|
292
|
+
expect(result.current.editorsStore.getSnapshot().editors).toEqual([
|
|
293
|
+
{ entryId: ARTIFACT_DOCUMENT_ENTRY_ID, path: ".stigmer/b.md", preview: true },
|
|
294
|
+
]);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("does not evict a pinned plan tab when an artifact opens", () => {
|
|
298
|
+
const { result } = renderPanel();
|
|
299
|
+
act(() => result.current.openPlanDocument());
|
|
300
|
+
act(() => result.current.openArtifact(artifact(".stigmer/a.md", "a.md")));
|
|
301
|
+
|
|
302
|
+
expect(result.current.editorsStore.getSnapshot().editors).toEqual([
|
|
303
|
+
{ entryId: PLAN_DOCUMENT_ENTRY_ID, path: PLAN_DOCUMENT_PATH, preview: false },
|
|
304
|
+
{ entryId: ARTIFACT_DOCUMENT_ENTRY_ID, path: ".stigmer/a.md", preview: true },
|
|
305
|
+
]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("pinArtifact promotes an open artifact preview to a persistent tab", () => {
|
|
309
|
+
const { result } = renderPanel();
|
|
310
|
+
const a = artifact(".stigmer/notes.md", "notes.md");
|
|
311
|
+
// The single-click open leaves a preview tab; the double-click pins it.
|
|
312
|
+
act(() => result.current.openArtifact(a));
|
|
313
|
+
act(() => result.current.pinArtifact(a));
|
|
314
|
+
|
|
315
|
+
expect(result.current.editorsStore.getSnapshot().editors).toEqual([
|
|
316
|
+
{ entryId: ARTIFACT_DOCUMENT_ENTRY_ID, path: artifactKey(a), preview: false },
|
|
317
|
+
]);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("pinArtifact is a no-op when the artifact is not open (mirrors the store's generic pin)", () => {
|
|
321
|
+
const { result } = renderPanel();
|
|
322
|
+
act(() => result.current.pinArtifact(artifact(".stigmer/x.md", "x.md")));
|
|
323
|
+
expect(result.current.editorsStore.getSnapshot().editors).toEqual([]);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
258
327
|
describe("useSessionPanel — defaultView (home view)", () => {
|
|
259
328
|
it("seeds the view to the provided defaultView (launcher homes on Config)", () => {
|
|
260
329
|
const { result } = renderPanel({ defaultView: "configure" });
|