@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
@@ -1,22 +1,27 @@
1
1
  "use client";
2
2
 
3
- import { useState } from "react";
3
+ import { useCallback, useState } from "react";
4
4
  import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
5
- import type { ExecutionArtifact } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
6
- import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
7
5
  import { cn } from "@stigmer/theme";
8
- import { useExecutionArtifacts } from "./useExecutionArtifacts";
9
- import { isTerminalPhase } from "./execution-phases";
6
+ import {
7
+ useSessionArtifacts,
8
+ type SessionArtifactEntry,
9
+ } from "../session/useSessionArtifacts";
10
10
  import { ArtifactCard } from "./ArtifactCard";
11
11
  import { ArtifactPreviewModal } from "./ArtifactPreviewModal";
12
12
  import type { ApplyResourceResult } from "../library/useApplyResource";
13
13
 
14
14
  export interface ArtifactsWidgetProps {
15
15
  /**
16
- * The execution to display artifacts for. Renders nothing when `null`
17
- * or when the execution has no artifacts.
16
+ * All executions for the current session both completed and
17
+ * actively streaming. The widget aggregates artifacts across every
18
+ * execution, deduplicates by `sandbox_path` (latest wins), and
19
+ * sorts alphabetically by name.
20
+ *
21
+ * Renders nothing when the list is empty or no execution has
22
+ * artifacts.
18
23
  */
19
- readonly execution: AgentExecution | null;
24
+ readonly executions: readonly AgentExecution[];
20
25
  /** Organization slug for the "Apply to [org]" CTA in the preview modal. */
21
26
  readonly org: string;
22
27
  /**
@@ -31,24 +36,22 @@ export interface ArtifactsWidgetProps {
31
36
  }
32
37
 
33
38
  /**
34
- * Right-sidebar widget that surfaces execution artifacts as a compact
35
- * card list with automatic Stigmer resource detection.
39
+ * Right-sidebar widget that surfaces all artifacts produced during a
40
+ * session as a unified, alphabetically-sorted file listing.
41
+ *
42
+ * Artifacts from multiple executions are aggregated and deduplicated
43
+ * by `sandbox_path` (latest execution wins), presenting the user with
44
+ * a file-explorer-like view of the conversation's output — no
45
+ * execution/turn concepts are exposed.
36
46
  *
37
47
  * Composes {@link ArtifactCard} (summary + detection badges) with
38
48
  * {@link ArtifactPreviewModal} (full content review + Apply/Push CTA).
39
49
  * The card's "Preview" action opens the modal; the modal is the sole
40
50
  * location for Apply/Push actions (review-before-apply pattern).
41
51
  *
42
- * Derives all data from the `execution` prop:
43
- *
44
- * - **Artifacts**: extracted via {@link useExecutionArtifacts}
45
- * - **Terminal phase**: derived via {@link isTerminalPhase} — controls
46
- * whether the Apply CTA in the modal is enabled
47
- * - **Execution ID**: read from `execution.metadata.id`
48
- *
49
- * Returns `null` when the execution is `null` or has no artifacts,
50
- * matching the conditional-render pattern of {@link ExecutionProgress}
51
- * and {@link ExecutionCostSummary}.
52
+ * Returns `null` when the executions list is empty or no execution
53
+ * has artifacts, matching the conditional-render pattern of
54
+ * {@link ExecutionProgress} and {@link ExecutionCostSummary}.
52
55
  *
53
56
  * Renders without card chrome — each {@link ArtifactCard} provides its
54
57
  * own border and padding. The consumer controls the container styling
@@ -58,10 +61,13 @@ export interface ArtifactsWidgetProps {
58
61
  *
59
62
  * @example
60
63
  * ```tsx
61
- * const stream = useExecutionStream(executionId);
64
+ * const conv = useSessionConversation(sessionId, org);
62
65
  *
63
66
  * <ArtifactsWidget
64
- * execution={stream.execution}
67
+ * executions={[
68
+ * ...conv.completedExecutions,
69
+ * ...(conv.activeStreamExecution ? [conv.activeStreamExecution] : []),
70
+ * ]}
65
71
  * org={activeOrg}
66
72
  * onApplied={(result) => toast(`${result.kind} applied`)}
67
73
  * />
@@ -69,29 +75,30 @@ export interface ArtifactsWidgetProps {
69
75
  *
70
76
  * @see {@link ArtifactCard} — compact summary card per artifact
71
77
  * @see {@link ArtifactPreviewModal} — full preview with Apply/Push CTA
72
- * @see {@link useExecutionArtifacts} — headless artifact extraction hook
78
+ * @see {@link useSessionArtifacts} — headless session-level artifact aggregation hook
79
+ * @see {@link useExecutionArtifacts} — headless single-execution artifact extraction hook
73
80
  */
74
81
  export function ArtifactsWidget({
75
- execution,
82
+ executions,
76
83
  org,
77
84
  onApplied,
78
85
  className,
79
86
  }: ArtifactsWidgetProps) {
80
87
  const { artifacts, hasArtifacts, artifactCount } =
81
- useExecutionArtifacts(execution);
88
+ useSessionArtifacts(executions);
82
89
 
83
- const [previewArtifact, setPreviewArtifact] =
84
- useState<ExecutionArtifact | null>(null);
90
+ const [previewEntry, setPreviewEntry] =
91
+ useState<SessionArtifactEntry | null>(null);
85
92
 
86
- if (!hasArtifacts) return null;
93
+ const handlePreview = useCallback(
94
+ (entry: SessionArtifactEntry) => setPreviewEntry(entry),
95
+ [],
96
+ );
87
97
 
88
- const phase =
89
- execution?.status?.phase ?? ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED;
90
- const isTerminal = isTerminalPhase(phase);
91
- const executionId = execution?.metadata?.id ?? "";
98
+ if (!hasArtifacts) return null;
92
99
 
93
100
  return (
94
- <section aria-label="Execution artifacts" className={cn(className)}>
101
+ <section aria-label="Artifacts" className={cn(className)}>
95
102
  <div className="mb-2 flex items-center gap-2">
96
103
  <h3 className="text-sm font-medium text-foreground">Artifacts</h3>
97
104
  <span className="inline-flex min-w-[1.25rem] items-center justify-center rounded-full bg-muted px-1.5 text-xs tabular-nums text-muted-foreground">
@@ -100,26 +107,27 @@ export function ArtifactsWidget({
100
107
  </div>
101
108
 
102
109
  <div role="list" className="space-y-2">
103
- {artifacts.map((artifact) => (
104
- <div key={artifact.storageKey} role="listitem">
110
+ {artifacts.map((entry) => (
111
+ <div key={entry.artifact.storageKey} role="listitem">
105
112
  <ArtifactCard
106
- artifact={artifact}
107
- executionId={executionId}
113
+ artifact={entry.artifact}
114
+ executionId={entry.executionId}
108
115
  org={org}
109
- onPreview={setPreviewArtifact}
116
+ hasNameCollision={entry.hasNameCollision}
117
+ onPreview={() => handlePreview(entry)}
110
118
  />
111
119
  </div>
112
120
  ))}
113
121
  </div>
114
122
 
115
- {previewArtifact && (
123
+ {previewEntry && (
116
124
  <ArtifactPreviewModal
117
- artifact={previewArtifact}
118
- executionId={executionId}
125
+ artifact={previewEntry.artifact}
126
+ executionId={previewEntry.executionId}
119
127
  org={org}
120
- isTerminal={isTerminal}
128
+ isTerminal={previewEntry.isTerminal}
121
129
  open
122
- onClose={() => setPreviewArtifact(null)}
130
+ onClose={() => setPreviewEntry(null)}
123
131
  onApplied={onApplied}
124
132
  />
125
133
  )}
@@ -0,0 +1,283 @@
1
+ "use client";
2
+
3
+ import type { ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
4
+ import { cn } from "@stigmer/theme";
5
+ import { formatDuration } from "./ToolCallDetail";
6
+ import { humanizeToolName } from "./tool-categories";
7
+ import {
8
+ CollapsiblePre,
9
+ CollapsibleJsonBlock,
10
+ McpServerIcon,
11
+ formatJson,
12
+ isScalar,
13
+ humanizeArgKey,
14
+ } from "./tool-rendering-primitives";
15
+
16
+ export interface McpToolDetailProps {
17
+ readonly toolCall: ToolCall;
18
+ readonly className?: string;
19
+ }
20
+
21
+ /**
22
+ * MCP-aware detail renderer for tool calls originating from an MCP
23
+ * server.
24
+ *
25
+ * Replaces the generic "dump args + result as raw JSON" fallback
26
+ * with structured formatting:
27
+ *
28
+ * - **Arguments** are rendered as a labelled key-value list.
29
+ * Scalars display inline; objects/arrays collapse into formatted
30
+ * JSON blocks.
31
+ * - **Results** are parsed through {@link parseMcpResult} which
32
+ * handles MCP content-block arrays, embedded JSON, and Python
33
+ * repr artefacts before rendering.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * <McpToolDetail toolCall={toolCall} />
38
+ * ```
39
+ */
40
+ export function McpToolDetail({ toolCall, className }: McpToolDetailProps) {
41
+ const duration = formatDuration(toolCall.startedAt, toolCall.completedAt);
42
+
43
+ return (
44
+ <div className={cn("space-y-3 text-xs", className)}>
45
+ <McpMetadataRow
46
+ mcpServerSlug={toolCall.mcpServerSlug}
47
+ toolName={toolCall.name}
48
+ duration={duration}
49
+ />
50
+
51
+ {toolCall.args && Object.keys(toolCall.args).length > 0 && (
52
+ <McpArgsView args={toolCall.args as Record<string, unknown>} />
53
+ )}
54
+
55
+ {toolCall.result && (
56
+ <McpResultView result={toolCall.result} />
57
+ )}
58
+ </div>
59
+ );
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Metadata
64
+ // ---------------------------------------------------------------------------
65
+
66
+ export function McpMetadataRow({
67
+ mcpServerSlug,
68
+ toolName,
69
+ duration,
70
+ }: {
71
+ mcpServerSlug: string;
72
+ toolName: string;
73
+ duration: string | null;
74
+ }) {
75
+ const hasMetadata = mcpServerSlug || duration;
76
+ if (!hasMetadata) return null;
77
+
78
+ return (
79
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-muted-foreground">
80
+ {mcpServerSlug && (
81
+ <span className="inline-flex items-center gap-1.5 rounded bg-muted px-1.5 py-0.5 font-mono">
82
+ <McpServerIcon />
83
+ {mcpServerSlug}
84
+ <span className="text-muted-foreground/60">/</span>
85
+ <span className="text-foreground">{humanizeToolName(toolName)}</span>
86
+ </span>
87
+ )}
88
+ {duration && <span>{duration}</span>}
89
+ </div>
90
+ );
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Arguments — structured key-value rendering
95
+ // ---------------------------------------------------------------------------
96
+
97
+ export function McpArgsView({ args }: { args: Record<string, unknown> }) {
98
+ const entries = Object.entries(args);
99
+ if (entries.length === 0) return null;
100
+
101
+ const scalars: [string, string][] = [];
102
+ const complex: [string, unknown][] = [];
103
+
104
+ for (const [key, value] of entries) {
105
+ if (isScalar(value)) {
106
+ scalars.push([key, String(value)]);
107
+ } else {
108
+ complex.push([key, value]);
109
+ }
110
+ }
111
+
112
+ return (
113
+ <div className="space-y-2">
114
+ <span className="font-medium text-muted-foreground">Arguments</span>
115
+
116
+ {scalars.length > 0 && (
117
+ <dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 rounded-md border border-border bg-muted/30 px-2.5 py-2">
118
+ {scalars.map(([key, value]) => (
119
+ <ScalarRow key={key} label={key} value={value} />
120
+ ))}
121
+ </dl>
122
+ )}
123
+
124
+ {complex.map(([key, value]) => (
125
+ <CollapsibleJsonBlock
126
+ key={key}
127
+ label={humanizeArgKey(key)}
128
+ content={formatJson(value)}
129
+ />
130
+ ))}
131
+ </div>
132
+ );
133
+ }
134
+
135
+ function ScalarRow({ label, value }: { label: string; value: string }) {
136
+ const isMultiline = value.includes("\n");
137
+
138
+ return (
139
+ <>
140
+ <dt className="whitespace-nowrap font-mono text-muted-foreground">
141
+ {humanizeArgKey(label)}
142
+ </dt>
143
+ {isMultiline ? (
144
+ <dd className="min-w-0">
145
+ <pre className="whitespace-pre-wrap break-words rounded border border-border bg-muted/40 px-2 py-1 font-mono text-foreground">
146
+ {value}
147
+ </pre>
148
+ </dd>
149
+ ) : (
150
+ <dd className="min-w-0 truncate font-mono text-foreground" title={value}>
151
+ {value}
152
+ </dd>
153
+ )}
154
+ </>
155
+ );
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Result — intelligent parsing
160
+ // ---------------------------------------------------------------------------
161
+
162
+ function McpResultView({ result }: { result: string }) {
163
+ const parsed = parseMcpResult(result);
164
+
165
+ return (
166
+ <div className="space-y-1">
167
+ <span className="font-medium text-muted-foreground">Result</span>
168
+ <CollapsiblePre
169
+ content={parsed}
170
+ className="max-h-80 overflow-auto rounded-md border border-border bg-muted/40 p-2 text-foreground"
171
+ />
172
+ </div>
173
+ );
174
+ }
175
+
176
+ /**
177
+ * Extracts human-readable content from an MCP tool result string.
178
+ *
179
+ * Handles three common formats that arrive from the backend:
180
+ *
181
+ * 1. **MCP content-block array** — `[{"type":"text","text":"..."}]`.
182
+ * Text parts are extracted and, if they are themselves valid
183
+ * JSON, pretty-printed.
184
+ * 2. **Python repr** — `[{'type': 'text', 'text': '...'}]`. Single
185
+ * quotes are normalised to double quotes before parsing.
186
+ * 3. **Plain JSON / text** — returned formatted when valid JSON,
187
+ * or as-is otherwise.
188
+ */
189
+ export function parseMcpResult(result: string): string {
190
+ const trimmed = result.trim();
191
+
192
+ // Fast path: try standard JSON parse first.
193
+ const jsonParsed = tryParseJson(trimmed);
194
+ if (jsonParsed !== undefined) {
195
+ const extracted = tryExtractContentBlocks(jsonParsed);
196
+ if (extracted !== null) return extracted;
197
+ return JSON.stringify(jsonParsed, null, 2);
198
+ }
199
+
200
+ // Attempt to fix Python repr (single-quoted dicts/lists).
201
+ const fixed = tryFixPythonRepr(trimmed);
202
+ if (fixed !== undefined) {
203
+ const extracted = tryExtractContentBlocks(fixed);
204
+ if (extracted !== null) return extracted;
205
+ return JSON.stringify(fixed, null, 2);
206
+ }
207
+
208
+ return trimmed;
209
+ }
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // Content-block extraction
213
+ // ---------------------------------------------------------------------------
214
+
215
+ interface McpContentBlock {
216
+ type: string;
217
+ text?: string;
218
+ }
219
+
220
+ function isMcpContentBlockArray(val: unknown): val is McpContentBlock[] {
221
+ if (!Array.isArray(val)) return false;
222
+ if (val.length === 0) return false;
223
+ return val.every(
224
+ (item) =>
225
+ typeof item === "object" &&
226
+ item !== null &&
227
+ "type" in item &&
228
+ typeof (item as Record<string, unknown>).type === "string",
229
+ );
230
+ }
231
+
232
+ function tryExtractContentBlocks(parsed: unknown): string | null {
233
+ if (!isMcpContentBlockArray(parsed)) return null;
234
+
235
+ const textParts: string[] = [];
236
+ for (const block of parsed) {
237
+ if (block.type === "text" && typeof block.text === "string") {
238
+ const innerJson = tryParseJson(block.text.trim());
239
+ if (innerJson !== undefined) {
240
+ textParts.push(JSON.stringify(innerJson, null, 2));
241
+ } else {
242
+ textParts.push(block.text);
243
+ }
244
+ }
245
+ }
246
+
247
+ return textParts.length > 0 ? textParts.join("\n\n") : null;
248
+ }
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // JSON / Python repr helpers
252
+ // ---------------------------------------------------------------------------
253
+
254
+ function tryParseJson(str: string): unknown | undefined {
255
+ try {
256
+ return JSON.parse(str);
257
+ } catch {
258
+ return undefined;
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Attempts to convert a Python repr string (single-quoted
264
+ * dicts/lists with True/False/None) into a parsed JS value.
265
+ *
266
+ * This is intentionally conservative: it only handles the
267
+ * most common patterns and bails on ambiguity.
268
+ */
269
+ function tryFixPythonRepr(str: string): unknown | undefined {
270
+ if (!str.startsWith("[") && !str.startsWith("{")) return undefined;
271
+
272
+ let fixed = str
273
+ .replace(/'/g, '"')
274
+ .replace(/\bTrue\b/g, "true")
275
+ .replace(/\bFalse\b/g, "false")
276
+ .replace(/\bNone\b/g, "null");
277
+
278
+ // Handle trailing commas before ] or } (common in Python repr).
279
+ fixed = fixed.replace(/,\s*([}\]])/g, "$1");
280
+
281
+ return tryParseJson(fixed);
282
+ }
283
+
@@ -22,6 +22,7 @@ import { SetupProgress } from "./SetupProgress";
22
22
  import { ApprovalCard } from "./ApprovalCard";
23
23
  import { FilePathContext, type FilePathContextValue } from "./FilePathContext";
24
24
  import type { ResolvedPathAction } from "./file-path-resolver";
25
+ import { SandboxContext, type SandboxContextValue } from "./SandboxContext";
25
26
 
26
27
  export interface MessageThreadProps {
27
28
  /** Completed executions in chronological order. */
@@ -87,6 +88,15 @@ export interface MessageThreadProps {
87
88
  path: string,
88
89
  resolved: ResolvedPathAction,
89
90
  ) => void;
91
+ /**
92
+ * Absolute sandbox workspace root (e.g. `/home/daytona/workspace`).
93
+ * When provided, shell commands and tool output normalize absolute
94
+ * sandbox paths to workspace-relative display paths.
95
+ *
96
+ * Pass an empty string or omit for local sessions where paths are
97
+ * the user's own filesystem (no normalization needed).
98
+ */
99
+ readonly sandboxWorkspaceRoot?: string;
90
100
  }
91
101
 
92
102
  const AUTO_SCROLL_THRESHOLD_PX = 80;
@@ -266,6 +276,7 @@ export function MessageThread({
266
276
  dismissedApprovalIds,
267
277
  workspaceEntries,
268
278
  onFilePathClick,
279
+ sandboxWorkspaceRoot,
269
280
  }: MessageThreadProps) {
270
281
  const scrollRef = useRef<HTMLDivElement>(null);
271
282
  const isNearBottomRef = useRef(true);
@@ -298,6 +309,11 @@ export function MessageThread({
298
309
  [workspaceEntries, onFilePathClick],
299
310
  );
300
311
 
312
+ const sandboxCtx = useMemo<SandboxContextValue>(
313
+ () => ({ sandboxWorkspaceRoot: sandboxWorkspaceRoot ?? "" }),
314
+ [sandboxWorkspaceRoot],
315
+ );
316
+
301
317
  return (
302
318
  <div
303
319
  ref={scrollRef}
@@ -314,6 +330,7 @@ export function MessageThread({
314
330
  className,
315
331
  )}
316
332
  >
333
+ <SandboxContext.Provider value={sandboxCtx}>
317
334
  <FilePathContext.Provider value={filePathCtx}>
318
335
  {items.map((item) => {
319
336
  switch (item.kind) {
@@ -370,6 +387,7 @@ export function MessageThread({
370
387
  }
371
388
  })}
372
389
  </FilePathContext.Provider>
390
+ </SandboxContext.Provider>
373
391
  </div>
374
392
  );
375
393
  }
@@ -0,0 +1,47 @@
1
+ import { createContext, useCallback, useContext } from "react";
2
+ import { normalizeSandboxPaths } from "./sandbox-path-normalizer";
3
+
4
+ /**
5
+ * Context value that carries the sandbox workspace root for display-time
6
+ * path normalization.
7
+ *
8
+ * Provided by {@link MessageThread} (or a platform builder's custom
9
+ * wrapper). When no provider is present, path normalization is a no-op.
10
+ */
11
+ export interface SandboxContextValue {
12
+ /**
13
+ * Absolute sandbox workspace root (e.g. `/home/daytona/workspace`).
14
+ * Empty string disables normalization (local mode, backward compat).
15
+ */
16
+ readonly sandboxWorkspaceRoot: string;
17
+ }
18
+
19
+ const DEFAULT_VALUE: SandboxContextValue = {
20
+ sandboxWorkspaceRoot: "",
21
+ };
22
+
23
+ export const SandboxContext =
24
+ createContext<SandboxContextValue>(DEFAULT_VALUE);
25
+
26
+ /**
27
+ * Returns a stable normalizer function that replaces absolute sandbox
28
+ * paths in the given text with workspace-relative display paths.
29
+ *
30
+ * When `sandboxWorkspaceRoot` is empty (no provider or local mode),
31
+ * returns the identity function — zero overhead.
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * function ShellCommand({ command }: { command: string }) {
36
+ * const normalize = useSandboxNormalize();
37
+ * return <code>{normalize(command)}</code>;
38
+ * }
39
+ * ```
40
+ */
41
+ export function useSandboxNormalize(): (text: string) => string {
42
+ const { sandboxWorkspaceRoot } = useContext(SandboxContext);
43
+ return useCallback(
44
+ (text: string) => normalizeSandboxPaths(text, sandboxWorkspaceRoot),
45
+ [sandboxWorkspaceRoot],
46
+ );
47
+ }