@kirosnn/mosaic 0.71.0 → 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 (75) hide show
  1. package/README.md +1 -5
  2. package/package.json +4 -2
  3. package/src/agent/Agent.ts +353 -131
  4. package/src/agent/context.ts +4 -4
  5. package/src/agent/prompts/systemPrompt.ts +15 -6
  6. package/src/agent/prompts/toolsPrompt.ts +75 -10
  7. package/src/agent/provider/anthropic.ts +100 -100
  8. package/src/agent/provider/google.ts +102 -102
  9. package/src/agent/provider/mistral.ts +95 -95
  10. package/src/agent/provider/ollama.ts +77 -60
  11. package/src/agent/provider/openai.ts +42 -38
  12. package/src/agent/provider/rateLimit.ts +178 -0
  13. package/src/agent/provider/xai.ts +99 -99
  14. package/src/agent/tools/definitions.ts +19 -9
  15. package/src/agent/tools/executor.ts +95 -85
  16. package/src/agent/tools/exploreExecutor.ts +8 -10
  17. package/src/agent/tools/grep.ts +30 -29
  18. package/src/agent/tools/question.ts +7 -1
  19. package/src/agent/types.ts +9 -8
  20. package/src/components/App.tsx +45 -45
  21. package/src/components/CustomInput.tsx +214 -36
  22. package/src/components/Main.tsx +1146 -954
  23. package/src/components/Setup.tsx +1 -1
  24. package/src/components/Welcome.tsx +1 -1
  25. package/src/components/main/ApprovalPanel.tsx +4 -3
  26. package/src/components/main/ChatPage.tsx +858 -675
  27. package/src/components/main/HomePage.tsx +53 -38
  28. package/src/components/main/QuestionPanel.tsx +52 -7
  29. package/src/components/main/ThinkingIndicator.tsx +2 -1
  30. package/src/index.tsx +50 -20
  31. package/src/mcp/approvalPolicy.ts +148 -0
  32. package/src/mcp/cli/add.ts +185 -0
  33. package/src/mcp/cli/doctor.ts +77 -0
  34. package/src/mcp/cli/index.ts +85 -0
  35. package/src/mcp/cli/list.ts +50 -0
  36. package/src/mcp/cli/logs.ts +24 -0
  37. package/src/mcp/cli/manage.ts +99 -0
  38. package/src/mcp/cli/show.ts +53 -0
  39. package/src/mcp/cli/tools.ts +77 -0
  40. package/src/mcp/config.ts +223 -0
  41. package/src/mcp/index.ts +80 -0
  42. package/src/mcp/processManager.ts +299 -0
  43. package/src/mcp/rateLimiter.ts +50 -0
  44. package/src/mcp/registry.ts +151 -0
  45. package/src/mcp/schemaConverter.ts +100 -0
  46. package/src/mcp/servers/navigation.ts +854 -0
  47. package/src/mcp/toolCatalog.ts +169 -0
  48. package/src/mcp/types.ts +95 -0
  49. package/src/utils/approvalBridge.ts +17 -5
  50. package/src/utils/commands/compact.ts +30 -0
  51. package/src/utils/commands/echo.ts +1 -1
  52. package/src/utils/commands/index.ts +4 -6
  53. package/src/utils/commands/new.ts +15 -0
  54. package/src/utils/commands/types.ts +3 -0
  55. package/src/utils/config.ts +3 -1
  56. package/src/utils/diffRendering.tsx +1 -3
  57. package/src/utils/exploreBridge.ts +10 -0
  58. package/src/utils/markdown.tsx +163 -99
  59. package/src/utils/models.ts +31 -9
  60. package/src/utils/questionBridge.ts +36 -1
  61. package/src/utils/tokenEstimator.ts +32 -0
  62. package/src/utils/toolFormatting.ts +268 -7
  63. package/src/web/app.tsx +72 -72
  64. package/src/web/components/HomePage.tsx +7 -7
  65. package/src/web/components/MessageItem.tsx +22 -22
  66. package/src/web/components/QuestionPanel.tsx +72 -12
  67. package/src/web/components/Sidebar.tsx +0 -2
  68. package/src/web/components/ThinkingIndicator.tsx +1 -0
  69. package/src/web/server.tsx +767 -683
  70. package/src/utils/commands/redo.ts +0 -74
  71. package/src/utils/commands/sessions.ts +0 -129
  72. package/src/utils/commands/undo.ts +0 -75
  73. package/src/utils/undoRedo.ts +0 -429
  74. package/src/utils/undoRedoBridge.ts +0 -45
  75. package/src/utils/undoRedoDb.ts +0 -338
@@ -60,7 +60,7 @@ IMPORTANT: Use "**/" prefix for recursive search:
60
60
  ### grep
61
61
  Search for text within files.
62
62
  - query (string, required): Text to search for
63
- - file_type (string, optional): ts, tsx, js, jsx, py, java, go, etc.
63
+ - file_type (string, optional): language or extension (ts, tsx, js, txt, .env)
64
64
  - pattern (string, optional): Glob pattern for files
65
65
  - regex (boolean, optional): Treat query as regex
66
66
  - context (number, optional): Lines around matches
@@ -84,8 +84,16 @@ Track progress on multi-step tasks.
84
84
  - step (string): Action description
85
85
  - status: "pending" | "in_progress" | "completed"
86
86
 
87
- Use plan for tasks with 3+ steps. Update as you progress.
88
- Always update the plan after each step is completed.
87
+ Use plan for any task that is not a single obvious step. Default to planning when unsure.
88
+ Use plan when there are 2+ actions, file changes, or unclear success criteria.
89
+ Plan rules:
90
+ - Keep plans short (3-6 steps) and outcome-focused
91
+ - Exactly one step can be "in_progress" at a time
92
+ - Mark a step "completed" before starting the next
93
+ - Keep unstarted steps "pending"
94
+ Always update the plan after each step.
95
+ Never output a plan as plain text, JSON, or tags. Use the plan tool call only.
96
+ Never mark multiple future steps as completed in a single update. Show progress incrementally as each step is done.
89
97
 
90
98
  ## Web Access
91
99
 
@@ -117,6 +125,11 @@ Ask user with predefined options. ONLY way to ask questions.
117
125
  - options (array, required): At least 2 options
118
126
  - label (string): Display text
119
127
  - value (string|null): Return value
128
+ - group (string, optional): Group header for consecutive options with the same group
129
+ - timeout (number, optional): Seconds before the question auto-rejects
130
+ - validation (object, optional): Regex validation for custom text input
131
+ - pattern (string): Regex pattern the custom text must match
132
+ - message (string, optional): Error message shown on validation failure
120
133
 
121
134
  CRITICAL: Never ask questions in plain text. Always use this tool.
122
135
 
@@ -210,12 +223,64 @@ NEVER ask questions in plain text. The question tool is MANDATORY.
210
223
 
211
224
  # Workflow Summary
212
225
 
213
- 1. COMMUNICATE: Say what you're about to do
214
- 2. READ: Always read files before modifying
215
- 3. ACT: Use the appropriate tool
216
- 4. VERIFY: Run tests/builds to confirm
217
- 5. REPORT: Summarize what was done`;
226
+ 1. PLAN: Use plan unless the task is trivial (single obvious action)
227
+ 2. COMMUNICATE: Say what you're about to do
228
+ 3. READ: Always read files before modifying
229
+ 4. ACT: Use the appropriate tool
230
+ 5. VERIFY: Run tests/builds to confirm
231
+ 6. REPORT: Summarize what was done`;
218
232
 
219
- export function getToolsPrompt(): string {
220
- return TOOLS_PROMPT;
233
+ export function getToolsPrompt(mcpToolInfos?: Array<{ serverId: string; name: string; description: string; inputSchema: Record<string, unknown>; canonicalId: string; safeId: string }>): string {
234
+ if (!mcpToolInfos || mcpToolInfos.length === 0) {
235
+ return TOOLS_PROMPT;
236
+ }
237
+
238
+ const mcpSection = buildMcpToolsSection(mcpToolInfos);
239
+ return TOOLS_PROMPT + '\n\n' + mcpSection;
240
+ }
241
+
242
+ function buildMcpToolsSection(tools: Array<{ serverId: string; name: string; description: string; inputSchema: Record<string, unknown>; canonicalId: string; safeId: string }>): string {
243
+ const lines: string[] = [];
244
+ lines.push('## External Tools (MCP)');
245
+ lines.push('');
246
+ lines.push('These tools are provided by external MCP servers. Call them by their tool name.');
247
+ lines.push('They may require approval before execution.');
248
+ lines.push('');
249
+
250
+ const byServer = new Map<string, typeof tools>();
251
+ for (const t of tools) {
252
+ const list = byServer.get(t.serverId) || [];
253
+ list.push(t);
254
+ byServer.set(t.serverId, list);
255
+ }
256
+
257
+ for (const [serverId, serverTools] of byServer) {
258
+ lines.push(`### Server: ${serverId}`);
259
+ lines.push('');
260
+ for (const t of serverTools) {
261
+ lines.push(`#### ${t.safeId}`);
262
+ if (t.description) {
263
+ lines.push(t.description);
264
+ }
265
+ const schema = t.inputSchema;
266
+ if (schema && typeof schema === 'object' && schema.properties) {
267
+ const props = schema.properties as Record<string, Record<string, unknown>>;
268
+ const required = (schema.required || []) as string[];
269
+ const paramLines: string[] = [];
270
+ for (const [key, propSchema] of Object.entries(props)) {
271
+ const type = propSchema.type || 'unknown';
272
+ const desc = propSchema.description || '';
273
+ const req = required.includes(key) ? 'required' : 'optional';
274
+ paramLines.push(`- ${key} (${type}, ${req})${desc ? ': ' + desc : ''}`);
275
+ }
276
+ if (paramLines.length > 0) {
277
+ lines.push('Parameters:');
278
+ lines.push(...paramLines);
279
+ }
280
+ }
281
+ lines.push('');
282
+ }
283
+ }
284
+
285
+ return lines.join('\n');
221
286
  }
@@ -2,6 +2,7 @@ import { streamText, CoreMessage } from 'ai';
2
2
  import { createAnthropic } from '@ai-sdk/anthropic';
3
3
  import { AgentEvent, Provider, ProviderConfig, ProviderSendOptions } from '../types';
4
4
  import { shouldEnableReasoning } from './reasoning';
5
+ import { getRetryDecision, normalizeError, runWithRetry } from './rateLimit';
5
6
 
6
7
  export class AnthropicProvider implements Provider {
7
8
  async *sendMessage(
@@ -17,107 +18,106 @@ export class AnthropicProvider implements Provider {
17
18
  apiKey: cleanApiKey,
18
19
  });
19
20
 
20
- const result = streamText({
21
- model: anthropic(cleanModel),
22
- messages: messages,
23
- system: config.systemPrompt,
24
- tools: config.tools,
25
- maxSteps: config.maxSteps || 10,
26
- abortSignal: options?.abortSignal,
27
- experimental_providerMetadata: reasoningEnabled
28
- ? {
29
- anthropic: {
30
- thinkingBudgetTokens: 10000,
31
- },
21
+ try {
22
+ let stepCounter = 0;
23
+
24
+ yield* runWithRetry(async function* () {
25
+ const result = streamText({
26
+ model: anthropic(cleanModel),
27
+ messages: messages,
28
+ system: config.systemPrompt,
29
+ tools: config.tools,
30
+ maxSteps: config.maxSteps || 100,
31
+ abortSignal: options?.abortSignal,
32
+ experimental_providerMetadata: reasoningEnabled
33
+ ? {
34
+ anthropic: {
35
+ thinkingBudgetTokens: 10000,
36
+ },
37
+ }
38
+ : undefined,
39
+ });
40
+
41
+ for await (const chunk of result.fullStream as any) {
42
+ const c: any = chunk;
43
+ switch (c.type) {
44
+ case 'reasoning':
45
+ if (c.textDelta) {
46
+ yield {
47
+ type: 'reasoning-delta',
48
+ content: c.textDelta,
49
+ };
50
+ }
51
+ break;
52
+
53
+ case 'text-delta':
54
+ yield {
55
+ type: 'text-delta',
56
+ content: c.textDelta,
57
+ };
58
+ break;
59
+
60
+ case 'step-start':
61
+ yield {
62
+ type: 'step-start',
63
+ stepNumber: typeof c.stepIndex === 'number' ? c.stepIndex : stepCounter,
64
+ };
65
+ stepCounter++;
66
+ break;
67
+
68
+ case 'step-finish':
69
+ yield {
70
+ type: 'step-finish',
71
+ stepNumber:
72
+ typeof c.stepIndex === 'number' ? c.stepIndex : Math.max(0, stepCounter - 1),
73
+ finishReason: String(c.finishReason ?? 'stop'),
74
+ };
75
+ break;
76
+
77
+ case 'tool-call':
78
+ yield {
79
+ type: 'tool-call-end',
80
+ toolCallId: String(c.toolCallId ?? ''),
81
+ toolName: String(c.toolName ?? ''),
82
+ args: (c.args ?? {}) as Record<string, unknown>,
83
+ };
84
+ break;
85
+
86
+ case 'tool-result':
87
+ yield {
88
+ type: 'tool-result',
89
+ toolCallId: String(c.toolCallId ?? ''),
90
+ toolName: String(c.toolName ?? ''),
91
+ result: c.result,
92
+ };
93
+ break;
94
+
95
+ case 'finish':
96
+ yield {
97
+ type: 'finish',
98
+ finishReason: String(c.finishReason ?? 'stop'),
99
+ usage: c.usage,
100
+ };
101
+ break;
102
+
103
+ case 'error': {
104
+ const err = normalizeError(c.error);
105
+ const decision = getRetryDecision(err);
106
+ if (decision.shouldRetry) {
107
+ throw err;
108
+ }
109
+ yield {
110
+ type: 'error',
111
+ error: err.message,
112
+ };
113
+ break;
114
+ }
115
+ }
32
116
  }
33
- : undefined,
34
- });
35
-
36
- try {
37
- let stepCounter = 0;
38
-
39
- for await (const chunk of result.fullStream as any) {
40
- const c: any = chunk;
41
- switch (c.type) {
42
- case 'reasoning':
43
- if (c.textDelta) {
44
- yield {
45
- type: 'reasoning-delta',
46
- content: c.textDelta,
47
- };
48
- }
49
- break;
50
-
51
- case 'text-delta':
52
- yield {
53
- type: 'text-delta',
54
- content: c.textDelta,
55
- };
56
- break;
57
-
58
- case 'step-start':
59
- yield {
60
- type: 'step-start',
61
- stepNumber: typeof c.stepIndex === 'number' ? c.stepIndex : stepCounter,
62
- };
63
- stepCounter++;
64
- break;
65
-
66
- case 'step-finish':
67
- yield {
68
- type: 'step-finish',
69
- stepNumber:
70
- typeof c.stepIndex === 'number' ? c.stepIndex : Math.max(0, stepCounter - 1),
71
- finishReason: String(c.finishReason ?? 'stop'),
72
- };
73
- break;
74
-
75
- case 'tool-call':
76
- yield {
77
- type: 'tool-call-end',
78
- toolCallId: String(c.toolCallId ?? ''),
79
- toolName: String(c.toolName ?? ''),
80
- args: (c.args ?? {}) as Record<string, unknown>,
81
- };
82
- break;
83
-
84
- case 'tool-result':
85
- yield {
86
- type: 'tool-result',
87
- toolCallId: String(c.toolCallId ?? ''),
88
- toolName: String(c.toolName ?? ''),
89
- result: c.result,
90
- };
91
- break;
92
-
93
- case 'finish':
94
- yield {
95
- type: 'finish',
96
- finishReason: String(c.finishReason ?? 'stop'),
97
- usage: c.usage,
98
- };
99
- break;
100
-
101
- case 'error':
102
- {
103
- const err = c.error;
104
- const msg =
105
- err instanceof Error
106
- ? err.message
107
- : typeof err === 'string'
108
- ? err
109
- : 'Unknown error';
110
- yield {
111
- type: 'error',
112
- error: msg,
113
- };
114
- }
115
- break;
116
- }
117
- }
118
- } catch (error) {
119
- if (options?.abortSignal?.aborted) return;
120
- yield {
117
+ }, { abortSignal: options?.abortSignal });
118
+ } catch (error) {
119
+ if (options?.abortSignal?.aborted) return;
120
+ yield {
121
121
  type: 'error',
122
122
  error: error instanceof Error ? error.message : 'Unknown error occurred',
123
123
  };
@@ -2,6 +2,7 @@ import { streamText, CoreMessage } from 'ai';
2
2
  import { createGoogleGenerativeAI } from '@ai-sdk/google';
3
3
  import { AgentEvent, Provider, ProviderConfig, ProviderSendOptions } from '../types';
4
4
  import { shouldEnableReasoning } from './reasoning';
5
+ import { getRetryDecision, normalizeError, runWithRetry } from './rateLimit';
5
6
 
6
7
  export class GoogleProvider implements Provider {
7
8
  async *sendMessage(
@@ -17,109 +18,108 @@ export class GoogleProvider implements Provider {
17
18
  apiKey: cleanApiKey,
18
19
  });
19
20
 
20
- const result = streamText({
21
- model: google(cleanModel),
22
- messages: messages,
23
- system: config.systemPrompt,
24
- tools: config.tools,
25
- maxSteps: config.maxSteps || 10,
26
- abortSignal: options?.abortSignal,
27
- providerOptions: reasoningEnabled
28
- ? {
29
- google: {
30
- thinkingConfig: {
31
- style: 'THINKING_STYLE_DETAILED',
32
- },
33
- },
21
+ try {
22
+ let stepCounter = 0;
23
+
24
+ yield* runWithRetry(async function* () {
25
+ const result = streamText({
26
+ model: google(cleanModel),
27
+ messages: messages,
28
+ system: config.systemPrompt,
29
+ tools: config.tools,
30
+ maxSteps: config.maxSteps || 100,
31
+ abortSignal: options?.abortSignal,
32
+ providerOptions: reasoningEnabled
33
+ ? {
34
+ google: {
35
+ thinkingConfig: {
36
+ style: 'THINKING_STYLE_DETAILED',
37
+ },
38
+ },
39
+ }
40
+ : undefined,
41
+ });
42
+
43
+ for await (const chunk of result.fullStream as any) {
44
+ const c: any = chunk;
45
+ switch (c.type) {
46
+ case 'reasoning':
47
+ if (c.textDelta) {
48
+ yield {
49
+ type: 'reasoning-delta',
50
+ content: c.textDelta,
51
+ };
52
+ }
53
+ break;
54
+
55
+ case 'text-delta':
56
+ yield {
57
+ type: 'text-delta',
58
+ content: c.textDelta,
59
+ };
60
+ break;
61
+
62
+ case 'step-start':
63
+ yield {
64
+ type: 'step-start',
65
+ stepNumber: typeof c.stepIndex === 'number' ? c.stepIndex : stepCounter,
66
+ };
67
+ stepCounter++;
68
+ break;
69
+
70
+ case 'step-finish':
71
+ yield {
72
+ type: 'step-finish',
73
+ stepNumber:
74
+ typeof c.stepIndex === 'number' ? c.stepIndex : Math.max(0, stepCounter - 1),
75
+ finishReason: String(c.finishReason ?? 'stop'),
76
+ };
77
+ break;
78
+
79
+ case 'tool-call':
80
+ yield {
81
+ type: 'tool-call-end',
82
+ toolCallId: String(c.toolCallId ?? ''),
83
+ toolName: String(c.toolName ?? ''),
84
+ args: (c.args ?? {}) as Record<string, unknown>,
85
+ };
86
+ break;
87
+
88
+ case 'tool-result':
89
+ yield {
90
+ type: 'tool-result',
91
+ toolCallId: String(c.toolCallId ?? ''),
92
+ toolName: String(c.toolName ?? ''),
93
+ result: c.result,
94
+ };
95
+ break;
96
+
97
+ case 'finish':
98
+ yield {
99
+ type: 'finish',
100
+ finishReason: String(c.finishReason ?? 'stop'),
101
+ usage: c.usage,
102
+ };
103
+ break;
104
+
105
+ case 'error': {
106
+ const err = normalizeError(c.error);
107
+ const decision = getRetryDecision(err);
108
+ if (decision.shouldRetry) {
109
+ throw err;
110
+ }
111
+ yield {
112
+ type: 'error',
113
+ error: err.message,
114
+ };
115
+ break;
116
+ }
117
+ }
34
118
  }
35
- : undefined,
36
- });
37
-
38
- try {
39
- let stepCounter = 0;
40
-
41
- for await (const chunk of result.fullStream as any) {
42
- const c: any = chunk;
43
- switch (c.type) {
44
- case 'reasoning':
45
- if (c.textDelta) {
46
- yield {
47
- type: 'reasoning-delta',
48
- content: c.textDelta,
49
- };
50
- }
51
- break;
52
-
53
- case 'text-delta':
54
- yield {
55
- type: 'text-delta',
56
- content: c.textDelta,
57
- };
58
- break;
59
-
60
- case 'step-start':
61
- yield {
62
- type: 'step-start',
63
- stepNumber: typeof c.stepIndex === 'number' ? c.stepIndex : stepCounter,
64
- };
65
- stepCounter++;
66
- break;
67
-
68
- case 'step-finish':
69
- yield {
70
- type: 'step-finish',
71
- stepNumber:
72
- typeof c.stepIndex === 'number' ? c.stepIndex : Math.max(0, stepCounter - 1),
73
- finishReason: String(c.finishReason ?? 'stop'),
74
- };
75
- break;
76
-
77
- case 'tool-call':
78
- yield {
79
- type: 'tool-call-end',
80
- toolCallId: String(c.toolCallId ?? ''),
81
- toolName: String(c.toolName ?? ''),
82
- args: (c.args ?? {}) as Record<string, unknown>,
83
- };
84
- break;
85
-
86
- case 'tool-result':
87
- yield {
88
- type: 'tool-result',
89
- toolCallId: String(c.toolCallId ?? ''),
90
- toolName: String(c.toolName ?? ''),
91
- result: c.result,
92
- };
93
- break;
94
-
95
- case 'finish':
96
- yield {
97
- type: 'finish',
98
- finishReason: String(c.finishReason ?? 'stop'),
99
- usage: c.usage,
100
- };
101
- break;
102
-
103
- case 'error':
104
- {
105
- const err = c.error;
106
- const msg =
107
- err instanceof Error
108
- ? err.message
109
- : typeof err === 'string'
110
- ? err
111
- : 'Unknown error';
112
- yield {
113
- type: 'error',
114
- error: msg,
115
- };
116
- }
117
- break;
118
- }
119
- }
120
- } catch (error) {
121
- if (options?.abortSignal?.aborted) return;
122
- yield {
119
+ }, { abortSignal: options?.abortSignal });
120
+ } catch (error) {
121
+ if (options?.abortSignal?.aborted) return;
122
+ yield {
123
123
  type: 'error',
124
124
  error: error instanceof Error ? error.message : 'Unknown error occurred',
125
125
  };