@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.
Files changed (151) hide show
  1. package/composer/SessionComposer.d.ts +31 -0
  2. package/composer/SessionComposer.d.ts.map +1 -1
  3. package/composer/SessionComposer.js +12 -4
  4. package/composer/SessionComposer.js.map +1 -1
  5. package/execution/ArtifactApplyButton.d.ts +25 -0
  6. package/execution/ArtifactApplyButton.d.ts.map +1 -0
  7. package/execution/ArtifactApplyButton.js +21 -0
  8. package/execution/ArtifactApplyButton.js.map +1 -0
  9. package/execution/ArtifactContentBody.d.ts +36 -0
  10. package/execution/ArtifactContentBody.d.ts.map +1 -0
  11. package/execution/ArtifactContentBody.js +62 -0
  12. package/execution/ArtifactContentBody.js.map +1 -0
  13. package/execution/ArtifactDocument.d.ts +48 -0
  14. package/execution/ArtifactDocument.d.ts.map +1 -0
  15. package/execution/ArtifactDocument.js +89 -0
  16. package/execution/ArtifactDocument.js.map +1 -0
  17. package/execution/ArtifactPreviewModal.d.ts +4 -4
  18. package/execution/ArtifactPreviewModal.d.ts.map +1 -1
  19. package/execution/ArtifactPreviewModal.js +15 -142
  20. package/execution/ArtifactPreviewModal.js.map +1 -1
  21. package/execution/ArtifactRow.d.ts +49 -0
  22. package/execution/ArtifactRow.d.ts.map +1 -0
  23. package/execution/ArtifactRow.js +56 -0
  24. package/execution/ArtifactRow.js.map +1 -0
  25. package/execution/ArtifactsWidget.d.ts +6 -9
  26. package/execution/ArtifactsWidget.d.ts.map +1 -1
  27. package/execution/ArtifactsWidget.js +15 -19
  28. package/execution/ArtifactsWidget.js.map +1 -1
  29. package/execution/index.d.ts +8 -2
  30. package/execution/index.d.ts.map +1 -1
  31. package/execution/index.js +4 -1
  32. package/execution/index.js.map +1 -1
  33. package/execution/useArtifactContent.d.ts +6 -1
  34. package/execution/useArtifactContent.d.ts.map +1 -1
  35. package/execution/useArtifactContent.js +7 -2
  36. package/execution/useArtifactContent.js.map +1 -1
  37. package/execution/useArtifactInspection.d.ts +85 -0
  38. package/execution/useArtifactInspection.d.ts.map +1 -0
  39. package/execution/useArtifactInspection.js +143 -0
  40. package/execution/useArtifactInspection.js.map +1 -0
  41. package/execution/useCreateAgentExecution.d.ts +11 -0
  42. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  43. package/execution/useCreateAgentExecution.js +1 -0
  44. package/execution/useCreateAgentExecution.js.map +1 -1
  45. package/index.d.ts +3 -3
  46. package/index.d.ts.map +1 -1
  47. package/index.js +2 -2
  48. package/index.js.map +1 -1
  49. package/internal/MermaidDiagram.d.ts +47 -0
  50. package/internal/MermaidDiagram.d.ts.map +1 -0
  51. package/internal/MermaidDiagram.js +118 -0
  52. package/internal/MermaidDiagram.js.map +1 -0
  53. package/internal/markdown-components.d.ts.map +1 -1
  54. package/internal/markdown-components.js +34 -0
  55. package/internal/markdown-components.js.map +1 -1
  56. package/internal/mermaid-loader.d.ts +26 -0
  57. package/internal/mermaid-loader.d.ts.map +1 -0
  58. package/internal/mermaid-loader.js +34 -0
  59. package/internal/mermaid-loader.js.map +1 -0
  60. package/internal/mermaid-theme-css.d.ts +41 -0
  61. package/internal/mermaid-theme-css.d.ts.map +1 -0
  62. package/internal/mermaid-theme-css.js +179 -0
  63. package/internal/mermaid-theme-css.js.map +1 -0
  64. package/internal/slug.d.ts +9 -5
  65. package/internal/slug.d.ts.map +1 -1
  66. package/internal/slug.js +10 -6
  67. package/internal/slug.js.map +1 -1
  68. package/package.json +5 -4
  69. package/session/SessionViewer.d.ts.map +1 -1
  70. package/session/SessionViewer.js +91 -18
  71. package/session/SessionViewer.js.map +1 -1
  72. package/session/artifact-document.d.ts +13 -0
  73. package/session/artifact-document.d.ts.map +1 -0
  74. package/session/artifact-document.js +22 -0
  75. package/session/artifact-document.js.map +1 -0
  76. package/session/facets/ArtifactsTab.d.ts +30 -4
  77. package/session/facets/ArtifactsTab.d.ts.map +1 -1
  78. package/session/facets/ArtifactsTab.js +32 -10
  79. package/session/facets/ArtifactsTab.js.map +1 -1
  80. package/session/index.d.ts +2 -1
  81. package/session/index.d.ts.map +1 -1
  82. package/session/index.js +4 -1
  83. package/session/index.js.map +1 -1
  84. package/session/useSessionArtifacts.d.ts +17 -0
  85. package/session/useSessionArtifacts.d.ts.map +1 -1
  86. package/session/useSessionArtifacts.js +16 -5
  87. package/session/useSessionArtifacts.js.map +1 -1
  88. package/session/useSessionConversation.d.ts +17 -1
  89. package/session/useSessionConversation.d.ts.map +1 -1
  90. package/session/useSessionConversation.js +32 -4
  91. package/session/useSessionConversation.js.map +1 -1
  92. package/session/useSessionPageFlow.d.ts.map +1 -1
  93. package/session/useSessionPageFlow.js +1 -0
  94. package/session/useSessionPageFlow.js.map +1 -1
  95. package/session/useSessionPanel.d.ts +20 -0
  96. package/session/useSessionPanel.d.ts.map +1 -1
  97. package/session/useSessionPanel.js +13 -0
  98. package/session/useSessionPanel.js.map +1 -1
  99. package/session/useSessionRailViews.d.ts +13 -1
  100. package/session/useSessionRailViews.d.ts.map +1 -1
  101. package/session/useSessionRailViews.js +5 -3
  102. package/session/useSessionRailViews.js.map +1 -1
  103. package/src/composer/SessionComposer.tsx +69 -1
  104. package/src/composer/__tests__/SessionComposer-editing.test.tsx +128 -0
  105. package/src/execution/ArtifactApplyButton.tsx +79 -0
  106. package/src/execution/ArtifactContentBody.tsx +255 -0
  107. package/src/execution/ArtifactDocument.tsx +344 -0
  108. package/src/execution/ArtifactPreviewModal.tsx +39 -405
  109. package/src/execution/ArtifactRow.tsx +165 -0
  110. package/src/execution/ArtifactsWidget.tsx +24 -33
  111. package/src/execution/__tests__/ArtifactContentBody.test.tsx +131 -0
  112. package/src/execution/__tests__/ArtifactDocument.test.tsx +100 -0
  113. package/src/execution/__tests__/ArtifactRow.test.tsx +139 -0
  114. package/src/execution/__tests__/ArtifactsWidget.test.tsx +78 -0
  115. package/src/execution/__tests__/message-entry.test.tsx +50 -2
  116. package/src/execution/__tests__/useArtifactInspection.test.tsx +151 -0
  117. package/src/execution/__tests__/useCreateAgentExecution.test.tsx +35 -0
  118. package/src/execution/index.ts +14 -2
  119. package/src/execution/useArtifactContent.ts +7 -0
  120. package/src/execution/useArtifactInspection.ts +252 -0
  121. package/src/execution/useCreateAgentExecution.ts +12 -0
  122. package/src/index.ts +11 -2
  123. package/src/internal/MermaidDiagram.tsx +164 -0
  124. package/src/internal/__tests__/a11y/mermaid-theme.a11y.test.tsx +122 -0
  125. package/src/internal/__tests__/markdown-components.test.tsx +93 -1
  126. package/src/internal/__tests__/mermaid-diagram.test.tsx +282 -0
  127. package/src/internal/markdown-components.tsx +36 -1
  128. package/src/internal/mermaid-loader.ts +44 -0
  129. package/src/internal/mermaid-theme-css.ts +178 -0
  130. package/src/internal/slug.ts +10 -6
  131. package/src/session/SessionViewer.tsx +177 -33
  132. package/src/session/__tests__/SessionViewer-artifactTabs.test.tsx +210 -0
  133. package/src/session/__tests__/SessionViewer-editResubmit.test.tsx +233 -0
  134. package/src/session/__tests__/useSessionArtifacts.test.ts +57 -0
  135. package/src/session/__tests__/useSessionConversation.test.tsx +172 -1
  136. package/src/session/__tests__/useSessionPanel.test.ts +70 -1
  137. package/src/session/artifact-document.ts +23 -0
  138. package/src/session/facets/ArtifactsTab.tsx +66 -18
  139. package/src/session/facets/__tests__/ArtifactsTab.test.tsx +111 -47
  140. package/src/session/index.ts +5 -1
  141. package/src/session/useSessionArtifacts.ts +16 -5
  142. package/src/session/useSessionConversation.ts +48 -6
  143. package/src/session/useSessionPageFlow.ts +1 -0
  144. package/src/session/useSessionPanel.ts +41 -0
  145. package/src/session/useSessionRailViews.tsx +21 -1
  146. package/styles.css +1 -1
  147. package/execution/ArtifactCard.d.ts +0 -81
  148. package/execution/ArtifactCard.d.ts.map +0 -1
  149. package/execution/ArtifactCard.js +0 -130
  150. package/execution/ArtifactCard.js.map +0 -1
  151. 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
+ }