@stigmer/react 0.0.43 → 0.0.45

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 (83) hide show
  1. package/execution/ArtifactContentRenderer.d.ts +58 -0
  2. package/execution/ArtifactContentRenderer.d.ts.map +1 -0
  3. package/execution/ArtifactContentRenderer.js +163 -0
  4. package/execution/ArtifactContentRenderer.js.map +1 -0
  5. package/execution/ArtifactPreviewModal.d.ts +3 -2
  6. package/execution/ArtifactPreviewModal.d.ts.map +1 -1
  7. package/execution/ArtifactPreviewModal.js +11 -62
  8. package/execution/ArtifactPreviewModal.js.map +1 -1
  9. package/execution/ExecutionPhaseBadge.js +1 -1
  10. package/execution/ExecutionPhaseBadge.js.map +1 -1
  11. package/execution/MessageThread.d.ts.map +1 -1
  12. package/execution/MessageThread.js +21 -2
  13. package/execution/MessageThread.js.map +1 -1
  14. package/execution/SetupProgress.d.ts +35 -0
  15. package/execution/SetupProgress.d.ts.map +1 -0
  16. package/execution/SetupProgress.js +65 -0
  17. package/execution/SetupProgress.js.map +1 -0
  18. package/execution/ToolCallGroup.d.ts.map +1 -1
  19. package/execution/ToolCallGroup.js +9 -1
  20. package/execution/ToolCallGroup.js.map +1 -1
  21. package/execution/ToolCallItem.js +8 -3
  22. package/execution/ToolCallItem.js.map +1 -1
  23. package/execution/artifact-utils.d.ts +50 -0
  24. package/execution/artifact-utils.d.ts.map +1 -1
  25. package/execution/artifact-utils.js +67 -5
  26. package/execution/artifact-utils.js.map +1 -1
  27. package/execution/index.d.ts +6 -1
  28. package/execution/index.d.ts.map +1 -1
  29. package/execution/index.js +3 -1
  30. package/execution/index.js.map +1 -1
  31. package/index.d.ts +4 -4
  32. package/index.d.ts.map +1 -1
  33. package/index.js +2 -2
  34. package/index.js.map +1 -1
  35. package/mcp-server/ApprovalPolicyGeneratorPanel.d.ts +34 -0
  36. package/mcp-server/ApprovalPolicyGeneratorPanel.d.ts.map +1 -0
  37. package/mcp-server/ApprovalPolicyGeneratorPanel.js +55 -0
  38. package/mcp-server/ApprovalPolicyGeneratorPanel.js.map +1 -0
  39. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  40. package/mcp-server/McpServerDetailView.js +101 -2
  41. package/mcp-server/McpServerDetailView.js.map +1 -1
  42. package/mcp-server/index.d.ts +8 -0
  43. package/mcp-server/index.d.ts.map +1 -1
  44. package/mcp-server/index.js +4 -0
  45. package/mcp-server/index.js.map +1 -1
  46. package/mcp-server/useDiscoverCapabilities.d.ts +59 -0
  47. package/mcp-server/useDiscoverCapabilities.d.ts.map +1 -0
  48. package/mcp-server/useDiscoverCapabilities.js +77 -0
  49. package/mcp-server/useDiscoverCapabilities.js.map +1 -0
  50. package/mcp-server/useMcpServerCredentials.d.ts +63 -0
  51. package/mcp-server/useMcpServerCredentials.d.ts.map +1 -0
  52. package/mcp-server/useMcpServerCredentials.js +64 -0
  53. package/mcp-server/useMcpServerCredentials.js.map +1 -0
  54. package/mcp-server/useTriggerApprovalPolicySession.d.ts +42 -0
  55. package/mcp-server/useTriggerApprovalPolicySession.d.ts.map +1 -0
  56. package/mcp-server/useTriggerApprovalPolicySession.js +111 -0
  57. package/mcp-server/useTriggerApprovalPolicySession.js.map +1 -0
  58. package/package.json +4 -4
  59. package/session/__tests__/useSessionConversation.test.js +223 -2
  60. package/session/__tests__/useSessionConversation.test.js.map +1 -1
  61. package/session/useSessionConversation.d.ts +8 -1
  62. package/session/useSessionConversation.d.ts.map +1 -1
  63. package/session/useSessionConversation.js +77 -6
  64. package/session/useSessionConversation.js.map +1 -1
  65. package/src/execution/ArtifactContentRenderer.tsx +376 -0
  66. package/src/execution/ArtifactPreviewModal.tsx +22 -114
  67. package/src/execution/ExecutionPhaseBadge.tsx +1 -1
  68. package/src/execution/MessageThread.tsx +35 -3
  69. package/src/execution/SetupProgress.tsx +120 -0
  70. package/src/execution/ToolCallGroup.tsx +15 -1
  71. package/src/execution/ToolCallItem.tsx +10 -3
  72. package/src/execution/artifact-utils.ts +88 -4
  73. package/src/execution/index.ts +9 -0
  74. package/src/index.ts +16 -0
  75. package/src/mcp-server/ApprovalPolicyGeneratorPanel.tsx +164 -0
  76. package/src/mcp-server/McpServerDetailView.tsx +428 -2
  77. package/src/mcp-server/index.ts +15 -0
  78. package/src/mcp-server/useDiscoverCapabilities.ts +117 -0
  79. package/src/mcp-server/useMcpServerCredentials.ts +108 -0
  80. package/src/mcp-server/useTriggerApprovalPolicySession.ts +161 -0
  81. package/src/session/__tests__/useSessionConversation.test.tsx +355 -2
  82. package/src/session/useSessionConversation.ts +104 -9
  83. package/styles.css +1 -1
@@ -1,21 +1,12 @@
1
1
  "use client";
2
2
 
3
- import {
4
- useCallback,
5
- useEffect,
6
- useRef,
7
- useState,
8
- type ReactNode,
9
- } from "react";
3
+ import { useCallback, useEffect, useRef, useState } from "react";
10
4
  import type { ExecutionArtifact } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
11
5
  import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
12
6
  import { cn } from "@stigmer/theme";
13
7
  import { useArtifactContent } from "./useArtifactContent";
14
- import {
15
- isTextArtifact,
16
- formatArtifactSize,
17
- getArtifactExtension,
18
- } from "./artifact-utils";
8
+ import { isTextArtifact, formatArtifactSize } from "./artifact-utils";
9
+ import { ArtifactContentRenderer } from "./ArtifactContentRenderer";
19
10
  import { useDetectStigmerResource } from "../library/useDetectStigmerResource";
20
11
  import { useDetectSkillPackage } from "../library/useDetectSkillPackage";
21
12
  import type { SkillPackageDetection } from "../library/detect-skill-package";
@@ -66,8 +57,9 @@ export interface ArtifactPreviewModalProps {
66
57
  * Internally orchestrates the same detection pipeline as {@link ArtifactCard}:
67
58
  *
68
59
  * - **FILE artifacts**: Fetches text content via {@link useArtifactContent},
69
- * displays with CSS-only YAML highlighting, and detects Agent/McpServer
70
- * resources via {@link useDetectStigmerResource}.
60
+ * renders via {@link ArtifactContentRenderer} (markdown, YAML, JSON, or
61
+ * plain text based on file type), and detects Agent/McpServer resources
62
+ * via {@link useDetectStigmerResource}.
71
63
  *
72
64
  * - **DIRECTORY artifacts**: Shows the file listing from
73
65
  * `artifact.entries` and detects skill packages via
@@ -253,14 +245,6 @@ export function ArtifactPreviewModal({
253
245
  if (!open) setCopied(false);
254
246
  }, [open]);
255
247
 
256
- // ---------------------------------------------------------------------------
257
- // Content mode
258
- // ---------------------------------------------------------------------------
259
-
260
- const ext = getArtifactExtension(artifact);
261
- const isYaml =
262
- ext === "yaml" || ext === "yml" || (contentType?.includes("yaml") ?? false);
263
-
264
248
  let ctaLabel: string | null = null;
265
249
  if (yamlDetection.detected) {
266
250
  ctaLabel = `Apply to ${org}`;
@@ -278,7 +262,7 @@ export function ArtifactPreviewModal({
278
262
  onCancel={handleCancel}
279
263
  aria-label={`Preview ${artifact.name}`}
280
264
  className={cn(
281
- "w-full max-w-2xl rounded-lg border border-border bg-background p-0 text-foreground shadow-lg outline-none",
265
+ "fixed inset-0 m-auto w-full max-w-3xl rounded-lg border border-border bg-background p-0 text-foreground shadow-lg outline-none",
282
266
  "[&::backdrop]:bg-black/50",
283
267
  className,
284
268
  )}
@@ -299,12 +283,13 @@ export function ArtifactPreviewModal({
299
283
  skillDetection={skillDetection}
300
284
  />
301
285
  ) : (
302
- <FileContentView
286
+ <FileContentStateView
287
+ artifact={artifact}
303
288
  content={content}
289
+ contentType={contentType}
304
290
  isLoading={isContentLoading}
305
291
  error={contentError}
306
292
  isTruncated={isTruncated}
307
- isYaml={isYaml}
308
293
  />
309
294
  )}
310
295
  </div>
@@ -406,23 +391,25 @@ function ModalHeader({
406
391
  }
407
392
 
408
393
  // ---------------------------------------------------------------------------
409
- // FileContentView (internal)
394
+ // FileContentStateView (internal — loading/error/empty states + renderer)
410
395
  // ---------------------------------------------------------------------------
411
396
 
412
397
  const SKELETON_LINE_WIDTHS = [85, 72, 90, 65, 78, 88, 70, 82] as const;
413
398
 
414
- function FileContentView({
399
+ function FileContentStateView({
400
+ artifact,
415
401
  content,
402
+ contentType,
416
403
  isLoading,
417
404
  error,
418
405
  isTruncated,
419
- isYaml,
420
406
  }: {
407
+ readonly artifact: ExecutionArtifact;
421
408
  readonly content: string | null;
409
+ readonly contentType: string | null;
422
410
  readonly isLoading: boolean;
423
411
  readonly error: string | null;
424
412
  readonly isTruncated: boolean;
425
- readonly isYaml: boolean;
426
413
  }) {
427
414
  if (isLoading) {
428
415
  return (
@@ -457,16 +444,12 @@ function FileContentView({
457
444
  }
458
445
 
459
446
  return (
460
- <div>
461
- <pre className="overflow-x-auto p-4 font-mono text-xs leading-relaxed text-foreground">
462
- <code>{isYaml ? highlightYaml(content) : content}</code>
463
- </pre>
464
- {isTruncated && (
465
- <div className="border-t border-border bg-warning/10 px-4 py-2 text-xs text-warning">
466
- Content was truncated. Download the full file for complete content.
467
- </div>
468
- )}
469
- </div>
447
+ <ArtifactContentRenderer
448
+ content={content}
449
+ fileName={artifact.name}
450
+ contentType={contentType}
451
+ isTruncated={isTruncated}
452
+ />
470
453
  );
471
454
  }
472
455
 
@@ -669,81 +652,6 @@ function ApplyButton({
669
652
  );
670
653
  }
671
654
 
672
- // ---------------------------------------------------------------------------
673
- // YAML syntax highlighting (CSS-only, zero dependencies)
674
- // ---------------------------------------------------------------------------
675
-
676
- /**
677
- * Applies lightweight CSS-only highlighting to YAML content.
678
- *
679
- * Processes content line-by-line, wrapping structural tokens in styled
680
- * `<span>` elements using `--stgm-*` theme tokens:
681
- *
682
- * - Keys → `text-primary`
683
- * - Comments → `text-muted-foreground italic`
684
- * - Document separators (`---`) → `text-muted-foreground`
685
- * - Boolean/null values → `font-medium`
686
- * - Multi-line scalar indicators → `text-muted-foreground`
687
- *
688
- * Values and block scalar content render in the default `text-foreground`.
689
- * Non-YAML content passes through unstyled.
690
- */
691
- function highlightYaml(content: string): ReactNode {
692
- return content.split("\n").map((line, i) => (
693
- <span key={i}>
694
- {i > 0 && "\n"}
695
- {highlightYamlLine(line)}
696
- </span>
697
- ));
698
- }
699
-
700
- function highlightYamlLine(line: string): ReactNode {
701
- if (!line.trim()) return line;
702
-
703
- if (/^---\s*$/.test(line) || /^\.\.\.\s*$/.test(line)) {
704
- return <span className="text-muted-foreground">{line}</span>;
705
- }
706
-
707
- const commentMatch = line.match(/^(\s*)(#.*)$/);
708
- if (commentMatch) {
709
- return (
710
- <>
711
- {commentMatch[1]}
712
- <span className="text-muted-foreground italic">{commentMatch[2]}</span>
713
- </>
714
- );
715
- }
716
-
717
- const kvMatch = line.match(/^(\s*(?:-\s+)?)([\w][\w.-]*)(:(?:\s|$))(.*)/);
718
- if (kvMatch) {
719
- const [, indent, key, colon, value] = kvMatch;
720
- return (
721
- <>
722
- {indent}
723
- <span className="text-primary">{key}</span>
724
- <span className="text-muted-foreground">{colon}</span>
725
- {value ? highlightYamlValue(value) : null}
726
- </>
727
- );
728
- }
729
-
730
- return <>{line}</>;
731
- }
732
-
733
- function highlightYamlValue(value: string): ReactNode {
734
- const trimmed = value.trim();
735
-
736
- if (/^[|>][-+]?\s*$/.test(trimmed)) {
737
- return <span className="text-muted-foreground">{value}</span>;
738
- }
739
-
740
- if (/^(true|false|null|~)$/.test(trimmed)) {
741
- return <span className="font-medium">{value}</span>;
742
- }
743
-
744
- return <>{value}</>;
745
- }
746
-
747
655
  // ---------------------------------------------------------------------------
748
656
  // Inline SVG icons (SDK pattern: no external icon dependency)
749
657
  // ---------------------------------------------------------------------------
@@ -19,7 +19,7 @@ const PHASE_CONFIG: ReadonlyMap<ExecutionPhase, PhaseConfig> = new Map([
19
19
  ExecutionPhase.EXECUTION_PENDING,
20
20
  {
21
21
  label: "Pending",
22
- icon: DotIcon,
22
+ icon: PulseDotIcon,
23
23
  colorClass: "text-muted-foreground",
24
24
  },
25
25
  ],
@@ -18,6 +18,7 @@ import { isTerminalPhase } from "./execution-phases";
18
18
  import { MessageEntry } from "./MessageEntry";
19
19
  import { ToolCallGroup } from "./ToolCallGroup";
20
20
  import { ExecutionPhaseBadge } from "./ExecutionPhaseBadge";
21
+ import { SetupProgress } from "./SetupProgress";
21
22
  import { ApprovalCard } from "./ApprovalCard";
22
23
  import { FilePathContext, type FilePathContextValue } from "./FilePathContext";
23
24
  import type { ResolvedPathAction } from "./file-path-resolver";
@@ -101,7 +102,17 @@ type ThreadItem =
101
102
  | { readonly kind: "tool-group"; readonly toolCalls: readonly ToolCall[]; readonly subAgentExecutions: readonly SubAgentExecution[]; readonly key: string }
102
103
  | { readonly kind: "phase-badge"; readonly phase: ExecutionPhase; readonly key: string }
103
104
  | { readonly kind: "pending-message"; readonly content: string; readonly key: string }
104
- | { readonly kind: "approval-request"; readonly pendingApproval: PendingApproval; readonly key: string };
105
+ | { readonly kind: "approval-request"; readonly pendingApproval: PendingApproval; readonly key: string }
106
+ | { readonly kind: "setup-progress"; readonly workspaceEntries: readonly WorkspaceEntry[]; readonly key: string };
107
+
108
+ function hasAiMessages(execution: AgentExecution): boolean {
109
+ const messages = execution.status?.messages;
110
+ if (!messages || messages.length === 0) return false;
111
+ return messages.some(
112
+ (m) =>
113
+ m.type === MessageType.MESSAGE_AI && (m.content.trim() || m.toolCalls.length > 0),
114
+ );
115
+ }
105
116
 
106
117
  function buildThreadItems(
107
118
  executions: readonly AgentExecution[],
@@ -109,6 +120,7 @@ function buildThreadItems(
109
120
  pendingUserMessage: string | null | undefined,
110
121
  includeApprovals: boolean,
111
122
  dismissedApprovalIds: ReadonlySet<string> | undefined,
123
+ workspaceEntries: readonly WorkspaceEntry[] | undefined,
112
124
  ): ThreadItem[] {
113
125
  const items: ThreadItem[] = [];
114
126
  const allExecutions = activeStreamExecution
@@ -168,6 +180,19 @@ function buildThreadItems(
168
180
  const lastPhase =
169
181
  lastExec?.status?.phase ?? ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED;
170
182
 
183
+ if (
184
+ activeStreamExecution &&
185
+ (lastPhase === ExecutionPhase.EXECUTION_PENDING ||
186
+ lastPhase === ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED) &&
187
+ !hasAiMessages(activeStreamExecution)
188
+ ) {
189
+ items.push({
190
+ kind: "setup-progress",
191
+ workspaceEntries: workspaceEntries ?? [],
192
+ key: "setup-progress",
193
+ });
194
+ }
195
+
171
196
  if (
172
197
  isTerminalPhase(lastPhase) &&
173
198
  lastPhase !== ExecutionPhase.EXECUTION_COMPLETED
@@ -247,8 +272,8 @@ export function MessageThread({
247
272
 
248
273
  const includeApprovals = onApprovalSubmit != null;
249
274
  const items = useMemo(
250
- () => buildThreadItems(executions, activeStreamExecution, pendingUserMessage, includeApprovals, dismissedApprovalIds),
251
- [executions, activeStreamExecution, pendingUserMessage, includeApprovals, dismissedApprovalIds],
275
+ () => buildThreadItems(executions, activeStreamExecution, pendingUserMessage, includeApprovals, dismissedApprovalIds, workspaceEntries),
276
+ [executions, activeStreamExecution, pendingUserMessage, includeApprovals, dismissedApprovalIds, workspaceEntries],
252
277
  );
253
278
 
254
279
  const handleScroll = useCallback(() => {
@@ -322,6 +347,13 @@ export function MessageThread({
322
347
  className="mx-4"
323
348
  />
324
349
  );
350
+ case "setup-progress":
351
+ return (
352
+ <SetupProgress
353
+ key={item.key}
354
+ workspaceEntries={item.workspaceEntries}
355
+ />
356
+ );
325
357
  case "pending-message":
326
358
  return (
327
359
  <div
@@ -0,0 +1,120 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import type { WorkspaceEntry } from "@stigmer/protos/ai/stigmer/agentic/session/v1/workspace_pb";
5
+ import { cn } from "@stigmer/theme";
6
+
7
+ export interface SetupProgressProps {
8
+ /**
9
+ * Workspace entries from the session spec. When git-sourced entries
10
+ * are present, the indicator shows workspace-specific messaging
11
+ * (e.g. "Setting up workspace...") to match the backend's actual
12
+ * setup sequence.
13
+ */
14
+ readonly workspaceEntries?: readonly WorkspaceEntry[];
15
+ readonly className?: string;
16
+ }
17
+
18
+ interface SetupStep {
19
+ readonly message: string;
20
+ readonly durationMs: number;
21
+ }
22
+
23
+ const STEP_INTERVAL_MS = 4000;
24
+
25
+ function buildSteps(
26
+ workspaceEntries: readonly WorkspaceEntry[] | undefined,
27
+ ): SetupStep[] {
28
+ const hasGitRepo = workspaceEntries?.some(
29
+ (e) => e.source?.source.case === "gitRepo",
30
+ );
31
+
32
+ const steps: SetupStep[] = [
33
+ { message: "Initializing execution\u2026", durationMs: STEP_INTERVAL_MS },
34
+ ];
35
+
36
+ if (hasGitRepo) {
37
+ steps.push({
38
+ message: "Setting up workspace\u2026",
39
+ durationMs: STEP_INTERVAL_MS,
40
+ });
41
+ }
42
+
43
+ steps.push(
44
+ {
45
+ message: "Preparing agent environment\u2026",
46
+ durationMs: STEP_INTERVAL_MS,
47
+ },
48
+ { message: "Almost ready\u2026", durationMs: STEP_INTERVAL_MS },
49
+ );
50
+
51
+ return steps;
52
+ }
53
+
54
+ /**
55
+ * Animated inline indicator shown in the message thread while an
56
+ * execution is in the `PENDING` phase and no AI messages have arrived.
57
+ *
58
+ * Cycles through contextual status messages derived from the session
59
+ * configuration (workspace entries, etc.) to communicate that the
60
+ * backend is actively setting up the sandbox, cloning repositories,
61
+ * merging environment variables, loading skills, and connecting MCP
62
+ * servers.
63
+ *
64
+ * Uses time-based progression that approximates the backend's actual
65
+ * setup sequence. A future backend-enriched phase (surfacing real
66
+ * setup phase labels through the execution stream) can replace the
67
+ * timer with server-reported progress.
68
+ *
69
+ * All visual properties flow through `--stgm-*` tokens.
70
+ *
71
+ * @example
72
+ * ```tsx
73
+ * <SetupProgress workspaceEntries={session.spec?.workspaceEntries} />
74
+ * ```
75
+ */
76
+ export function SetupProgress({
77
+ workspaceEntries,
78
+ className,
79
+ }: SetupProgressProps) {
80
+ const steps = useMemo(() => buildSteps(workspaceEntries), [workspaceEntries]);
81
+ const [stepIndex, setStepIndex] = useState(0);
82
+
83
+ useEffect(() => {
84
+ setStepIndex(0);
85
+ }, [steps]);
86
+
87
+ useEffect(() => {
88
+ if (stepIndex >= steps.length - 1) return;
89
+
90
+ const timer = setTimeout(() => {
91
+ setStepIndex((i) => Math.min(i + 1, steps.length - 1));
92
+ }, steps[stepIndex].durationMs);
93
+
94
+ return () => clearTimeout(timer);
95
+ }, [stepIndex, steps]);
96
+
97
+ const currentMessage = steps[Math.min(stepIndex, steps.length - 1)].message;
98
+
99
+ return (
100
+ <div
101
+ role="status"
102
+ aria-label={currentMessage}
103
+ className={cn("flex items-center gap-2.5 px-4 py-2", className)}
104
+ >
105
+ <PulseIndicator />
106
+ <span className="text-sm text-muted-foreground animate-in fade-in duration-300">
107
+ {currentMessage}
108
+ </span>
109
+ </div>
110
+ );
111
+ }
112
+
113
+ function PulseIndicator() {
114
+ return (
115
+ <span className="relative flex h-2 w-2 shrink-0" aria-hidden="true">
116
+ <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-muted-foreground opacity-75" />
117
+ <span className="relative inline-flex h-2 w-2 rounded-full bg-muted-foreground" />
118
+ </span>
119
+ );
120
+ }
@@ -3,7 +3,10 @@
3
3
  import { useEffect, useMemo, useRef, useState } from "react";
4
4
  import type { ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
5
5
  import type { SubAgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
6
- import { ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
6
+ import {
7
+ ApprovalAction,
8
+ ToolCallStatus,
9
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
7
10
  import { cn } from "@stigmer/theme";
8
11
  import { ToolCallItem } from "./ToolCallItem";
9
12
  import { resolveToolCategory, extractPrimaryArg } from "./tool-categories";
@@ -32,6 +35,14 @@ export interface ToolCallGroupProps {
32
35
 
33
36
  type AggregateStatus = "running" | "waiting" | "failed" | "completed" | "pending";
34
37
 
38
+ function isResolvedApproval(tc: ToolCall): boolean {
39
+ return (
40
+ tc.approvalAction === ApprovalAction.APPROVE ||
41
+ tc.approvalAction === ApprovalAction.SKIP ||
42
+ tc.approvalAction === ApprovalAction.REJECT
43
+ );
44
+ }
45
+
35
46
  function deriveAggregateStatus(toolCalls: readonly ToolCall[]): AggregateStatus {
36
47
  let hasRunning = false;
37
48
  let hasWaiting = false;
@@ -45,6 +56,9 @@ function deriveAggregateStatus(toolCalls: readonly ToolCall[]): AggregateStatus
45
56
  allTerminal = false;
46
57
  break;
47
58
  case ToolCallStatus.TOOL_CALL_WAITING_APPROVAL:
59
+ if (isResolvedApproval(tc)) {
60
+ break;
61
+ }
48
62
  hasWaiting = true;
49
63
  allTerminal = false;
50
64
  break;
@@ -61,7 +61,7 @@ export function ToolCallItem({
61
61
  }: ToolCallItemProps) {
62
62
  const [expanded, setExpanded] = useState(defaultExpanded);
63
63
 
64
- const status = mapToolCallStatus(toolCall.status);
64
+ const status = mapToolCallStatus(toolCall);
65
65
  const StatusIcon = STATUS_ICON[status];
66
66
  const duration = formatDuration(toolCall.startedAt, toolCall.completedAt);
67
67
  const isSubAgent = subAgentExecution != null;
@@ -220,11 +220,18 @@ function getApprovalBadge(toolCall: ToolCall): ApprovalBadgeInfo | null {
220
220
 
221
221
  type ItemStatus = "running" | "waiting" | "failed" | "completed" | "pending";
222
222
 
223
- function mapToolCallStatus(status: ToolCallStatus): ItemStatus {
224
- switch (status) {
223
+ function mapToolCallStatus(toolCall: ToolCall): ItemStatus {
224
+ switch (toolCall.status) {
225
225
  case ToolCallStatus.TOOL_CALL_RUNNING:
226
226
  return "running";
227
227
  case ToolCallStatus.TOOL_CALL_WAITING_APPROVAL:
228
+ if (
229
+ toolCall.approvalAction === ApprovalAction.APPROVE ||
230
+ toolCall.approvalAction === ApprovalAction.SKIP ||
231
+ toolCall.approvalAction === ApprovalAction.REJECT
232
+ ) {
233
+ return "completed";
234
+ }
228
235
  return "waiting";
229
236
  case ToolCallStatus.TOOL_CALL_FAILED:
230
237
  return "failed";
@@ -41,6 +41,7 @@ const TEXT_EXTENSIONS = new Set([
41
41
  * Extracts the lowercase file extension from an artifact's name.
42
42
  *
43
43
  * Returns `null` when the name has no extension or is empty.
44
+ * Delegates to {@link getFileExtension} for the actual parsing.
44
45
  *
45
46
  * @example
46
47
  * ```ts
@@ -51,10 +52,7 @@ const TEXT_EXTENSIONS = new Set([
51
52
  export function getArtifactExtension(
52
53
  artifact: ExecutionArtifact,
53
54
  ): string | null {
54
- const name = artifact.name;
55
- const lastDot = name.lastIndexOf(".");
56
- if (lastDot === -1 || lastDot === name.length - 1) return null;
57
- return name.slice(lastDot + 1).toLowerCase();
55
+ return getFileExtension(artifact.name);
58
56
  }
59
57
 
60
58
  /**
@@ -100,6 +98,92 @@ export function isArtifactExpired(artifact: ExecutionArtifact): boolean {
100
98
  return Date.now() >= expiresMs;
101
99
  }
102
100
 
101
+ // ---------------------------------------------------------------------------
102
+ // Render mode classification
103
+ // ---------------------------------------------------------------------------
104
+
105
+ /**
106
+ * Content rendering strategy for artifact file preview.
107
+ *
108
+ * Used by {@link ArtifactContentRenderer} to dispatch to the correct
109
+ * renderer, and by platform builders who want to implement custom
110
+ * rendering logic based on file type.
111
+ *
112
+ * - `"markdown"` — rendered HTML via `react-markdown` with themed components
113
+ * - `"yaml"` — CSS-only YAML syntax highlighting
114
+ * - `"json"` — pretty-printed JSON with key/value coloring
115
+ * - `"text"` — monospace plain text with line numbers
116
+ */
117
+ export type ArtifactRenderMode = "markdown" | "yaml" | "json" | "text";
118
+
119
+ const YAML_EXTENSIONS = new Set(["yaml", "yml"]);
120
+ const JSON_EXTENSIONS = new Set(["json"]);
121
+ const MARKDOWN_EXTENSIONS = new Set(["md", "mdx"]);
122
+
123
+ /**
124
+ * Extracts the lowercase file extension from a file name string.
125
+ *
126
+ * Returns `null` when the name has no extension or is empty.
127
+ * Unlike {@link getArtifactExtension}, this operates on a plain
128
+ * string — usable without a full `ExecutionArtifact` object.
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * getFileExtension("agent.yaml"); // "yaml"
133
+ * getFileExtension("README.md"); // "md"
134
+ * getFileExtension("Makefile"); // null
135
+ * ```
136
+ */
137
+ export function getFileExtension(fileName: string): string | null {
138
+ const lastDot = fileName.lastIndexOf(".");
139
+ if (lastDot === -1 || lastDot === fileName.length - 1) return null;
140
+ return fileName.slice(lastDot + 1).toLowerCase();
141
+ }
142
+
143
+ /**
144
+ * Determines the optimal rendering strategy for a text artifact.
145
+ *
146
+ * Inspects the file extension first, then falls back to the optional
147
+ * `contentType` MIME string returned by the server. Platform builders
148
+ * can use this to implement custom rendering or to display a mode
149
+ * indicator in their UI.
150
+ *
151
+ * @param fileName - Artifact file name (e.g., `"README.md"`, `"config.yaml"`)
152
+ * @param contentType - Optional MIME content type from the server response
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * getArtifactRenderMode("README.md"); // "markdown"
157
+ * getArtifactRenderMode("config.yaml"); // "yaml"
158
+ * getArtifactRenderMode("data.json"); // "json"
159
+ * getArtifactRenderMode("script.py"); // "text"
160
+ * getArtifactRenderMode("unknown", "application/json"); // "json"
161
+ * ```
162
+ */
163
+ export function getArtifactRenderMode(
164
+ fileName: string,
165
+ contentType?: string | null,
166
+ ): ArtifactRenderMode {
167
+ const ext = getFileExtension(fileName);
168
+
169
+ if (ext && MARKDOWN_EXTENSIONS.has(ext)) return "markdown";
170
+ if (ext && YAML_EXTENSIONS.has(ext)) return "yaml";
171
+ if (ext && JSON_EXTENSIONS.has(ext)) return "json";
172
+
173
+ if (contentType) {
174
+ const ct = contentType.toLowerCase();
175
+ if (ct.includes("markdown")) return "markdown";
176
+ if (ct.includes("yaml")) return "yaml";
177
+ if (ct.includes("json")) return "json";
178
+ }
179
+
180
+ return "text";
181
+ }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Size formatting
185
+ // ---------------------------------------------------------------------------
186
+
103
187
  const SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"] as const;
104
188
 
105
189
  /**
@@ -24,11 +24,17 @@ export {
24
24
  isArtifactExpired,
25
25
  formatArtifactSize,
26
26
  getArtifactExtension,
27
+ getFileExtension,
28
+ getArtifactRenderMode,
27
29
  } from "./artifact-utils";
30
+ export type { ArtifactRenderMode } from "./artifact-utils";
28
31
 
29
32
  export { ExecutionPhaseBadge } from "./ExecutionPhaseBadge";
30
33
  export type { ExecutionPhaseBadgeProps } from "./ExecutionPhaseBadge";
31
34
 
35
+ export { SetupProgress } from "./SetupProgress";
36
+ export type { SetupProgressProps } from "./SetupProgress";
37
+
32
38
  export { ToolCallGroup } from "./ToolCallGroup";
33
39
  export type { ToolCallGroupProps } from "./ToolCallGroup";
34
40
 
@@ -65,6 +71,9 @@ export type { ApprovalCardProps } from "./ApprovalCard";
65
71
  export { ArtifactCard } from "./ArtifactCard";
66
72
  export type { ArtifactCardProps } from "./ArtifactCard";
67
73
 
74
+ export { ArtifactContentRenderer } from "./ArtifactContentRenderer";
75
+ export type { ArtifactContentRendererProps } from "./ArtifactContentRenderer";
76
+
68
77
  export { ArtifactPreviewModal } from "./ArtifactPreviewModal";
69
78
  export type { ArtifactPreviewModalProps } from "./ArtifactPreviewModal";
70
79