@kirosnn/mosaic 0.71.0 → 0.74.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 (79) 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 +136 -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 +552 -339
  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 +156 -0
  32. package/src/mcp/cli/add.ts +185 -0
  33. package/src/mcp/cli/doctor.ts +74 -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 +234 -0
  41. package/src/mcp/index.ts +80 -0
  42. package/src/mcp/processManager.ts +304 -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/browser.ts +151 -0
  47. package/src/mcp/servers/navigation/index.ts +23 -0
  48. package/src/mcp/servers/navigation/tools.ts +263 -0
  49. package/src/mcp/servers/navigation/types.ts +17 -0
  50. package/src/mcp/servers/navigation/utils.ts +20 -0
  51. package/src/mcp/toolCatalog.ts +182 -0
  52. package/src/mcp/types.ts +116 -0
  53. package/src/utils/approvalBridge.ts +17 -5
  54. package/src/utils/commands/compact.ts +30 -0
  55. package/src/utils/commands/echo.ts +1 -1
  56. package/src/utils/commands/index.ts +4 -6
  57. package/src/utils/commands/new.ts +15 -0
  58. package/src/utils/commands/types.ts +3 -0
  59. package/src/utils/config.ts +3 -1
  60. package/src/utils/diffRendering.tsx +1 -3
  61. package/src/utils/exploreBridge.ts +10 -0
  62. package/src/utils/markdown.tsx +220 -122
  63. package/src/utils/models.ts +31 -9
  64. package/src/utils/questionBridge.ts +36 -1
  65. package/src/utils/tokenEstimator.ts +32 -0
  66. package/src/utils/toolFormatting.ts +317 -7
  67. package/src/web/app.tsx +72 -72
  68. package/src/web/components/HomePage.tsx +7 -7
  69. package/src/web/components/MessageItem.tsx +66 -35
  70. package/src/web/components/QuestionPanel.tsx +72 -12
  71. package/src/web/components/Sidebar.tsx +0 -2
  72. package/src/web/components/ThinkingIndicator.tsx +1 -0
  73. package/src/web/server.tsx +767 -683
  74. package/src/utils/commands/redo.ts +0 -74
  75. package/src/utils/commands/sessions.ts +0 -129
  76. package/src/utils/commands/undo.ts +0 -75
  77. package/src/utils/undoRedo.ts +0 -429
  78. package/src/utils/undoRedoBridge.ts +0 -45
  79. package/src/utils/undoRedoDb.ts +0 -338
@@ -1,12 +1,11 @@
1
- import { readFile, writeFile, readdir, appendFile, stat, mkdir } from 'fs/promises';
1
+ import { readFile, writeFile, readdir, appendFile, stat, mkdir, realpath } from 'fs/promises';
2
2
  import { join, resolve, dirname, extname, sep } from 'path';
3
3
  import { exec } from 'child_process';
4
4
  import { promisify } from 'util';
5
5
  import { requestApproval } from '../../utils/approvalBridge';
6
6
  import { shouldRequireApprovals } from '../../utils/config';
7
7
  import { generateDiff, formatDiffForDisplay } from '../../utils/diff';
8
- import { captureFileSnapshot } from '../../utils/undoRedo';
9
- import { trackFileChange, trackFileCreated, trackFileDeleted } from '../../utils/fileChangeTracker';
8
+ import { trackFileChange, trackFileCreated } from '../../utils/fileChangeTracker';
10
9
  import TurndownService from 'turndown';
11
10
  import { Readability } from '@mozilla/readability';
12
11
  import { parseHTML } from 'linkedom';
@@ -189,24 +188,23 @@ export interface ToolResult {
189
188
  diff?: string[];
190
189
  }
191
190
 
192
- const pathValidationCache = new Map<string, boolean>();
193
191
  const globPatternCache = new Map<string, RegExp>();
194
192
 
193
+ async function validatePath(fullPath: string, workspace: string): Promise<boolean> {
194
+ const normalizedWorkspace = workspace.endsWith(sep) ? workspace : workspace + sep;
195
195
 
196
- function validatePath(fullPath: string, workspace: string): boolean {
197
- const cacheKey = `${fullPath}|${workspace}`;
198
- const cached = pathValidationCache.get(cacheKey);
199
- if (cached !== undefined) return cached;
200
-
201
- const result = fullPath.startsWith(workspace);
202
- pathValidationCache.set(cacheKey, result);
203
-
204
- if (pathValidationCache.size > 1000) {
205
- const firstKey = pathValidationCache.keys().next().value;
206
- if (firstKey) pathValidationCache.delete(firstKey);
196
+ try {
197
+ const resolved = await realpath(fullPath);
198
+ return resolved === workspace || resolved.startsWith(normalizedWorkspace);
199
+ } catch {
200
+ const parent = dirname(fullPath);
201
+ try {
202
+ const resolvedParent = await realpath(parent);
203
+ return resolvedParent === workspace || resolvedParent.startsWith(normalizedWorkspace);
204
+ } catch {
205
+ return fullPath === workspace || fullPath.startsWith(normalizedWorkspace);
206
+ }
207
207
  }
208
-
209
- return result;
210
208
  }
211
209
 
212
210
  const EXCLUDED_DIRECTORIES = new Set([
@@ -284,15 +282,6 @@ interface SearchOptions {
284
282
 
285
283
  const DEFAULT_MAX_FILE_SIZE = 1024 * 1024;
286
284
 
287
- function isValidRegex(pattern: string): { valid: boolean; error?: string } {
288
- try {
289
- new RegExp(pattern);
290
- return { valid: true };
291
- } catch (e) {
292
- return { valid: false, error: e instanceof Error ? e.message : 'Invalid regular expression' };
293
- }
294
- }
295
-
296
285
  function isBinaryFile(buffer: Buffer, bytesToCheck = 8000): boolean {
297
286
  const checkLength = Math.min(buffer.length, bytesToCheck);
298
287
  let nullCount = 0;
@@ -465,12 +454,18 @@ interface WalkResult {
465
454
  excluded?: boolean;
466
455
  }
467
456
 
468
- async function walkDirectory(dir: string, filePattern?: string, includeHidden = false): Promise<WalkResult[]> {
457
+ interface WalkOutput {
458
+ results: WalkResult[];
459
+ errors: string[];
460
+ }
461
+
462
+ async function walkDirectory(dir: string, filePattern?: string, includeHidden = false): Promise<WalkOutput> {
469
463
  const results: WalkResult[] = [];
464
+ const errors: string[] = [];
470
465
 
471
466
  try {
472
467
  const entries = await readdir(dir, { withFileTypes: true });
473
- const subDirPromises: Promise<WalkResult[]>[] = [];
468
+ const subDirPromises: Promise<WalkOutput>[] = [];
474
469
 
475
470
  for (const entry of entries) {
476
471
  if (!includeHidden && entry.name.startsWith('.')) continue;
@@ -491,27 +486,31 @@ async function walkDirectory(dir: string, filePattern?: string, includeHidden =
491
486
  }
492
487
 
493
488
  if (subDirPromises.length > 0) {
494
- const subResults = await Promise.all(subDirPromises);
495
- for (const subResult of subResults) {
496
- results.push(...subResult);
489
+ const subOutputs = await Promise.all(subDirPromises);
490
+ for (const sub of subOutputs) {
491
+ results.push(...sub.results);
492
+ errors.push(...sub.errors);
497
493
  }
498
494
  }
499
- } catch {
500
- return results;
495
+ } catch (e) {
496
+ errors.push(`${dir}: ${e instanceof Error ? e.message : String(e)}`);
501
497
  }
502
498
 
503
- return results;
499
+ return { results, errors };
504
500
  }
505
501
 
506
- async function listFilesRecursive(dirPath: string, workspace: string, filterPattern?: string, includeHidden = false): Promise<WalkResult[]> {
502
+ async function listFilesRecursive(dirPath: string, workspace: string, filterPattern?: string, includeHidden = false): Promise<WalkOutput> {
507
503
  const fullPath = resolve(workspace, dirPath);
508
- const files = await walkDirectory(fullPath, filterPattern, includeHidden);
504
+ const { results, errors } = await walkDirectory(fullPath, filterPattern, includeHidden);
509
505
  const separator = workspace.endsWith(sep) ? '' : sep;
510
506
 
511
- return files.map(file => ({
512
- ...file,
513
- path: file.path.replace(workspace + separator, '')
514
- }));
507
+ return {
508
+ results: results.map(file => ({
509
+ ...file,
510
+ path: file.path.replace(workspace + separator, '')
511
+ })),
512
+ errors,
513
+ };
515
514
  }
516
515
 
517
516
  async function findFilesByPattern(pattern: string, searchPath: string): Promise<string[]> {
@@ -520,7 +519,7 @@ async function findFilesByPattern(pattern: string, searchPath: string): Promise<
520
519
  const hasDoubleStar = pattern.includes('**');
521
520
 
522
521
  if (hasDoubleStar) {
523
- const files = await walkDirectory(searchPath, undefined, false);
522
+ const { results: files } = await walkDirectory(searchPath, undefined, false);
524
523
  const separator = searchPath.endsWith(sep) ? '' : sep;
525
524
  const root = searchPath + separator;
526
525
 
@@ -723,7 +722,7 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
723
722
  const endLine = args.end_line as number | undefined;
724
723
  const fullPath = resolve(workspace, path);
725
724
 
726
- if (!validatePath(fullPath, workspace)) {
725
+ if (!await validatePath(fullPath, workspace)) {
727
726
  return {
728
727
  success: false,
729
728
  error: 'Access denied: path is outside workspace'
@@ -760,19 +759,17 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
760
759
  case 'write': {
761
760
  const path = args.path as string;
762
761
  let content = typeof args.content === 'string' ? args.content : '';
763
- if (content) content = content.trimEnd(); // Ensure no trailing empty lines
762
+ if (content) content = content.trimEnd();
764
763
  const append = args.append === true;
765
764
  const fullPath = resolve(workspace, path);
766
765
 
767
- if (!validatePath(fullPath, workspace)) {
766
+ if (!await validatePath(fullPath, workspace)) {
768
767
  return {
769
768
  success: false,
770
769
  error: 'Access denied: path is outside workspace'
771
770
  };
772
771
  }
773
772
 
774
- captureFileSnapshot(path);
775
-
776
773
  await mkdir(dirname(fullPath), { recursive: true });
777
774
 
778
775
  let oldContent = '';
@@ -817,7 +814,7 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
817
814
  const includeHidden = args.include_hidden === null ? undefined : (args.include_hidden as boolean | undefined);
818
815
  const fullPath = resolve(workspace, path);
819
816
 
820
- if (!validatePath(fullPath, workspace)) {
817
+ if (!await validatePath(fullPath, workspace)) {
821
818
  return {
822
819
  success: false,
823
820
  error: 'Access denied: path is outside workspace'
@@ -825,7 +822,7 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
825
822
  }
826
823
 
827
824
  if (recursive) {
828
- const files = await listFilesRecursive(path, workspace, filter, includeHidden);
825
+ const { results: files, errors: walkErrors } = await listFilesRecursive(path, workspace, filter, includeHidden);
829
826
  const fileStats = await Promise.all(
830
827
  files.map(async (file) => {
831
828
  if (file.excluded) {
@@ -836,17 +833,29 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
836
833
  };
837
834
  }
838
835
  const filePath = resolve(workspace, file.path);
839
- const stats = await stat(filePath);
840
- return {
841
- path: file.path,
842
- type: stats.isDirectory() ? 'directory' : 'file',
843
- size: stats.size,
844
- };
836
+ try {
837
+ const stats = await stat(filePath);
838
+ return {
839
+ path: file.path,
840
+ type: stats.isDirectory() ? 'directory' : 'file',
841
+ size: stats.size,
842
+ };
843
+ } catch {
844
+ return {
845
+ path: file.path,
846
+ type: 'unknown',
847
+ error: 'access denied',
848
+ };
849
+ }
845
850
  })
846
851
  );
852
+ const output: Record<string, unknown> = { files: fileStats };
853
+ if (walkErrors.length > 0) {
854
+ output.errors = walkErrors.slice(0, 10);
855
+ }
847
856
  return {
848
857
  success: true,
849
- result: JSON.stringify(fileStats, null, 2)
858
+ result: JSON.stringify(output, null, 2)
850
859
  };
851
860
  } else {
852
861
  const entries = await readdir(fullPath, { withFileTypes: true });
@@ -910,7 +919,7 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
910
919
  : `Command timed out after ${timeout}ms and produced no output.\n\n[Process may be running in background]`;
911
920
 
912
921
  return {
913
- success: true,
922
+ success: false,
914
923
  result: output
915
924
  };
916
925
  }
@@ -922,7 +931,7 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
922
931
  : `Command failed: ${errorMessage}`;
923
932
 
924
933
  return {
925
- success: true,
934
+ success: false,
926
935
  result: fullOutput
927
936
  };
928
937
  }
@@ -933,7 +942,7 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
933
942
  const searchPath = (args.path === null ? undefined : (args.path as string | undefined)) || '.';
934
943
  const fullPath = resolve(workspace, searchPath);
935
944
 
936
- if (!validatePath(fullPath, workspace)) {
945
+ if (!await validatePath(fullPath, workspace)) {
937
946
  return {
938
947
  success: false,
939
948
  error: 'Access denied: path is outside workspace'
@@ -971,7 +980,7 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
971
980
 
972
981
  const fullPath = resolve(workspace, searchPath);
973
982
 
974
- if (!validatePath(fullPath, workspace)) {
983
+ if (!await validatePath(fullPath, workspace)) {
975
984
  return {
976
985
  success: false,
977
986
  error: 'Access denied: path is outside workspace'
@@ -996,22 +1005,24 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
996
1005
  };
997
1006
  }
998
1007
 
1008
+ const normalizedFileType = typeof fileType === 'string' ? fileType.trim().toLowerCase() : undefined;
1009
+ const fileTypeParts = normalizedFileType
1010
+ ? normalizedFileType.split(',').map(p => p.trim()).filter(Boolean)
1011
+ : [];
1012
+ const resolvedExtensions = fileTypeParts.length > 0
1013
+ ? Array.from(new Set(fileTypeParts.flatMap((part) => {
1014
+ const mapped = FILE_TYPE_EXTENSIONS[part];
1015
+ if (mapped && mapped.length > 0) return mapped;
1016
+ if (part.startsWith('.')) return [part];
1017
+ return [`.${part}`];
1018
+ })))
1019
+ : undefined;
1020
+
999
1021
  let finalPattern: string;
1000
1022
  if (pattern) {
1001
1023
  finalPattern = pattern.includes('**') ? pattern : `**/${pattern}`;
1002
- } else if (fileType) {
1003
- const extensions = FILE_TYPE_EXTENSIONS[fileType.toLowerCase()];
1004
- if (!extensions) {
1005
- return {
1006
- success: false,
1007
- error: `Unknown file type: ${fileType}. Available types: ${Object.keys(FILE_TYPE_EXTENSIONS).join(', ')}`
1008
- };
1009
- }
1010
- if (extensions.length === 1) {
1011
- finalPattern = `**/*${extensions[0]}`;
1012
- } else {
1013
- finalPattern = '**/*';
1014
- }
1024
+ } else if (resolvedExtensions && resolvedExtensions.length === 1) {
1025
+ finalPattern = `**/*${resolvedExtensions[0]}`;
1015
1026
  } else {
1016
1027
  finalPattern = '**/*';
1017
1028
  }
@@ -1022,11 +1033,8 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
1022
1033
  allFiles = allFiles.filter(f => !f.split('/').some(part => part.startsWith('.')));
1023
1034
  }
1024
1035
 
1025
- if (fileType && !pattern) {
1026
- const extensions = FILE_TYPE_EXTENSIONS[fileType.toLowerCase()];
1027
- if (extensions) {
1028
- allFiles = allFiles.filter(f => extensions.some(ext => f.toLowerCase().endsWith(ext)));
1029
- }
1036
+ if (resolvedExtensions && !pattern) {
1037
+ allFiles = allFiles.filter(f => resolvedExtensions.some(ext => f.toLowerCase().endsWith(ext)));
1030
1038
  }
1031
1039
 
1032
1040
  if (excludePattern) {
@@ -1104,12 +1112,16 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
1104
1112
 
1105
1113
  let formattedResult: string;
1106
1114
 
1115
+ const skippedDetails = skippedFiles.length > 0
1116
+ ? skippedFiles.slice(0, 5).map(s => ({ file: s.file, reason: s.reason }))
1117
+ : undefined;
1118
+
1107
1119
  if (outputMode === 'files') {
1108
1120
  const filesOnly = results.map(r => r.file);
1109
1121
  const summary = {
1110
1122
  files_found: filesOnly.length,
1111
1123
  files: filesOnly,
1112
- ...(skippedFiles.length > 0 && { skipped: skippedFiles.length })
1124
+ ...(skippedFiles.length > 0 && { skipped: skippedFiles.length, skipped_details: skippedDetails })
1113
1125
  };
1114
1126
  formattedResult = JSON.stringify(summary, null, 2);
1115
1127
  } else if (outputMode === 'count') {
@@ -1118,7 +1130,7 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
1118
1130
  total_matches: totalMatchCount,
1119
1131
  files_with_matches: counts.length,
1120
1132
  counts,
1121
- ...(skippedFiles.length > 0 && { skipped: skippedFiles.length })
1133
+ ...(skippedFiles.length > 0 && { skipped: skippedFiles.length, skipped_details: skippedDetails })
1122
1134
  };
1123
1135
  formattedResult = JSON.stringify(summary, null, 2);
1124
1136
  } else {
@@ -1126,7 +1138,7 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
1126
1138
  total_matches: totalMatchCount,
1127
1139
  files_searched: allFiles.length,
1128
1140
  files_with_matches: results.length,
1129
- ...(skippedFiles.length > 0 && { skipped_files: skippedFiles.length }),
1141
+ ...(skippedFiles.length > 0 && { skipped_files: skippedFiles.length, skipped_details: skippedDetails }),
1130
1142
  ...(totalResults >= maxResults && { truncated: true, max_results: maxResults }),
1131
1143
  results: results.map(r => ({
1132
1144
  file: r.file,
@@ -1156,19 +1168,17 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
1156
1168
  const path = args.path as string;
1157
1169
  const oldContent = args.old_content as string;
1158
1170
  let newContent = args.new_content as string;
1159
- if (newContent) newContent = newContent.trimEnd(); // Ensure no trailing empty lines
1171
+ if (newContent) newContent = newContent.trimEnd();
1160
1172
  const occurrence = ((args.occurrence === null ? undefined : (args.occurrence as number | undefined)) ?? 1);
1161
1173
  const fullPath = resolve(workspace, path);
1162
1174
 
1163
- if (!validatePath(fullPath, workspace)) {
1175
+ if (!await validatePath(fullPath, workspace)) {
1164
1176
  return {
1165
1177
  success: false,
1166
1178
  error: 'Access denied: path is outside workspace'
1167
1179
  };
1168
1180
  }
1169
1181
 
1170
- captureFileSnapshot(path);
1171
-
1172
1182
  await mkdir(dirname(fullPath), { recursive: true });
1173
1183
 
1174
1184
  let content = '';
@@ -1240,7 +1250,7 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
1240
1250
  }
1241
1251
  const fullPath = resolve(workspace, path);
1242
1252
 
1243
- if (!validatePath(fullPath, workspace)) {
1253
+ if (!await validatePath(fullPath, workspace)) {
1244
1254
  return {
1245
1255
  success: false,
1246
1256
  error: 'Access denied: path is outside workspace'
@@ -7,7 +7,7 @@ import { createXai } from '@ai-sdk/xai';
7
7
  import { z } from 'zod';
8
8
  import { readConfig } from '../../utils/config';
9
9
  import { executeTool } from './executor';
10
- import { getExploreAbortSignal, isExploreAborted, notifyExploreTool } from '../../utils/exploreBridge';
10
+ import { getExploreAbortSignal, isExploreAborted, notifyExploreTool, getExploreContext } from '../../utils/exploreBridge';
11
11
 
12
12
  interface ExploreLog {
13
13
  tool: string;
@@ -36,7 +36,7 @@ IMPORTANT RULES:
36
36
  5. Summarize findings clearly and include relevant file paths and code snippets
37
37
  6. You MUST call the "done" tool when finished - this is the only way to complete the exploration`;
38
38
 
39
- const MAX_STEPS = 50;
39
+ const MAX_STEPS = 100;
40
40
 
41
41
  interface ExploreResult {
42
42
  success: boolean;
@@ -78,13 +78,6 @@ function createModelProvider(config: { provider: string; model: string; apiKey?:
78
78
 
79
79
  let exploreDoneResult: string | null = null;
80
80
 
81
- function getResultPreview(result: string | undefined): string {
82
- if (!result) return '';
83
- const lines = result.split('\n');
84
- if (lines.length <= 3) return result.substring(0, 200);
85
- return lines.slice(0, 3).join('\n').substring(0, 200) + '...';
86
- }
87
-
88
81
  function createExploreTools() {
89
82
  return {
90
83
  read: createTool({
@@ -240,6 +233,11 @@ export async function executeExploreTool(purpose: string): Promise<ExploreResult
240
233
 
241
234
  const tools = createExploreTools();
242
235
 
236
+ const parentContext = getExploreContext();
237
+ const systemPrompt = parentContext
238
+ ? `${EXPLORE_SYSTEM_PROMPT}\n\nCONTEXT FROM PARENT CONVERSATION:\n${parentContext}`
239
+ : EXPLORE_SYSTEM_PROMPT;
240
+
243
241
  const result = streamText({
244
242
  model,
245
243
  messages: [
@@ -248,7 +246,7 @@ export async function executeExploreTool(purpose: string): Promise<ExploreResult
248
246
  content: `Explore the codebase to: ${purpose}`,
249
247
  },
250
248
  ],
251
- system: EXPLORE_SYSTEM_PROMPT,
249
+ system: systemPrompt,
252
250
  tools,
253
251
  maxSteps: MAX_STEPS,
254
252
  abortSignal,
@@ -2,31 +2,32 @@ import { tool, type CoreTool } from 'ai';
2
2
  import { z } from 'zod';
3
3
  import { executeTool } from './executor';
4
4
 
5
- const FILE_TYPE_EXTENSIONS: Record<string, string[]> = {
6
- ts: ['.ts', '.tsx', '.mts', '.cts'],
7
- js: ['.js', '.jsx', '.mjs', '.cjs'],
8
- py: ['.py', '.pyw', '.pyi'],
9
- java: ['.java'],
10
- go: ['.go'],
11
- rust: ['.rs'],
12
- c: ['.c', '.h'],
13
- cpp: ['.cpp', '.cc', '.cxx', '.hpp', '.hh', '.hxx'],
14
- cs: ['.cs'],
15
- rb: ['.rb', '.rake', '.gemspec'],
16
- php: ['.php', '.phtml'],
17
- swift: ['.swift'],
18
- kt: ['.kt', '.kts'],
19
- scala: ['.scala'],
20
- html: ['.html', '.htm', '.xhtml'],
21
- css: ['.css', '.scss', '.sass', '.less'],
22
- json: ['.json', '.jsonc'],
23
- yaml: ['.yaml', '.yml'],
24
- md: ['.md', '.markdown'],
25
- sh: ['.sh', '.bash', '.zsh'],
26
- sql: ['.sql'],
27
- vue: ['.vue'],
28
- svelte: ['.svelte'],
29
- };
5
+ const FILE_TYPE_EXTENSIONS: Record<string, string[]> = {
6
+ ts: ['.ts', '.tsx', '.mts', '.cts'],
7
+ js: ['.js', '.jsx', '.mjs', '.cjs'],
8
+ py: ['.py', '.pyw', '.pyi'],
9
+ java: ['.java'],
10
+ go: ['.go'],
11
+ rust: ['.rs'],
12
+ c: ['.c', '.h'],
13
+ cpp: ['.cpp', '.cc', '.cxx', '.hpp', '.hh', '.hxx'],
14
+ cs: ['.cs'],
15
+ rb: ['.rb', '.rake', '.gemspec'],
16
+ php: ['.php', '.phtml'],
17
+ swift: ['.swift'],
18
+ kt: ['.kt', '.kts'],
19
+ scala: ['.scala'],
20
+ html: ['.html', '.htm', '.xhtml'],
21
+ css: ['.css', '.scss', '.sass', '.less'],
22
+ json: ['.json', '.jsonc'],
23
+ yaml: ['.yaml', '.yml'],
24
+ md: ['.md', '.markdown'],
25
+ txt: ['.txt'],
26
+ sh: ['.sh', '.bash', '.zsh'],
27
+ sql: ['.sql'],
28
+ vue: ['.vue'],
29
+ svelte: ['.svelte'],
30
+ };
30
31
 
31
32
  export { FILE_TYPE_EXTENSIONS };
32
33
 
@@ -41,10 +42,10 @@ Examples:
41
42
  - grep(query="TODO") - Search in all files
42
43
  - grep(query="class.*Component", file_type="ts") - Reuse regex search
43
44
  - grep(query="handleClick", output_mode="files") - Just list matching files`,
44
- parameters: z.object({
45
- query: z.string().describe('Regular expression pattern to search for'),
46
- file_type: z.string().optional().describe('File type: ts, js, tsx, jsx, py, java, go, rust, c, cpp, rb, php, json, yaml, md, html, css'),
47
- pattern: z.string().optional().describe('Glob pattern for files (e.g., "**/*.config.ts"). Usually file_type is easier.'),
45
+ parameters: z.object({
46
+ query: z.string().describe('Regular expression pattern to search for'),
47
+ file_type: z.string().optional().describe('File type or extension (e.g. ts, tsx, js, txt, .env). Unknown types are treated as extensions.'),
48
+ pattern: z.string().optional().describe('Glob pattern for files (e.g., "**/*.config.ts"). Usually file_type is easier.'),
48
49
  path: z.string().optional().describe('Directory to search (defaults to workspace root)'),
49
50
  case_sensitive: z.boolean().optional().describe('Case-sensitive (default: false)'),
50
51
  whole_word: z.boolean().optional().describe('Match whole words only (default: false)'),
@@ -10,11 +10,17 @@ export const question: CoreTool = tool({
10
10
  z.object({
11
11
  label: z.string().describe('The option label shown to the user.'),
12
12
  value: z.string().nullable().optional().describe('Optional value returned for the selected option. Use null if not needed.'),
13
+ group: z.string().optional().describe('Optional group name. Consecutive options with the same group are displayed under a shared header.'),
13
14
  })
14
15
  ).describe('List of options the user can pick from. A text input field is automatically displayed below the options where the user can type a custom response instead.'),
16
+ timeout: z.number().optional().describe('Optional timeout in seconds. The question is automatically rejected when time runs out.'),
17
+ validation: z.object({
18
+ pattern: z.string().describe('Regex pattern the custom text must match.'),
19
+ message: z.string().optional().describe('Error message shown when validation fails.'),
20
+ }).optional().describe('Optional validation for the custom text input.'),
15
21
  }),
16
22
  execute: async (args) => {
17
- const answer = await askQuestion(args.prompt, args.options);
23
+ const answer = await askQuestion(args.prompt, args.options, args.timeout, args.validation);
18
24
  return answer;
19
25
  },
20
26
  });
@@ -117,14 +117,15 @@ export type AgentEvent =
117
117
  | FinishEvent
118
118
  | ErrorEvent;
119
119
 
120
- export interface ProviderConfig {
121
- provider: string;
122
- model: string;
123
- apiKey?: string;
124
- systemPrompt: string;
125
- tools?: Record<string, CoreTool>;
126
- maxSteps?: number;
127
- }
120
+ export interface ProviderConfig {
121
+ provider: string;
122
+ model: string;
123
+ apiKey?: string;
124
+ systemPrompt: string;
125
+ tools?: Record<string, CoreTool>;
126
+ maxSteps?: number;
127
+ maxContextTokens?: number;
128
+ }
128
129
 
129
130
  export interface AgentConfig {
130
131
  maxSteps?: number;
@@ -7,13 +7,13 @@ import { Setup } from './Setup';
7
7
  import { Main } from './Main';
8
8
  import { ShortcutsModal } from './ShortcutsModal';
9
9
  import { CommandModal } from './CommandsModal';
10
- import { Notification, type NotificationData } from './Notification';
11
- import { exec } from 'child_process';
12
- import { promisify } from 'util';
13
- import { subscribeNotifications } from '../utils/notificationBridge';
14
- import { shouldRequireApprovals, setRequireApprovals } from '../utils/config';
15
- import { getCurrentApproval, respondApproval } from '../utils/approvalBridge';
16
- import { emitApprovalMode } from '../utils/approvalModeBridge';
10
+ import { Notification, type NotificationData } from './Notification';
11
+ import { exec } from 'child_process';
12
+ import { promisify } from 'util';
13
+ import { subscribeNotifications } from '../utils/notificationBridge';
14
+ import { shouldRequireApprovals, setRequireApprovals } from '../utils/config';
15
+ import { getCurrentApproval, respondApproval } from '../utils/approvalBridge';
16
+ import { emitApprovalMode } from '../utils/approvalModeBridge';
17
17
 
18
18
  const execAsync = promisify(exec);
19
19
 
@@ -32,7 +32,7 @@ export function App({ initialMessage }: AppProps) {
32
32
  const [shortcutsTab, setShortcutsTab] = useState<0 | 1>(0);
33
33
  const [commandsOpen, setCommandsOpen] = useState(false);
34
34
  const [notifications, setNotifications] = useState<NotificationData[]>([]);
35
- const [pendingMessage, setPendingMessage] = useState<string | undefined>(initialMessage);
35
+ const [pendingMessage] = useState<string | undefined>(initialMessage);
36
36
  const lastSelectionRef = useRef<{ text: string; at: number } | null>(null);
37
37
 
38
38
  const renderer = useRenderer();
@@ -61,8 +61,8 @@ export function App({ initialMessage }: AppProps) {
61
61
  }
62
62
  }, []);
63
63
 
64
- useEffect(() => {
65
- const handler = (selection: any) => {
64
+ useEffect(() => {
65
+ const handler = (selection: any) => {
66
66
  if (!selection || selection.isSelecting || !selection.isActive) return;
67
67
  const text = typeof selection.getSelectedText === 'function' ? selection.getSelectedText() : '';
68
68
  if (!text) return;
@@ -79,41 +79,41 @@ export function App({ initialMessage }: AppProps) {
79
79
  return () => {
80
80
  rendererAny.off?.('selection', handler);
81
81
  };
82
- }, [renderer, copyToClipboard, addNotification]);
83
-
84
- useEffect(() => {
85
- return subscribeNotifications((payload) => {
86
- addNotification(payload.message, payload.type ?? 'info', payload.duration);
87
- });
88
- }, [addNotification]);
89
-
90
- useEffect(() => {
91
- const isDarwin = process.platform === 'darwin';
92
-
93
- const handleKeyPress = (key: KeyEvent) => {
94
- const k = key as any;
95
-
96
- if (k.name === 'escape') {
97
- setShortcutsOpen(false);
98
- setCommandsOpen(false);
99
- return;
100
- }
101
-
102
- if (k.name === 'tab' && k.shift) {
103
- const next = !shouldRequireApprovals();
104
- setRequireApprovals(next);
105
- if (!next && getCurrentApproval()) {
106
- respondApproval(true);
107
- }
108
- emitApprovalMode(next);
109
- addNotification(next ? 'Approvals enabled' : 'Auto-approve enabled', 'info', 2500);
110
- return;
111
- }
112
-
113
- if (shortcutsOpen && (k.name === 'f1' || k.name === 'f2')) {
114
- setShortcutsTab(k.name === 'f2' ? 1 : 0);
115
- return;
116
- }
82
+ }, [renderer, copyToClipboard, addNotification]);
83
+
84
+ useEffect(() => {
85
+ return subscribeNotifications((payload) => {
86
+ addNotification(payload.message, payload.type ?? 'info', payload.duration);
87
+ });
88
+ }, [addNotification]);
89
+
90
+ useEffect(() => {
91
+ const isDarwin = process.platform === 'darwin';
92
+
93
+ const handleKeyPress = (key: KeyEvent) => {
94
+ const k = key as any;
95
+
96
+ if (k.name === 'escape') {
97
+ setShortcutsOpen(false);
98
+ setCommandsOpen(false);
99
+ return;
100
+ }
101
+
102
+ if (k.name === 'tab' && k.shift) {
103
+ const next = !shouldRequireApprovals();
104
+ setRequireApprovals(next);
105
+ if (!next && getCurrentApproval()) {
106
+ respondApproval(true);
107
+ }
108
+ emitApprovalMode(next);
109
+ addNotification(next ? 'Approvals enabled' : 'Auto-approve enabled', 'info', 2500);
110
+ return;
111
+ }
112
+
113
+ if (shortcutsOpen && (k.name === 'f1' || k.name === 'f2')) {
114
+ setShortcutsTab(k.name === 'f2' ? 1 : 0);
115
+ return;
116
+ }
117
117
 
118
118
  const seq = k.sequence;
119
119