@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
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ExecutionArtifact } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
4
|
+
import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
5
|
+
import { ArtifactContentRenderer } from "./ArtifactContentRenderer.js";
|
|
6
|
+
import type { SkillPackageDetection } from "../library/detect-skill-package.js";
|
|
7
|
+
|
|
8
|
+
/** Props for {@link ArtifactContentBody}. */
|
|
9
|
+
export interface ArtifactContentBodyProps {
|
|
10
|
+
/** The artifact whose body is rendered. */
|
|
11
|
+
readonly artifact: ExecutionArtifact;
|
|
12
|
+
/** Decoded text content, or `null` (directory / binary / loading / error). */
|
|
13
|
+
readonly content: string | null;
|
|
14
|
+
/** Server-detected content type (rendering-strategy fallback). */
|
|
15
|
+
readonly contentType: string | null;
|
|
16
|
+
/** `true` while the content request is in-flight. */
|
|
17
|
+
readonly isLoading: boolean;
|
|
18
|
+
/** Content-fetch error, or `null`. */
|
|
19
|
+
readonly error: Error | null;
|
|
20
|
+
/** Whether the fetched content was truncated by the server's size cap. */
|
|
21
|
+
readonly isTruncated: boolean;
|
|
22
|
+
/** Skill-package detection for directory artifacts. */
|
|
23
|
+
readonly skillDetection: SkillPackageDetection;
|
|
24
|
+
/** Additional CSS classes for the root element. */
|
|
25
|
+
readonly className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The shared, chrome-agnostic body of an artifact view: the file-content
|
|
30
|
+
* states (loading / error / binary / text via {@link ArtifactContentRenderer})
|
|
31
|
+
* for FILE artifacts, or the file listing + skill banner for DIRECTORY
|
|
32
|
+
* artifacts.
|
|
33
|
+
*
|
|
34
|
+
* Extracted so the preview modal and the editor-area `ArtifactDocument` render
|
|
35
|
+
* artifact bodies identically — only the surrounding chrome (modal header +
|
|
36
|
+
* action bar vs. document toolbar) differs. Pair with
|
|
37
|
+
* {@link useArtifactInspection}, which supplies every prop here.
|
|
38
|
+
*
|
|
39
|
+
* All visual properties flow through `--stgm-*` tokens (DD-005).
|
|
40
|
+
*/
|
|
41
|
+
export function ArtifactContentBody({
|
|
42
|
+
artifact,
|
|
43
|
+
content,
|
|
44
|
+
contentType,
|
|
45
|
+
isLoading,
|
|
46
|
+
error,
|
|
47
|
+
isTruncated,
|
|
48
|
+
skillDetection,
|
|
49
|
+
className,
|
|
50
|
+
}: ArtifactContentBodyProps) {
|
|
51
|
+
const isDirectory = artifact.kind === ExecutionArtifactKind.DIRECTORY;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className={className}>
|
|
55
|
+
{isDirectory ? (
|
|
56
|
+
<DirectoryContentView artifact={artifact} skillDetection={skillDetection} />
|
|
57
|
+
) : (
|
|
58
|
+
<FileContentStateView
|
|
59
|
+
artifact={artifact}
|
|
60
|
+
content={content}
|
|
61
|
+
contentType={contentType}
|
|
62
|
+
isLoading={isLoading}
|
|
63
|
+
error={error}
|
|
64
|
+
isTruncated={isTruncated}
|
|
65
|
+
/>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// File content states (loading / error / binary / text)
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
const SKELETON_LINE_WIDTHS = [85, 72, 90, 65, 78, 88, 70, 82] as const;
|
|
76
|
+
|
|
77
|
+
function FileContentStateView({
|
|
78
|
+
artifact,
|
|
79
|
+
content,
|
|
80
|
+
contentType,
|
|
81
|
+
isLoading,
|
|
82
|
+
error,
|
|
83
|
+
isTruncated,
|
|
84
|
+
}: {
|
|
85
|
+
readonly artifact: ExecutionArtifact;
|
|
86
|
+
readonly content: string | null;
|
|
87
|
+
readonly contentType: string | null;
|
|
88
|
+
readonly isLoading: boolean;
|
|
89
|
+
readonly error: Error | null;
|
|
90
|
+
readonly isTruncated: boolean;
|
|
91
|
+
}) {
|
|
92
|
+
if (isLoading) {
|
|
93
|
+
return (
|
|
94
|
+
<div className="space-y-2 p-4" aria-busy="true" aria-label="Loading content">
|
|
95
|
+
{SKELETON_LINE_WIDTHS.map((width, i) => (
|
|
96
|
+
<div
|
|
97
|
+
key={i}
|
|
98
|
+
className="h-4 animate-pulse rounded bg-muted"
|
|
99
|
+
style={{ width: `${width}%` }}
|
|
100
|
+
aria-hidden="true"
|
|
101
|
+
/>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (error) {
|
|
108
|
+
return (
|
|
109
|
+
<div className="flex flex-col items-center justify-center gap-2 p-8 text-center">
|
|
110
|
+
<ErrorAlertIcon />
|
|
111
|
+
<p className="text-sm text-destructive">{error.message}</p>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (content === null) {
|
|
117
|
+
return (
|
|
118
|
+
<div className="p-8 text-center text-sm text-muted-foreground">
|
|
119
|
+
Content not available for preview.
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<ArtifactContentRenderer
|
|
126
|
+
content={content}
|
|
127
|
+
fileName={artifact.name}
|
|
128
|
+
contentType={contentType}
|
|
129
|
+
isTruncated={isTruncated}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Directory content (skill banner + file listing)
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
function DirectoryContentView({
|
|
139
|
+
artifact,
|
|
140
|
+
skillDetection,
|
|
141
|
+
}: {
|
|
142
|
+
readonly artifact: ExecutionArtifact;
|
|
143
|
+
readonly skillDetection: SkillPackageDetection;
|
|
144
|
+
}) {
|
|
145
|
+
const entries = artifact.entries;
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className="p-4">
|
|
149
|
+
{skillDetection.detected && (
|
|
150
|
+
<div className="mb-4 rounded-md bg-primary-subtle p-3">
|
|
151
|
+
<p className="text-sm font-medium text-foreground">
|
|
152
|
+
{skillDetection.skillName}
|
|
153
|
+
</p>
|
|
154
|
+
{skillDetection.skillDescription && (
|
|
155
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
156
|
+
{skillDetection.skillDescription}
|
|
157
|
+
</p>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
|
|
162
|
+
{entries.length > 0 ? (
|
|
163
|
+
<div>
|
|
164
|
+
<h3 className="mb-2 text-xs font-medium text-muted-foreground">
|
|
165
|
+
Files ({entries.length})
|
|
166
|
+
</h3>
|
|
167
|
+
<ul className="space-y-0.5" role="list">
|
|
168
|
+
{entries.map((entry) => (
|
|
169
|
+
<li
|
|
170
|
+
key={entry}
|
|
171
|
+
className="flex items-center gap-2 rounded-sm px-2 py-1 font-mono text-xs text-foreground"
|
|
172
|
+
>
|
|
173
|
+
<EntryIcon name={entry} />
|
|
174
|
+
<span className="min-w-0 truncate">{entry}</span>
|
|
175
|
+
</li>
|
|
176
|
+
))}
|
|
177
|
+
</ul>
|
|
178
|
+
</div>
|
|
179
|
+
) : (
|
|
180
|
+
<p className="text-sm text-muted-foreground">
|
|
181
|
+
File listing not available.
|
|
182
|
+
</p>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function EntryIcon({ name }: { readonly name: string }) {
|
|
189
|
+
if (name.endsWith("/")) return <FolderSmallIcon />;
|
|
190
|
+
return <FileSmallIcon />;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Inline SVG icons (SDK independence — no lucide dependency)
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
function FileSmallIcon() {
|
|
198
|
+
return (
|
|
199
|
+
<svg
|
|
200
|
+
width="12"
|
|
201
|
+
height="12"
|
|
202
|
+
viewBox="0 0 14 14"
|
|
203
|
+
fill="none"
|
|
204
|
+
stroke="currentColor"
|
|
205
|
+
strokeWidth="1.5"
|
|
206
|
+
strokeLinecap="round"
|
|
207
|
+
strokeLinejoin="round"
|
|
208
|
+
className="shrink-0 text-muted-foreground"
|
|
209
|
+
aria-hidden="true"
|
|
210
|
+
>
|
|
211
|
+
<path d="M8 1H4C3.45 1 3 1.45 3 2V12C3 12.55 3.45 13 4 13H10C10.55 13 11 12.55 11 12V4L8 1Z" />
|
|
212
|
+
<path d="M8 1V4H11" />
|
|
213
|
+
</svg>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function FolderSmallIcon() {
|
|
218
|
+
return (
|
|
219
|
+
<svg
|
|
220
|
+
width="12"
|
|
221
|
+
height="12"
|
|
222
|
+
viewBox="0 0 14 14"
|
|
223
|
+
fill="none"
|
|
224
|
+
stroke="currentColor"
|
|
225
|
+
strokeWidth="1.5"
|
|
226
|
+
strokeLinecap="round"
|
|
227
|
+
strokeLinejoin="round"
|
|
228
|
+
className="shrink-0 text-muted-foreground"
|
|
229
|
+
aria-hidden="true"
|
|
230
|
+
>
|
|
231
|
+
<path d="M13 11C13 11.55 12.55 12 12 12H2C1.45 12 1 11.55 1 11V3C1 2.45 1.45 2 2 2H5L7 4H12C12.55 4 13 4.45 13 5V11Z" />
|
|
232
|
+
</svg>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function ErrorAlertIcon() {
|
|
237
|
+
return (
|
|
238
|
+
<svg
|
|
239
|
+
width="16"
|
|
240
|
+
height="16"
|
|
241
|
+
viewBox="0 0 16 16"
|
|
242
|
+
fill="none"
|
|
243
|
+
stroke="currentColor"
|
|
244
|
+
strokeWidth="1.5"
|
|
245
|
+
strokeLinecap="round"
|
|
246
|
+
strokeLinejoin="round"
|
|
247
|
+
className="text-destructive"
|
|
248
|
+
aria-hidden="true"
|
|
249
|
+
>
|
|
250
|
+
<circle cx="8" cy="8" r="6.5" />
|
|
251
|
+
<path d="M8 5.5V8.5" />
|
|
252
|
+
<circle cx="8" cy="11" r="0.5" fill="currentColor" stroke="none" />
|
|
253
|
+
</svg>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ExecutionArtifact } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
4
|
+
import { cn } from "@stigmer/theme";
|
|
5
|
+
import { useArtifactInspection } from "./useArtifactInspection.js";
|
|
6
|
+
import { ArtifactContentBody } from "./ArtifactContentBody.js";
|
|
7
|
+
import { ArtifactApplyButton } from "./ArtifactApplyButton.js";
|
|
8
|
+
import { useArtifactDownload } from "./useArtifactDownload.js";
|
|
9
|
+
import { formatArtifactSize } from "./artifact-utils.js";
|
|
10
|
+
import type { ApplyResourceResult } from "../library/useApplyResource.js";
|
|
11
|
+
|
|
12
|
+
/** Props for {@link ArtifactDocument}. */
|
|
13
|
+
export interface ArtifactDocumentProps {
|
|
14
|
+
/** The artifact to render. */
|
|
15
|
+
readonly artifact: ExecutionArtifact;
|
|
16
|
+
/** Execution that produced this artifact version (content + skill fetching). */
|
|
17
|
+
readonly executionId: string;
|
|
18
|
+
/** Organization for the "Apply to [org]" / "Push Skill to [org]" CTA. */
|
|
19
|
+
readonly org: string;
|
|
20
|
+
/**
|
|
21
|
+
* Whether the producing execution is terminal. Gates the Apply/Push action
|
|
22
|
+
* (a mid-run artifact's content may still change).
|
|
23
|
+
*/
|
|
24
|
+
readonly isTerminal: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Called after a resource is applied or a skill pushed — the same host
|
|
27
|
+
* callback the preview modal fires, so a document-apply behaves identically
|
|
28
|
+
* (toast / Library refresh).
|
|
29
|
+
*/
|
|
30
|
+
readonly onApplied?: (result: ApplyResourceResult) => void;
|
|
31
|
+
/** Additional CSS classes for the root element. */
|
|
32
|
+
readonly className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The editor-pane rendering of a single execution artifact — the
|
|
37
|
+
* {@link SurfaceVirtualDocument} counterpart of the {@link ArtifactPreviewModal}
|
|
38
|
+
* popup, mounted as an artifact-family virtual document under
|
|
39
|
+
* {@link ARTIFACT_DOCUMENT_ENTRY_ID}. Opening an artifact from the session
|
|
40
|
+
* panel's Artifacts facet lands here (VS Code "each file is a tab") rather than
|
|
41
|
+
* a modal.
|
|
42
|
+
*
|
|
43
|
+
* Shares the modal's exact behavior and body via the two extracted pieces —
|
|
44
|
+
* {@link useArtifactInspection} (content + detection + apply/push + copy) and
|
|
45
|
+
* {@link ArtifactContentBody} (loading / error / binary / text / directory) —
|
|
46
|
+
* so the two surfaces can never drift. Only the chrome differs: a document
|
|
47
|
+
* toolbar (name + size + detection badge; Copy / Download / Apply) over the
|
|
48
|
+
* shared body, instead of the modal's header + footer action bar.
|
|
49
|
+
*
|
|
50
|
+
* Holds no must-survive local state: the panel region unmounts on collapse and
|
|
51
|
+
* switching tabs remounts the active document, resetting transient apply/copy
|
|
52
|
+
* state exactly as the modal resets on close. A `cacheKey` on the content fetch
|
|
53
|
+
* keeps reopening a recently-viewed artifact instant (DD-014).
|
|
54
|
+
*
|
|
55
|
+
* All visual properties flow through `--stgm-*` tokens (DD-005).
|
|
56
|
+
*/
|
|
57
|
+
export function ArtifactDocument({
|
|
58
|
+
artifact,
|
|
59
|
+
executionId,
|
|
60
|
+
org,
|
|
61
|
+
isTerminal,
|
|
62
|
+
onApplied,
|
|
63
|
+
className,
|
|
64
|
+
}: ArtifactDocumentProps) {
|
|
65
|
+
// Instant reopen: cache the content per (execution, storage key, version).
|
|
66
|
+
// contentHash is included so a later overwrite of the same path never serves
|
|
67
|
+
// a stale cached body.
|
|
68
|
+
const cacheKey = `artifact-doc:${executionId}:${artifact.storageKey}:${artifact.contentHash}`;
|
|
69
|
+
const inspection = useArtifactInspection(artifact, executionId, org, {
|
|
70
|
+
cacheKey,
|
|
71
|
+
onApplied,
|
|
72
|
+
});
|
|
73
|
+
const { download, isDownloading } = useArtifactDownload(executionId);
|
|
74
|
+
|
|
75
|
+
const showCopy = !inspection.isDirectory && inspection.content !== null;
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
role="article"
|
|
80
|
+
aria-label={`Artifact ${artifact.name}`}
|
|
81
|
+
className={cn("flex min-h-0 min-w-0 flex-1 flex-col", className)}
|
|
82
|
+
>
|
|
83
|
+
{/* Toolbar sticks to the top of the editor pane's scroll container so the
|
|
84
|
+
file identity and actions stay visible while the body scrolls. Rows
|
|
85
|
+
wrap on narrow panes (min-w-0 + flex-wrap) rather than forcing a
|
|
86
|
+
horizontal scrollbar — the DD-20 reflow contract. */}
|
|
87
|
+
<div className="sticky top-0 z-10 flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1.5 border-b border-border bg-background px-4 py-2">
|
|
88
|
+
<span className="shrink-0 text-muted-foreground">
|
|
89
|
+
{inspection.isDirectory ? <FolderIcon /> : <FileIcon />}
|
|
90
|
+
</span>
|
|
91
|
+
<span className="truncate text-sm font-medium text-foreground">
|
|
92
|
+
{artifact.name}
|
|
93
|
+
{inspection.isDirectory && "/"}
|
|
94
|
+
</span>
|
|
95
|
+
<span className="shrink-0 text-[0.65rem] tabular-nums text-muted-foreground-faint">
|
|
96
|
+
{formatArtifactSize(artifact.sizeBytes)}
|
|
97
|
+
</span>
|
|
98
|
+
{inspection.isDetecting && !inspection.detectionLabel && (
|
|
99
|
+
<span
|
|
100
|
+
className="h-4 w-24 shrink-0 animate-pulse rounded-full bg-muted"
|
|
101
|
+
aria-hidden="true"
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
{inspection.detectionLabel && (
|
|
105
|
+
<span className="inline-flex shrink-0 items-center rounded-full bg-primary-subtle px-2 py-0.5 text-xs font-medium text-primary">
|
|
106
|
+
{inspection.detectionLabel}
|
|
107
|
+
</span>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
<div className="ml-auto flex min-w-0 flex-wrap items-center gap-3">
|
|
111
|
+
{showCopy && (
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
onClick={inspection.copy}
|
|
115
|
+
aria-label={
|
|
116
|
+
inspection.copied ? "Copied to clipboard" : "Copy content"
|
|
117
|
+
}
|
|
118
|
+
className={cn(
|
|
119
|
+
"inline-flex items-center gap-1.5 text-xs font-medium transition-colors",
|
|
120
|
+
inspection.copied
|
|
121
|
+
? "text-success"
|
|
122
|
+
: "text-muted-foreground hover:text-foreground",
|
|
123
|
+
FOCUS_RING_CLASSES,
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
{inspection.copied ? <CheckIcon /> : <CopyIcon />}
|
|
127
|
+
{inspection.copied ? "Copied" : "Copy"}
|
|
128
|
+
</button>
|
|
129
|
+
)}
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
onClick={() => download(artifact.storageKey, artifact.name)}
|
|
133
|
+
disabled={isDownloading}
|
|
134
|
+
className={cn(
|
|
135
|
+
"inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground disabled:opacity-50",
|
|
136
|
+
FOCUS_RING_CLASSES,
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
<DownloadIcon />
|
|
140
|
+
{isDownloading
|
|
141
|
+
? "Preparing\u2026"
|
|
142
|
+
: inspection.isDirectory
|
|
143
|
+
? "Download ZIP"
|
|
144
|
+
: "Download"}
|
|
145
|
+
</button>
|
|
146
|
+
<ApplyCluster
|
|
147
|
+
isDetected={inspection.isDetected}
|
|
148
|
+
ctaLabel={inspection.ctaLabel}
|
|
149
|
+
isTerminal={isTerminal}
|
|
150
|
+
isApplying={inspection.isApplying}
|
|
151
|
+
applyResult={inspection.applyResult}
|
|
152
|
+
applyError={inspection.applyError}
|
|
153
|
+
onApply={inspection.apply}
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<ArtifactContentBody
|
|
159
|
+
artifact={artifact}
|
|
160
|
+
content={inspection.content}
|
|
161
|
+
contentType={inspection.contentType}
|
|
162
|
+
isLoading={inspection.isLoading}
|
|
163
|
+
error={inspection.error}
|
|
164
|
+
isTruncated={inspection.isTruncated}
|
|
165
|
+
skillDetection={inspection.skillDetection}
|
|
166
|
+
className="min-w-0"
|
|
167
|
+
/>
|
|
168
|
+
|
|
169
|
+
<div role="status" aria-live="polite" aria-atomic="true" className="sr-only">
|
|
170
|
+
{inspection.copied && "Content copied to clipboard"}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The Apply/Push affordance: the terminal-gated {@link ArtifactApplyButton}, or
|
|
178
|
+
* the post-apply success / error-with-retry states. Mirrors the modal action
|
|
179
|
+
* bar's apply cluster so both surfaces read identically.
|
|
180
|
+
*/
|
|
181
|
+
function ApplyCluster({
|
|
182
|
+
isDetected,
|
|
183
|
+
ctaLabel,
|
|
184
|
+
isTerminal,
|
|
185
|
+
isApplying,
|
|
186
|
+
applyResult,
|
|
187
|
+
applyError,
|
|
188
|
+
onApply,
|
|
189
|
+
}: {
|
|
190
|
+
readonly isDetected: boolean;
|
|
191
|
+
readonly ctaLabel: string | null;
|
|
192
|
+
readonly isTerminal: boolean;
|
|
193
|
+
readonly isApplying: boolean;
|
|
194
|
+
readonly applyResult: ApplyResourceResult | null;
|
|
195
|
+
readonly applyError: Error | null;
|
|
196
|
+
readonly onApply: () => void;
|
|
197
|
+
}) {
|
|
198
|
+
if (applyResult) {
|
|
199
|
+
return (
|
|
200
|
+
<span className="inline-flex items-center gap-1.5 text-xs font-medium text-success">
|
|
201
|
+
<CheckIcon />
|
|
202
|
+
Applied {"\u00B7"} {applyResult.name || applyResult.kind}
|
|
203
|
+
</span>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
if (applyError) {
|
|
207
|
+
return (
|
|
208
|
+
<span className="inline-flex items-center gap-2" role="alert">
|
|
209
|
+
<span className="text-xs text-destructive">{applyError.message}</span>
|
|
210
|
+
<button
|
|
211
|
+
type="button"
|
|
212
|
+
onClick={onApply}
|
|
213
|
+
className={cn(
|
|
214
|
+
"text-xs font-medium text-destructive underline transition-colors hover:text-destructive-muted",
|
|
215
|
+
FOCUS_RING_CLASSES,
|
|
216
|
+
)}
|
|
217
|
+
>
|
|
218
|
+
Retry
|
|
219
|
+
</button>
|
|
220
|
+
</span>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (isDetected && ctaLabel) {
|
|
224
|
+
return (
|
|
225
|
+
<span data-cursor-target="apply-resource-button">
|
|
226
|
+
<ArtifactApplyButton
|
|
227
|
+
label={ctaLabel}
|
|
228
|
+
isTerminal={isTerminal}
|
|
229
|
+
isApplying={isApplying}
|
|
230
|
+
onApply={onApply}
|
|
231
|
+
/>
|
|
232
|
+
</span>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// Shared style constants
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
const FOCUS_RING_CLASSES =
|
|
243
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:rounded-sm";
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Inline SVG icons (SDK independence — no lucide dependency)
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
function FileIcon() {
|
|
250
|
+
return (
|
|
251
|
+
<svg
|
|
252
|
+
width="16"
|
|
253
|
+
height="16"
|
|
254
|
+
viewBox="0 0 16 16"
|
|
255
|
+
fill="none"
|
|
256
|
+
stroke="currentColor"
|
|
257
|
+
strokeWidth="1.5"
|
|
258
|
+
strokeLinecap="round"
|
|
259
|
+
strokeLinejoin="round"
|
|
260
|
+
aria-hidden="true"
|
|
261
|
+
>
|
|
262
|
+
<path d="M9.5 1.5H5C4.17 1.5 3.5 2.17 3.5 3V13C3.5 13.83 4.17 14.5 5 14.5H11C11.83 14.5 12.5 13.83 12.5 13V4.5L9.5 1.5Z" />
|
|
263
|
+
<path d="M9.5 1.5V4.5H12.5" />
|
|
264
|
+
</svg>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function FolderIcon() {
|
|
269
|
+
return (
|
|
270
|
+
<svg
|
|
271
|
+
width="16"
|
|
272
|
+
height="16"
|
|
273
|
+
viewBox="0 0 16 16"
|
|
274
|
+
fill="none"
|
|
275
|
+
stroke="currentColor"
|
|
276
|
+
strokeWidth="1.5"
|
|
277
|
+
strokeLinecap="round"
|
|
278
|
+
strokeLinejoin="round"
|
|
279
|
+
aria-hidden="true"
|
|
280
|
+
>
|
|
281
|
+
<path d="M14.5 12.5C14.5 13.33 13.83 14 13 14H3C2.17 14 1.5 13.33 1.5 12.5V3.5C1.5 2.67 2.17 2 3 2H6L8 4.5H13C13.83 4.5 14.5 5.17 14.5 6V12.5Z" />
|
|
282
|
+
</svg>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function CopyIcon() {
|
|
287
|
+
return (
|
|
288
|
+
<svg
|
|
289
|
+
width="12"
|
|
290
|
+
height="12"
|
|
291
|
+
viewBox="0 0 12 12"
|
|
292
|
+
fill="none"
|
|
293
|
+
stroke="currentColor"
|
|
294
|
+
strokeWidth="1.2"
|
|
295
|
+
strokeLinecap="round"
|
|
296
|
+
strokeLinejoin="round"
|
|
297
|
+
className="shrink-0"
|
|
298
|
+
aria-hidden="true"
|
|
299
|
+
>
|
|
300
|
+
<rect x="4" y="4" width="6.5" height="6.5" rx="1" />
|
|
301
|
+
<path d="M8 4V2.5C8 1.95 7.55 1.5 7 1.5H2.5C1.95 1.5 1.5 1.95 1.5 2.5V7C1.5 7.55 1.95 8 2.5 8H4" />
|
|
302
|
+
</svg>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function DownloadIcon() {
|
|
307
|
+
return (
|
|
308
|
+
<svg
|
|
309
|
+
width="12"
|
|
310
|
+
height="12"
|
|
311
|
+
viewBox="0 0 12 12"
|
|
312
|
+
fill="none"
|
|
313
|
+
stroke="currentColor"
|
|
314
|
+
strokeWidth="1.5"
|
|
315
|
+
strokeLinecap="round"
|
|
316
|
+
strokeLinejoin="round"
|
|
317
|
+
className="shrink-0"
|
|
318
|
+
aria-hidden="true"
|
|
319
|
+
>
|
|
320
|
+
<path d="M6 1.5V8.5" />
|
|
321
|
+
<path d="M3 6L6 9L9 6" />
|
|
322
|
+
<path d="M2 10.5H10" />
|
|
323
|
+
</svg>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function CheckIcon() {
|
|
328
|
+
return (
|
|
329
|
+
<svg
|
|
330
|
+
width="12"
|
|
331
|
+
height="12"
|
|
332
|
+
viewBox="0 0 12 12"
|
|
333
|
+
fill="none"
|
|
334
|
+
stroke="currentColor"
|
|
335
|
+
strokeWidth="1.5"
|
|
336
|
+
strokeLinecap="round"
|
|
337
|
+
strokeLinejoin="round"
|
|
338
|
+
className="shrink-0"
|
|
339
|
+
aria-hidden="true"
|
|
340
|
+
>
|
|
341
|
+
<path d="M2 6.5L4.5 9L10 3" />
|
|
342
|
+
</svg>
|
|
343
|
+
);
|
|
344
|
+
}
|