@stigmer/react 3.1.3 → 3.1.4
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/index.d.ts +8 -2
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +4 -1
- 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/index.d.ts +3 -3
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- 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/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.map +1 -1
- package/session/SessionViewer.js +91 -18
- 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 +2 -1
- package/session/index.d.ts.map +1 -1
- package/session/index.js +4 -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/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/__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__/message-entry.test.tsx +50 -2
- package/src/execution/__tests__/useArtifactInspection.test.tsx +151 -0
- package/src/execution/__tests__/useCreateAgentExecution.test.tsx +35 -0
- package/src/execution/index.ts +14 -2
- package/src/execution/useArtifactContent.ts +7 -0
- package/src/execution/useArtifactInspection.ts +252 -0
- package/src/execution/useCreateAgentExecution.ts +12 -0
- package/src/index.ts +11 -2
- package/src/internal/MermaidDiagram.tsx +164 -0
- 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 +177 -33
- 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/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 +5 -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/styles.css +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
|
@@ -5,9 +5,10 @@ import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexe
|
|
|
5
5
|
import { cn } from "@stigmer/theme";
|
|
6
6
|
import {
|
|
7
7
|
useSessionArtifacts,
|
|
8
|
+
artifactKey,
|
|
8
9
|
type SessionArtifactEntry,
|
|
9
10
|
} from "../session/useSessionArtifacts.js";
|
|
10
|
-
import {
|
|
11
|
+
import { ArtifactRow } from "./ArtifactRow.js";
|
|
11
12
|
import { ArtifactPreviewModal } from "./ArtifactPreviewModal.js";
|
|
12
13
|
import type { ApplyResourceResult } from "../library/useApplyResource.js";
|
|
13
14
|
|
|
@@ -45,19 +46,16 @@ export interface ArtifactsWidgetProps {
|
|
|
45
46
|
* a file-explorer-like view of the conversation's output — no
|
|
46
47
|
* execution/turn concepts are exposed.
|
|
47
48
|
*
|
|
48
|
-
*
|
|
49
|
-
* {@link
|
|
50
|
-
*
|
|
51
|
-
*
|
|
49
|
+
* The panel-less counterpart of the session panel's Artifacts facet: it
|
|
50
|
+
* renders the same dense {@link ArtifactRow} list, but — having no editor pane
|
|
51
|
+
* to open document tabs into — a click opens the {@link ArtifactPreviewModal}
|
|
52
|
+
* (the review-before-apply popup, and the sole home of the Apply/Push CTA).
|
|
53
|
+
* This is the embeddable path a platform builder drops into their own sidebar.
|
|
52
54
|
*
|
|
53
55
|
* Returns `null` when the executions list is empty or no execution
|
|
54
56
|
* has artifacts, matching the conditional-render pattern of
|
|
55
57
|
* {@link ExecutionProgress} and {@link UsageWidget}.
|
|
56
58
|
*
|
|
57
|
-
* Renders without card chrome — each {@link ArtifactCard} provides its
|
|
58
|
-
* own border and padding. The consumer controls the container styling
|
|
59
|
-
* (or places the widget directly in a flex/grid layout).
|
|
60
|
-
*
|
|
61
59
|
* All visual properties flow through `--stgm-*` tokens.
|
|
62
60
|
*
|
|
63
61
|
* @example
|
|
@@ -74,7 +72,7 @@ export interface ArtifactsWidgetProps {
|
|
|
74
72
|
* />
|
|
75
73
|
* ```
|
|
76
74
|
*
|
|
77
|
-
* @see {@link
|
|
75
|
+
* @see {@link ArtifactRow} — the dense row rendered per artifact
|
|
78
76
|
* @see {@link ArtifactPreviewModal} — full preview with Apply/Push CTA
|
|
79
77
|
* @see {@link useSessionArtifacts} — headless session-level artifact aggregation hook
|
|
80
78
|
* @see {@link useExecutionArtifacts} — headless single-execution artifact extraction hook
|
|
@@ -88,27 +86,22 @@ export function ArtifactsWidget({
|
|
|
88
86
|
const { artifacts, hasArtifacts, artifactCount } =
|
|
89
87
|
useSessionArtifacts(executions);
|
|
90
88
|
|
|
91
|
-
// Store the
|
|
92
|
-
// the
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
// updated artifact for the same path while the modal is open.
|
|
89
|
+
// Store the artifact identity (not a snapshot of the full entry). The entry is
|
|
90
|
+
// re-derived from the live artifacts list on every render, so the modal always
|
|
91
|
+
// reflects the latest artifact version — even when a newer execution publishes
|
|
92
|
+
// an updated artifact for the same path while the modal is open.
|
|
96
93
|
const [previewKey, setPreviewKey] = useState<string | null>(null);
|
|
97
94
|
|
|
98
95
|
const previewEntry = useMemo<SessionArtifactEntry | null>(
|
|
99
96
|
() =>
|
|
100
97
|
previewKey !== null
|
|
101
|
-
? artifacts.find(
|
|
102
|
-
(e) =>
|
|
103
|
-
(e.artifact.sandboxPath || e.artifact.name) === previewKey,
|
|
104
|
-
) ?? null
|
|
98
|
+
? artifacts.find((e) => artifactKey(e.artifact) === previewKey) ?? null
|
|
105
99
|
: null,
|
|
106
100
|
[previewKey, artifacts],
|
|
107
101
|
);
|
|
108
102
|
|
|
109
|
-
const
|
|
110
|
-
(entry: SessionArtifactEntry) =>
|
|
111
|
-
setPreviewKey(entry.artifact.sandboxPath || entry.artifact.name),
|
|
103
|
+
const handleOpen = useCallback(
|
|
104
|
+
(entry: SessionArtifactEntry) => setPreviewKey(artifactKey(entry.artifact)),
|
|
112
105
|
[],
|
|
113
106
|
);
|
|
114
107
|
|
|
@@ -123,19 +116,17 @@ export function ArtifactsWidget({
|
|
|
123
116
|
</span>
|
|
124
117
|
</div>
|
|
125
118
|
|
|
126
|
-
<
|
|
119
|
+
<ul role="list" className="flex flex-col">
|
|
127
120
|
{artifacts.map((entry) => (
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
/>
|
|
136
|
-
</div>
|
|
121
|
+
<ArtifactRow
|
|
122
|
+
key={artifactKey(entry.artifact)}
|
|
123
|
+
artifact={entry.artifact}
|
|
124
|
+
executionId={entry.executionId}
|
|
125
|
+
hasNameCollision={entry.hasNameCollision}
|
|
126
|
+
onOpen={() => handleOpen(entry)}
|
|
127
|
+
/>
|
|
137
128
|
))}
|
|
138
|
-
</
|
|
129
|
+
</ul>
|
|
139
130
|
|
|
140
131
|
{previewEntry && (
|
|
141
132
|
<ArtifactPreviewModal
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
5
|
+
import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
6
|
+
import { ArtifactContentBody } from "../ArtifactContentBody";
|
|
7
|
+
import type { SkillPackageDetection } from "../../library/detect-skill-package";
|
|
8
|
+
|
|
9
|
+
const NOT_SKILL: SkillPackageDetection = { detected: false };
|
|
10
|
+
|
|
11
|
+
function fileArtifact(name: string) {
|
|
12
|
+
return create(ExecutionArtifactSchema, {
|
|
13
|
+
name,
|
|
14
|
+
kind: ExecutionArtifactKind.FILE,
|
|
15
|
+
sizeBytes: 64n,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function dirArtifact(name: string, entries: string[]) {
|
|
20
|
+
return create(ExecutionArtifactSchema, {
|
|
21
|
+
name,
|
|
22
|
+
kind: ExecutionArtifactKind.DIRECTORY,
|
|
23
|
+
sizeBytes: 512n,
|
|
24
|
+
entries,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
afterEach(cleanup);
|
|
29
|
+
|
|
30
|
+
describe("ArtifactContentBody — file states", () => {
|
|
31
|
+
it("renders a loading skeleton", () => {
|
|
32
|
+
render(
|
|
33
|
+
<ArtifactContentBody
|
|
34
|
+
artifact={fileArtifact("notes.txt")}
|
|
35
|
+
content={null}
|
|
36
|
+
contentType={null}
|
|
37
|
+
isLoading
|
|
38
|
+
error={null}
|
|
39
|
+
isTruncated={false}
|
|
40
|
+
skillDetection={NOT_SKILL}
|
|
41
|
+
/>,
|
|
42
|
+
);
|
|
43
|
+
expect(screen.getByLabelText("Loading content")).toBeTruthy();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("renders an error message", () => {
|
|
47
|
+
render(
|
|
48
|
+
<ArtifactContentBody
|
|
49
|
+
artifact={fileArtifact("notes.txt")}
|
|
50
|
+
content={null}
|
|
51
|
+
contentType={null}
|
|
52
|
+
isLoading={false}
|
|
53
|
+
error={new Error("boom")}
|
|
54
|
+
isTruncated={false}
|
|
55
|
+
skillDetection={NOT_SKILL}
|
|
56
|
+
/>,
|
|
57
|
+
);
|
|
58
|
+
expect(screen.getByText("boom")).toBeTruthy();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("renders the binary / unavailable state when content is null and settled", () => {
|
|
62
|
+
render(
|
|
63
|
+
<ArtifactContentBody
|
|
64
|
+
artifact={fileArtifact("image.bin")}
|
|
65
|
+
content={null}
|
|
66
|
+
contentType={null}
|
|
67
|
+
isLoading={false}
|
|
68
|
+
error={null}
|
|
69
|
+
isTruncated={false}
|
|
70
|
+
skillDetection={NOT_SKILL}
|
|
71
|
+
/>,
|
|
72
|
+
);
|
|
73
|
+
expect(screen.getByText(/Content not available for preview/)).toBeTruthy();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("renders text content", () => {
|
|
77
|
+
render(
|
|
78
|
+
<ArtifactContentBody
|
|
79
|
+
artifact={fileArtifact("notes.txt")}
|
|
80
|
+
content={"hello from the artifact"}
|
|
81
|
+
contentType={"text/plain"}
|
|
82
|
+
isLoading={false}
|
|
83
|
+
error={null}
|
|
84
|
+
isTruncated={false}
|
|
85
|
+
skillDetection={NOT_SKILL}
|
|
86
|
+
/>,
|
|
87
|
+
);
|
|
88
|
+
expect(screen.getByText(/hello from the artifact/)).toBeTruthy();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("ArtifactContentBody — directory state", () => {
|
|
93
|
+
it("lists directory entries", () => {
|
|
94
|
+
render(
|
|
95
|
+
<ArtifactContentBody
|
|
96
|
+
artifact={dirArtifact("pack", ["SKILL.md", "run.sh"])}
|
|
97
|
+
content={null}
|
|
98
|
+
contentType={null}
|
|
99
|
+
isLoading={false}
|
|
100
|
+
error={null}
|
|
101
|
+
isTruncated={false}
|
|
102
|
+
skillDetection={NOT_SKILL}
|
|
103
|
+
/>,
|
|
104
|
+
);
|
|
105
|
+
expect(screen.getByText("Files (2)")).toBeTruthy();
|
|
106
|
+
expect(screen.getByText("SKILL.md")).toBeTruthy();
|
|
107
|
+
expect(screen.getByText("run.sh")).toBeTruthy();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("shows the skill banner when a skill package is detected", () => {
|
|
111
|
+
const detection = {
|
|
112
|
+
detected: true,
|
|
113
|
+
skillName: "My Skill",
|
|
114
|
+
skillDescription: "does things",
|
|
115
|
+
fileCount: 2,
|
|
116
|
+
} as unknown as SkillPackageDetection;
|
|
117
|
+
render(
|
|
118
|
+
<ArtifactContentBody
|
|
119
|
+
artifact={dirArtifact("pack", ["SKILL.md"])}
|
|
120
|
+
content={null}
|
|
121
|
+
contentType={null}
|
|
122
|
+
isLoading={false}
|
|
123
|
+
error={null}
|
|
124
|
+
isTruncated={false}
|
|
125
|
+
skillDetection={detection}
|
|
126
|
+
/>,
|
|
127
|
+
);
|
|
128
|
+
expect(screen.getByText("My Skill")).toBeTruthy();
|
|
129
|
+
expect(screen.getByText("does things")).toBeTruthy();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup, waitFor } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import type { Stigmer } from "@stigmer/sdk";
|
|
5
|
+
import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
6
|
+
import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
7
|
+
import { StigmerContext } from "../../context";
|
|
8
|
+
import { ArtifactDocument } from "../ArtifactDocument";
|
|
9
|
+
|
|
10
|
+
function fileArtifact(name: string) {
|
|
11
|
+
return create(ExecutionArtifactSchema, {
|
|
12
|
+
name,
|
|
13
|
+
kind: ExecutionArtifactKind.FILE,
|
|
14
|
+
sizeBytes: 64n,
|
|
15
|
+
storageKey: `artifacts/aex_1/${name}`,
|
|
16
|
+
contentHash: "hash-1",
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function dirArtifact(name: string) {
|
|
21
|
+
return create(ExecutionArtifactSchema, {
|
|
22
|
+
name,
|
|
23
|
+
kind: ExecutionArtifactKind.DIRECTORY,
|
|
24
|
+
sizeBytes: 512n,
|
|
25
|
+
storageKey: `artifacts/aex_1/${name}`,
|
|
26
|
+
entries: ["a.txt", "b.txt"],
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stigmerReturning(text: string): Stigmer {
|
|
31
|
+
return {
|
|
32
|
+
agentExecution: {
|
|
33
|
+
getArtifactContent: vi.fn().mockResolvedValue({
|
|
34
|
+
content: new TextEncoder().encode(text),
|
|
35
|
+
contentType: "text/plain",
|
|
36
|
+
truncated: false,
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
} as unknown as Stigmer;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderDoc(stigmer: Stigmer, artifact = fileArtifact("notes.txt")) {
|
|
43
|
+
return render(
|
|
44
|
+
<StigmerContext.Provider value={stigmer}>
|
|
45
|
+
<ArtifactDocument
|
|
46
|
+
artifact={artifact}
|
|
47
|
+
executionId="aex_1"
|
|
48
|
+
org="acme"
|
|
49
|
+
isTerminal
|
|
50
|
+
/>
|
|
51
|
+
</StigmerContext.Provider>,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const AGENT_YAML = [
|
|
56
|
+
"apiVersion: agentic.stigmer.ai/v1",
|
|
57
|
+
"kind: Agent",
|
|
58
|
+
"metadata:",
|
|
59
|
+
" name: my-agent",
|
|
60
|
+
"spec: {}",
|
|
61
|
+
"",
|
|
62
|
+
].join("\n");
|
|
63
|
+
|
|
64
|
+
afterEach(cleanup);
|
|
65
|
+
|
|
66
|
+
describe("ArtifactDocument", () => {
|
|
67
|
+
it("renders the toolbar (name + Download) and the fetched content", async () => {
|
|
68
|
+
renderDoc(stigmerReturning("hello document"));
|
|
69
|
+
|
|
70
|
+
expect(screen.getByRole("article", { name: "Artifact notes.txt" })).toBeTruthy();
|
|
71
|
+
expect(screen.getByText("notes.txt")).toBeTruthy();
|
|
72
|
+
expect(screen.getByText("Download")).toBeTruthy();
|
|
73
|
+
await waitFor(() => expect(screen.getByText(/hello document/)).toBeTruthy());
|
|
74
|
+
expect(screen.getByText("Copy")).toBeTruthy();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("surfaces resource detection and an Apply CTA for a detected Agent", async () => {
|
|
78
|
+
renderDoc(stigmerReturning(AGENT_YAML), fileArtifact("agent.yaml"));
|
|
79
|
+
|
|
80
|
+
await waitFor(() => expect(screen.getByText("Agent detected")).toBeTruthy());
|
|
81
|
+
expect(screen.getByText("Apply to acme")).toBeTruthy();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("renders a directory artifact with a Download ZIP action and no Copy", () => {
|
|
85
|
+
render(
|
|
86
|
+
<StigmerContext.Provider value={stigmerReturning("")}>
|
|
87
|
+
<ArtifactDocument
|
|
88
|
+
artifact={dirArtifact("pack")}
|
|
89
|
+
executionId="aex_1"
|
|
90
|
+
org="acme"
|
|
91
|
+
isTerminal
|
|
92
|
+
/>
|
|
93
|
+
</StigmerContext.Provider>,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(screen.getByText("Download ZIP")).toBeTruthy();
|
|
97
|
+
expect(screen.queryByText("Copy")).toBeNull();
|
|
98
|
+
expect(screen.getByText("Files (2)")).toBeTruthy();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import type { Stigmer } from "@stigmer/sdk";
|
|
5
|
+
import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
6
|
+
import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
7
|
+
import { StigmerContext } from "../../context";
|
|
8
|
+
import { ArtifactRow } from "../ArtifactRow";
|
|
9
|
+
|
|
10
|
+
function fileArtifact(name: string, sandboxPath = `.stigmer/${name}`) {
|
|
11
|
+
return create(ExecutionArtifactSchema, {
|
|
12
|
+
name,
|
|
13
|
+
kind: ExecutionArtifactKind.FILE,
|
|
14
|
+
sizeBytes: 2048n,
|
|
15
|
+
sandboxPath,
|
|
16
|
+
storageKey: `artifacts/aex_1/${name}`,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function dirArtifact(name: string) {
|
|
21
|
+
return create(ExecutionArtifactSchema, {
|
|
22
|
+
name,
|
|
23
|
+
kind: ExecutionArtifactKind.DIRECTORY,
|
|
24
|
+
sizeBytes: 4096n,
|
|
25
|
+
sandboxPath: `.stigmer/${name}`,
|
|
26
|
+
storageKey: `artifacts/aex_1/${name}`,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const getArtifactDownloadUrl = vi.fn().mockResolvedValue({ downloadUrl: "" });
|
|
31
|
+
|
|
32
|
+
function createStigmerMock(): Stigmer {
|
|
33
|
+
return {
|
|
34
|
+
agentExecution: { getArtifactDownloadUrl },
|
|
35
|
+
} as unknown as Stigmer;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderRow(props: Partial<Parameters<typeof ArtifactRow>[0]> = {}) {
|
|
39
|
+
const merged = {
|
|
40
|
+
artifact: fileArtifact("notes.md"),
|
|
41
|
+
executionId: "aex_1",
|
|
42
|
+
onOpen: vi.fn(),
|
|
43
|
+
...props,
|
|
44
|
+
} as Parameters<typeof ArtifactRow>[0];
|
|
45
|
+
return render(
|
|
46
|
+
<StigmerContext.Provider value={createStigmerMock()}>
|
|
47
|
+
<ul>
|
|
48
|
+
<ArtifactRow {...merged} />
|
|
49
|
+
</ul>
|
|
50
|
+
</StigmerContext.Provider>,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
cleanup();
|
|
56
|
+
getArtifactDownloadUrl.mockClear();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("ArtifactRow — presentation", () => {
|
|
60
|
+
it("renders a file-type icon, the name, and the formatted size", () => {
|
|
61
|
+
renderRow({ artifact: fileArtifact("notes.md") });
|
|
62
|
+
const open = screen.getByText("notes.md").closest("button")!;
|
|
63
|
+
expect(open.querySelector("svg")).toBeTruthy();
|
|
64
|
+
expect(screen.getByText("2.0 KB")).toBeTruthy();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("appends a trailing slash for a directory artifact", () => {
|
|
68
|
+
renderRow({ artifact: dirArtifact("return-policy") });
|
|
69
|
+
// The name span's own text is "return-policy" + "/".
|
|
70
|
+
expect(screen.getByText("return-policy/")).toBeTruthy();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("shows the parent-directory subtitle only on a name collision", () => {
|
|
74
|
+
const artifact = fileArtifact("agent.yaml", "/workspace/configs/agent.yaml");
|
|
75
|
+
const { rerender } = renderRow({ artifact, hasNameCollision: true });
|
|
76
|
+
expect(screen.getByText("configs/")).toBeTruthy();
|
|
77
|
+
|
|
78
|
+
rerender(
|
|
79
|
+
<StigmerContext.Provider value={createStigmerMock()}>
|
|
80
|
+
<ul>
|
|
81
|
+
<ArtifactRow artifact={artifact} executionId="aex_1" onOpen={vi.fn()} />
|
|
82
|
+
</ul>
|
|
83
|
+
</StigmerContext.Provider>,
|
|
84
|
+
);
|
|
85
|
+
expect(screen.queryByText("configs/")).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("ArtifactRow — nested-interactive avoidance (DD-22)", () => {
|
|
90
|
+
it("renders the Download control as a SIBLING of the open button, never nested", () => {
|
|
91
|
+
renderRow({ artifact: fileArtifact("notes.md") });
|
|
92
|
+
const open = screen.getByText("notes.md").closest("button")!;
|
|
93
|
+
const download = screen.getByLabelText("Download notes.md");
|
|
94
|
+
expect(download.tagName).toBe("BUTTON");
|
|
95
|
+
// A <button> inside a <button> is a WCAG 4.1.2 nested-interactive violation.
|
|
96
|
+
expect(open.contains(download)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("ArtifactRow — open / activate interaction (file-tree parity)", () => {
|
|
101
|
+
it("fires onOpen on a single click of the open target", () => {
|
|
102
|
+
const onOpen = vi.fn();
|
|
103
|
+
renderRow({ onOpen });
|
|
104
|
+
fireEvent.click(screen.getByText("notes.md"));
|
|
105
|
+
expect(onOpen).toHaveBeenCalledTimes(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("fires onActivate on a double-click when provided", () => {
|
|
109
|
+
const onActivate = vi.fn();
|
|
110
|
+
renderRow({ onActivate });
|
|
111
|
+
fireEvent.doubleClick(screen.getByText("notes.md"));
|
|
112
|
+
expect(onActivate).toHaveBeenCalledTimes(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("binds no double-click handler when onActivate is omitted (harmless)", () => {
|
|
116
|
+
const onOpen = vi.fn();
|
|
117
|
+
renderRow({ onOpen });
|
|
118
|
+
// With no onActivate, a dblclick fires nothing (onOpen only responds to click).
|
|
119
|
+
fireEvent.doubleClick(screen.getByText("notes.md"));
|
|
120
|
+
expect(onOpen).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("ArtifactRow — download", () => {
|
|
125
|
+
it("mints a download URL for the artifact's storage key on Download click", () => {
|
|
126
|
+
renderRow({ artifact: fileArtifact("notes.md") });
|
|
127
|
+
fireEvent.click(screen.getByLabelText("Download notes.md"));
|
|
128
|
+
expect(getArtifactDownloadUrl).toHaveBeenCalledTimes(1);
|
|
129
|
+
expect(getArtifactDownloadUrl.mock.calls[0][0]).toMatchObject({
|
|
130
|
+
executionId: "aex_1",
|
|
131
|
+
storageKey: "artifacts/aex_1/notes.md",
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("labels the Download control 'Download ZIP' for a directory", () => {
|
|
136
|
+
renderRow({ artifact: dirArtifact("return-policy") });
|
|
137
|
+
expect(screen.getByTitle("Download ZIP")).toBeTruthy();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import type { Stigmer } from "@stigmer/sdk";
|
|
5
|
+
import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
6
|
+
import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
7
|
+
import {
|
|
8
|
+
ExecutionArtifactKind,
|
|
9
|
+
ExecutionPhase,
|
|
10
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
11
|
+
import { StigmerContext } from "../../context";
|
|
12
|
+
import { ArtifactsWidget } from "../ArtifactsWidget";
|
|
13
|
+
|
|
14
|
+
function artifact(name: string) {
|
|
15
|
+
return create(ExecutionArtifactSchema, {
|
|
16
|
+
name,
|
|
17
|
+
kind: ExecutionArtifactKind.FILE,
|
|
18
|
+
sizeBytes: 1024n,
|
|
19
|
+
sandboxPath: `.stigmer/${name}`,
|
|
20
|
+
storageKey: `artifacts/aex_1/${name}`,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const execution = create(AgentExecutionSchema, {
|
|
25
|
+
metadata: { id: "aex_1" },
|
|
26
|
+
status: {
|
|
27
|
+
phase: ExecutionPhase.EXECUTION_COMPLETED,
|
|
28
|
+
artifacts: [artifact("agent.yaml"), artifact("notes.md")],
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** Modal content fetch — keep pending so nothing rejects during the test. */
|
|
33
|
+
function createStigmerMock(): Stigmer {
|
|
34
|
+
return {
|
|
35
|
+
agentExecution: {
|
|
36
|
+
getArtifactContent: vi.fn().mockReturnValue(new Promise(() => {})),
|
|
37
|
+
getArtifactDownloadUrl: vi.fn().mockResolvedValue({ downloadUrl: "" }),
|
|
38
|
+
},
|
|
39
|
+
} as unknown as Stigmer;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderWidget(executions = [execution]) {
|
|
43
|
+
return render(
|
|
44
|
+
<StigmerContext.Provider value={createStigmerMock()}>
|
|
45
|
+
<ArtifactsWidget executions={executions} org="acme" />
|
|
46
|
+
</StigmerContext.Provider>,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
afterEach(cleanup);
|
|
51
|
+
|
|
52
|
+
describe("ArtifactsWidget — dense list (panel-less)", () => {
|
|
53
|
+
it("renders nothing when there are no artifacts", () => {
|
|
54
|
+
const { container } = renderWidget([]);
|
|
55
|
+
expect(container.firstChild).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("renders a heading, count, and one dense row per artifact (no ArtifactCard chrome)", () => {
|
|
59
|
+
renderWidget();
|
|
60
|
+
expect(screen.getByRole("heading", { name: "Artifacts" })).toBeTruthy();
|
|
61
|
+
// Count badge reflects the two deduped artifacts.
|
|
62
|
+
expect(screen.getByText("2")).toBeTruthy();
|
|
63
|
+
expect(screen.getAllByRole("listitem")).toHaveLength(2);
|
|
64
|
+
expect(screen.getByText("agent.yaml")).toBeTruthy();
|
|
65
|
+
expect(screen.getByText("notes.md")).toBeTruthy();
|
|
66
|
+
// The retired ArtifactCard rendered a "Preview" button and role="article".
|
|
67
|
+
expect(screen.queryByText("Preview")).toBeNull();
|
|
68
|
+
expect(screen.queryByRole("article")).toBeNull();
|
|
69
|
+
expect(screen.getByLabelText("Download agent.yaml")).toBeTruthy();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("opens the preview modal (the panel-less fallback) on a row click", () => {
|
|
73
|
+
renderWidget();
|
|
74
|
+
expect(document.querySelector("dialog")).toBeNull();
|
|
75
|
+
fireEvent.click(screen.getByText("notes.md"));
|
|
76
|
+
expect(document.querySelector("dialog")).toBeTruthy();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
-
import { render, cleanup } from "@testing-library/react";
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
|
+
import { render, cleanup, waitFor } from "@testing-library/react";
|
|
3
3
|
import { create } from "@bufbuild/protobuf";
|
|
4
4
|
import { AgentMessageSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
5
5
|
import { MessageType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
6
6
|
import { MessageEntry } from "../MessageEntry";
|
|
7
|
+
import type { MermaidModule } from "../../internal/mermaid-loader";
|
|
8
|
+
|
|
9
|
+
// Stub only the library loader: the real MermaidDiagram + markdown overrides
|
|
10
|
+
// run end-to-end through Streamdown, so these tests cover the full chat path
|
|
11
|
+
// from fenced source to rendered diagram.
|
|
12
|
+
vi.mock("../../internal/mermaid-loader", () => ({
|
|
13
|
+
loadMermaid: vi.fn().mockResolvedValue({
|
|
14
|
+
initialize: vi.fn(),
|
|
15
|
+
render: vi
|
|
16
|
+
.fn()
|
|
17
|
+
.mockResolvedValue({ svg: "<svg data-testid='mermaid-svg'></svg>" }),
|
|
18
|
+
} as unknown as MermaidModule),
|
|
19
|
+
}));
|
|
7
20
|
|
|
8
21
|
afterEach(cleanup);
|
|
9
22
|
|
|
@@ -169,6 +182,41 @@ describe("MessageEntry — AI messages (Streamdown)", () => {
|
|
|
169
182
|
article!.querySelectorAll('span[class*="hljs-"]').length,
|
|
170
183
|
).toBeGreaterThan(0);
|
|
171
184
|
});
|
|
185
|
+
|
|
186
|
+
it("renders a completed ```mermaid fence as a diagram, not a code block", async () => {
|
|
187
|
+
const msg = makeMessage(
|
|
188
|
+
MessageType.MESSAGE_AI,
|
|
189
|
+
"Here is the flow:\n\n```mermaid\nflowchart LR\n A --> B\n```",
|
|
190
|
+
);
|
|
191
|
+
const { container } = render(<MessageEntry message={msg} />);
|
|
192
|
+
|
|
193
|
+
const article = queryArticle(container, "AI response");
|
|
194
|
+
await waitFor(() =>
|
|
195
|
+
expect(
|
|
196
|
+
article!.querySelector('[role="img"][aria-label="Mermaid diagram"]'),
|
|
197
|
+
).not.toBeNull(),
|
|
198
|
+
);
|
|
199
|
+
expect(article!.querySelector("[data-testid='mermaid-svg']")).not.toBeNull();
|
|
200
|
+
// The fenced source is no longer shown as a code block.
|
|
201
|
+
expect(article!.querySelector("pre")).toBeNull();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("keeps an unclosed ```mermaid fence as a code block while streaming", () => {
|
|
205
|
+
const msg = makeMessage(
|
|
206
|
+
MessageType.MESSAGE_AI,
|
|
207
|
+
"```mermaid\nflowchart LR\n A --> B",
|
|
208
|
+
{ isStreaming: true },
|
|
209
|
+
);
|
|
210
|
+
const { container } = render(<MessageEntry message={msg} />);
|
|
211
|
+
|
|
212
|
+
const article = queryArticle(container, "AI response");
|
|
213
|
+
// Mid-stream the source stays a plain code block — no diagram, and no
|
|
214
|
+
// half-parsed error flashing.
|
|
215
|
+
expect(
|
|
216
|
+
article!.querySelector('[role="img"][aria-label="Mermaid diagram"]'),
|
|
217
|
+
).toBeNull();
|
|
218
|
+
expect(article!.textContent).toContain("flowchart LR");
|
|
219
|
+
});
|
|
172
220
|
});
|
|
173
221
|
|
|
174
222
|
// ---------------------------------------------------------------------------
|