@stigmer/react 0.0.52 → 0.0.54

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 (160) hide show
  1. package/deployment-mode.d.ts +35 -0
  2. package/deployment-mode.d.ts.map +1 -0
  3. package/deployment-mode.js +41 -0
  4. package/deployment-mode.js.map +1 -0
  5. package/execution/ApprovalCard.d.ts +8 -6
  6. package/execution/ApprovalCard.d.ts.map +1 -1
  7. package/execution/ApprovalCard.js +34 -96
  8. package/execution/ApprovalCard.js.map +1 -1
  9. package/execution/ArtifactCard.d.ts +11 -1
  10. package/execution/ArtifactCard.d.ts.map +1 -1
  11. package/execution/ArtifactCard.js +22 -3
  12. package/execution/ArtifactCard.js.map +1 -1
  13. package/execution/ArtifactPreviewModal.d.ts.map +1 -1
  14. package/execution/ArtifactPreviewModal.js +1 -1
  15. package/execution/ArtifactPreviewModal.js.map +1 -1
  16. package/execution/ArtifactsWidget.d.ts +26 -19
  17. package/execution/ArtifactsWidget.d.ts.map +1 -1
  18. package/execution/ArtifactsWidget.js +24 -26
  19. package/execution/ArtifactsWidget.js.map +1 -1
  20. package/execution/McpToolDetail.d.ts +48 -0
  21. package/execution/McpToolDetail.d.ts.map +1 -0
  22. package/execution/McpToolDetail.js +159 -0
  23. package/execution/McpToolDetail.js.map +1 -0
  24. package/execution/MessageThread.d.ts +10 -1
  25. package/execution/MessageThread.d.ts.map +1 -1
  26. package/execution/MessageThread.js +19 -17
  27. package/execution/MessageThread.js.map +1 -1
  28. package/execution/SandboxContext.d.ts +32 -0
  29. package/execution/SandboxContext.d.ts.map +1 -0
  30. package/execution/SandboxContext.js +26 -0
  31. package/execution/SandboxContext.js.map +1 -0
  32. package/execution/ToolArgsView.d.ts +41 -0
  33. package/execution/ToolArgsView.d.ts.map +1 -0
  34. package/execution/ToolArgsView.js +134 -0
  35. package/execution/ToolArgsView.js.map +1 -0
  36. package/execution/ToolCallDetail.d.ts +11 -4
  37. package/execution/ToolCallDetail.d.ts.map +1 -1
  38. package/execution/ToolCallDetail.js +32 -101
  39. package/execution/ToolCallDetail.js.map +1 -1
  40. package/execution/ToolCallGroup.d.ts.map +1 -1
  41. package/execution/ToolCallGroup.js +3 -2
  42. package/execution/ToolCallGroup.js.map +1 -1
  43. package/execution/ToolCallItem.d.ts +2 -0
  44. package/execution/ToolCallItem.d.ts.map +1 -1
  45. package/execution/ToolCallItem.js +13 -3
  46. package/execution/ToolCallItem.js.map +1 -1
  47. package/execution/WriteBackCard.d.ts +34 -0
  48. package/execution/WriteBackCard.d.ts.map +1 -0
  49. package/execution/WriteBackCard.js +75 -0
  50. package/execution/WriteBackCard.js.map +1 -0
  51. package/execution/WriteBacksWidget.d.ts +49 -0
  52. package/execution/WriteBacksWidget.d.ts.map +1 -0
  53. package/execution/WriteBacksWidget.js +44 -0
  54. package/execution/WriteBacksWidget.js.map +1 -0
  55. package/execution/__tests__/file-path-resolver.test.d.ts +2 -0
  56. package/execution/__tests__/file-path-resolver.test.d.ts.map +1 -0
  57. package/execution/__tests__/file-path-resolver.test.js +180 -0
  58. package/execution/__tests__/file-path-resolver.test.js.map +1 -0
  59. package/execution/file-path-resolver.d.ts +3 -3
  60. package/execution/file-path-resolver.d.ts.map +1 -1
  61. package/execution/file-path-resolver.js +23 -12
  62. package/execution/file-path-resolver.js.map +1 -1
  63. package/execution/index.d.ts +16 -1
  64. package/execution/index.d.ts.map +1 -1
  65. package/execution/index.js +9 -1
  66. package/execution/index.js.map +1 -1
  67. package/execution/sandbox-path-normalizer.d.ts +46 -0
  68. package/execution/sandbox-path-normalizer.d.ts.map +1 -0
  69. package/execution/sandbox-path-normalizer.js +73 -0
  70. package/execution/sandbox-path-normalizer.js.map +1 -0
  71. package/execution/tool-categories.d.ts +35 -8
  72. package/execution/tool-categories.d.ts.map +1 -1
  73. package/execution/tool-categories.js +76 -10
  74. package/execution/tool-categories.js.map +1 -1
  75. package/execution/tool-rendering-primitives.d.ts +61 -0
  76. package/execution/tool-rendering-primitives.d.ts.map +1 -0
  77. package/execution/tool-rendering-primitives.js +106 -0
  78. package/execution/tool-rendering-primitives.js.map +1 -0
  79. package/execution/useArtifactContent.d.ts +5 -1
  80. package/execution/useArtifactContent.d.ts.map +1 -1
  81. package/execution/useArtifactContent.js +6 -2
  82. package/execution/useArtifactContent.js.map +1 -1
  83. package/execution/useWorkspaceWriteBacks.d.ts +40 -0
  84. package/execution/useWorkspaceWriteBacks.d.ts.map +1 -0
  85. package/execution/useWorkspaceWriteBacks.js +41 -0
  86. package/execution/useWorkspaceWriteBacks.js.map +1 -0
  87. package/github/GitHubRepoPicker.d.ts +5 -2
  88. package/github/GitHubRepoPicker.d.ts.map +1 -1
  89. package/github/GitHubRepoPicker.js +133 -36
  90. package/github/GitHubRepoPicker.js.map +1 -1
  91. package/github/index.d.ts +1 -0
  92. package/github/index.d.ts.map +1 -1
  93. package/github/index.js +1 -0
  94. package/github/index.js.map +1 -1
  95. package/github/useGitHubSearch.d.ts +20 -0
  96. package/github/useGitHubSearch.d.ts.map +1 -0
  97. package/github/useGitHubSearch.js +127 -0
  98. package/github/useGitHubSearch.js.map +1 -0
  99. package/index.d.ts +9 -6
  100. package/index.d.ts.map +1 -1
  101. package/index.js +7 -3
  102. package/index.js.map +1 -1
  103. package/internal/CloudFeatureNotice.d.ts +19 -0
  104. package/internal/CloudFeatureNotice.d.ts.map +1 -0
  105. package/internal/CloudFeatureNotice.js +21 -0
  106. package/internal/CloudFeatureNotice.js.map +1 -0
  107. package/mcp-server/McpServerDetailView.d.ts +15 -1
  108. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  109. package/mcp-server/McpServerDetailView.js +11 -3
  110. package/mcp-server/McpServerDetailView.js.map +1 -1
  111. package/package.json +4 -4
  112. package/provider.d.ts +14 -2
  113. package/provider.d.ts.map +1 -1
  114. package/provider.js +3 -2
  115. package/provider.js.map +1 -1
  116. package/session/index.d.ts +4 -0
  117. package/session/index.d.ts.map +1 -1
  118. package/session/index.js +2 -0
  119. package/session/index.js.map +1 -1
  120. package/session/useSessionArtifacts.d.ts +73 -0
  121. package/session/useSessionArtifacts.d.ts.map +1 -0
  122. package/session/useSessionArtifacts.js +95 -0
  123. package/session/useSessionArtifacts.js.map +1 -0
  124. package/session/useSessionWriteBacks.d.ts +56 -0
  125. package/session/useSessionWriteBacks.d.ts.map +1 -0
  126. package/session/useSessionWriteBacks.js +56 -0
  127. package/session/useSessionWriteBacks.js.map +1 -0
  128. package/src/deployment-mode.ts +46 -0
  129. package/src/execution/ApprovalCard.tsx +130 -283
  130. package/src/execution/ArtifactCard.tsx +40 -0
  131. package/src/execution/ArtifactPreviewModal.tsx +2 -0
  132. package/src/execution/ArtifactsWidget.tsx +51 -43
  133. package/src/execution/McpToolDetail.tsx +283 -0
  134. package/src/execution/MessageThread.tsx +18 -0
  135. package/src/execution/SandboxContext.ts +47 -0
  136. package/src/execution/ToolArgsView.tsx +279 -0
  137. package/src/execution/ToolCallDetail.tsx +54 -220
  138. package/src/execution/ToolCallGroup.tsx +3 -2
  139. package/src/execution/ToolCallItem.tsx +21 -3
  140. package/src/execution/WriteBackCard.tsx +210 -0
  141. package/src/execution/WriteBacksWidget.tsx +82 -0
  142. package/src/execution/__tests__/file-path-resolver.test.ts +295 -0
  143. package/src/execution/file-path-resolver.ts +24 -12
  144. package/src/execution/index.ts +38 -0
  145. package/src/execution/sandbox-path-normalizer.ts +80 -0
  146. package/src/execution/tool-categories.ts +89 -9
  147. package/src/execution/tool-rendering-primitives.tsx +253 -0
  148. package/src/execution/useArtifactContent.ts +6 -1
  149. package/src/execution/useWorkspaceWriteBacks.ts +56 -0
  150. package/src/github/GitHubRepoPicker.tsx +413 -108
  151. package/src/github/index.ts +5 -0
  152. package/src/github/useGitHubSearch.ts +162 -0
  153. package/src/index.ts +27 -0
  154. package/src/internal/CloudFeatureNotice.tsx +60 -0
  155. package/src/mcp-server/McpServerDetailView.tsx +24 -2
  156. package/src/provider.tsx +18 -2
  157. package/src/session/index.ts +12 -0
  158. package/src/session/useSessionArtifacts.ts +143 -0
  159. package/src/session/useSessionWriteBacks.ts +94 -0
  160. package/styles.css +1 -1
@@ -8,7 +8,8 @@ import {
8
8
  resolveToolCategory,
9
9
  extractPrimaryArgFromPreview,
10
10
  } from "./tool-categories";
11
- import { FilePathLink } from "./FilePathLink";
11
+ import { CATEGORY_ICON } from "./ToolCallItem";
12
+ import { ToolArgsView } from "./ToolArgsView";
12
13
 
13
14
  export interface ApprovalCardProps {
14
15
  readonly pendingApproval: PendingApproval;
@@ -24,16 +25,16 @@ export interface ApprovalCardProps {
24
25
  readonly className?: string;
25
26
  }
26
27
 
27
- const TRUNCATION_LINE_LIMIT = 8;
28
-
29
28
  /**
30
- * Renders a pending tool-call approval request as a prominent card
31
- * with Approve, Skip, and Reject action buttons.
29
+ * Renders a pending tool-call approval request with the same visual
30
+ * structure as an expanded {@link ToolCallItem} in the history list.
31
+ *
32
+ * The compact header row matches the ToolCallItem layout:
33
+ * [CategoryIcon] Label primaryArg [⏳ waiting Xm]
32
34
  *
33
- * Tool-type-aware: shell tools render the command in a terminal-style
34
- * block, file tools show the path prominently, and destructive tools
35
- * (delete) use warning styling. Falls back to generic JSON args
36
- * display for unrecognized tools.
35
+ * Arguments are rendered through the shared {@link ToolArgsView}
36
+ * dispatch, ensuring pixel-level parity between the approval
37
+ * preview and the post-execution detail view.
37
38
  *
38
39
  * @example
39
40
  * ```tsx
@@ -66,250 +67,141 @@ export function ApprovalCard({
66
67
  }
67
68
  }, [isSubmitting]);
68
69
 
69
- const categoryInfo = resolveToolCategory(pendingApproval.toolName);
70
- const primaryArg = extractPrimaryArgFromPreview(
70
+ const categoryInfo = resolveToolCategory(
71
71
  pendingApproval.toolName,
72
- pendingApproval.argsPreview,
72
+ pendingApproval.mcpServerSlug,
73
73
  );
74
74
 
75
- const { header, headerIcon, borderClass } = getApprovalHeader(
76
- categoryInfo.category,
77
- pendingApproval.fromSubAgent,
75
+ const CategoryIcon = CATEGORY_ICON[categoryInfo.category];
76
+
77
+ const primaryArg = useMemo(
78
+ () =>
79
+ extractPrimaryArgFromPreview(
80
+ pendingApproval.toolName,
81
+ pendingApproval.argsPreview,
82
+ pendingApproval.mcpServerSlug,
83
+ ),
84
+ [
85
+ pendingApproval.toolName,
86
+ pendingApproval.argsPreview,
87
+ pendingApproval.mcpServerSlug,
88
+ ],
78
89
  );
79
90
 
91
+ const parsedArgs = useMemo<Record<string, unknown> | null>(() => {
92
+ if (!pendingApproval.argsPreview) return null;
93
+ try {
94
+ const parsed = JSON.parse(pendingApproval.argsPreview);
95
+ if (typeof parsed === "object" && parsed !== null) {
96
+ return parsed as Record<string, unknown>;
97
+ }
98
+ return null;
99
+ } catch {
100
+ return null;
101
+ }
102
+ }, [pendingApproval.argsPreview]);
103
+
104
+ const borderClass =
105
+ categoryInfo.category === "delete"
106
+ ? "border-destructive/30 bg-destructive/5"
107
+ : "border-warning/30 bg-warning/5";
108
+
80
109
  return (
81
110
  <div
82
111
  role="alert"
83
112
  aria-label={`Approval required for ${pendingApproval.toolName}`}
84
- className={cn(
85
- "rounded-lg border px-4 py-3",
86
- borderClass,
87
- className,
88
- )}
113
+ className={cn("rounded-lg border", borderClass, className)}
89
114
  >
90
- {/* Header */}
91
- <div className="flex items-center gap-2 text-sm font-medium text-warning">
92
- {headerIcon}
93
- <span>{header}</span>
94
- </div>
95
-
96
- {/* Sub-agent attribution */}
97
- {pendingApproval.fromSubAgent && pendingApproval.subAgentName && (
98
- <p className="mt-1.5 text-xs text-muted-foreground">
99
- Sub-agent{" "}
100
- <span className="font-medium text-foreground">
101
- {pendingApproval.subAgentName}
102
- </span>{" "}
103
- wants to execute a tool
104
- </p>
105
- )}
115
+ {/* Compact header row — matches ToolCallItem layout */}
116
+ <div
117
+ className={cn(
118
+ "flex items-center gap-2 px-2.5 py-1.5 text-xs",
119
+ "border-b border-border/30",
120
+ )}
121
+ >
122
+ <span className="shrink-0 text-warning" aria-hidden="true">
123
+ <CategoryIcon />
124
+ </span>
106
125
 
107
- {/* Tool name badge */}
108
- <div className="mt-2 flex items-center gap-2">
109
- <span className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground">
110
- {categoryInfo.label}
126
+ <span className="min-w-0 flex-1 flex items-baseline gap-1.5 overflow-hidden">
127
+ <span className="shrink-0 font-medium text-foreground">
128
+ {categoryInfo.label}
129
+ </span>
130
+ {primaryArg && (
131
+ <span className="min-w-0 truncate font-mono text-muted-foreground">
132
+ {primaryArg}
133
+ </span>
134
+ )}
111
135
  </span>
112
- <WaitingDuration requestedAt={pendingApproval.requestedAt} />
113
- </div>
114
136
 
115
- {/* Approval message */}
116
- {pendingApproval.message && (
117
- <p className="mt-2 text-sm text-foreground">
118
- {pendingApproval.message}
119
- </p>
120
- )}
137
+ <WaitingDuration requestedAt={pendingApproval.requestedAt} />
121
138
 
122
- {/* Category-specific args preview */}
123
- <div className="mt-2">
124
- <CategoryArgsPreview
125
- category={categoryInfo.category}
126
- primaryArg={primaryArg}
127
- argsPreview={pendingApproval.argsPreview}
128
- />
139
+ <span className="shrink-0 text-warning" aria-hidden="true">
140
+ <ClockIcon />
141
+ </span>
129
142
  </div>
130
143
 
131
- {/* Action buttons */}
132
- <div className="mt-3 flex items-center gap-2">
133
- <ActionButton
134
- label="Approve"
135
- action={ApprovalAction.APPROVE}
136
- activeAction={activeAction}
137
- isSubmitting={isSubmitting}
138
- onClick={handleAction}
139
- variant="approve"
140
- />
141
- <ActionButton
142
- label="Skip"
143
- action={ApprovalAction.SKIP}
144
- activeAction={activeAction}
145
- isSubmitting={isSubmitting}
146
- onClick={handleAction}
147
- variant="skip"
148
- />
149
- <ActionButton
150
- label="Reject"
151
- action={ApprovalAction.REJECT}
152
- activeAction={activeAction}
153
- isSubmitting={isSubmitting}
154
- onClick={handleAction}
155
- variant="reject"
156
- />
144
+ {/* Body */}
145
+ <div className="px-3 py-2.5 space-y-2">
146
+ {/* Sub-agent attribution */}
147
+ {pendingApproval.fromSubAgent && pendingApproval.subAgentName && (
148
+ <p className="text-xs text-muted-foreground">
149
+ Sub-agent{" "}
150
+ <span className="font-medium text-foreground">
151
+ {pendingApproval.subAgentName}
152
+ </span>{" "}
153
+ wants to execute this tool
154
+ </p>
155
+ )}
156
+
157
+ {/* Approval message */}
158
+ {pendingApproval.message && (
159
+ <p className="text-xs text-foreground">
160
+ {pendingApproval.message}
161
+ </p>
162
+ )}
163
+
164
+ {/* Category-specific args preview — shared with ToolCallDetail */}
165
+ {parsedArgs && (
166
+ <ToolArgsView
167
+ toolName={pendingApproval.toolName}
168
+ args={parsedArgs}
169
+ mcpServerSlug={pendingApproval.mcpServerSlug}
170
+ />
171
+ )}
172
+
173
+ {/* Action buttons */}
174
+ <div className="flex items-center gap-2 pt-1">
175
+ <ActionButton
176
+ label="Approve"
177
+ action={ApprovalAction.APPROVE}
178
+ activeAction={activeAction}
179
+ isSubmitting={isSubmitting}
180
+ onClick={handleAction}
181
+ variant="approve"
182
+ />
183
+ <ActionButton
184
+ label="Skip"
185
+ action={ApprovalAction.SKIP}
186
+ activeAction={activeAction}
187
+ isSubmitting={isSubmitting}
188
+ onClick={handleAction}
189
+ variant="skip"
190
+ />
191
+ <ActionButton
192
+ label="Reject"
193
+ action={ApprovalAction.REJECT}
194
+ activeAction={activeAction}
195
+ isSubmitting={isSubmitting}
196
+ onClick={handleAction}
197
+ variant="reject"
198
+ />
199
+ </div>
157
200
  </div>
158
201
  </div>
159
202
  );
160
203
  }
161
204
 
162
- // ---------------------------------------------------------------------------
163
- // Category-aware header
164
- // ---------------------------------------------------------------------------
165
-
166
- function getApprovalHeader(
167
- category: string,
168
- fromSubAgent: boolean,
169
- ): {
170
- header: string;
171
- headerIcon: React.JSX.Element;
172
- borderClass: string;
173
- } {
174
- if (fromSubAgent) {
175
- return {
176
- header: "Sub-agent approval required",
177
- headerIcon: <ShieldIcon />,
178
- borderClass: "border-warning/30 bg-warning/5",
179
- };
180
- }
181
-
182
- switch (category) {
183
- case "shell":
184
- return {
185
- header: "Execute command",
186
- headerIcon: <TerminalApprovalIcon />,
187
- borderClass: "border-warning/30 bg-warning/5",
188
- };
189
- case "delete":
190
- return {
191
- header: "Delete file",
192
- headerIcon: <ShieldIcon />,
193
- borderClass: "border-destructive/30 bg-destructive/5",
194
- };
195
- default:
196
- return {
197
- header: "Approval required",
198
- headerIcon: <ShieldIcon />,
199
- borderClass: "border-warning/30 bg-warning/5",
200
- };
201
- }
202
- }
203
-
204
- // ---------------------------------------------------------------------------
205
- // Category-specific args preview
206
- // ---------------------------------------------------------------------------
207
-
208
- function CategoryArgsPreview({
209
- category,
210
- primaryArg,
211
- argsPreview,
212
- }: {
213
- category: string;
214
- primaryArg: string | null;
215
- argsPreview: string;
216
- }) {
217
- if (category === "shell" && primaryArg) {
218
- return <ShellArgsPreview command={primaryArg} />;
219
- }
220
-
221
- if ((category === "read" || category === "write" || category === "edit" || category === "delete") && primaryArg) {
222
- return <FileArgsPreview path={primaryArg} argsPreview={argsPreview} />;
223
- }
224
-
225
- if ((category === "search" || category === "list") && primaryArg) {
226
- return <SearchArgsPreview pattern={primaryArg} />;
227
- }
228
-
229
- if (!argsPreview) return null;
230
- return <GenericArgsPreview content={argsPreview} />;
231
- }
232
-
233
- function ShellArgsPreview({ command }: { command: string }) {
234
- return (
235
- <div className="rounded-md border border-border bg-[var(--stgm-terminal-bg,#1a1a2e)] p-2.5">
236
- <pre className="whitespace-pre-wrap break-words font-mono text-xs text-[var(--stgm-terminal-fg,#e0e0e0)]">
237
- <span className="select-none text-[var(--stgm-terminal-prompt,#6b7280)]">$ </span>
238
- {command}
239
- </pre>
240
- </div>
241
- );
242
- }
243
-
244
- function FileArgsPreview({
245
- path,
246
- argsPreview,
247
- }: {
248
- path: string;
249
- argsPreview: string;
250
- }) {
251
- return (
252
- <div className="space-y-1.5">
253
- <div className="flex items-center gap-1.5 text-xs">
254
- <FilePathIcon />
255
- <FilePathLink path={path} className="text-xs" />
256
- </div>
257
- {argsPreview && <GenericArgsPreview content={argsPreview} />}
258
- </div>
259
- );
260
- }
261
-
262
- function SearchArgsPreview({ pattern }: { pattern: string }) {
263
- return (
264
- <div className="flex items-center gap-1.5 text-xs">
265
- <span className="text-muted-foreground">Pattern:</span>
266
- <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">
267
- {pattern}
268
- </code>
269
- </div>
270
- );
271
- }
272
-
273
- function GenericArgsPreview({ content }: { content: string }) {
274
- const formatted = useMemo(() => {
275
- try {
276
- const parsed = JSON.parse(content);
277
- return JSON.stringify(parsed, null, 2);
278
- } catch {
279
- return content;
280
- }
281
- }, [content]);
282
-
283
- const lines = formatted.split("\n");
284
- const needsTruncation = lines.length > TRUNCATION_LINE_LIMIT;
285
- const [isExpanded, setIsExpanded] = useState(false);
286
-
287
- const displayContent =
288
- needsTruncation && !isExpanded
289
- ? lines.slice(0, TRUNCATION_LINE_LIMIT).join("\n") + "\n\u2026"
290
- : formatted;
291
-
292
- return (
293
- <div className="space-y-1">
294
- <span className="text-xs font-medium text-muted-foreground">
295
- Arguments
296
- </span>
297
- <pre className="max-h-48 overflow-auto whitespace-pre-wrap break-words rounded-md border border-border bg-muted/40 p-2 font-mono text-xs text-foreground">
298
- {displayContent}
299
- </pre>
300
- {needsTruncation && (
301
- <button
302
- type="button"
303
- onClick={() => setIsExpanded((v) => !v)}
304
- className="text-xs font-medium text-primary transition-colors hover:text-primary/80"
305
- >
306
- {isExpanded ? "Show less" : `Show all ${lines.length} lines`}
307
- </button>
308
- )}
309
- </div>
310
- );
311
- }
312
-
313
205
  // ---------------------------------------------------------------------------
314
206
  // Internal sub-components
315
207
  // ---------------------------------------------------------------------------
@@ -366,9 +258,6 @@ function ActionButton({
366
258
  );
367
259
  }
368
260
 
369
- /**
370
- * Live-ticking elapsed time since the approval was requested.
371
- */
372
261
  function WaitingDuration({ requestedAt }: { requestedAt: string }) {
373
262
  const startMs = useMemo(() => {
374
263
  if (!requestedAt) return null;
@@ -393,8 +282,8 @@ function WaitingDuration({ requestedAt }: { requestedAt: string }) {
393
282
  if (elapsed === null) return null;
394
283
 
395
284
  return (
396
- <span className="text-xs text-muted-foreground">
397
- waiting {formatElapsed(elapsed)}
285
+ <span className="shrink-0 text-xs text-muted-foreground">
286
+ {formatElapsed(elapsed)}
398
287
  </span>
399
288
  );
400
289
  }
@@ -421,47 +310,7 @@ function formatElapsed(ms: number): string {
421
310
  // Inline SVG icons
422
311
  // ---------------------------------------------------------------------------
423
312
 
424
- function ShieldIcon() {
425
- return (
426
- <svg
427
- width="14"
428
- height="14"
429
- viewBox="0 0 14 14"
430
- fill="none"
431
- stroke="currentColor"
432
- strokeWidth="1.5"
433
- strokeLinecap="round"
434
- strokeLinejoin="round"
435
- aria-hidden="true"
436
- >
437
- <path d="M7 1L2 3.5V6.5C2 9.75 4.1 12.35 7 13C9.9 12.35 12 9.75 12 6.5V3.5L7 1Z" />
438
- <path d="M7 5V8" />
439
- <circle cx="7" cy="10" r="0.5" fill="currentColor" stroke="none" />
440
- </svg>
441
- );
442
- }
443
-
444
- function TerminalApprovalIcon() {
445
- return (
446
- <svg
447
- width="14"
448
- height="14"
449
- viewBox="0 0 14 14"
450
- fill="none"
451
- stroke="currentColor"
452
- strokeWidth="1.5"
453
- strokeLinecap="round"
454
- strokeLinejoin="round"
455
- aria-hidden="true"
456
- >
457
- <rect x="1" y="2" width="12" height="10" rx="2" />
458
- <path d="M4 5.5L6 7.5L4 9.5" />
459
- <path d="M7.5 9.5H10" />
460
- </svg>
461
- );
462
- }
463
-
464
- function FilePathIcon() {
313
+ function ClockIcon() {
465
314
  return (
466
315
  <svg
467
316
  width="10"
@@ -469,14 +318,12 @@ function FilePathIcon() {
469
318
  viewBox="0 0 12 12"
470
319
  fill="none"
471
320
  stroke="currentColor"
472
- strokeWidth="1.2"
321
+ strokeWidth="1.5"
473
322
  strokeLinecap="round"
474
323
  strokeLinejoin="round"
475
- className="shrink-0 text-muted-foreground"
476
- aria-hidden="true"
477
324
  >
478
- <path d="M7 1H3C2.45 1 2 1.45 2 2V10C2 10.55 2.45 11 3 11H9C9.55 11 10 10.55 10 10V4L7 1Z" />
479
- <path d="M7 1V4H10" />
325
+ <circle cx="6" cy="6" r="4.5" />
326
+ <path d="M6 3.5V6L7.5 7.5" />
480
327
  </svg>
481
328
  );
482
329
  }
@@ -33,6 +33,16 @@ export interface ArtifactCardProps {
33
33
  * When omitted, the Preview action is not rendered.
34
34
  */
35
35
  readonly onPreview?: (artifact: ExecutionArtifact) => void;
36
+ /**
37
+ * When `true`, another artifact in the list shares the same display
38
+ * `name` but has a different `sandbox_path`. The card renders the
39
+ * parent directory from `sandbox_path` as a muted subtitle for
40
+ * disambiguation.
41
+ *
42
+ * Set by {@link ArtifactsWidget} via {@link useSessionArtifacts}.
43
+ * Defaults to `false`.
44
+ */
45
+ readonly hasNameCollision?: boolean;
36
46
  /** Additional CSS classes for the root element. */
37
47
  readonly className?: string;
38
48
  }
@@ -89,6 +99,7 @@ export function ArtifactCard({
89
99
  executionId,
90
100
  org,
91
101
  onPreview,
102
+ hasNameCollision = false,
92
103
  className,
93
104
  }: ArtifactCardProps) {
94
105
  // ---------------------------------------------------------------------------
@@ -104,6 +115,8 @@ export function ArtifactCard({
104
115
  const { content, isLoading: isContentLoading } = useArtifactContent(
105
116
  canDetectYaml ? executionId : null,
106
117
  canDetectYaml ? artifact.storageKey : null,
118
+ undefined,
119
+ artifact.contentHash || undefined,
107
120
  );
108
121
 
109
122
  const yamlDetection = useDetectStigmerResource(
@@ -134,6 +147,11 @@ export function ArtifactCard({
134
147
 
135
148
  const canPreview = !!onPreview && (isTextArtifact(artifact) || isDirectory);
136
149
 
150
+ const parentDir =
151
+ hasNameCollision && artifact.sandboxPath
152
+ ? parentDirectory(artifact.sandboxPath)
153
+ : null;
154
+
137
155
  // ---------------------------------------------------------------------------
138
156
  // Render
139
157
  // ---------------------------------------------------------------------------
@@ -154,6 +172,11 @@ export function ArtifactCard({
154
172
  {artifact.name}
155
173
  {isDirectory && "/"}
156
174
  </div>
175
+ {parentDir && (
176
+ <div className="truncate text-xs text-muted-foreground">
177
+ {parentDir}
178
+ </div>
179
+ )}
157
180
  <div className="text-xs tabular-nums text-muted-foreground">
158
181
  {formatArtifactSize(artifact.sizeBytes)}
159
182
  </div>
@@ -204,6 +227,23 @@ export function ArtifactCard({
204
227
  );
205
228
  }
206
229
 
230
+ // ---------------------------------------------------------------------------
231
+ // Helpers
232
+ // ---------------------------------------------------------------------------
233
+
234
+ /**
235
+ * Extracts a human-readable parent directory label from a sandbox path.
236
+ * Given `/workspace/configs/agent.yaml` returns `configs/`.
237
+ * Returns `null` when the path has no meaningful parent segment.
238
+ */
239
+ function parentDirectory(sandboxPath: string): string | null {
240
+ const lastSlash = sandboxPath.lastIndexOf("/");
241
+ if (lastSlash <= 0) return null;
242
+ const parent = sandboxPath.slice(0, lastSlash);
243
+ const segment = parent.slice(parent.lastIndexOf("/") + 1);
244
+ return segment ? `${segment}/` : null;
245
+ }
246
+
207
247
  // ---------------------------------------------------------------------------
208
248
  // Shared style constants
209
249
  // ---------------------------------------------------------------------------
@@ -143,6 +143,8 @@ export function ArtifactPreviewModal({
143
143
  } = useArtifactContent(
144
144
  canFetchContent ? executionId : null,
145
145
  canFetchContent ? artifact.storageKey : null,
146
+ undefined,
147
+ artifact.contentHash || undefined,
146
148
  );
147
149
 
148
150
  const yamlDetection = useDetectStigmerResource(