@stigmer/react 0.0.51 → 0.0.53

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 (71) 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/McpToolDetail.d.ts +48 -0
  10. package/execution/McpToolDetail.d.ts.map +1 -0
  11. package/execution/McpToolDetail.js +159 -0
  12. package/execution/McpToolDetail.js.map +1 -0
  13. package/execution/ToolArgsView.d.ts +41 -0
  14. package/execution/ToolArgsView.d.ts.map +1 -0
  15. package/execution/ToolArgsView.js +132 -0
  16. package/execution/ToolArgsView.js.map +1 -0
  17. package/execution/ToolCallDetail.d.ts +11 -4
  18. package/execution/ToolCallDetail.d.ts.map +1 -1
  19. package/execution/ToolCallDetail.js +30 -101
  20. package/execution/ToolCallDetail.js.map +1 -1
  21. package/execution/ToolCallGroup.d.ts.map +1 -1
  22. package/execution/ToolCallGroup.js +3 -2
  23. package/execution/ToolCallGroup.js.map +1 -1
  24. package/execution/ToolCallItem.d.ts +2 -0
  25. package/execution/ToolCallItem.d.ts.map +1 -1
  26. package/execution/ToolCallItem.js +6 -2
  27. package/execution/ToolCallItem.js.map +1 -1
  28. package/execution/index.d.ts +7 -1
  29. package/execution/index.d.ts.map +1 -1
  30. package/execution/index.js +4 -1
  31. package/execution/index.js.map +1 -1
  32. package/execution/tool-categories.d.ts +35 -8
  33. package/execution/tool-categories.d.ts.map +1 -1
  34. package/execution/tool-categories.js +76 -10
  35. package/execution/tool-categories.js.map +1 -1
  36. package/execution/tool-rendering-primitives.d.ts +61 -0
  37. package/execution/tool-rendering-primitives.d.ts.map +1 -0
  38. package/execution/tool-rendering-primitives.js +106 -0
  39. package/execution/tool-rendering-primitives.js.map +1 -0
  40. package/index.d.ts +5 -2
  41. package/index.d.ts.map +1 -1
  42. package/index.js +5 -1
  43. package/index.js.map +1 -1
  44. package/internal/CloudFeatureNotice.d.ts +19 -0
  45. package/internal/CloudFeatureNotice.d.ts.map +1 -0
  46. package/internal/CloudFeatureNotice.js +21 -0
  47. package/internal/CloudFeatureNotice.js.map +1 -0
  48. package/mcp-server/McpServerDetailView.d.ts +15 -1
  49. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  50. package/mcp-server/McpServerDetailView.js +11 -3
  51. package/mcp-server/McpServerDetailView.js.map +1 -1
  52. package/package.json +4 -4
  53. package/provider.d.ts +14 -2
  54. package/provider.d.ts.map +1 -1
  55. package/provider.js +3 -2
  56. package/provider.js.map +1 -1
  57. package/src/deployment-mode.ts +46 -0
  58. package/src/execution/ApprovalCard.tsx +130 -283
  59. package/src/execution/McpToolDetail.tsx +283 -0
  60. package/src/execution/ToolArgsView.tsx +277 -0
  61. package/src/execution/ToolCallDetail.tsx +51 -219
  62. package/src/execution/ToolCallGroup.tsx +3 -2
  63. package/src/execution/ToolCallItem.tsx +14 -2
  64. package/src/execution/index.ts +25 -0
  65. package/src/execution/tool-categories.ts +89 -9
  66. package/src/execution/tool-rendering-primitives.tsx +253 -0
  67. package/src/index.ts +13 -0
  68. package/src/internal/CloudFeatureNotice.tsx +60 -0
  69. package/src/mcp-server/McpServerDetailView.tsx +24 -2
  70. package/src/provider.tsx +18 -2
  71. package/styles.css +1 -1
@@ -1,28 +1,38 @@
1
1
  "use client";
2
2
 
3
- import { useState } from "react";
4
3
  import type { ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
5
4
  import { ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
6
5
  import { cn } from "@stigmer/theme";
7
- import { resolveToolCategory, extractPrimaryArg } from "./tool-categories";
8
- import { FilePathLink } from "./FilePathLink";
6
+ import { resolveToolCategory } from "./tool-categories";
7
+ import { McpToolDetail } from "./McpToolDetail";
8
+ import { ToolArgsView } from "./ToolArgsView";
9
+ import {
10
+ CollapsibleCode,
11
+ CollapsiblePre,
12
+ formatResult,
13
+ } from "./tool-rendering-primitives";
9
14
 
10
15
  export interface ToolCallDetailProps {
11
16
  readonly toolCall: ToolCall;
12
17
  readonly className?: string;
13
18
  }
14
19
 
15
- const TRUNCATION_LINE_LIMIT = 10;
16
-
17
20
  /**
18
21
  * Renders the detail panel for a single tool call with
19
22
  * category-specific visual treatments.
20
23
  *
21
- * - **Shell tools**: terminal-style code block for command + output
22
- * - **File tools (read/write/edit)**: file path header + content block
23
- * - **Search tools (grep/glob)**: pattern header + results list
24
+ * Arguments are rendered through the shared {@link ToolArgsView}
25
+ * dispatch (same component used by {@link ApprovalCard}), ensuring
26
+ * visual parity between pre-execution approval previews and
27
+ * post-execution detail views. Result/output sections are layered
28
+ * on top by this component.
29
+ *
30
+ * - **Shell tools**: terminal-style command + output
31
+ * - **File tools (read/write/edit/delete)**: file path + content + result
32
+ * - **Search tools (grep/glob)**: pattern + results
24
33
  * - **Think**: muted italic thought block
25
- * - **Unknown/MCP tools**: generic args + result JSON rendering
34
+ * - **MCP tools**: structured args + parsed result via {@link McpToolDetail}
35
+ * - **Unknown tools**: generic args + result JSON rendering
26
36
  *
27
37
  * Used inside {@link ToolCallItem} when expanded, but also
28
38
  * independently importable by platform builders who compose
@@ -34,7 +44,7 @@ const TRUNCATION_LINE_LIMIT = 10;
34
44
  * ```
35
45
  */
36
46
  export function ToolCallDetail({ toolCall, className }: ToolCallDetailProps) {
37
- const category = resolveToolCategory(toolCall.name);
47
+ const category = resolveToolCategory(toolCall.name, toolCall.mcpServerSlug);
38
48
  const isFailed = toolCall.status === ToolCallStatus.TOOL_CALL_FAILED;
39
49
 
40
50
  return (
@@ -55,6 +65,12 @@ export function ToolCallDetail({ toolCall, className }: ToolCallDetailProps) {
55
65
 
56
66
  // ---------------------------------------------------------------------------
57
67
  // Category-specific renderers
68
+ //
69
+ // Each renderer composes:
70
+ // MetadataRow (duration, slug) + ToolArgsView (shared args) + result section
71
+ //
72
+ // Think and MCP have fully custom rendering that doesn't fit the
73
+ // MetadataRow + ToolArgsView + Result pattern.
58
74
  // ---------------------------------------------------------------------------
59
75
 
60
76
  function CategoryRenderer({
@@ -80,38 +96,26 @@ function CategoryRenderer({
80
96
  return <SearchToolDetail toolCall={toolCall} />;
81
97
  case "think":
82
98
  return <ThinkToolDetail toolCall={toolCall} />;
99
+ case "mcp":
100
+ return <McpToolDetail toolCall={toolCall} />;
83
101
  default:
84
102
  return <GenericToolDetail toolCall={toolCall} />;
85
103
  }
86
104
  }
87
105
 
88
- /**
89
- * Terminal-style rendering for shell/execute tools.
90
- * Shows the command in a dark terminal block and output below.
91
- */
92
106
  function ShellToolDetail({ toolCall }: { toolCall: ToolCall }) {
93
- const command = extractPrimaryArg(toolCall);
94
107
  const duration = formatDuration(toolCall.startedAt, toolCall.completedAt);
95
108
 
96
109
  return (
97
110
  <>
98
- {/* Metadata */}
99
111
  <MetadataRow toolCall={toolCall} duration={duration} />
100
112
 
101
- {/* Command in terminal-style block */}
102
- {command && (
103
- <div className="space-y-1">
104
- <span className="font-medium text-muted-foreground">Command</span>
105
- <div className="rounded-md border border-border bg-[var(--stgm-terminal-bg,#1a1a2e)] p-2.5">
106
- <pre className="whitespace-pre-wrap break-words font-mono text-[var(--stgm-terminal-fg,#e0e0e0)]">
107
- <span className="select-none text-[var(--stgm-terminal-prompt,#6b7280)]">$ </span>
108
- {command}
109
- </pre>
110
- </div>
111
- </div>
112
- )}
113
+ <ToolArgsView
114
+ toolName={toolCall.name}
115
+ args={toolCall.args as Record<string, unknown> | null}
116
+ mcpServerSlug={toolCall.mcpServerSlug}
117
+ />
113
118
 
114
- {/* Output */}
115
119
  {toolCall.result && (
116
120
  <div className="space-y-1">
117
121
  <span className="font-medium text-muted-foreground">Output</span>
@@ -128,17 +132,8 @@ function ShellToolDetail({ toolCall }: { toolCall: ToolCall }) {
128
132
  }
129
133
 
130
134
  /**
131
- * File-oriented rendering for read/write/edit/delete tools.
132
- *
133
- * For **read** mode: shows only the metadata row and a clickable
134
- * path. Content is intentionally omitted — the Read tool's purpose
135
- * is for the *agent* to consume the file, and the content is either
136
- * truncated, omitted, or simply noise for the user. The clickable
137
- * path provides direct access to the source file.
138
- *
139
- * For **write/edit/delete** modes: shows the clickable path followed
140
- * by the content block (what was written/edited) and any result
141
- * confirmation.
135
+ * For **read**: metadata + path only (content is noise for the user).
136
+ * For **write/edit/delete**: metadata + path + content (via ToolArgsView) + result.
142
137
  */
143
138
  function FileToolDetail({
144
139
  toolCall,
@@ -147,49 +142,19 @@ function FileToolDetail({
147
142
  toolCall: ToolCall;
148
143
  mode: "read" | "write" | "edit" | "delete";
149
144
  }) {
150
- const filePath = extractPrimaryArg(toolCall);
151
145
  const duration = formatDuration(toolCall.startedAt, toolCall.completedAt);
152
146
 
153
- if (mode === "read") {
154
- return (
155
- <>
156
- <MetadataRow toolCall={toolCall} duration={duration} />
157
- {filePath && (
158
- <div className="flex items-center gap-1.5">
159
- <FilePathIcon />
160
- <FilePathLink path={filePath} className="text-xs" />
161
- </div>
162
- )}
163
- </>
164
- );
165
- }
166
-
167
- const contentFromArgs =
168
- mode === "write" || mode === "edit"
169
- ? extractWriteContent(toolCall)
170
- : null;
171
-
172
- const displayContent = contentFromArgs || toolCall.result;
173
-
174
147
  return (
175
148
  <>
176
149
  <MetadataRow toolCall={toolCall} duration={duration} />
177
150
 
178
- {filePath && (
179
- <div className="flex items-center gap-1.5">
180
- <FilePathIcon />
181
- <FilePathLink path={filePath} className="text-xs" />
182
- </div>
183
- )}
184
-
185
- {displayContent && (
186
- <CollapsibleCode
187
- label={mode === "delete" ? "Result" : "Content"}
188
- content={formatResult(displayContent)}
189
- />
190
- )}
151
+ <ToolArgsView
152
+ toolName={toolCall.name}
153
+ args={toolCall.args as Record<string, unknown> | null}
154
+ mcpServerSlug={toolCall.mcpServerSlug}
155
+ />
191
156
 
192
- {contentFromArgs && toolCall.result && (
157
+ {mode !== "read" && toolCall.result && (
193
158
  <CollapsibleCode
194
159
  label="Result"
195
160
  content={formatResult(toolCall.result)}
@@ -199,24 +164,18 @@ function FileToolDetail({
199
164
  );
200
165
  }
201
166
 
202
- /**
203
- * Search/discovery rendering for grep, glob, list tools.
204
- * Shows search pattern/path and results.
205
- */
206
167
  function SearchToolDetail({ toolCall }: { toolCall: ToolCall }) {
207
- const pattern = extractPrimaryArg(toolCall);
208
168
  const duration = formatDuration(toolCall.startedAt, toolCall.completedAt);
209
169
 
210
170
  return (
211
171
  <>
212
172
  <MetadataRow toolCall={toolCall} duration={duration} />
213
173
 
214
- {pattern && (
215
- <div className="flex items-center gap-1.5">
216
- <span className="font-medium text-muted-foreground">Pattern:</span>
217
- <span className="font-mono text-foreground">{pattern}</span>
218
- </div>
219
- )}
174
+ <ToolArgsView
175
+ toolName={toolCall.name}
176
+ args={toolCall.args as Record<string, unknown> | null}
177
+ mcpServerSlug={toolCall.mcpServerSlug}
178
+ />
220
179
 
221
180
  {toolCall.result && (
222
181
  <CollapsibleCode
@@ -228,10 +187,6 @@ function SearchToolDetail({ toolCall }: { toolCall: ToolCall }) {
228
187
  );
229
188
  }
230
189
 
231
- /**
232
- * Thought rendering. Muted, italic presentation distinct from
233
- * regular tool output.
234
- */
235
190
  function ThinkToolDetail({ toolCall }: { toolCall: ToolCall }) {
236
191
  const thought =
237
192
  (toolCall.args?.["thought"] as string | undefined) || toolCall.result;
@@ -248,10 +203,6 @@ function ThinkToolDetail({ toolCall }: { toolCall: ToolCall }) {
248
203
  );
249
204
  }
250
205
 
251
- /**
252
- * Fallback rendering for unknown/MCP tools. Preserves the original
253
- * generic args + result JSON display.
254
- */
255
206
  function GenericToolDetail({ toolCall }: { toolCall: ToolCall }) {
256
207
  const duration = formatDuration(toolCall.startedAt, toolCall.completedAt);
257
208
 
@@ -259,12 +210,11 @@ function GenericToolDetail({ toolCall }: { toolCall: ToolCall }) {
259
210
  <>
260
211
  <MetadataRow toolCall={toolCall} duration={duration} />
261
212
 
262
- {toolCall.args && Object.keys(toolCall.args).length > 0 && (
263
- <CollapsibleCode
264
- label="Arguments"
265
- content={formatJson(toolCall.args)}
266
- />
267
- )}
213
+ <ToolArgsView
214
+ toolName={toolCall.name}
215
+ args={toolCall.args as Record<string, unknown> | null}
216
+ mcpServerSlug={toolCall.mcpServerSlug}
217
+ />
268
218
 
269
219
  {toolCall.result && (
270
220
  <CollapsibleCode
@@ -302,128 +252,10 @@ function MetadataRow({
302
252
  );
303
253
  }
304
254
 
305
- function CollapsibleCode({
306
- label,
307
- content,
308
- }: {
309
- label: string;
310
- content: string;
311
- }) {
312
- const lines = content.split("\n");
313
- const needsTruncation = lines.length > TRUNCATION_LINE_LIMIT;
314
- const [isExpanded, setIsExpanded] = useState(false);
315
-
316
- const displayContent =
317
- needsTruncation && !isExpanded
318
- ? lines.slice(0, TRUNCATION_LINE_LIMIT).join("\n") + "\n\u2026"
319
- : content;
320
-
321
- return (
322
- <div className="space-y-1">
323
- <span className="font-medium text-muted-foreground">{label}</span>
324
- <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">
325
- {displayContent}
326
- </pre>
327
- {needsTruncation && (
328
- <button
329
- type="button"
330
- onClick={() => setIsExpanded((v) => !v)}
331
- className="text-primary hover:text-primary/80 text-xs font-medium transition-colors"
332
- >
333
- {isExpanded
334
- ? "Show less"
335
- : `Show all ${lines.length} lines`}
336
- </button>
337
- )}
338
- </div>
339
- );
340
- }
341
-
342
- function CollapsiblePre({
343
- content,
344
- className,
345
- }: {
346
- content: string;
347
- className?: string;
348
- }) {
349
- const lines = content.split("\n");
350
- const needsTruncation = lines.length > TRUNCATION_LINE_LIMIT;
351
- const [isExpanded, setIsExpanded] = useState(false);
352
-
353
- const displayContent =
354
- needsTruncation && !isExpanded
355
- ? lines.slice(0, TRUNCATION_LINE_LIMIT).join("\n") + "\n\u2026"
356
- : content;
357
-
358
- return (
359
- <>
360
- <pre className={cn("whitespace-pre-wrap break-words font-mono", className)}>
361
- {displayContent}
362
- </pre>
363
- {needsTruncation && (
364
- <button
365
- type="button"
366
- onClick={() => setIsExpanded((v) => !v)}
367
- className="mt-1 text-primary hover:text-primary/80 text-xs font-medium transition-colors"
368
- >
369
- {isExpanded ? "Show less" : `Show all ${lines.length} lines`}
370
- </button>
371
- )}
372
- </>
373
- );
374
- }
375
-
376
- function FilePathIcon() {
377
- return (
378
- <svg
379
- width="10"
380
- height="10"
381
- viewBox="0 0 12 12"
382
- fill="none"
383
- stroke="currentColor"
384
- strokeWidth="1.2"
385
- strokeLinecap="round"
386
- strokeLinejoin="round"
387
- className="shrink-0 text-muted-foreground"
388
- aria-hidden="true"
389
- >
390
- <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" />
391
- <path d="M7 1V4H10" />
392
- </svg>
393
- );
394
- }
395
-
396
255
  // ---------------------------------------------------------------------------
397
256
  // Utilities
398
257
  // ---------------------------------------------------------------------------
399
258
 
400
- function extractWriteContent(toolCall: ToolCall): string | null {
401
- if (!toolCall.args) return null;
402
- const fields = ["contents", "content", "file_content", "new_text", "new_string", "replacement"];
403
- for (const field of fields) {
404
- const val = toolCall.args[field];
405
- if (typeof val === "string" && val.length > 0) return val;
406
- }
407
- return null;
408
- }
409
-
410
- function formatJson(obj: object): string {
411
- try {
412
- return JSON.stringify(obj, null, 2);
413
- } catch {
414
- return String(obj);
415
- }
416
- }
417
-
418
- function formatResult(result: string): string {
419
- try {
420
- const parsed = JSON.parse(result);
421
- return JSON.stringify(parsed, null, 2);
422
- } catch {
423
- return result;
424
- }
425
- }
426
-
427
259
  /**
428
260
  * Returns a human-readable duration string from two ISO 8601
429
261
  * timestamps. Returns `null` when either timestamp is empty or
@@ -86,8 +86,9 @@ function defaultFormatSummary(
86
86
  status: AggregateStatus,
87
87
  ): string {
88
88
  if (toolCalls.length === 1) {
89
- const cat = resolveToolCategory(toolCalls[0].name);
90
- const primary = extractPrimaryArg(toolCalls[0]);
89
+ const tc = toolCalls[0];
90
+ const cat = resolveToolCategory(tc.name, tc.mcpServerSlug);
91
+ const primary = extractPrimaryArg(tc);
91
92
  if (primary) {
92
93
  const truncated =
93
94
  primary.length > 60 ? primary.slice(0, 57) + "\u2026" : primary;
@@ -66,7 +66,7 @@ export function ToolCallItem({
66
66
  const duration = formatDuration(toolCall.startedAt, toolCall.completedAt);
67
67
  const isSubAgent = subAgentExecution != null;
68
68
 
69
- const categoryInfo = resolveToolCategory(toolCall.name);
69
+ const categoryInfo = resolveToolCategory(toolCall.name, toolCall.mcpServerSlug);
70
70
  const CategoryIcon = CATEGORY_ICON[categoryInfo.category];
71
71
  const primaryArg = extractPrimaryArg(toolCall);
72
72
 
@@ -255,7 +255,7 @@ const STATUS_COLOR: Record<ItemStatus, string> = {
255
255
  // Category-specific icons (inline SVG, SDK pattern)
256
256
  // ---------------------------------------------------------------------------
257
257
 
258
- const CATEGORY_ICON: Record<ToolCategory, () => React.JSX.Element> = {
258
+ export const CATEGORY_ICON: Record<ToolCategory, () => React.JSX.Element> = {
259
259
  shell: TerminalIcon,
260
260
  read: FileIcon,
261
261
  write: FilePenIcon,
@@ -265,6 +265,7 @@ const CATEGORY_ICON: Record<ToolCategory, () => React.JSX.Element> = {
265
265
  list: FolderIcon,
266
266
  think: BrainIcon,
267
267
  "sub-agent": BotIcon,
268
+ mcp: McpPlugIcon,
268
269
  unknown: WrenchIcon,
269
270
  };
270
271
 
@@ -355,6 +356,17 @@ function WrenchIcon() {
355
356
  );
356
357
  }
357
358
 
359
+ function McpPlugIcon() {
360
+ return (
361
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round">
362
+ <path d="M4 1.5V4" />
363
+ <path d="M8 1.5V4" />
364
+ <path d="M2.5 4H9.5V6.5C9.5 8.43 7.93 10 6 10C4.07 10 2.5 8.43 2.5 6.5V4Z" />
365
+ <path d="M6 10V11" />
366
+ </svg>
367
+ );
368
+ }
369
+
358
370
  // ---------------------------------------------------------------------------
359
371
  // Status icons
360
372
  // ---------------------------------------------------------------------------
@@ -41,6 +41,30 @@ export type { ToolCallGroupProps } from "./ToolCallGroup";
41
41
  export { ToolCallDetail, formatDuration } from "./ToolCallDetail";
42
42
  export type { ToolCallDetailProps } from "./ToolCallDetail";
43
43
 
44
+ export { McpToolDetail, McpArgsView, McpMetadataRow, parseMcpResult } from "./McpToolDetail";
45
+ export type { McpToolDetailProps } from "./McpToolDetail";
46
+
47
+ export { ToolArgsView } from "./ToolArgsView";
48
+ export type { ToolArgsViewProps } from "./ToolArgsView";
49
+
50
+ export {
51
+ CollapsibleCode,
52
+ CollapsiblePre,
53
+ CollapsibleJsonBlock,
54
+ FilePathIcon,
55
+ McpServerIcon,
56
+ TRUNCATION_LINE_LIMIT,
57
+ formatJson,
58
+ formatResult,
59
+ isScalar,
60
+ humanizeArgKey,
61
+ } from "./tool-rendering-primitives";
62
+ export type {
63
+ CollapsibleCodeProps,
64
+ CollapsiblePreProps,
65
+ CollapsibleJsonBlockProps,
66
+ } from "./tool-rendering-primitives";
67
+
44
68
  export { ToolCallItem } from "./ToolCallItem";
45
69
  export type { ToolCallItemProps } from "./ToolCallItem";
46
70
 
@@ -84,6 +108,7 @@ export {
84
108
  resolveToolCategory,
85
109
  extractPrimaryArg,
86
110
  extractPrimaryArgFromPreview,
111
+ humanizeToolName,
87
112
  } from "./tool-categories";
88
113
  export type { ToolCategory, ToolCategoryInfo } from "./tool-categories";
89
114
 
@@ -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
+ }