@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
@@ -1,22 +1,14 @@
1
1
  "use client";
2
2
 
3
- import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { useCallback, useEffect, useRef } from "react";
4
4
  import type { ExecutionArtifact } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
5
- import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
6
5
  import { cn } from "@stigmer/theme";
7
- import { useArtifactContent } from "./useArtifactContent.js";
8
6
  import { useArtifactDownload } from "./useArtifactDownload.js";
9
- import { isTextArtifact, formatArtifactSize } from "./artifact-utils.js";
10
- import { ArtifactContentRenderer } from "./ArtifactContentRenderer.js";
11
- import { useDetectStigmerResource } from "../library/useDetectStigmerResource.js";
12
- import { useDetectSkillPackage } from "../library/useDetectSkillPackage.js";
13
- import type { SkillPackageDetection } from "../library/detect-skill-package.js";
14
- import {
15
- useApplyResource,
16
- type ApplyResourceResult,
17
- } from "../library/useApplyResource.js";
18
-
19
- const COPIED_FEEDBACK_MS = 2000;
7
+ import { formatArtifactSize } from "./artifact-utils.js";
8
+ import { ArtifactContentBody } from "./ArtifactContentBody.js";
9
+ import { ArtifactApplyButton } from "./ArtifactApplyButton.js";
10
+ import { useArtifactInspection } from "./useArtifactInspection.js";
11
+ import type { ApplyResourceResult } from "../library/useApplyResource.js";
20
12
 
21
13
  // ---------------------------------------------------------------------------
22
14
  // ArtifactPreviewContent — standalone content component
@@ -66,7 +58,7 @@ export interface ArtifactPreviewContentProps {
66
58
  * it inside a `<dialog>`, modal, sheet, overlay, or inline context as
67
59
  * needed.
68
60
  *
69
- * Orchestrates the same detection pipeline as {@link ArtifactCard}:
61
+ * Orchestrates the shared detection pipeline (see {@link useArtifactInspection}):
70
62
  *
71
63
  * - **FILE artifacts**: Fetches text content via {@link useArtifactContent},
72
64
  * renders via {@link ArtifactContentRenderer} (markdown, YAML, JSON, or
@@ -96,7 +88,7 @@ export interface ArtifactPreviewContentProps {
96
88
  * ```
97
89
  *
98
90
  * @see {@link ArtifactPreviewModal} — wraps this component in a native `<dialog>`
99
- * @see {@link ArtifactCard} — compact card that triggers preview via `onPreview`
91
+ * @see {@link ArtifactRow} — dense list row that triggers preview via `onOpen`
100
92
  * @see {@link useArtifactContent} — content-fetching hook (headless alternative)
101
93
  * @see {@link useDetectStigmerResource} — YAML resource detection (headless)
102
94
  * @see {@link useDetectSkillPackage} — skill package detection (headless)
@@ -112,121 +104,12 @@ export function ArtifactPreviewContent({
112
104
  onImplement,
113
105
  className,
114
106
  }: ArtifactPreviewContentProps) {
115
- const isDirectory = artifact.kind === ExecutionArtifactKind.DIRECTORY;
116
- const canFetchContent = !isDirectory && isTextArtifact(artifact);
117
-
118
- // ---------------------------------------------------------------------------
119
- // Detection orchestration
120
- // ---------------------------------------------------------------------------
121
-
122
- const {
123
- content,
124
- contentType,
125
- isTruncated,
126
- isLoading: isContentLoading,
127
- error: contentError,
128
- } = useArtifactContent(
129
- canFetchContent ? executionId : null,
130
- canFetchContent ? artifact.storageKey : null,
131
- undefined,
132
- artifact.contentHash || undefined,
133
- );
134
-
135
- const yamlDetection = useDetectStigmerResource(
136
- canFetchContent ? content : null,
137
- );
138
-
139
- const { detection: skillDetection, isLoading: isSkillLoading } =
140
- useDetectSkillPackage(
141
- isDirectory ? artifact : null,
142
- isDirectory ? executionId : null,
143
- );
144
-
145
- // ---------------------------------------------------------------------------
146
- // Derived detection state
147
- // ---------------------------------------------------------------------------
148
-
149
- const isDetected = yamlDetection.detected || skillDetection.detected;
150
- const isDetecting =
151
- (canFetchContent && isContentLoading) || (isDirectory && isSkillLoading);
152
-
153
- let detectionLabel: string | null = null;
154
- if (yamlDetection.detected) {
155
- detectionLabel = `${yamlDetection.displayName} detected`;
156
- } else if (skillDetection.detected) {
157
- const count = skillDetection.fileCount;
158
- detectionLabel = `Skill \u00B7 ${count} ${count === 1 ? "file" : "files"}`;
159
- }
160
-
161
- // ---------------------------------------------------------------------------
162
- // Apply state
163
- // ---------------------------------------------------------------------------
164
-
165
- const {
166
- applyYamlResource,
167
- pushSkillPackage,
168
- isApplying,
169
- error: applyError,
170
- clearError,
171
- } = useApplyResource();
172
-
173
- const [applyResult, setApplyResult] = useState<ApplyResourceResult | null>(
174
- null,
175
- );
176
-
177
- const handleApply = useCallback(async () => {
178
- clearError();
179
- try {
180
- let result: ApplyResourceResult;
181
- if (yamlDetection.detected && content) {
182
- result = await applyYamlResource(content, org);
183
- } else if (skillDetection.detected) {
184
- result = await pushSkillPackage({
185
- org,
186
- executionId,
187
- storageKey: artifact.storageKey,
188
- });
189
- } else {
190
- return;
191
- }
192
- setApplyResult(result);
193
- onApplied?.(result);
194
- } catch {
195
- // error state managed by useApplyResource
196
- }
197
- }, [
198
- yamlDetection.detected,
199
- skillDetection.detected,
200
- content,
201
- org,
202
- executionId,
203
- artifact.storageKey,
204
- applyYamlResource,
205
- pushSkillPackage,
206
- clearError,
107
+ // The whole inspect-and-act pipeline (content, detection, apply/push, copy)
108
+ // lives in one headless hook, shared with the editor-area ArtifactDocument so
109
+ // the two surfaces can never drift.
110
+ const inspection = useArtifactInspection(artifact, executionId, org, {
207
111
  onApplied,
208
- ]);
209
-
210
- // ---------------------------------------------------------------------------
211
- // Copy to clipboard
212
- // ---------------------------------------------------------------------------
213
-
214
- const [copied, setCopied] = useState(false);
215
-
216
- const handleCopy = useCallback(() => {
217
- if (!content) return;
218
- navigator.clipboard.writeText(content).then(() => {
219
- setCopied(true);
220
- setTimeout(() => setCopied(false), COPIED_FEEDBACK_MS);
221
- });
222
- }, [content]);
223
-
224
- let ctaLabel: string | null = null;
225
- if (yamlDetection.detected) {
226
- ctaLabel = `Apply to ${org}`;
227
- } else if (skillDetection.detected) {
228
- ctaLabel = `Push Skill to ${org}`;
229
- }
112
+ });
230
113
 
231
114
  // "Build" runs the plan; closing the modal lets the host's submit pipeline
232
115
  // take over (switch to Agent + send). Single combined handler keeps the
@@ -236,52 +119,41 @@ export function ArtifactPreviewContent({
236
119
  onClose();
237
120
  }, [onImplement, onClose]);
238
121
 
239
- // ---------------------------------------------------------------------------
240
- // Render
241
- // ---------------------------------------------------------------------------
242
-
243
122
  return (
244
123
  <div className={cn("flex max-h-[80vh] flex-col", className)}>
245
124
  <ContentHeader
246
125
  artifact={artifact}
247
- isDirectory={isDirectory}
248
- detectionLabel={detectionLabel}
249
- isDetecting={isDetecting}
126
+ isDirectory={inspection.isDirectory}
127
+ detectionLabel={inspection.detectionLabel}
128
+ isDetecting={inspection.isDetecting}
250
129
  onClose={onClose}
251
130
  />
252
131
 
253
- <div className="min-h-0 flex-1 overflow-y-auto border-t border-border">
254
- {isDirectory ? (
255
- <DirectoryContentView
256
- artifact={artifact}
257
- skillDetection={skillDetection}
258
- />
259
- ) : (
260
- <FileContentStateView
261
- artifact={artifact}
262
- content={content}
263
- contentType={contentType}
264
- isLoading={isContentLoading}
265
- error={contentError}
266
- isTruncated={isTruncated}
267
- />
268
- )}
269
- </div>
132
+ <ArtifactContentBody
133
+ artifact={artifact}
134
+ content={inspection.content}
135
+ contentType={inspection.contentType}
136
+ isLoading={inspection.isLoading}
137
+ error={inspection.error}
138
+ isTruncated={inspection.isTruncated}
139
+ skillDetection={inspection.skillDetection}
140
+ className="min-h-0 flex-1 overflow-y-auto border-t border-border"
141
+ />
270
142
 
271
143
  <ActionBar
272
144
  artifact={artifact}
273
145
  executionId={executionId}
274
- isDirectory={isDirectory}
275
- hasContent={content !== null}
276
- copied={copied}
277
- onCopy={handleCopy}
278
- isDetected={isDetected}
279
- ctaLabel={ctaLabel}
146
+ isDirectory={inspection.isDirectory}
147
+ hasContent={inspection.content !== null}
148
+ copied={inspection.copied}
149
+ onCopy={inspection.copy}
150
+ isDetected={inspection.isDetected}
151
+ ctaLabel={inspection.ctaLabel}
280
152
  isTerminal={isTerminal}
281
- isApplying={isApplying}
282
- applyResult={applyResult}
283
- applyError={applyError}
284
- onApply={handleApply}
153
+ isApplying={inspection.isApplying}
154
+ applyResult={inspection.applyResult}
155
+ applyError={inspection.applyError}
156
+ onApply={inspection.apply}
285
157
  onImplement={onImplement ? handleImplement : undefined}
286
158
  />
287
159
 
@@ -291,7 +163,7 @@ export function ArtifactPreviewContent({
291
163
  aria-atomic="true"
292
164
  className="sr-only"
293
165
  >
294
- {copied && "Content copied to clipboard"}
166
+ {inspection.copied && "Content copied to clipboard"}
295
167
  </div>
296
168
  </div>
297
169
  );
@@ -368,7 +240,7 @@ export interface ArtifactPreviewModalProps {
368
240
  * ```
369
241
  *
370
242
  * @see {@link ArtifactPreviewContent} — the content component (use directly for non-dialog contexts)
371
- * @see {@link ArtifactCard} — compact card that triggers preview via `onPreview`
243
+ * @see {@link ArtifactRow} — dense list row that triggers preview via `onOpen`
372
244
  */
373
245
  export function ArtifactPreviewModal({
374
246
  artifact,
@@ -496,128 +368,6 @@ function ContentHeader({
496
368
  );
497
369
  }
498
370
 
499
- // ---------------------------------------------------------------------------
500
- // FileContentStateView (internal — loading/error/empty states + renderer)
501
- // ---------------------------------------------------------------------------
502
-
503
- const SKELETON_LINE_WIDTHS = [85, 72, 90, 65, 78, 88, 70, 82] as const;
504
-
505
- function FileContentStateView({
506
- artifact,
507
- content,
508
- contentType,
509
- isLoading,
510
- error,
511
- isTruncated,
512
- }: {
513
- readonly artifact: ExecutionArtifact;
514
- readonly content: string | null;
515
- readonly contentType: string | null;
516
- readonly isLoading: boolean;
517
- readonly error: Error | null;
518
- readonly isTruncated: boolean;
519
- }) {
520
- if (isLoading) {
521
- return (
522
- <div className="space-y-2 p-4" aria-busy="true" aria-label="Loading content">
523
- {SKELETON_LINE_WIDTHS.map((width, i) => (
524
- <div
525
- key={i}
526
- className="h-4 animate-pulse rounded bg-muted"
527
- style={{ width: `${width}%` }}
528
- aria-hidden="true"
529
- />
530
- ))}
531
- </div>
532
- );
533
- }
534
-
535
- if (error) {
536
- return (
537
- <div className="flex flex-col items-center justify-center gap-2 p-8 text-center">
538
- <ErrorAlertIcon />
539
- <p className="text-sm text-destructive">{error.message}</p>
540
- </div>
541
- );
542
- }
543
-
544
- if (content === null) {
545
- return (
546
- <div className="p-8 text-center text-sm text-muted-foreground">
547
- Content not available for preview.
548
- </div>
549
- );
550
- }
551
-
552
- return (
553
- <ArtifactContentRenderer
554
- content={content}
555
- fileName={artifact.name}
556
- contentType={contentType}
557
- isTruncated={isTruncated}
558
- />
559
- );
560
- }
561
-
562
- // ---------------------------------------------------------------------------
563
- // DirectoryContentView (internal)
564
- // ---------------------------------------------------------------------------
565
-
566
- function DirectoryContentView({
567
- artifact,
568
- skillDetection,
569
- }: {
570
- readonly artifact: ExecutionArtifact;
571
- readonly skillDetection: SkillPackageDetection;
572
- }) {
573
- const entries = artifact.entries;
574
-
575
- return (
576
- <div className="p-4">
577
- {skillDetection.detected && (
578
- <div className="mb-4 rounded-md bg-primary-subtle p-3">
579
- <p className="text-sm font-medium text-foreground">
580
- {skillDetection.skillName}
581
- </p>
582
- {skillDetection.skillDescription && (
583
- <p className="mt-1 text-xs text-muted-foreground">
584
- {skillDetection.skillDescription}
585
- </p>
586
- )}
587
- </div>
588
- )}
589
-
590
- {entries.length > 0 ? (
591
- <div>
592
- <h3 className="mb-2 text-xs font-medium text-muted-foreground">
593
- Files ({entries.length})
594
- </h3>
595
- <ul className="space-y-0.5" role="list">
596
- {entries.map((entry) => (
597
- <li
598
- key={entry}
599
- className="flex items-center gap-2 rounded-sm px-2 py-1 font-mono text-xs text-foreground"
600
- >
601
- <EntryIcon name={entry} />
602
- <span className="min-w-0 truncate">{entry}</span>
603
- </li>
604
- ))}
605
- </ul>
606
- </div>
607
- ) : (
608
- <p className="text-sm text-muted-foreground">
609
- File listing not available.
610
- </p>
611
- )}
612
- </div>
613
- );
614
- }
615
-
616
- function EntryIcon({ name }: { readonly name: string }) {
617
- if (name.endsWith("/")) return <FolderSmallIcon />;
618
- return <FileSmallIcon />;
619
- }
620
-
621
371
  // ---------------------------------------------------------------------------
622
372
  // ActionBar (internal)
623
373
  // ---------------------------------------------------------------------------
@@ -727,7 +477,7 @@ function ActionBar({
727
477
  </span>
728
478
  ) : isDetected && ctaLabel ? (
729
479
  <span data-cursor-target="apply-resource-button">
730
- <ApplyButton
480
+ <ArtifactApplyButton
731
481
  label={ctaLabel}
732
482
  isTerminal={isTerminal}
733
483
  isApplying={isApplying}
@@ -740,46 +490,6 @@ function ActionBar({
740
490
  );
741
491
  }
742
492
 
743
- function ApplyButton({
744
- label,
745
- isTerminal,
746
- isApplying,
747
- onApply,
748
- }: {
749
- readonly label: string;
750
- readonly isTerminal: boolean;
751
- readonly isApplying: boolean;
752
- readonly onApply: () => void;
753
- }) {
754
- const canApply = isTerminal && !isApplying;
755
-
756
- return (
757
- <button
758
- type="button"
759
- onClick={canApply ? onApply : undefined}
760
- disabled={!canApply}
761
- aria-busy={isApplying}
762
- className={cn(
763
- "rounded-md px-4 py-1.5 text-xs font-medium transition-colors",
764
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
765
- "disabled:cursor-not-allowed",
766
- canApply
767
- ? "bg-primary text-primary-foreground hover:bg-primary-hover"
768
- : "bg-muted text-muted-foreground",
769
- )}
770
- >
771
- {isApplying ? (
772
- <span className="inline-flex items-center gap-1.5">
773
- <SpinnerIcon />
774
- Applying{"\u2026"}
775
- </span>
776
- ) : (
777
- label
778
- )}
779
- </button>
780
- );
781
- }
782
-
783
493
  // ---------------------------------------------------------------------------
784
494
  // Inline SVG icons (SDK pattern: no external icon dependency)
785
495
  // ---------------------------------------------------------------------------
@@ -840,45 +550,6 @@ function FolderIcon() {
840
550
  );
841
551
  }
842
552
 
843
- function FileSmallIcon() {
844
- return (
845
- <svg
846
- width="12"
847
- height="12"
848
- viewBox="0 0 14 14"
849
- fill="none"
850
- stroke="currentColor"
851
- strokeWidth="1.5"
852
- strokeLinecap="round"
853
- strokeLinejoin="round"
854
- className="shrink-0 text-muted-foreground"
855
- aria-hidden="true"
856
- >
857
- <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" />
858
- <path d="M8 1V4H11" />
859
- </svg>
860
- );
861
- }
862
-
863
- function FolderSmallIcon() {
864
- return (
865
- <svg
866
- width="12"
867
- height="12"
868
- viewBox="0 0 14 14"
869
- fill="none"
870
- stroke="currentColor"
871
- strokeWidth="1.5"
872
- strokeLinecap="round"
873
- strokeLinejoin="round"
874
- className="shrink-0 text-muted-foreground"
875
- aria-hidden="true"
876
- >
877
- <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" />
878
- </svg>
879
- );
880
- }
881
-
882
553
  function CopyIcon() {
883
554
  return (
884
555
  <svg
@@ -958,40 +629,3 @@ function CheckIcon() {
958
629
  );
959
630
  }
960
631
 
961
- function SpinnerIcon() {
962
- return (
963
- <svg
964
- width="12"
965
- height="12"
966
- viewBox="0 0 12 12"
967
- fill="none"
968
- stroke="currentColor"
969
- strokeWidth="1.5"
970
- className="shrink-0 animate-spin"
971
- aria-hidden="true"
972
- >
973
- <path d="M6 1.5A4.5 4.5 0 1 1 1.5 6" strokeLinecap="round" />
974
- </svg>
975
- );
976
- }
977
-
978
- function ErrorAlertIcon() {
979
- return (
980
- <svg
981
- width="16"
982
- height="16"
983
- viewBox="0 0 16 16"
984
- fill="none"
985
- stroke="currentColor"
986
- strokeWidth="1.5"
987
- strokeLinecap="round"
988
- strokeLinejoin="round"
989
- className="text-destructive"
990
- aria-hidden="true"
991
- >
992
- <circle cx="8" cy="8" r="6.5" />
993
- <path d="M8 5.5V8.5" />
994
- <circle cx="8" cy="11" r="0.5" fill="currentColor" stroke="none" />
995
- </svg>
996
- );
997
- }
@@ -0,0 +1,165 @@
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 { cn } from "@stigmer/theme";
6
+ import { FileTypeIcon, FolderTypeIcon } from "../internal/file-icons/index.js";
7
+ import { useArtifactDownload } from "./useArtifactDownload.js";
8
+ import { formatArtifactSize } from "./artifact-utils.js";
9
+
10
+ /** Props for {@link ArtifactRow}. */
11
+ export interface ArtifactRowProps {
12
+ /** The execution artifact to render. */
13
+ readonly artifact: ExecutionArtifact;
14
+ /** ID of the execution that produced this artifact — used to mint its download URL. */
15
+ readonly executionId: string;
16
+ /**
17
+ * When `true`, another artifact in the list shares this artifact's display
18
+ * `name` but has a different `sandbox_path`; the row renders the parent
19
+ * directory as a muted subtitle to disambiguate. Typically supplied from
20
+ * {@link SessionArtifactEntry.hasNameCollision}.
21
+ */
22
+ readonly hasNameCollision?: boolean;
23
+ /** Single click / Enter / Space: open the artifact (a preview tab, or a modal). */
24
+ readonly onOpen: () => void;
25
+ /**
26
+ * Double click: promote the artifact to a persistent (pinned) tab. Mirrors the
27
+ * file tree's `onOpenFile` (preview) / `onActivateFile` (pin) split — the
28
+ * leading single click of the double click has already opened the preview, so
29
+ * this promotes it. Omit in panel-less hosts (e.g. the modal-only widget):
30
+ * with no handler the row binds no `onDoubleClick`, so a double click is just
31
+ * two harmless single clicks, exactly as `FileTreeNode` behaves.
32
+ */
33
+ readonly onActivate?: () => void;
34
+ /** Additional CSS classes for the row's `<li>`. */
35
+ readonly className?: string;
36
+ }
37
+
38
+ /**
39
+ * One dense, VS Code-style artifact row: a full-width open button (shared
40
+ * file-type icon + name + optional collision subtitle + size) with a sibling,
41
+ * hover/focus-revealed Download button.
42
+ *
43
+ * The open target and the Download control are SIBLINGS, never nested — a
44
+ * `<button>` inside a `<button>` is an axe `nested-interactive` (WCAG 4.1.2)
45
+ * violation. This mirrors `ExplorerRoot`'s header + remove-control pattern.
46
+ *
47
+ * Deliberately execution-pure and presentational: it takes only the artifact,
48
+ * its `executionId`, and open/activate closures — no session-domain types and
49
+ * no content fetch (resource detection and Apply/Push live in the opened
50
+ * document/modal, not the list). The two hosts — the session panel's
51
+ * `ArtifactsTab` facet and the panel-less {@link ArtifactsWidget} — map
52
+ * their entries onto these props and own the behavior wiring.
53
+ *
54
+ * @see ArtifactsTab — session-panel facet (rows open editor-pane document tabs)
55
+ * @see ArtifactsWidget — panel-less embeddable (rows open the preview modal)
56
+ */
57
+ export function ArtifactRow({
58
+ artifact,
59
+ executionId,
60
+ hasNameCollision = false,
61
+ onOpen,
62
+ onActivate,
63
+ className,
64
+ }: ArtifactRowProps) {
65
+ const isDirectory = artifact.kind === ExecutionArtifactKind.DIRECTORY;
66
+ const { download, isDownloading } = useArtifactDownload(executionId);
67
+
68
+ const parentDir =
69
+ hasNameCollision && artifact.sandboxPath
70
+ ? parentDirectory(artifact.sandboxPath)
71
+ : null;
72
+
73
+ return (
74
+ <li className={cn("group flex items-stretch", className)}>
75
+ <button
76
+ type="button"
77
+ onClick={onOpen}
78
+ onDoubleClick={onActivate}
79
+ title={artifact.sandboxPath || artifact.name}
80
+ className={cn(
81
+ "flex min-w-0 flex-1 items-center gap-2 px-2 py-1 text-left text-xs text-muted-foreground transition-colors",
82
+ "hover:bg-muted hover:text-foreground",
83
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
84
+ )}
85
+ >
86
+ <span className="shrink-0 text-muted-foreground">
87
+ {isDirectory ? (
88
+ <FolderTypeIcon open={false} />
89
+ ) : (
90
+ <FileTypeIcon fileName={artifact.name} />
91
+ )}
92
+ </span>
93
+ <span className="min-w-0 flex-1 truncate text-foreground">
94
+ {artifact.name}
95
+ {isDirectory && "/"}
96
+ {parentDir && (
97
+ <span className="ml-1.5 text-[0.65rem] text-muted-foreground">
98
+ {parentDir}
99
+ </span>
100
+ )}
101
+ </span>
102
+ <span className="shrink-0 tabular-nums text-[0.65rem] text-muted-foreground-faint">
103
+ {formatArtifactSize(artifact.sizeBytes)}
104
+ </span>
105
+ </button>
106
+ <button
107
+ type="button"
108
+ onClick={() => download(artifact.storageKey, artifact.name)}
109
+ disabled={isDownloading}
110
+ aria-label={
111
+ isDownloading
112
+ ? `Preparing ${artifact.name}`
113
+ : `Download ${artifact.name}`
114
+ }
115
+ title={isDirectory ? "Download ZIP" : "Download"}
116
+ className={cn(
117
+ "flex shrink-0 items-center px-2 text-muted-foreground opacity-0 transition-opacity",
118
+ "group-hover:opacity-100 focus-visible:opacity-100",
119
+ "hover:text-foreground disabled:opacity-50",
120
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
121
+ )}
122
+ >
123
+ <DownloadIcon />
124
+ </button>
125
+ </li>
126
+ );
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Helpers
131
+ // ---------------------------------------------------------------------------
132
+
133
+ /**
134
+ * Extracts a human-readable parent directory label from a sandbox path.
135
+ * Given `/workspace/configs/agent.yaml` returns `configs/`.
136
+ * Returns `null` when the path has no meaningful parent segment.
137
+ */
138
+ function parentDirectory(sandboxPath: string): string | null {
139
+ const lastSlash = sandboxPath.lastIndexOf("/");
140
+ if (lastSlash <= 0) return null;
141
+ const parent = sandboxPath.slice(0, lastSlash);
142
+ const segment = parent.slice(parent.lastIndexOf("/") + 1);
143
+ return segment ? `${segment}/` : null;
144
+ }
145
+
146
+ function DownloadIcon() {
147
+ return (
148
+ <svg
149
+ width="12"
150
+ height="12"
151
+ viewBox="0 0 12 12"
152
+ fill="none"
153
+ stroke="currentColor"
154
+ strokeWidth="1.5"
155
+ strokeLinecap="round"
156
+ strokeLinejoin="round"
157
+ className="shrink-0"
158
+ aria-hidden="true"
159
+ >
160
+ <path d="M6 1.5V8.5" />
161
+ <path d="M3 6L6 9L9 6" />
162
+ <path d="M2 10.5H10" />
163
+ </svg>
164
+ );
165
+ }