@stigmer/react 0.0.52 → 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
@@ -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
+
@@ -0,0 +1,277 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import { cn } from "@stigmer/theme";
5
+ import {
6
+ resolveToolCategory,
7
+ extractWriteContentFromPreview,
8
+ } from "./tool-categories";
9
+ import type { ToolCategory, ToolCategoryInfo } from "./tool-categories";
10
+ import { FilePathLink } from "./FilePathLink";
11
+ import { McpArgsView, McpMetadataRow } from "./McpToolDetail";
12
+ import {
13
+ CollapsibleCode,
14
+ FilePathIcon,
15
+ formatJson,
16
+ } from "./tool-rendering-primitives";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Public API
20
+ // ---------------------------------------------------------------------------
21
+
22
+ export interface ToolArgsViewProps {
23
+ /**
24
+ * Raw tool name as it appears on the ToolCall or PendingApproval.
25
+ * Used for category resolution and MCP metadata display.
26
+ */
27
+ readonly toolName: string;
28
+ /**
29
+ * Parsed tool arguments — either from `ToolCall.args` or
30
+ * `JSON.parse(PendingApproval.argsPreview)`.
31
+ */
32
+ readonly args: Record<string, unknown> | null;
33
+ /** MCP server slug for MCP tool classification and metadata. */
34
+ readonly mcpServerSlug?: string;
35
+ readonly className?: string;
36
+ }
37
+
38
+ /**
39
+ * Unified tool-arguments renderer used by both {@link ApprovalCard}
40
+ * (pre-execution) and {@link ToolCallDetail} (post-execution).
41
+ *
42
+ * Resolves the tool category from `toolName` + `mcpServerSlug`,
43
+ * extracts the relevant primary argument, and dispatches to the
44
+ * appropriate category-specific view:
45
+ *
46
+ * - **Shell** — terminal-style command block
47
+ * - **File (read/write/edit/delete)** — file icon + path + optional content
48
+ * - **Search / List** — pattern display
49
+ * - **MCP** — metadata row + scalar key-value grid + collapsible JSON
50
+ * - **Generic / Unknown** — formatted JSON args
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * // In approval card (from argsPreview string):
55
+ * const args = JSON.parse(pendingApproval.argsPreview);
56
+ * <ToolArgsView toolName={toolName} args={args} mcpServerSlug={slug} />
57
+ *
58
+ * // In detail view (from ToolCall):
59
+ * <ToolArgsView toolName={tc.name} args={tc.args} mcpServerSlug={tc.mcpServerSlug} />
60
+ * ```
61
+ */
62
+ export function ToolArgsView({
63
+ toolName,
64
+ args,
65
+ mcpServerSlug,
66
+ className,
67
+ }: ToolArgsViewProps) {
68
+ const categoryInfo = useMemo(
69
+ () => resolveToolCategory(toolName, mcpServerSlug),
70
+ [toolName, mcpServerSlug],
71
+ );
72
+
73
+ const primaryArg = useMemo(
74
+ () => extractPrimaryArgValue(args, categoryInfo),
75
+ [args, categoryInfo],
76
+ );
77
+
78
+ return (
79
+ <div className={cn("text-xs", className)}>
80
+ <CategoryArgsDispatch
81
+ category={categoryInfo.category}
82
+ toolName={toolName}
83
+ args={args}
84
+ primaryArg={primaryArg}
85
+ mcpServerSlug={mcpServerSlug}
86
+ />
87
+ </div>
88
+ );
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Dispatch
93
+ // ---------------------------------------------------------------------------
94
+
95
+ function CategoryArgsDispatch({
96
+ category,
97
+ toolName,
98
+ args,
99
+ primaryArg,
100
+ mcpServerSlug,
101
+ }: {
102
+ category: ToolCategory;
103
+ toolName: string;
104
+ args: Record<string, unknown> | null;
105
+ primaryArg: string | null;
106
+ mcpServerSlug?: string;
107
+ }) {
108
+ switch (category) {
109
+ case "shell":
110
+ return primaryArg ? <ShellArgsView command={primaryArg} /> : null;
111
+
112
+ case "read":
113
+ case "write":
114
+ case "edit":
115
+ case "delete":
116
+ return primaryArg ? (
117
+ <FileArgsView path={primaryArg} category={category} args={args} />
118
+ ) : null;
119
+
120
+ case "search":
121
+ case "list":
122
+ return primaryArg ? <SearchArgsView pattern={primaryArg} /> : null;
123
+
124
+ case "mcp":
125
+ return (
126
+ <McpArgsPreview
127
+ toolName={toolName}
128
+ args={args}
129
+ mcpServerSlug={mcpServerSlug ?? ""}
130
+ />
131
+ );
132
+
133
+ default:
134
+ return args && Object.keys(args).length > 0 ? (
135
+ <GenericArgsView args={args} />
136
+ ) : null;
137
+ }
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Category-specific views
142
+ // ---------------------------------------------------------------------------
143
+
144
+ function ShellArgsView({ command }: { command: string }) {
145
+ return (
146
+ <div className="rounded-md border border-border bg-[var(--stgm-terminal-bg,#1a1a2e)] p-2.5">
147
+ <pre className="whitespace-pre-wrap break-words font-mono text-xs text-[var(--stgm-terminal-fg,#e0e0e0)]">
148
+ <span className="select-none text-[var(--stgm-terminal-prompt,#6b7280)]">
149
+ ${" "}
150
+ </span>
151
+ {command}
152
+ </pre>
153
+ </div>
154
+ );
155
+ }
156
+
157
+ function FileArgsView({
158
+ path,
159
+ category,
160
+ args,
161
+ }: {
162
+ path: string;
163
+ category: string;
164
+ args: Record<string, unknown> | null;
165
+ }) {
166
+ const writeContent = useMemo(() => {
167
+ if (category !== "write" && category !== "edit") return null;
168
+ if (!args) return null;
169
+ return extractWriteContentFromArgs(args);
170
+ }, [category, args]);
171
+
172
+ return (
173
+ <div className="space-y-1.5">
174
+ <div className="flex items-center gap-1.5 text-xs">
175
+ <FilePathIcon />
176
+ <FilePathLink path={path} className="text-xs" />
177
+ </div>
178
+ {writeContent && (
179
+ <CollapsibleCode label="Content" content={writeContent} />
180
+ )}
181
+ </div>
182
+ );
183
+ }
184
+
185
+ function SearchArgsView({ pattern }: { pattern: string }) {
186
+ return (
187
+ <div className="flex items-center gap-1.5 text-xs">
188
+ <span className="text-muted-foreground">Pattern:</span>
189
+ <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">
190
+ {pattern}
191
+ </code>
192
+ </div>
193
+ );
194
+ }
195
+
196
+ function McpArgsPreview({
197
+ toolName,
198
+ args,
199
+ mcpServerSlug,
200
+ }: {
201
+ toolName: string;
202
+ args: Record<string, unknown> | null;
203
+ mcpServerSlug: string;
204
+ }) {
205
+ return (
206
+ <div className="space-y-2">
207
+ <McpMetadataRow
208
+ mcpServerSlug={mcpServerSlug}
209
+ toolName={toolName}
210
+ duration={null}
211
+ />
212
+ {args && Object.keys(args).length > 0 && <McpArgsView args={args} />}
213
+ </div>
214
+ );
215
+ }
216
+
217
+ function GenericArgsView({ args }: { args: Record<string, unknown> }) {
218
+ return (
219
+ <CollapsibleCode label="Arguments" content={formatJson(args)} />
220
+ );
221
+ }
222
+
223
+ // ---------------------------------------------------------------------------
224
+ // Utilities
225
+ // ---------------------------------------------------------------------------
226
+
227
+ const WRITE_CONTENT_FIELDS = [
228
+ "contents",
229
+ "content",
230
+ "file_content",
231
+ "new_text",
232
+ "new_string",
233
+ "replacement",
234
+ ] as const;
235
+
236
+ function extractWriteContentFromArgs(
237
+ args: Record<string, unknown>,
238
+ ): string | null {
239
+ for (const field of WRITE_CONTENT_FIELDS) {
240
+ const val = args[field];
241
+ if (typeof val === "string" && val.length > 0) return val;
242
+ }
243
+ return null;
244
+ }
245
+
246
+ function extractPrimaryArgValue(
247
+ args: Record<string, unknown> | null,
248
+ info: ToolCategoryInfo,
249
+ ): string | null {
250
+ if (!args) return null;
251
+
252
+ const tryField = (field: string): string | null => {
253
+ const val = args[field];
254
+ if (typeof val === "string" && val.length > 0) return val;
255
+ return null;
256
+ };
257
+
258
+ if (info.primaryArgField) {
259
+ const v = tryField(info.primaryArgField);
260
+ if (v) return v;
261
+ }
262
+
263
+ for (const fb of info.fallbackArgFields) {
264
+ const v = tryField(fb);
265
+ if (v) return v;
266
+ }
267
+
268
+ if (info.category === "unknown" || info.category === "mcp") {
269
+ const keys = Object.keys(args);
270
+ if (keys.length > 0) {
271
+ const val = args[keys[0]];
272
+ if (typeof val === "string") return val;
273
+ }
274
+ }
275
+
276
+ return null;
277
+ }