@kirosnn/mosaic 0.0.91 → 0.73.0
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.
- package/LICENSE +1 -1
- package/README.md +2 -6
- package/package.json +55 -48
- package/src/agent/Agent.ts +353 -131
- package/src/agent/context.ts +4 -4
- package/src/agent/prompts/systemPrompt.ts +209 -70
- package/src/agent/prompts/toolsPrompt.ts +285 -138
- package/src/agent/provider/anthropic.ts +109 -105
- package/src/agent/provider/google.ts +111 -107
- package/src/agent/provider/mistral.ts +95 -95
- package/src/agent/provider/ollama.ts +73 -17
- package/src/agent/provider/openai.ts +146 -102
- package/src/agent/provider/rateLimit.ts +178 -0
- package/src/agent/provider/reasoning.ts +29 -0
- package/src/agent/provider/xai.ts +108 -104
- package/src/agent/tools/definitions.ts +15 -1
- package/src/agent/tools/executor.ts +717 -98
- package/src/agent/tools/exploreExecutor.ts +20 -22
- package/src/agent/tools/fetch.ts +58 -0
- package/src/agent/tools/glob.ts +20 -4
- package/src/agent/tools/grep.ts +64 -9
- package/src/agent/tools/plan.ts +27 -0
- package/src/agent/tools/question.ts +7 -1
- package/src/agent/tools/read.ts +2 -0
- package/src/agent/types.ts +15 -14
- package/src/components/App.tsx +50 -8
- package/src/components/CustomInput.tsx +461 -77
- package/src/components/Main.tsx +1459 -1112
- package/src/components/Setup.tsx +1 -1
- package/src/components/ShortcutsModal.tsx +11 -8
- package/src/components/Welcome.tsx +1 -1
- package/src/components/main/ApprovalPanel.tsx +4 -3
- package/src/components/main/ChatPage.tsx +858 -516
- package/src/components/main/HomePage.tsx +58 -39
- package/src/components/main/QuestionPanel.tsx +52 -7
- package/src/components/main/ThinkingIndicator.tsx +13 -2
- package/src/components/main/types.ts +11 -10
- package/src/index.tsx +53 -25
- package/src/mcp/approvalPolicy.ts +148 -0
- package/src/mcp/cli/add.ts +185 -0
- package/src/mcp/cli/doctor.ts +77 -0
- package/src/mcp/cli/index.ts +85 -0
- package/src/mcp/cli/list.ts +50 -0
- package/src/mcp/cli/logs.ts +24 -0
- package/src/mcp/cli/manage.ts +99 -0
- package/src/mcp/cli/show.ts +53 -0
- package/src/mcp/cli/tools.ts +77 -0
- package/src/mcp/config.ts +223 -0
- package/src/mcp/index.ts +80 -0
- package/src/mcp/processManager.ts +299 -0
- package/src/mcp/rateLimiter.ts +50 -0
- package/src/mcp/registry.ts +151 -0
- package/src/mcp/schemaConverter.ts +100 -0
- package/src/mcp/servers/navigation.ts +854 -0
- package/src/mcp/toolCatalog.ts +169 -0
- package/src/mcp/types.ts +95 -0
- package/src/utils/approvalBridge.ts +45 -12
- package/src/utils/approvalModeBridge.ts +17 -0
- package/src/utils/commands/approvals.ts +48 -0
- package/src/utils/commands/compact.ts +30 -0
- package/src/utils/commands/echo.ts +1 -1
- package/src/utils/commands/image.ts +109 -0
- package/src/utils/commands/index.ts +9 -7
- package/src/utils/commands/new.ts +15 -0
- package/src/utils/commands/types.ts +3 -0
- package/src/utils/config.ts +3 -1
- package/src/utils/diffRendering.tsx +13 -16
- package/src/utils/exploreBridge.ts +10 -0
- package/src/utils/history.ts +82 -40
- package/src/utils/imageBridge.ts +28 -0
- package/src/utils/images.ts +31 -0
- package/src/utils/markdown.tsx +163 -99
- package/src/utils/models.ts +31 -16
- package/src/utils/notificationBridge.ts +23 -0
- package/src/utils/questionBridge.ts +36 -1
- package/src/utils/tokenEstimator.ts +32 -0
- package/src/utils/toolFormatting.ts +428 -48
- package/src/web/app.tsx +65 -5
- package/src/web/assets/css/ChatPage.css +102 -30
- package/src/web/assets/css/MessageItem.css +26 -29
- package/src/web/assets/css/ThinkingIndicator.css +44 -6
- package/src/web/assets/css/ToolMessage.css +36 -14
- package/src/web/components/ChatPage.tsx +228 -105
- package/src/web/components/HomePage.tsx +3 -3
- package/src/web/components/MessageItem.tsx +80 -81
- package/src/web/components/QuestionPanel.tsx +72 -12
- package/src/web/components/Setup.tsx +1 -1
- package/src/web/components/Sidebar.tsx +1 -3
- package/src/web/components/ThinkingIndicator.tsx +41 -21
- package/src/web/router.ts +1 -1
- package/src/web/server.tsx +894 -662
- package/src/web/storage.ts +23 -1
- package/src/web/types.ts +7 -6
- package/src/utils/commands/redo.ts +0 -74
- package/src/utils/commands/sessions.ts +0 -129
- package/src/utils/commands/undo.ts +0 -75
- package/src/utils/undoRedo.ts +0 -429
- package/src/utils/undoRedoBridge.ts +0 -45
- package/src/utils/undoRedoDb.ts +0 -338
|
@@ -9,15 +9,34 @@ const TOOL_DISPLAY_NAMES: Record<string, string> = {
|
|
|
9
9
|
write: 'Write',
|
|
10
10
|
edit: 'Edit',
|
|
11
11
|
list: 'List',
|
|
12
|
-
create_directory: 'Mkdir',
|
|
13
12
|
glob: 'Glob',
|
|
14
13
|
grep: 'Grep',
|
|
15
14
|
bash: 'Command',
|
|
16
15
|
question: 'Question',
|
|
17
16
|
explore: 'Explore',
|
|
17
|
+
fetch: 'Fetch',
|
|
18
|
+
plan: 'Plan',
|
|
18
19
|
};
|
|
19
20
|
|
|
21
|
+
function parseMcpSafeId(toolName: string): { serverId: string; tool: string } | null {
|
|
22
|
+
if (!toolName.startsWith('mcp__')) return null;
|
|
23
|
+
const parts = toolName.slice(5).split('__');
|
|
24
|
+
if (parts.length < 2) return null;
|
|
25
|
+
const tool = parts.pop()!;
|
|
26
|
+
const serverId = parts.join('__');
|
|
27
|
+
return { serverId, tool };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getMcpToolDisplayName(tool: string): string {
|
|
31
|
+
const words = tool.replace(/[-_]+/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
32
|
+
return words.split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
function getToolDisplayName(toolName: string): string {
|
|
36
|
+
const mcp = parseMcpSafeId(toolName);
|
|
37
|
+
if (mcp) {
|
|
38
|
+
return getMcpToolDisplayName(mcp.tool);
|
|
39
|
+
}
|
|
21
40
|
return TOOL_DISPLAY_NAMES[toolName] || toolName;
|
|
22
41
|
}
|
|
23
42
|
|
|
@@ -42,17 +61,65 @@ export function formatToolResult(result: unknown): string {
|
|
|
42
61
|
}
|
|
43
62
|
}
|
|
44
63
|
|
|
64
|
+
function getMcpHeaderInfo(_tool: string, args: Record<string, unknown>): string {
|
|
65
|
+
const query = args.query ?? args.search ?? args.prompt ?? args.input ?? args.text ?? args.q;
|
|
66
|
+
if (typeof query === 'string' && query.trim()) {
|
|
67
|
+
const clean = query.replace(/[\r\n]+/g, ' ').trim();
|
|
68
|
+
return clean.length > 50 ? clean.slice(0, 50) + '...' : clean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const url = args.url ?? args.uri ?? args.href;
|
|
72
|
+
if (typeof url === 'string' && url.trim()) {
|
|
73
|
+
try {
|
|
74
|
+
const u = new URL(url);
|
|
75
|
+
return u.hostname + (u.pathname !== '/' ? u.pathname : '');
|
|
76
|
+
} catch {
|
|
77
|
+
return url.length > 50 ? url.slice(0, 50) + '...' : url;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const urls = args.urls;
|
|
82
|
+
if (Array.isArray(urls) && urls.length > 0) {
|
|
83
|
+
const first = typeof urls[0] === 'string' ? urls[0] : '';
|
|
84
|
+
if (urls.length === 1) {
|
|
85
|
+
try {
|
|
86
|
+
return new URL(first).hostname;
|
|
87
|
+
} catch {
|
|
88
|
+
return first.length > 40 ? first.slice(0, 40) + '...' : first;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return `${urls.length} URLs`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const path = args.path ?? args.file ?? args.filename ?? args.filepath ?? args.name;
|
|
95
|
+
if (typeof path === 'string' && path.trim()) {
|
|
96
|
+
return path.length > 50 ? path.slice(0, 50) + '...' : path;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const command = args.command ?? args.cmd;
|
|
100
|
+
if (typeof command === 'string' && command.trim()) {
|
|
101
|
+
const clean = command.replace(/[\r\n]+/g, ' ').trim();
|
|
102
|
+
return clean.length > 50 ? clean.slice(0, 50) + '...' : clean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return '';
|
|
106
|
+
}
|
|
107
|
+
|
|
45
108
|
function formatKnownToolArgs(toolName: string, args: Record<string, unknown>): string | null {
|
|
46
109
|
switch (toolName) {
|
|
47
110
|
case 'read':
|
|
48
111
|
case 'write':
|
|
49
112
|
case 'edit':
|
|
50
113
|
case 'list':
|
|
51
|
-
case 'create_directory':
|
|
52
114
|
case 'glob':
|
|
53
115
|
case 'grep':
|
|
54
116
|
case 'bash':
|
|
55
|
-
case 'explore':
|
|
117
|
+
case 'explore':
|
|
118
|
+
case 'fetch': {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
case 'plan': {
|
|
56
123
|
return null;
|
|
57
124
|
}
|
|
58
125
|
|
|
@@ -62,6 +129,9 @@ function formatKnownToolArgs(toolName: string, args: Record<string, unknown>): s
|
|
|
62
129
|
}
|
|
63
130
|
|
|
64
131
|
default: {
|
|
132
|
+
if (toolName.startsWith('mcp__')) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
65
135
|
const keys = Object.keys(args);
|
|
66
136
|
if (keys.length === 0) return null;
|
|
67
137
|
try {
|
|
@@ -89,35 +159,86 @@ export function isToolSuccess(result: unknown): boolean {
|
|
|
89
159
|
|
|
90
160
|
function formatToolHeader(toolName: string, args: Record<string, unknown>): string {
|
|
91
161
|
const displayName = getToolDisplayName(toolName);
|
|
92
|
-
const path = typeof args.path === 'string' ? args.path : '';
|
|
93
162
|
|
|
94
163
|
switch (toolName) {
|
|
95
164
|
case 'read':
|
|
165
|
+
const readPath = typeof args.path === 'string' ? args.path : '';
|
|
166
|
+
return readPath ? `${displayName} (${readPath})` : displayName;
|
|
96
167
|
case 'write':
|
|
168
|
+
const writePath = typeof args.path === 'string' ? args.path : '';
|
|
169
|
+
return writePath ? `${displayName} (${writePath})` : displayName;
|
|
97
170
|
case 'edit':
|
|
171
|
+
const editPath = typeof args.path === 'string' ? args.path : '';
|
|
172
|
+
return editPath ? `${displayName} (${editPath})` : displayName;
|
|
98
173
|
case 'list':
|
|
99
|
-
|
|
100
|
-
return
|
|
174
|
+
const listPath = typeof args.path === 'string' ? args.path : '';
|
|
175
|
+
return listPath ? `${displayName} (${listPath})` : displayName;
|
|
101
176
|
case 'glob': {
|
|
102
177
|
const pattern = typeof args.pattern === 'string' ? args.pattern : '';
|
|
103
178
|
return pattern ? `${displayName} (${pattern})` : displayName;
|
|
104
179
|
}
|
|
105
180
|
case 'grep': {
|
|
106
|
-
const
|
|
107
|
-
|
|
181
|
+
const query = typeof args.query === 'string' ? args.query : '';
|
|
182
|
+
const fileType = typeof args.file_type === 'string' ? args.file_type : '';
|
|
183
|
+
const pattern = typeof args.pattern === 'string' ? args.pattern : '';
|
|
184
|
+
const info = fileType ? `*.${fileType}` : pattern;
|
|
185
|
+
const cleanQuery = query.replace(/[\r\n]+/g, ' ').trim();
|
|
186
|
+
const queryShort = cleanQuery.length > 30 ? cleanQuery.slice(0, 30) + '...' : cleanQuery;
|
|
187
|
+
return info ? `${displayName} ("${queryShort}" in ${info})` : `${displayName} ("${queryShort}")`;
|
|
108
188
|
}
|
|
109
189
|
case 'bash': {
|
|
110
190
|
const command = typeof args.command === 'string' ? args.command : '';
|
|
111
|
-
const cleanCommand = command.replace(/\s+--timeout\s+\d+$/, '');
|
|
191
|
+
const cleanCommand = command.replace(/[\r\n]+/g, ' ').trim().replace(/\s+--timeout\s+\d+$/, '');
|
|
112
192
|
return cleanCommand ? `${displayName} (${cleanCommand})` : displayName;
|
|
113
193
|
}
|
|
114
194
|
case 'explore': {
|
|
115
195
|
const purpose = typeof args.purpose === 'string' ? args.purpose : '';
|
|
116
|
-
|
|
196
|
+
const cleanPurpose = purpose.replace(/[\r\n]+/g, ' ').trim();
|
|
197
|
+
return cleanPurpose ? `${displayName} (${cleanPurpose})` : displayName;
|
|
117
198
|
}
|
|
118
|
-
|
|
199
|
+
case 'fetch': {
|
|
200
|
+
const url = typeof args.url === 'string' ? args.url : '';
|
|
201
|
+
try {
|
|
202
|
+
const urlObj = new URL(url);
|
|
203
|
+
const shortUrl = urlObj.hostname + (urlObj.pathname !== '/' ? urlObj.pathname : '');
|
|
204
|
+
return shortUrl ? `${displayName} (${shortUrl})` : displayName;
|
|
205
|
+
} catch {
|
|
206
|
+
return url ? `${displayName} (${url})` : displayName;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
case 'plan':
|
|
210
|
+
return displayName;
|
|
211
|
+
default: {
|
|
212
|
+
if (toolName.startsWith('mcp__')) {
|
|
213
|
+
const info = getMcpHeaderInfo('', args);
|
|
214
|
+
return info ? `${displayName} ("${info}")` : displayName;
|
|
215
|
+
}
|
|
119
216
|
return displayName;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function formatPlanHeader(result: unknown): string {
|
|
222
|
+
const displayName = getToolDisplayName('plan');
|
|
223
|
+
if (!result || typeof result !== 'object') return displayName;
|
|
224
|
+
const obj = result as Record<string, unknown>;
|
|
225
|
+
const planItems = Array.isArray(obj.plan) ? obj.plan : [];
|
|
226
|
+
const total = planItems.length;
|
|
227
|
+
if (total === 0) return displayName;
|
|
228
|
+
|
|
229
|
+
let completed = 0;
|
|
230
|
+
let inProgress = 0;
|
|
231
|
+
|
|
232
|
+
for (const item of planItems) {
|
|
233
|
+
if (!item || typeof item !== 'object') continue;
|
|
234
|
+
const status = typeof (item as Record<string, unknown>).status === 'string'
|
|
235
|
+
? (item as Record<string, unknown>).status
|
|
236
|
+
: 'pending';
|
|
237
|
+
if (status === 'completed') completed += 1;
|
|
238
|
+
if (status === 'in_progress') inProgress += 1;
|
|
120
239
|
}
|
|
240
|
+
|
|
241
|
+
return `${displayName} (${completed}/${total} done, ${inProgress} in progress)`;
|
|
121
242
|
}
|
|
122
243
|
|
|
123
244
|
export function parseToolHeader(toolName: string, args: Record<string, unknown>): { name: string; info: string | null } {
|
|
@@ -136,20 +257,43 @@ export function parseToolHeader(toolName: string, args: Record<string, unknown>)
|
|
|
136
257
|
return { name: displayName, info: pattern || null };
|
|
137
258
|
}
|
|
138
259
|
case 'grep': {
|
|
139
|
-
const
|
|
140
|
-
|
|
260
|
+
const query = typeof args.query === 'string' ? args.query : '';
|
|
261
|
+
const fileType = typeof args.file_type === 'string' ? args.file_type : '';
|
|
262
|
+
const pattern = typeof args.pattern === 'string' ? args.pattern : '';
|
|
263
|
+
const fileInfo = fileType ? `*.${fileType}` : pattern;
|
|
264
|
+
const cleanQuery = query.replace(/[\r\n]+/g, ' ').trim();
|
|
265
|
+
const queryShort = cleanQuery.length > 30 ? cleanQuery.slice(0, 30) + '...' : cleanQuery;
|
|
266
|
+
const info = fileInfo ? `"${queryShort}" in ${fileInfo}` : `"${queryShort}"`;
|
|
267
|
+
return { name: displayName, info };
|
|
141
268
|
}
|
|
142
269
|
case 'bash': {
|
|
143
270
|
const command = typeof args.command === 'string' ? args.command : '';
|
|
144
|
-
const cleanCommand = command.replace(/\s+--timeout\s+\d+$/, '');
|
|
271
|
+
const cleanCommand = command.replace(/[\r\n]+/g, ' ').trim().replace(/\s+--timeout\s+\d+$/, '');
|
|
145
272
|
return { name: displayName, info: cleanCommand || null };
|
|
146
273
|
}
|
|
147
274
|
case 'explore': {
|
|
148
275
|
const purpose = typeof args.purpose === 'string' ? args.purpose : '';
|
|
149
276
|
return { name: displayName, info: purpose || null };
|
|
150
277
|
}
|
|
151
|
-
|
|
278
|
+
case 'fetch': {
|
|
279
|
+
const url = typeof args.url === 'string' ? args.url : '';
|
|
280
|
+
try {
|
|
281
|
+
const urlObj = new URL(url);
|
|
282
|
+
const shortUrl = urlObj.hostname + (urlObj.pathname !== '/' ? urlObj.pathname : '');
|
|
283
|
+
return { name: displayName, info: shortUrl || null };
|
|
284
|
+
} catch {
|
|
285
|
+
return { name: displayName, info: url || null };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
case 'plan':
|
|
289
|
+
return { name: displayName, info: null };
|
|
290
|
+
default: {
|
|
291
|
+
if (toolName.startsWith('mcp__')) {
|
|
292
|
+
const info = getMcpHeaderInfo('', args);
|
|
293
|
+
return { name: displayName, info: info || null };
|
|
294
|
+
}
|
|
152
295
|
return { name: displayName, info: null };
|
|
296
|
+
}
|
|
153
297
|
}
|
|
154
298
|
}
|
|
155
299
|
|
|
@@ -161,10 +305,25 @@ function getLineCount(text: string): number {
|
|
|
161
305
|
function formatListTree(result: unknown): string[] {
|
|
162
306
|
if (typeof result !== 'string') return [];
|
|
163
307
|
try {
|
|
164
|
-
const parsed = JSON.parse(result)
|
|
165
|
-
if (!Array.isArray(parsed)) return [];
|
|
308
|
+
const parsed = JSON.parse(result);
|
|
166
309
|
|
|
167
|
-
|
|
310
|
+
let items: Array<{ name?: string; path?: string; type?: string }>;
|
|
311
|
+
let errors: string[] = [];
|
|
312
|
+
|
|
313
|
+
if (Array.isArray(parsed)) {
|
|
314
|
+
items = parsed;
|
|
315
|
+
} else if (parsed && typeof parsed === 'object' && Array.isArray(parsed.files)) {
|
|
316
|
+
items = parsed.files;
|
|
317
|
+
if (Array.isArray(parsed.errors)) {
|
|
318
|
+
errors = parsed.errors
|
|
319
|
+
.map((e: unknown) => (typeof e === 'string' ? e : ''))
|
|
320
|
+
.filter((e: string) => e);
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const entries = items
|
|
168
327
|
.map((e) => ({
|
|
169
328
|
name: typeof e.name === 'string' ? e.name : (typeof e.path === 'string' ? e.path : ''),
|
|
170
329
|
type: typeof e.type === 'string' ? e.type : '',
|
|
@@ -181,45 +340,34 @@ function formatListTree(result: unknown): string[] {
|
|
|
181
340
|
.map((e) => e.name)
|
|
182
341
|
.sort((a, b) => a.localeCompare(b));
|
|
183
342
|
|
|
184
|
-
|
|
343
|
+
const lines = [...dirs, ...files];
|
|
344
|
+
|
|
345
|
+
if (errors.length > 0) {
|
|
346
|
+
lines.push('', `Errors (${errors.length}):`);
|
|
347
|
+
for (const err of errors) {
|
|
348
|
+
lines.push(` ${err}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return lines;
|
|
185
353
|
} catch {
|
|
186
354
|
return [];
|
|
187
355
|
}
|
|
188
356
|
}
|
|
189
357
|
|
|
190
358
|
function formatGrepResult(result: unknown): string[] {
|
|
191
|
-
if (typeof result !== 'string') return [];
|
|
359
|
+
if (typeof result !== 'string') return ['No results returned'];
|
|
360
|
+
|
|
192
361
|
try {
|
|
193
362
|
const parsed = JSON.parse(result);
|
|
194
363
|
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (typeof parsed[0] === 'string') {
|
|
199
|
-
return parsed.map(file => ` ${file}`);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (typeof parsed[0] === 'object' && parsed[0] !== null) {
|
|
203
|
-
const lines: string[] = [];
|
|
204
|
-
for (const item of parsed) {
|
|
205
|
-
if (item.file) {
|
|
206
|
-
lines.push(`${item.file}:`);
|
|
207
|
-
if (Array.isArray(item.matches)) {
|
|
208
|
-
for (const match of item.matches) {
|
|
209
|
-
if (match.line && match.content) {
|
|
210
|
-
lines.push(` ${match.line}: ${match.content.trim()}`);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return lines.length > 0 ? lines : ['No matches'];
|
|
217
|
-
}
|
|
364
|
+
if (typeof parsed.total_matches === 'number' && typeof parsed.files_with_matches === 'number') {
|
|
365
|
+
return [`${parsed.total_matches} matches in ${parsed.files_with_matches} files`];
|
|
218
366
|
}
|
|
219
367
|
|
|
220
|
-
return [
|
|
368
|
+
return ['No results returned'];
|
|
221
369
|
} catch {
|
|
222
|
-
return
|
|
370
|
+
return ['No results returned'];
|
|
223
371
|
}
|
|
224
372
|
}
|
|
225
373
|
|
|
@@ -236,9 +384,176 @@ function getToolErrorText(result: unknown): string | null {
|
|
|
236
384
|
return typeof error === 'string' && error.trim() ? error.trim() : null;
|
|
237
385
|
}
|
|
238
386
|
|
|
387
|
+
function formatMcpResultBody(result: unknown): string[] {
|
|
388
|
+
if (typeof result !== 'string') {
|
|
389
|
+
if (result && typeof result === 'object') {
|
|
390
|
+
const obj = result as Record<string, unknown>;
|
|
391
|
+
if (typeof obj.error === 'string') {
|
|
392
|
+
return [`Error: ${obj.error}`];
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return ['Completed'];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const text = result.trim();
|
|
399
|
+
if (!text) return ['(empty result)'];
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const parsed = JSON.parse(text);
|
|
403
|
+
|
|
404
|
+
if (Array.isArray(parsed)) {
|
|
405
|
+
return formatMcpArray(parsed);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
409
|
+
return formatMcpObject(parsed);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return [String(parsed)];
|
|
413
|
+
} catch {
|
|
414
|
+
const lines = text.split(/\r?\n/).filter(l => l.trim());
|
|
415
|
+
return lines.length > 0 ? lines : ['Completed'];
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function formatMcpArray(arr: unknown[]): string[] {
|
|
420
|
+
if (arr.length === 0) return ['(no results)'];
|
|
421
|
+
|
|
422
|
+
const lines: string[] = [];
|
|
423
|
+
|
|
424
|
+
for (const item of arr) {
|
|
425
|
+
if (typeof item === 'string') {
|
|
426
|
+
lines.push(` ${item}`);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (item && typeof item === 'object') {
|
|
431
|
+
const obj = item as Record<string, unknown>;
|
|
432
|
+
|
|
433
|
+
if (typeof obj.error === 'string') {
|
|
434
|
+
const url = typeof obj.url === 'string' ? obj.url : '';
|
|
435
|
+
const detail = obj.details && typeof obj.details === 'object'
|
|
436
|
+
? (obj.details as Record<string, unknown>).detail
|
|
437
|
+
: '';
|
|
438
|
+
const errMsg = typeof detail === 'string' && detail ? detail : obj.error;
|
|
439
|
+
lines.push(url ? ` ${url} - ${errMsg}` : ` Error: ${errMsg}`);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const title = obj.title ?? obj.name ?? obj.label;
|
|
444
|
+
const desc = obj.description ?? obj.summary ?? obj.snippet ?? obj.text ?? obj.content;
|
|
445
|
+
const url = obj.url ?? obj.link ?? obj.href;
|
|
446
|
+
|
|
447
|
+
if (typeof title === 'string' && title) {
|
|
448
|
+
let line = ` ${title}`;
|
|
449
|
+
if (typeof url === 'string' && url) {
|
|
450
|
+
try {
|
|
451
|
+
line += ` (${new URL(url).hostname})`;
|
|
452
|
+
} catch {
|
|
453
|
+
line += ` (${url.length > 40 ? url.slice(0, 40) + '...' : url})`;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
lines.push(line);
|
|
457
|
+
if (typeof desc === 'string' && desc.trim()) {
|
|
458
|
+
const short = desc.trim().replace(/[\r\n]+/g, ' ');
|
|
459
|
+
lines.push(` ${short.length > 80 ? short.slice(0, 80) + '...' : short}`);
|
|
460
|
+
}
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (typeof url === 'string' && url) {
|
|
465
|
+
let line = ` ${url}`;
|
|
466
|
+
if (typeof desc === 'string' && desc.trim()) {
|
|
467
|
+
const short = desc.trim().replace(/[\r\n]+/g, ' ');
|
|
468
|
+
line += ` - ${short.length > 60 ? short.slice(0, 60) + '...' : short}`;
|
|
469
|
+
}
|
|
470
|
+
lines.push(line);
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const keys = Object.keys(obj);
|
|
475
|
+
const summary = keys.slice(0, 3).map(k => {
|
|
476
|
+
const v = obj[k];
|
|
477
|
+
const s = typeof v === 'string' ? v : JSON.stringify(v);
|
|
478
|
+
const short = s && s.length > 30 ? s.slice(0, 30) + '...' : s;
|
|
479
|
+
return `${k}: ${short}`;
|
|
480
|
+
}).join(', ');
|
|
481
|
+
lines.push(` ${summary}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (arr.length > 1) {
|
|
486
|
+
lines.unshift(`${arr.length} results:`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return lines.length > 0 ? lines : ['Completed'];
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function formatMcpObject(obj: Record<string, unknown>): string[] {
|
|
493
|
+
const lines: string[] = [];
|
|
494
|
+
|
|
495
|
+
if (typeof obj.error === 'string') {
|
|
496
|
+
const detail = obj.details && typeof obj.details === 'object'
|
|
497
|
+
? (obj.details as Record<string, unknown>).detail
|
|
498
|
+
: '';
|
|
499
|
+
const errMsg = typeof detail === 'string' && detail ? detail : obj.error;
|
|
500
|
+
return [`Error: ${errMsg}`];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const status = obj.status ?? obj.statusCode ?? obj.code;
|
|
504
|
+
const message = obj.message ?? obj.result ?? obj.output ?? obj.text ?? obj.content ?? obj.data;
|
|
505
|
+
|
|
506
|
+
if (typeof status === 'number' || typeof status === 'string') {
|
|
507
|
+
lines.push(`Status: ${status}`);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (typeof message === 'string' && message.trim()) {
|
|
511
|
+
const msgLines = message.trim().split(/\r?\n/);
|
|
512
|
+
lines.push(...msgLines);
|
|
513
|
+
} else if (message && typeof message === 'object') {
|
|
514
|
+
if (Array.isArray(message)) {
|
|
515
|
+
lines.push(...formatMcpArray(message));
|
|
516
|
+
} else {
|
|
517
|
+
const entries = Object.entries(message as Record<string, unknown>).slice(0, 5);
|
|
518
|
+
for (const [k, v] of entries) {
|
|
519
|
+
const s = typeof v === 'string' ? v : JSON.stringify(v);
|
|
520
|
+
const short = s && s.length > 60 ? s.slice(0, 60) + '...' : s;
|
|
521
|
+
lines.push(` ${k}: ${short}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (lines.length === 0) {
|
|
527
|
+
const entries = Object.entries(obj).slice(0, 5);
|
|
528
|
+
for (const [k, v] of entries) {
|
|
529
|
+
const s = typeof v === 'string' ? v : JSON.stringify(v);
|
|
530
|
+
const short = s && s.length > 60 ? s.slice(0, 60) + '...' : s;
|
|
531
|
+
lines.push(` ${k}: ${short}`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return lines.length > 0 ? lines : ['Completed'];
|
|
536
|
+
}
|
|
537
|
+
|
|
239
538
|
function formatToolBodyLines(toolName: string, args: Record<string, unknown>, result: unknown): string[] {
|
|
240
539
|
const errorText = getToolErrorText(result);
|
|
241
|
-
if (errorText)
|
|
540
|
+
if (errorText) {
|
|
541
|
+
if (toolName === 'fetch') {
|
|
542
|
+
const statusMatch = errorText.match(/HTTP (\d+(?: [a-zA-Z ]+)?)/);
|
|
543
|
+
if (statusMatch) {
|
|
544
|
+
return [`${statusMatch[1]} - Failed to fetch`];
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (toolName.startsWith('mcp__')) {
|
|
548
|
+
const statusMatch = errorText.match(/status code (\d+)/i);
|
|
549
|
+
if (statusMatch) {
|
|
550
|
+
return [`Error ${statusMatch[1]}`];
|
|
551
|
+
}
|
|
552
|
+
const short = errorText.length > 80 ? errorText.slice(0, 80) + '...' : errorText;
|
|
553
|
+
return [`Error: ${short}`];
|
|
554
|
+
}
|
|
555
|
+
return [`Tool error: ${errorText}`];
|
|
556
|
+
}
|
|
242
557
|
|
|
243
558
|
switch (toolName) {
|
|
244
559
|
case 'read': {
|
|
@@ -318,7 +633,71 @@ function formatToolBodyLines(toolName: string, args: Record<string, unknown>, re
|
|
|
318
633
|
return ['Exploration completed'];
|
|
319
634
|
}
|
|
320
635
|
|
|
636
|
+
case 'fetch': {
|
|
637
|
+
if (typeof result === 'string') {
|
|
638
|
+
const url = typeof args.url === 'string' ? args.url : 'URL';
|
|
639
|
+
const statusMatch = result.match(/\*\*Status:\*\* (\d+(?: [a-zA-Z ]+)?)/);
|
|
640
|
+
if (statusMatch) {
|
|
641
|
+
return [`${statusMatch[1]} - Fetched ${url}`];
|
|
642
|
+
}
|
|
643
|
+
return [`Fetched ${url}`];
|
|
644
|
+
}
|
|
645
|
+
return ['Fetch completed'];
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
case 'plan': {
|
|
649
|
+
if (result && typeof result === 'object') {
|
|
650
|
+
const obj = result as Record<string, unknown>;
|
|
651
|
+
const explanation = typeof obj.explanation === 'string' ? obj.explanation.trim() : '';
|
|
652
|
+
const planItems = Array.isArray(obj.plan) ? obj.plan : [];
|
|
653
|
+
const lines: string[] = [];
|
|
654
|
+
|
|
655
|
+
if (explanation) {
|
|
656
|
+
lines.push(explanation);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const normalized = planItems
|
|
660
|
+
.map((item) => {
|
|
661
|
+
if (!item || typeof item !== 'object') return null;
|
|
662
|
+
const entry = item as Record<string, unknown>;
|
|
663
|
+
const step = typeof entry.step === 'string' ? entry.step : '';
|
|
664
|
+
const status = typeof entry.status === 'string' ? entry.status : 'pending';
|
|
665
|
+
if (!step.trim()) return null;
|
|
666
|
+
return { step: step.trim(), status };
|
|
667
|
+
})
|
|
668
|
+
.filter((item): item is { step: string; status: string } => !!item);
|
|
669
|
+
|
|
670
|
+
const inProgressItems = normalized.filter(item => item.status === 'in_progress');
|
|
671
|
+
const pendingItems = normalized.filter(item => item.status === 'pending');
|
|
672
|
+
const completedItems = normalized.filter(item => item.status === 'completed');
|
|
673
|
+
|
|
674
|
+
const sectionPrefix = ' ';
|
|
675
|
+
const itemPrefix = ' ';
|
|
676
|
+
const arrowPrefix = '> ';
|
|
677
|
+
|
|
678
|
+
const addSection = (label: string, items: Array<{ step: string; status: string }>, activeStep: string | null, marker: string) => {
|
|
679
|
+
if (items.length === 0) return;
|
|
680
|
+
lines.push(`${sectionPrefix}${label} (${items.length})`);
|
|
681
|
+
for (const item of items) {
|
|
682
|
+
const isActive = activeStep !== null && item.step === activeStep;
|
|
683
|
+
const prefix = isActive ? arrowPrefix : arrowPrefix;
|
|
684
|
+
lines.push(`${itemPrefix}${prefix}${marker} ${item.step}`);
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
addSection('In progress', inProgressItems, inProgressItems[0]?.step ?? null, '[~]');
|
|
689
|
+
addSection('Todo', pendingItems, null, '[ ]');
|
|
690
|
+
addSection('Completed', completedItems, null, '[✓]');
|
|
691
|
+
|
|
692
|
+
return lines.length > 0 ? lines : ['(no steps)'];
|
|
693
|
+
}
|
|
694
|
+
return ['(no steps)'];
|
|
695
|
+
}
|
|
696
|
+
|
|
321
697
|
default: {
|
|
698
|
+
if (toolName.startsWith('mcp__')) {
|
|
699
|
+
return formatMcpResultBody(result);
|
|
700
|
+
}
|
|
322
701
|
const toolResultText = formatToolResult(result);
|
|
323
702
|
if (!toolResultText) return [];
|
|
324
703
|
return toolResultText.split(/\r?\n/);
|
|
@@ -334,7 +713,8 @@ export function formatToolContent(
|
|
|
334
713
|
maxLines?: number;
|
|
335
714
|
}
|
|
336
715
|
): string {
|
|
337
|
-
const
|
|
716
|
+
const header = toolName === 'plan' ? formatPlanHeader(result) : formatToolHeader(toolName, args);
|
|
717
|
+
const lines: string[] = [header];
|
|
338
718
|
|
|
339
719
|
const argsLine = formatKnownToolArgs(toolName, args);
|
|
340
720
|
if (argsLine) lines.push(argsLine);
|
|
@@ -342,7 +722,7 @@ export function formatToolContent(
|
|
|
342
722
|
const bodyLines = formatToolBodyLines(toolName, args, result);
|
|
343
723
|
for (const line of bodyLines) lines.push(line);
|
|
344
724
|
|
|
345
|
-
const skipTruncate = toolName === 'write' || toolName === 'edit';
|
|
725
|
+
const skipTruncate = toolName === 'write' || toolName === 'edit' || toolName === 'plan';
|
|
346
726
|
if (skipTruncate) {
|
|
347
727
|
return lines.join('\n');
|
|
348
728
|
}
|