@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
@@ -62,7 +62,15 @@ export function resolveGitBrowseUrl(
62
62
  if (!repoPath) return null;
63
63
 
64
64
  const ref = commit || branch || "HEAD";
65
- const cleanRelPath = relPath.replace(/^\/+/, "");
65
+ let cleanRelPath = relPath.replace(/^\/+/, "");
66
+
67
+ // Guard against relPath that accidentally duplicates the org/repo
68
+ // already encoded in gitUrl (e.g. relPath = "acme/app/src/main.go"
69
+ // when repoPath is "acme/app"). Strip the redundant prefix so the
70
+ // final URL doesn't contain the repo path twice.
71
+ if (cleanRelPath.startsWith(repoPath + "/")) {
72
+ cleanRelPath = cleanRelPath.slice(repoPath.length + 1);
73
+ }
66
74
 
67
75
  return `https://github.com/${repoPath}/blob/${ref}/${cleanRelPath}`;
68
76
  }
@@ -83,9 +91,9 @@ export type ResolvedPathAction =
83
91
  * 1. **Platform paths** (`.stigmer/` prefix) → copy raw path.
84
92
  * 2. **Workspace paths** → match against workspace entries:
85
93
  * - Single entry: treat path as relative to that entry.
86
- * - Multiple entries: match first path segment against entry names.
87
- * Unmatched segments fall back to the first entry (optimistic —
88
- * single-workspace sessions are the common case).
94
+ * - Multiple entries: scan path segments for an entry name match
95
+ * (handles org-prefixed paths like `plantonhq/agent-fleet/...`).
96
+ * Unmatched paths fall back to the first entry.
89
97
  * 3. **Git source** → construct GitHub blob URL.
90
98
  * 4. **Local source** → join with absolute local path for copy.
91
99
  * 5. **Fallback** → copy the raw path.
@@ -139,8 +147,11 @@ export function resolvePathAction(
139
147
  /**
140
148
  * Matches a workspace-relative path against entries. With a single
141
149
  * entry the path is used as-is. With multiple entries the first path
142
- * segment is tested against entry names; unmatched paths fall back to
143
- * the first entry (same strategy as the CLI).
150
+ * segment is tested against entry names. When the first segment
151
+ * doesn't match, deeper segments are scanned this handles tool-call
152
+ * paths that embed an org prefix (e.g. `plantonhq/agent-fleet/...`
153
+ * where the entry name is `agent-fleet`). Unmatched paths fall back
154
+ * to the first entry.
144
155
  */
145
156
  function matchWorkspaceEntry(
146
157
  relPath: string,
@@ -150,13 +161,14 @@ function matchWorkspaceEntry(
150
161
  return { entry: entries[0], relPath };
151
162
  }
152
163
 
153
- const slashIdx = relPath.indexOf("/");
154
- const firstSegment = slashIdx >= 0 ? relPath.slice(0, slashIdx) : relPath;
164
+ const segments = relPath.split("/");
155
165
 
156
- for (const entry of entries) {
157
- if (entry.name === firstSegment) {
158
- const rest = slashIdx >= 0 ? relPath.slice(slashIdx + 1) : "";
159
- return { entry, relPath: rest };
166
+ for (let i = 0; i < segments.length; i++) {
167
+ for (const entry of entries) {
168
+ if (entry.name === segments[i]) {
169
+ const rest = segments.slice(i + 1).join("/");
170
+ return { entry, relPath: rest };
171
+ }
160
172
  }
161
173
  }
162
174
 
@@ -19,6 +19,12 @@ export type { UseExecutionArtifactsReturn } from "./useExecutionArtifacts";
19
19
  export { useArtifactContent } from "./useArtifactContent";
20
20
  export type { UseArtifactContentReturn } from "./useArtifactContent";
21
21
 
22
+ export { useWorkspaceWriteBacks } from "./useWorkspaceWriteBacks";
23
+ export type { UseWorkspaceWriteBacksReturn } from "./useWorkspaceWriteBacks";
24
+
25
+ export { WriteBackCard } from "./WriteBackCard";
26
+ export type { WriteBackCardProps } from "./WriteBackCard";
27
+
22
28
  export {
23
29
  isTextArtifact,
24
30
  isArtifactExpired,
@@ -41,6 +47,30 @@ export type { ToolCallGroupProps } from "./ToolCallGroup";
41
47
  export { ToolCallDetail, formatDuration } from "./ToolCallDetail";
42
48
  export type { ToolCallDetailProps } from "./ToolCallDetail";
43
49
 
50
+ export { McpToolDetail, McpArgsView, McpMetadataRow, parseMcpResult } from "./McpToolDetail";
51
+ export type { McpToolDetailProps } from "./McpToolDetail";
52
+
53
+ export { ToolArgsView } from "./ToolArgsView";
54
+ export type { ToolArgsViewProps } from "./ToolArgsView";
55
+
56
+ export {
57
+ CollapsibleCode,
58
+ CollapsiblePre,
59
+ CollapsibleJsonBlock,
60
+ FilePathIcon,
61
+ McpServerIcon,
62
+ TRUNCATION_LINE_LIMIT,
63
+ formatJson,
64
+ formatResult,
65
+ isScalar,
66
+ humanizeArgKey,
67
+ } from "./tool-rendering-primitives";
68
+ export type {
69
+ CollapsibleCodeProps,
70
+ CollapsiblePreProps,
71
+ CollapsibleJsonBlockProps,
72
+ } from "./tool-rendering-primitives";
73
+
44
74
  export { ToolCallItem } from "./ToolCallItem";
45
75
  export type { ToolCallItemProps } from "./ToolCallItem";
46
76
 
@@ -80,10 +110,14 @@ export type { ArtifactPreviewModalProps } from "./ArtifactPreviewModal";
80
110
  export { ArtifactsWidget } from "./ArtifactsWidget";
81
111
  export type { ArtifactsWidgetProps } from "./ArtifactsWidget";
82
112
 
113
+ export { WriteBacksWidget } from "./WriteBacksWidget";
114
+ export type { WriteBacksWidgetProps } from "./WriteBacksWidget";
115
+
83
116
  export {
84
117
  resolveToolCategory,
85
118
  extractPrimaryArg,
86
119
  extractPrimaryArgFromPreview,
120
+ humanizeToolName,
87
121
  } from "./tool-categories";
88
122
  export type { ToolCategory, ToolCategoryInfo } from "./tool-categories";
89
123
 
@@ -93,6 +127,10 @@ export type { FilePathLinkProps } from "./FilePathLink";
93
127
  export { FilePathContext } from "./FilePathContext";
94
128
  export type { FilePathContextValue } from "./FilePathContext";
95
129
 
130
+ export { normalizeSandboxPaths } from "./sandbox-path-normalizer";
131
+ export { SandboxContext, useSandboxNormalize } from "./SandboxContext";
132
+ export type { SandboxContextValue } from "./SandboxContext";
133
+
96
134
  export { classifyPath, resolveGitBrowseUrl, resolvePathAction } from "./file-path-resolver";
97
135
  export type { PathClassification, ResolvedPathAction } from "./file-path-resolver";
98
136
 
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Display-time normalization for sandbox workspace paths.
3
+ *
4
+ * Mirrors the Python `humanize_sandbox_paths` in
5
+ * `graphton.core.backends.platform_mount` — same semantics, same
6
+ * replacement order. The SDK version acts as a safety net for
7
+ * historical data (persisted before backend humanization was added)
8
+ * and streaming edge cases.
9
+ *
10
+ * @module
11
+ */
12
+
13
+ /**
14
+ * Replace absolute sandbox workspace paths with workspace-relative
15
+ * display paths.
16
+ *
17
+ * Performs three ordered replacements:
18
+ *
19
+ * 1. `workspaceRoot + "/"` → empty string (makes paths workspace-relative)
20
+ * 2. `workspaceRoot` (exact) → `"."` (the workspace root itself)
21
+ * 3. Parent of `workspaceRoot` (sandbox home) → `"~"` (Unix convention)
22
+ *
23
+ * Returns `text` unchanged when `workspaceRoot` is empty.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * normalizeSandboxPaths(
28
+ * "ls /home/daytona/workspace/plantonhq/",
29
+ * "/home/daytona/workspace",
30
+ * );
31
+ * // => "ls plantonhq/"
32
+ *
33
+ * normalizeSandboxPaths(
34
+ * "cd /home/daytona/workspace && ls",
35
+ * "/home/daytona/workspace",
36
+ * );
37
+ * // => "cd . && ls"
38
+ *
39
+ * normalizeSandboxPaths(
40
+ * "cat /home/daytona/.bashrc",
41
+ * "/home/daytona/workspace",
42
+ * );
43
+ * // => "cat ~/.bashrc"
44
+ * ```
45
+ */
46
+ export function normalizeSandboxPaths(
47
+ text: string,
48
+ workspaceRoot: string,
49
+ ): string {
50
+ if (!text || !workspaceRoot) return text;
51
+
52
+ const wsRoot = workspaceRoot.replace(/\/+$/, "");
53
+
54
+ // 1) Strip workspace root prefix (with trailing slash) → workspace-relative
55
+ text = replaceAll(text, wsRoot + "/", "");
56
+
57
+ // 2) Replace bare workspace root → "."
58
+ text = replaceAll(text, wsRoot, ".");
59
+
60
+ // 3) Replace sandbox home prefix → "~"
61
+ const lastSlash = wsRoot.lastIndexOf("/");
62
+ if (lastSlash > 0) {
63
+ const sandboxHome = wsRoot.slice(0, lastSlash);
64
+ text = replaceAll(text, sandboxHome + "/", "~/");
65
+ text = replaceAll(text, sandboxHome, "~");
66
+ }
67
+
68
+ return text;
69
+ }
70
+
71
+ function replaceAll(text: string, search: string, replacement: string): string {
72
+ if (!search) return text;
73
+ let result = text;
74
+ let idx = result.indexOf(search);
75
+ while (idx !== -1) {
76
+ result = result.slice(0, idx) + replacement + result.slice(idx + search.length);
77
+ idx = result.indexOf(search, idx + replacement.length);
78
+ }
79
+ return result;
80
+ }
@@ -6,6 +6,10 @@ import type { JsonObject } from "@bufbuild/protobuf";
6
6
  *
7
7
  * Mirrors the CLI's `toolDisplayMap` in
8
8
  * `client-apps/cli/pkg/toolrender/render.go`.
9
+ *
10
+ * `"mcp"` covers tools originating from an MCP server whose
11
+ * names are dynamic and cannot be statically listed in
12
+ * {@link TOOL_DISPLAY_MAP}.
9
13
  */
10
14
  export type ToolCategory =
11
15
  | "shell"
@@ -17,6 +21,7 @@ export type ToolCategory =
17
21
  | "list"
18
22
  | "think"
19
23
  | "sub-agent"
24
+ | "mcp"
20
25
  | "unknown";
21
26
 
22
27
  export interface ToolCategoryInfo {
@@ -68,10 +73,19 @@ const TOOL_DISPLAY_MAP: ReadonlyMap<string, ToolDisplayEntry> = new Map([
68
73
 
69
74
  /**
70
75
  * Resolves a tool name to its category metadata for type-aware
71
- * rendering. Returns a stable `"unknown"` entry for unrecognized
72
- * tools using the raw tool name as the label.
76
+ * rendering.
77
+ *
78
+ * When `mcpServerSlug` is provided and the tool name is not a
79
+ * known built-in, the tool is categorised as `"mcp"` with a
80
+ * human-readable label derived from the raw tool name.
81
+ *
82
+ * Falls back to `"unknown"` only when the tool is neither
83
+ * built-in nor MCP-originated.
73
84
  */
74
- export function resolveToolCategory(toolName: string): ToolCategoryInfo {
85
+ export function resolveToolCategory(
86
+ toolName: string,
87
+ mcpServerSlug?: string,
88
+ ): ToolCategoryInfo {
75
89
  const entry = TOOL_DISPLAY_MAP.get(toolName);
76
90
  if (entry) {
77
91
  return {
@@ -81,6 +95,16 @@ export function resolveToolCategory(toolName: string): ToolCategoryInfo {
81
95
  fallbackArgFields: entry.fallbackFields ?? [],
82
96
  };
83
97
  }
98
+
99
+ if (mcpServerSlug) {
100
+ return {
101
+ category: "mcp",
102
+ label: humanizeToolName(toolName),
103
+ primaryArgField: "slug",
104
+ fallbackArgFields: ["name", "org"],
105
+ };
106
+ }
107
+
84
108
  return {
85
109
  category: "unknown",
86
110
  label: toolName,
@@ -89,6 +113,26 @@ export function resolveToolCategory(toolName: string): ToolCategoryInfo {
89
113
  };
90
114
  }
91
115
 
116
+ /**
117
+ * Converts a snake_case or camelCase tool name into a
118
+ * human-readable title.
119
+ *
120
+ * @example
121
+ * humanizeToolName("apply_mcp_server") // "Apply MCP Server"
122
+ * humanizeToolName("deleteAgent") // "Delete Agent"
123
+ */
124
+ export function humanizeToolName(name: string): string {
125
+ return name
126
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
127
+ .replace(/[_-]+/g, " ")
128
+ .replace(/\b[a-z]/g, (c) => c.toUpperCase())
129
+ .replace(/\bMcp\b/gi, "MCP")
130
+ .replace(/\bApi\b/gi, "API")
131
+ .replace(/\bId\b/gi, "ID")
132
+ .replace(/\bUrl\b/gi, "URL")
133
+ .replace(/\bIam\b/gi, "IAM");
134
+ }
135
+
92
136
  function extractArgValue(
93
137
  args: JsonObject | undefined,
94
138
  primary: string,
@@ -118,13 +162,12 @@ function extractArgValue(
118
162
  /**
119
163
  * Extracts the most relevant argument value from a tool call
120
164
  * based on its category (command for shell tools, path for file
121
- * tools, pattern for search tools, etc.).
165
+ * tools, slug for MCP tools, etc.).
122
166
  *
123
- * Returns `null` when the tool is unknown and has no arguments,
124
- * or when the expected argument fields are missing.
167
+ * Returns `null` when the tool has no recognised arguments.
125
168
  */
126
169
  export function extractPrimaryArg(toolCall: ToolCall): string | null {
127
- const info = resolveToolCategory(toolCall.name);
170
+ const info = resolveToolCategory(toolCall.name, toolCall.mcpServerSlug);
128
171
  const result = extractArgValue(
129
172
  toolCall.args,
130
173
  info.primaryArgField,
@@ -133,7 +176,7 @@ export function extractPrimaryArg(toolCall: ToolCall): string | null {
133
176
 
134
177
  if (result) return result;
135
178
 
136
- if (info.category === "unknown" && toolCall.args) {
179
+ if ((info.category === "unknown" || info.category === "mcp") && toolCall.args) {
137
180
  const keys = Object.keys(toolCall.args);
138
181
  if (keys.length > 0) {
139
182
  const val = toolCall.args[keys[0]];
@@ -152,6 +195,7 @@ export function extractPrimaryArg(toolCall: ToolCall): string | null {
152
195
  export function extractPrimaryArgFromPreview(
153
196
  toolName: string,
154
197
  argsPreview: string,
198
+ mcpServerSlug?: string,
155
199
  ): string | null {
156
200
  if (!argsPreview) return null;
157
201
 
@@ -159,7 +203,7 @@ export function extractPrimaryArgFromPreview(
159
203
  const parsed = JSON.parse(argsPreview);
160
204
  if (typeof parsed !== "object" || parsed === null) return null;
161
205
 
162
- const info = resolveToolCategory(toolName);
206
+ const info = resolveToolCategory(toolName, mcpServerSlug);
163
207
  return extractArgValue(
164
208
  parsed as JsonObject,
165
209
  info.primaryArgField,
@@ -169,3 +213,39 @@ export function extractPrimaryArgFromPreview(
169
213
  return null;
170
214
  }
171
215
  }
216
+
217
+ const WRITE_CONTENT_FIELDS = [
218
+ "contents",
219
+ "content",
220
+ "file_content",
221
+ "new_text",
222
+ "new_string",
223
+ "replacement",
224
+ ] as const;
225
+
226
+ /**
227
+ * Extracts the file content body from a JSON `argsPreview` string
228
+ * for write/edit tool categories. Scans the same field names used
229
+ * by the post-execution {@link ToolCallDetail} renderer so that
230
+ * the approval preview matches the completed tool call display.
231
+ *
232
+ * Returns `null` if parsing fails or no content field is found.
233
+ */
234
+ export function extractWriteContentFromPreview(
235
+ argsPreview: string,
236
+ ): string | null {
237
+ if (!argsPreview) return null;
238
+
239
+ try {
240
+ const parsed = JSON.parse(argsPreview);
241
+ if (typeof parsed !== "object" || parsed === null) return null;
242
+
243
+ for (const field of WRITE_CONTENT_FIELDS) {
244
+ const val = (parsed as Record<string, unknown>)[field];
245
+ if (typeof val === "string" && val.length > 0) return val;
246
+ }
247
+ return null;
248
+ } catch {
249
+ return null;
250
+ }
251
+ }
@@ -0,0 +1,253 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { cn } from "@stigmer/theme";
5
+
6
+ /**
7
+ * Shared truncation threshold for all collapsible tool rendering
8
+ * primitives. Applied consistently across detail views and approval
9
+ * card previews.
10
+ */
11
+ export const TRUNCATION_LINE_LIMIT = 10;
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // CollapsibleCode — labeled code block with line-based truncation
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export interface CollapsibleCodeProps {
18
+ readonly label: string;
19
+ readonly content: string;
20
+ readonly className?: string;
21
+ }
22
+
23
+ /**
24
+ * A labeled `<pre>` block with automatic line-based truncation and
25
+ * an expand/collapse toggle.
26
+ *
27
+ * Used for tool arguments, file content previews, and result blocks
28
+ * across both the detail view and the approval card.
29
+ */
30
+ export function CollapsibleCode({ label, content, className }: CollapsibleCodeProps) {
31
+ const lines = content.split("\n");
32
+ const needsTruncation = lines.length > TRUNCATION_LINE_LIMIT;
33
+ const [isExpanded, setIsExpanded] = useState(false);
34
+
35
+ const displayContent =
36
+ needsTruncation && !isExpanded
37
+ ? lines.slice(0, TRUNCATION_LINE_LIMIT).join("\n") + "\n\u2026"
38
+ : content;
39
+
40
+ return (
41
+ <div className={cn("space-y-1", className)}>
42
+ <span className="font-medium text-muted-foreground">{label}</span>
43
+ <pre className="max-h-80 overflow-auto whitespace-pre-wrap break-words rounded-md border border-border bg-muted/40 p-2 font-mono text-foreground">
44
+ {displayContent}
45
+ </pre>
46
+ {needsTruncation && (
47
+ <button
48
+ type="button"
49
+ onClick={() => setIsExpanded((v) => !v)}
50
+ className="text-xs font-medium text-primary transition-colors hover:text-primary/80"
51
+ >
52
+ {isExpanded ? "Show less" : `Show all ${lines.length} lines`}
53
+ </button>
54
+ )}
55
+ </div>
56
+ );
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // CollapsiblePre — raw pre with line-based truncation (no label/border)
61
+ // ---------------------------------------------------------------------------
62
+
63
+ export interface CollapsiblePreProps {
64
+ readonly content: string;
65
+ readonly className?: string;
66
+ }
67
+
68
+ /**
69
+ * A bare `<pre>` element with line-based truncation. Unlike
70
+ * {@link CollapsibleCode}, this has no label, border, or background
71
+ * — the caller controls container styling via `className`.
72
+ *
73
+ * Pass container styles (border, background, max-height) through
74
+ * `className` when rendering standalone; omit when the parent
75
+ * already provides a styled container (e.g. terminal blocks).
76
+ */
77
+ export function CollapsiblePre({ content, className }: CollapsiblePreProps) {
78
+ const lines = content.split("\n");
79
+ const needsTruncation = lines.length > TRUNCATION_LINE_LIMIT;
80
+ const [isExpanded, setIsExpanded] = useState(false);
81
+
82
+ const displayContent =
83
+ needsTruncation && !isExpanded
84
+ ? lines.slice(0, TRUNCATION_LINE_LIMIT).join("\n") + "\n\u2026"
85
+ : content;
86
+
87
+ return (
88
+ <>
89
+ <pre className={cn("whitespace-pre-wrap break-words font-mono", className)}>
90
+ {displayContent}
91
+ </pre>
92
+ {needsTruncation && (
93
+ <button
94
+ type="button"
95
+ onClick={() => setIsExpanded((v) => !v)}
96
+ className="mt-1 text-xs font-medium text-primary transition-colors hover:text-primary/80"
97
+ >
98
+ {isExpanded ? "Show less" : `Show all ${lines.length} lines`}
99
+ </button>
100
+ )}
101
+ </>
102
+ );
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // CollapsibleJsonBlock — chevron-toggled JSON section
107
+ // ---------------------------------------------------------------------------
108
+
109
+ export interface CollapsibleJsonBlockProps {
110
+ readonly label: string;
111
+ readonly content: string;
112
+ }
113
+
114
+ /**
115
+ * A collapsible JSON section with a chevron toggle. Initially
116
+ * collapsed, showing the label and line count. Useful for complex
117
+ * (non-scalar) tool arguments.
118
+ */
119
+ export function CollapsibleJsonBlock({ label, content }: CollapsibleJsonBlockProps) {
120
+ const [isExpanded, setIsExpanded] = useState(false);
121
+ const lines = content.split("\n");
122
+ const isLong = lines.length > 3;
123
+
124
+ return (
125
+ <div className="space-y-1">
126
+ <button
127
+ type="button"
128
+ onClick={() => setIsExpanded((v) => !v)}
129
+ className="flex items-center gap-1 font-medium text-muted-foreground transition-colors hover:text-foreground"
130
+ >
131
+ <svg
132
+ width="8"
133
+ height="8"
134
+ viewBox="0 0 8 8"
135
+ fill="none"
136
+ stroke="currentColor"
137
+ strokeWidth="1.5"
138
+ strokeLinecap="round"
139
+ strokeLinejoin="round"
140
+ className={cn(
141
+ "shrink-0 transition-transform duration-150",
142
+ isExpanded && "rotate-90",
143
+ )}
144
+ aria-hidden="true"
145
+ >
146
+ <path d="M2 1L6 4L2 7" />
147
+ </svg>
148
+ {label}
149
+ {!isExpanded && isLong && (
150
+ <span className="font-normal text-muted-foreground/60">
151
+ ({lines.length} lines)
152
+ </span>
153
+ )}
154
+ </button>
155
+ {isExpanded && (
156
+ <pre className="max-h-80 overflow-auto whitespace-pre-wrap break-words rounded-md border border-border bg-muted/40 p-2 font-mono text-foreground">
157
+ {content}
158
+ </pre>
159
+ )}
160
+ </div>
161
+ );
162
+ }
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Inline SVG icons
166
+ // ---------------------------------------------------------------------------
167
+
168
+ /** Small document icon for file path displays (10x10). */
169
+ export function FilePathIcon() {
170
+ return (
171
+ <svg
172
+ width="10"
173
+ height="10"
174
+ viewBox="0 0 12 12"
175
+ fill="none"
176
+ stroke="currentColor"
177
+ strokeWidth="1.2"
178
+ strokeLinecap="round"
179
+ strokeLinejoin="round"
180
+ className="shrink-0 text-muted-foreground"
181
+ aria-hidden="true"
182
+ >
183
+ <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" />
184
+ <path d="M7 1V4H10" />
185
+ </svg>
186
+ );
187
+ }
188
+
189
+ /** MCP server node/link icon (10x10). */
190
+ export function McpServerIcon() {
191
+ return (
192
+ <svg
193
+ width="10"
194
+ height="10"
195
+ viewBox="0 0 12 12"
196
+ fill="none"
197
+ stroke="currentColor"
198
+ strokeWidth="1.2"
199
+ strokeLinecap="round"
200
+ strokeLinejoin="round"
201
+ className="shrink-0"
202
+ aria-hidden="true"
203
+ >
204
+ <circle cx="6" cy="3" r="1.5" />
205
+ <circle cx="6" cy="9" r="1.5" />
206
+ <path d="M6 4.5V7.5" />
207
+ <path d="M3 6H4.5" />
208
+ <path d="M7.5 6H9" />
209
+ </svg>
210
+ );
211
+ }
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // Shared utilities
215
+ // ---------------------------------------------------------------------------
216
+
217
+ /** Safely serialise an object to pretty JSON. */
218
+ export function formatJson(obj: unknown): string {
219
+ try {
220
+ return JSON.stringify(obj, null, 2);
221
+ } catch {
222
+ return String(obj);
223
+ }
224
+ }
225
+
226
+ /** Pretty-print a result string if it's valid JSON, otherwise return as-is. */
227
+ export function formatResult(result: string): string {
228
+ try {
229
+ const parsed = JSON.parse(result);
230
+ return JSON.stringify(parsed, null, 2);
231
+ } catch {
232
+ return result;
233
+ }
234
+ }
235
+
236
+ /** Detect scalar values (string, number, boolean). */
237
+ export function isScalar(value: unknown): value is string | number | boolean {
238
+ const t = typeof value;
239
+ return t === "string" || t === "number" || t === "boolean";
240
+ }
241
+
242
+ /**
243
+ * Title-case a snake_case or camelCase argument key for display.
244
+ *
245
+ * @example
246
+ * humanizeArgKey("mcp_server_slug") // "Mcp Server Slug"
247
+ */
248
+ export function humanizeArgKey(key: string): string {
249
+ return key
250
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
251
+ .replace(/[_-]+/g, " ")
252
+ .replace(/\b[a-z]/g, (c) => c.toUpperCase());
253
+ }
@@ -80,6 +80,10 @@ export interface UseArtifactContentReturn {
80
80
  * @param storageKey - Storage key from `ExecutionArtifact.storageKey`, or `null` to skip.
81
81
  * @param entryPath - For directory artifacts: relative path of a file within
82
82
  * the archive to extract. `null` returns the full artifact (existing behavior).
83
+ * @param contentHash - SHA-256 hex digest from `ExecutionArtifact.contentHash`.
84
+ * When the same file is overwritten during execution, the `storageKey` stays
85
+ * stable but `contentHash` changes, triggering a re-fetch so the UI never
86
+ * shows stale content. Pass `undefined` or omit for backwards compatibility.
83
87
  *
84
88
  * @see useExecutionArtifacts — extracts artifact metadata from an execution
85
89
  * @see isTextArtifact — heuristic for whether content is fetchable as text
@@ -88,6 +92,7 @@ export function useArtifactContent(
88
92
  executionId: string | null,
89
93
  storageKey: string | null,
90
94
  entryPath?: string | null,
95
+ contentHash?: string,
91
96
  ): UseArtifactContentReturn {
92
97
  const stigmer = useStigmer();
93
98
 
@@ -150,7 +155,7 @@ export function useArtifactContent(
150
155
  return () => {
151
156
  cancelled.current = true;
152
157
  };
153
- }, [executionId, storageKey, entryPath, stigmer, fetchKey]);
158
+ }, [executionId, storageKey, entryPath, contentHash, stigmer, fetchKey]);
154
159
 
155
160
  return { content, contentType, isTruncated, isLoading, error, refetch };
156
161
  }