@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
@@ -121,112 +121,176 @@ export function parseMarkdownContent(content: string): ParsedMarkdownLine[] {
121
121
  return result;
122
122
  }
123
123
 
124
- export function wrapMarkdownText(text: string, maxWidth: number): { text: string; segments: MarkdownSegment[] }[] {
125
- if (!text || maxWidth <= 0) return [{ text: '', segments: [] }];
126
-
127
- const segments = parseMarkdownLine(text);
128
- const lines: { text: string; segments: MarkdownSegment[] }[] = [];
129
- let currentLine = '';
130
- let currentSegments: MarkdownSegment[] = [];
131
-
132
- for (const segment of segments) {
133
- const content = segment.content;
134
- const fullText = content;
135
-
136
- if (!currentLine) {
137
- if (fullText.length <= maxWidth) {
138
- currentLine = fullText;
139
- currentSegments.push(segment);
140
- } else {
141
- let remaining = content;
142
- while (remaining) {
143
- if (remaining.length <= maxWidth) {
144
- currentLine = remaining;
145
- currentSegments.push({ ...segment, content: remaining });
146
- remaining = '';
147
- } else {
148
- const breakPoint = remaining.lastIndexOf(' ', maxWidth);
149
- if (breakPoint > 0) {
150
- const chunk = remaining.slice(0, breakPoint);
151
- currentLine = chunk;
152
- currentSegments.push({ ...segment, content: chunk });
153
- lines.push({ text: currentLine, segments: currentSegments });
154
- currentLine = '';
155
- currentSegments = [];
156
- remaining = remaining.slice(breakPoint + 1);
157
- } else {
158
- const chunk = remaining.slice(0, maxWidth);
159
- currentLine = chunk;
160
- currentSegments.push({ ...segment, content: chunk });
161
- lines.push({ text: currentLine, segments: currentSegments });
162
- currentLine = '';
163
- currentSegments = [];
164
- remaining = remaining.slice(maxWidth);
165
- }
166
- }
167
- }
168
- }
124
+ export function wrapMarkdownText(text: string, maxWidth: number): { text: string; segments: MarkdownSegment[] }[] {
125
+ if (!text || maxWidth <= 0) return [{ text: '', segments: [] }];
126
+
127
+ const segments = parseMarkdownLine(text);
128
+ const lines: { text: string; segments: MarkdownSegment[] }[] = [];
129
+ let currentLine = '';
130
+ let currentSegments: MarkdownSegment[] = [];
131
+
132
+ const splitSegment = (segment: MarkdownSegment): MarkdownSegment[] => {
133
+ if (segment.type === 'code') return [segment];
134
+ const parts = segment.content.match(/\s+|[^\s]+/g);
135
+ if (!parts) return [segment];
136
+ return parts.map(part => ({ ...segment, content: part }));
137
+ };
138
+
139
+ const pushLine = () => {
140
+ if (!currentLine) return;
141
+ lines.push({ text: currentLine, segments: currentSegments });
142
+ currentLine = '';
143
+ currentSegments = [];
144
+ };
145
+
146
+ const addPiece = (piece: MarkdownSegment) => {
147
+ let remaining = piece.content;
148
+ while (remaining.length > 0) {
149
+ if (!currentLine) {
150
+ if (remaining.trim() === '') {
151
+ return;
152
+ }
153
+ if (remaining.length <= maxWidth) {
154
+ currentLine = remaining;
155
+ currentSegments = [{ ...piece, content: remaining }];
156
+ return;
157
+ }
158
+ const chunk = remaining.slice(0, maxWidth);
159
+ lines.push({ text: chunk, segments: [{ ...piece, content: chunk }] });
160
+ remaining = remaining.slice(maxWidth);
161
+ continue;
162
+ }
163
+
164
+ if ((currentLine + remaining).length <= maxWidth) {
165
+ currentLine += remaining;
166
+ currentSegments.push({ ...piece, content: remaining });
167
+ return;
168
+ }
169
+
170
+ pushLine();
171
+ }
172
+ };
173
+
174
+ for (const segment of segments) {
175
+ const pieces = splitSegment(segment);
176
+ for (const piece of pieces) {
177
+ addPiece(piece);
178
+ }
179
+ }
180
+
181
+ if (currentLine) {
182
+ lines.push({ text: currentLine, segments: currentSegments });
183
+ }
184
+
185
+ return lines.length > 0 ? lines : [{ text: '', segments: [] }];
186
+ }
187
+
188
+ export interface WrappedMarkdownBlock {
189
+ type: 'line' | 'code';
190
+ wrappedLines?: { text: string; segments: MarkdownSegment[] }[];
191
+ codeLines?: string[];
192
+ language?: string;
193
+ }
194
+
195
+ function wrapCodeLine(line: string, maxWidth: number): string[] {
196
+ if (!line) return [''];
197
+ if (maxWidth <= 0) return [line];
198
+ if (line.length <= maxWidth) return [line];
199
+
200
+ const chunks: string[] = [];
201
+ let i = 0;
202
+ while (i < line.length) {
203
+ chunks.push(line.slice(i, i + maxWidth));
204
+ i += maxWidth;
205
+ }
206
+ return chunks;
207
+ }
208
+
209
+ function reflowParagraphs(text: string): string {
210
+ const rawLines = text.split('\n');
211
+ const result: string[] = [];
212
+ let inCodeBlock = false;
213
+
214
+ for (let i = 0; i < rawLines.length; i++) {
215
+ const line = rawLines[i]!;
216
+
217
+ if (/^```/.test(line)) {
218
+ inCodeBlock = !inCodeBlock;
219
+ result.push(line);
220
+ continue;
221
+ }
222
+
223
+ if (inCodeBlock) {
224
+ result.push(line);
225
+ continue;
226
+ }
227
+
228
+ if (line.trim() === '') {
229
+ result.push(line);
230
+ continue;
231
+ }
232
+
233
+ if (/^#{1,6}\s/.test(line) || /^[-*+]\s/.test(line) || /^\d+\.\s/.test(line)) {
234
+ result.push(line);
235
+ continue;
236
+ }
237
+
238
+ const prev = result.length > 0 ? result[result.length - 1]! : '';
239
+ const prevIsText = prev.trim() !== '' &&
240
+ !/^```/.test(prev) &&
241
+ !/^#{1,6}\s/.test(prev) &&
242
+ !/^[-*+]\s/.test(prev) &&
243
+ !/^\d+\.\s/.test(prev);
244
+
245
+ if (prevIsText) {
246
+ result[result.length - 1] = prev + ' ' + line;
169
247
  } else {
170
- const needsSpace = !currentLine.endsWith(' ') && !fullText.startsWith(' ');
171
- const separator = needsSpace ? ' ' : '';
248
+ result.push(line);
249
+ }
250
+ }
172
251
 
173
- if ((currentLine + separator + fullText).length <= maxWidth) {
174
- currentLine += separator + fullText;
175
- currentSegments.push(segment);
252
+ return result.join('\n');
253
+ }
254
+
255
+ export function parseAndWrapMarkdown(text: string, maxWidth: number): WrappedMarkdownBlock[] {
256
+ const lines = reflowParagraphs(text).split('\n');
257
+ const blocks: WrappedMarkdownBlock[] = [];
258
+ let inCodeBlock = false;
259
+ let codeLines: string[] = [];
260
+ let language: string | undefined;
261
+
262
+ for (const line of lines) {
263
+ const fenceMatch = line.match(/^```([A-Za-z0-9_-]+)?\s*$/);
264
+ if (fenceMatch) {
265
+ if (!inCodeBlock) {
266
+ inCodeBlock = true;
267
+ language = fenceMatch[1];
268
+ codeLines = [];
176
269
  } else {
177
- lines.push({ text: currentLine, segments: currentSegments });
178
- currentLine = fullText;
179
- currentSegments = [segment];
180
-
181
- if (fullText.length > maxWidth) {
182
- let remaining = content;
183
- while (remaining) {
184
- if (remaining.length <= maxWidth) {
185
- currentLine = remaining;
186
- currentSegments = [{ ...segment, content: remaining }];
187
- remaining = '';
188
- } else {
189
- const breakPoint = remaining.lastIndexOf(' ', maxWidth);
190
- if (breakPoint > 0) {
191
- const chunk = remaining.slice(0, breakPoint);
192
- currentLine = chunk;
193
- currentSegments = [{ ...segment, content: chunk }];
194
- lines.push({ text: currentLine, segments: currentSegments });
195
- currentLine = '';
196
- currentSegments = [];
197
- remaining = remaining.slice(breakPoint + 1);
198
- } else {
199
- const chunk = remaining.slice(0, maxWidth);
200
- currentLine = chunk;
201
- currentSegments = [{ ...segment, content: chunk }];
202
- lines.push({ text: currentLine, segments: currentSegments });
203
- currentLine = '';
204
- currentSegments = [];
205
- remaining = remaining.slice(maxWidth);
206
- }
207
- }
208
- }
209
- }
270
+ const wrapped = codeLines.flatMap(codeLine => wrapCodeLine(codeLine, maxWidth));
271
+ blocks.push({ type: 'code', codeLines: wrapped, language });
272
+ inCodeBlock = false;
273
+ codeLines = [];
274
+ language = undefined;
210
275
  }
276
+ continue;
277
+ }
278
+
279
+ if (inCodeBlock) {
280
+ codeLines.push(line);
281
+ continue;
211
282
  }
212
- }
213
283
 
214
- if (currentLine) {
215
- lines.push({ text: currentLine, segments: currentSegments });
284
+ blocks.push({
285
+ type: 'line',
286
+ wrappedLines: wrapMarkdownText(line, maxWidth)
287
+ });
216
288
  }
217
289
 
218
- return lines.length > 0 ? lines : [{ text: '', segments: [] }];
219
- }
290
+ if (inCodeBlock) {
291
+ const wrapped = codeLines.flatMap(codeLine => wrapCodeLine(codeLine, maxWidth));
292
+ blocks.push({ type: 'code', codeLines: wrapped, language });
293
+ }
220
294
 
221
- export interface WrappedMarkdownBlock {
222
- type: 'line';
223
- wrappedLines?: { text: string; segments: MarkdownSegment[] }[];
295
+ return blocks;
224
296
  }
225
-
226
- export function parseAndWrapMarkdown(text: string, maxWidth: number): WrappedMarkdownBlock[] {
227
- const lines = text.split('\n');
228
- return lines.map((line) => ({
229
- type: 'line',
230
- wrappedLines: wrapMarkdownText(line, maxWidth)
231
- }));
232
- }
@@ -286,12 +286,34 @@ export async function findModelsDevModelById(
286
286
  return modelsDev.getModelById(modelId, options);
287
287
  }
288
288
 
289
- export async function searchModelsDev(query: ModelsDevSearchQuery, options: { refresh?: boolean } = {}): Promise<ModelsDevSearchResult[]> {
290
- return modelsDev.search(query, options);
291
- }
292
-
293
- export function modelAcceptsImages(model: ModelsDevModel): boolean {
294
- if (!model.modalities) return false;
295
- const { input } = model.modalities;
296
- return Array.isArray(input) && input.includes("image");
297
- }
289
+ export async function searchModelsDev(query: ModelsDevSearchQuery, options: { refresh?: boolean } = {}): Promise<ModelsDevSearchResult[]> {
290
+ return modelsDev.search(query, options);
291
+ }
292
+
293
+ export async function getModelsDevContextLimit(
294
+ providerId: string,
295
+ modelId: string,
296
+ options: { refresh?: boolean } = {}
297
+ ): Promise<number | null> {
298
+ try {
299
+ const direct = await getModelsDevModel(providerId, modelId, options);
300
+ const limit = direct?.limit?.context;
301
+ if (typeof limit === "number" && Number.isFinite(limit) && limit > 0) return limit;
302
+ } catch {
303
+ }
304
+
305
+ try {
306
+ const byId = await findModelsDevModelById(modelId, options);
307
+ const limit = byId?.model?.limit?.context;
308
+ if (typeof limit === "number" && Number.isFinite(limit) && limit > 0) return limit;
309
+ } catch {
310
+ }
311
+
312
+ return null;
313
+ }
314
+
315
+ export function modelAcceptsImages(model: ModelsDevModel): boolean {
316
+ if (!model.modalities) return false;
317
+ const { input } = model.modalities;
318
+ return Array.isArray(input) && input.includes("image");
319
+ }
@@ -1,12 +1,15 @@
1
1
  export interface QuestionOption {
2
2
  label: string;
3
3
  value?: string | null;
4
+ group?: string;
4
5
  }
5
6
 
6
7
  export interface QuestionRequest {
7
8
  id: string;
8
9
  prompt: string;
9
10
  options: QuestionOption[];
11
+ timeout?: number;
12
+ validation?: { pattern: string; message?: string };
10
13
  }
11
14
 
12
15
  export interface QuestionAnswer {
@@ -23,6 +26,7 @@ let currentRequest: QuestionRequest | null = null;
23
26
  let listeners = new Set<QuestionListener>();
24
27
  let pendingResolve: ((answer: QuestionAnswer) => void) | null = null;
25
28
  let pendingReject: ((reason?: any) => void) | null = null;
29
+ let pendingTimeoutId: ReturnType<typeof setTimeout> | null = null;
26
30
 
27
31
  function notify(): void {
28
32
  for (const listener of listeners) {
@@ -46,7 +50,12 @@ export function getCurrentQuestion(): QuestionRequest | null {
46
50
  return currentRequest;
47
51
  }
48
52
 
49
- export async function askQuestion(prompt: string, options: QuestionOption[]): Promise<QuestionAnswer> {
53
+ export async function askQuestion(
54
+ prompt: string,
55
+ options: QuestionOption[],
56
+ timeout?: number,
57
+ validation?: { pattern: string; message?: string },
58
+ ): Promise<QuestionAnswer> {
50
59
  if (pendingResolve) {
51
60
  throw new Error('A question is already pending');
52
61
  }
@@ -63,6 +72,8 @@ export async function askQuestion(prompt: string, options: QuestionOption[]): Pr
63
72
  id: createId(),
64
73
  prompt,
65
74
  options,
75
+ ...(timeout !== undefined && { timeout }),
76
+ ...(validation !== undefined && { validation }),
66
77
  };
67
78
 
68
79
  currentRequest = request;
@@ -71,6 +82,20 @@ export async function askQuestion(prompt: string, options: QuestionOption[]): Pr
71
82
  const answer = await new Promise<QuestionAnswer>((resolve, reject) => {
72
83
  pendingResolve = resolve;
73
84
  pendingReject = reject;
85
+
86
+ if (timeout !== undefined && timeout > 0) {
87
+ pendingTimeoutId = setTimeout(() => {
88
+ pendingTimeoutId = null;
89
+ if (pendingReject) {
90
+ const rej = pendingReject;
91
+ pendingResolve = null;
92
+ pendingReject = null;
93
+ currentRequest = null;
94
+ notify();
95
+ rej(new Error(`Question timed out after ${timeout}s`));
96
+ }
97
+ }, timeout * 1000);
98
+ }
74
99
  });
75
100
 
76
101
  return answer;
@@ -101,6 +126,11 @@ export function answerQuestion(index: number, customText?: string): void {
101
126
 
102
127
  if (!answer) return;
103
128
 
129
+ if (pendingTimeoutId !== null) {
130
+ clearTimeout(pendingTimeoutId);
131
+ pendingTimeoutId = null;
132
+ }
133
+
104
134
  const resolve = pendingResolve;
105
135
  pendingResolve = null;
106
136
  pendingReject = null;
@@ -112,6 +142,11 @@ export function answerQuestion(index: number, customText?: string): void {
112
142
  export function cancelQuestion(): void {
113
143
  if (!currentRequest || !pendingReject) return;
114
144
 
145
+ if (pendingTimeoutId !== null) {
146
+ clearTimeout(pendingTimeoutId);
147
+ pendingTimeoutId = null;
148
+ }
149
+
115
150
  const reject = pendingReject;
116
151
  pendingResolve = null;
117
152
  pendingReject = null;
@@ -0,0 +1,32 @@
1
+ const CODE_CHARS = /[{}()\[\]=<>:;]/g;
2
+ const CODE_DENSITY_THRESHOLD = 0.04;
3
+
4
+ export function estimateTokensFromText(text: string): number {
5
+ if (!text) return 0;
6
+ const matches = text.match(CODE_CHARS);
7
+ const density = matches ? matches.length / text.length : 0;
8
+ const ratio = density >= CODE_DENSITY_THRESHOLD ? 2.8 : 3.3;
9
+ return Math.ceil(text.length / ratio);
10
+ }
11
+
12
+ export function estimateTokensForContent(content: string, thinkingContent?: string): number {
13
+ const contentTokens = estimateTokensFromText(content);
14
+ const thinkingTokens = thinkingContent ? estimateTokensFromText(thinkingContent) : 0;
15
+ return contentTokens + thinkingTokens + 4;
16
+ }
17
+
18
+ const CONTEXT_BUDGETS: Record<string, number> = {
19
+ anthropic: 180000,
20
+ openai: 115000,
21
+ google: 900000,
22
+ mistral: 28000,
23
+ xai: 117000,
24
+ ollama: 7000,
25
+ };
26
+
27
+ const DEFAULT_BUDGET = 12000;
28
+
29
+ export function getDefaultContextBudget(provider?: string): number {
30
+ if (!provider) return DEFAULT_BUDGET;
31
+ return CONTEXT_BUDGETS[provider] ?? DEFAULT_BUDGET;
32
+ }