@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.
Files changed (99) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -6
  3. package/package.json +55 -48
  4. package/src/agent/Agent.ts +353 -131
  5. package/src/agent/context.ts +4 -4
  6. package/src/agent/prompts/systemPrompt.ts +209 -70
  7. package/src/agent/prompts/toolsPrompt.ts +285 -138
  8. package/src/agent/provider/anthropic.ts +109 -105
  9. package/src/agent/provider/google.ts +111 -107
  10. package/src/agent/provider/mistral.ts +95 -95
  11. package/src/agent/provider/ollama.ts +73 -17
  12. package/src/agent/provider/openai.ts +146 -102
  13. package/src/agent/provider/rateLimit.ts +178 -0
  14. package/src/agent/provider/reasoning.ts +29 -0
  15. package/src/agent/provider/xai.ts +108 -104
  16. package/src/agent/tools/definitions.ts +15 -1
  17. package/src/agent/tools/executor.ts +717 -98
  18. package/src/agent/tools/exploreExecutor.ts +20 -22
  19. package/src/agent/tools/fetch.ts +58 -0
  20. package/src/agent/tools/glob.ts +20 -4
  21. package/src/agent/tools/grep.ts +64 -9
  22. package/src/agent/tools/plan.ts +27 -0
  23. package/src/agent/tools/question.ts +7 -1
  24. package/src/agent/tools/read.ts +2 -0
  25. package/src/agent/types.ts +15 -14
  26. package/src/components/App.tsx +50 -8
  27. package/src/components/CustomInput.tsx +461 -77
  28. package/src/components/Main.tsx +1459 -1112
  29. package/src/components/Setup.tsx +1 -1
  30. package/src/components/ShortcutsModal.tsx +11 -8
  31. package/src/components/Welcome.tsx +1 -1
  32. package/src/components/main/ApprovalPanel.tsx +4 -3
  33. package/src/components/main/ChatPage.tsx +858 -516
  34. package/src/components/main/HomePage.tsx +58 -39
  35. package/src/components/main/QuestionPanel.tsx +52 -7
  36. package/src/components/main/ThinkingIndicator.tsx +13 -2
  37. package/src/components/main/types.ts +11 -10
  38. package/src/index.tsx +53 -25
  39. package/src/mcp/approvalPolicy.ts +148 -0
  40. package/src/mcp/cli/add.ts +185 -0
  41. package/src/mcp/cli/doctor.ts +77 -0
  42. package/src/mcp/cli/index.ts +85 -0
  43. package/src/mcp/cli/list.ts +50 -0
  44. package/src/mcp/cli/logs.ts +24 -0
  45. package/src/mcp/cli/manage.ts +99 -0
  46. package/src/mcp/cli/show.ts +53 -0
  47. package/src/mcp/cli/tools.ts +77 -0
  48. package/src/mcp/config.ts +223 -0
  49. package/src/mcp/index.ts +80 -0
  50. package/src/mcp/processManager.ts +299 -0
  51. package/src/mcp/rateLimiter.ts +50 -0
  52. package/src/mcp/registry.ts +151 -0
  53. package/src/mcp/schemaConverter.ts +100 -0
  54. package/src/mcp/servers/navigation.ts +854 -0
  55. package/src/mcp/toolCatalog.ts +169 -0
  56. package/src/mcp/types.ts +95 -0
  57. package/src/utils/approvalBridge.ts +45 -12
  58. package/src/utils/approvalModeBridge.ts +17 -0
  59. package/src/utils/commands/approvals.ts +48 -0
  60. package/src/utils/commands/compact.ts +30 -0
  61. package/src/utils/commands/echo.ts +1 -1
  62. package/src/utils/commands/image.ts +109 -0
  63. package/src/utils/commands/index.ts +9 -7
  64. package/src/utils/commands/new.ts +15 -0
  65. package/src/utils/commands/types.ts +3 -0
  66. package/src/utils/config.ts +3 -1
  67. package/src/utils/diffRendering.tsx +13 -16
  68. package/src/utils/exploreBridge.ts +10 -0
  69. package/src/utils/history.ts +82 -40
  70. package/src/utils/imageBridge.ts +28 -0
  71. package/src/utils/images.ts +31 -0
  72. package/src/utils/markdown.tsx +163 -99
  73. package/src/utils/models.ts +31 -16
  74. package/src/utils/notificationBridge.ts +23 -0
  75. package/src/utils/questionBridge.ts +36 -1
  76. package/src/utils/tokenEstimator.ts +32 -0
  77. package/src/utils/toolFormatting.ts +428 -48
  78. package/src/web/app.tsx +65 -5
  79. package/src/web/assets/css/ChatPage.css +102 -30
  80. package/src/web/assets/css/MessageItem.css +26 -29
  81. package/src/web/assets/css/ThinkingIndicator.css +44 -6
  82. package/src/web/assets/css/ToolMessage.css +36 -14
  83. package/src/web/components/ChatPage.tsx +228 -105
  84. package/src/web/components/HomePage.tsx +3 -3
  85. package/src/web/components/MessageItem.tsx +80 -81
  86. package/src/web/components/QuestionPanel.tsx +72 -12
  87. package/src/web/components/Setup.tsx +1 -1
  88. package/src/web/components/Sidebar.tsx +1 -3
  89. package/src/web/components/ThinkingIndicator.tsx +41 -21
  90. package/src/web/router.ts +1 -1
  91. package/src/web/server.tsx +894 -662
  92. package/src/web/storage.ts +23 -1
  93. package/src/web/types.ts +7 -6
  94. package/src/utils/commands/redo.ts +0 -74
  95. package/src/utils/commands/sessions.ts +0 -129
  96. package/src/utils/commands/undo.ts +0 -75
  97. package/src/utils/undoRedo.ts +0 -429
  98. package/src/utils/undoRedoBridge.ts +0 -45
  99. 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
- case 'create_directory':
100
- return path ? `${displayName} (${path})` : displayName;
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 pattern = typeof args.file_pattern === 'string' ? args.file_pattern : '';
107
- return pattern ? `${displayName} (pattern: ${pattern})` : displayName;
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
- return purpose ? `${displayName} (${purpose})` : displayName;
196
+ const cleanPurpose = purpose.replace(/[\r\n]+/g, ' ').trim();
197
+ return cleanPurpose ? `${displayName} (${cleanPurpose})` : displayName;
117
198
  }
118
- default:
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 pattern = typeof args.file_pattern === 'string' ? args.file_pattern : '';
140
- return { name: displayName, info: pattern ? `pattern: ${pattern}` : null };
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
- default:
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) as Array<{ name?: string; path?: string; type?: string }>;
165
- if (!Array.isArray(parsed)) return [];
308
+ const parsed = JSON.parse(result);
166
309
 
167
- const entries = parsed
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
- return [...dirs, ...files];
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 (Array.isArray(parsed)) {
196
- if (parsed.length === 0) return ['No results'];
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 [result];
368
+ return ['No results returned'];
221
369
  } catch {
222
- return typeof result === 'string' ? [result] : [];
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) return [`Tool error: ${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 lines: string[] = [formatToolHeader(toolName, args)];
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
  }