@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.
- package/README.md +1 -5
- package/package.json +4 -2
- package/src/agent/Agent.ts +353 -131
- package/src/agent/context.ts +4 -4
- package/src/agent/prompts/systemPrompt.ts +15 -6
- package/src/agent/prompts/toolsPrompt.ts +75 -10
- package/src/agent/provider/anthropic.ts +100 -100
- package/src/agent/provider/google.ts +102 -102
- package/src/agent/provider/mistral.ts +95 -95
- package/src/agent/provider/ollama.ts +77 -60
- package/src/agent/provider/openai.ts +42 -38
- package/src/agent/provider/rateLimit.ts +178 -0
- package/src/agent/provider/xai.ts +99 -99
- package/src/agent/tools/definitions.ts +19 -9
- package/src/agent/tools/executor.ts +95 -85
- package/src/agent/tools/exploreExecutor.ts +8 -10
- package/src/agent/tools/grep.ts +30 -29
- package/src/agent/tools/question.ts +7 -1
- package/src/agent/types.ts +9 -8
- package/src/components/App.tsx +45 -45
- package/src/components/CustomInput.tsx +214 -36
- package/src/components/Main.tsx +1146 -954
- package/src/components/Setup.tsx +1 -1
- package/src/components/Welcome.tsx +1 -1
- package/src/components/main/ApprovalPanel.tsx +4 -3
- package/src/components/main/ChatPage.tsx +858 -675
- package/src/components/main/HomePage.tsx +53 -38
- package/src/components/main/QuestionPanel.tsx +52 -7
- package/src/components/main/ThinkingIndicator.tsx +2 -1
- package/src/index.tsx +50 -20
- package/src/mcp/approvalPolicy.ts +148 -0
- package/src/mcp/cli/add.ts +185 -0
- package/src/mcp/cli/doctor.ts +77 -0
- package/src/mcp/cli/index.ts +85 -0
- package/src/mcp/cli/list.ts +50 -0
- package/src/mcp/cli/logs.ts +24 -0
- package/src/mcp/cli/manage.ts +99 -0
- package/src/mcp/cli/show.ts +53 -0
- package/src/mcp/cli/tools.ts +77 -0
- package/src/mcp/config.ts +223 -0
- package/src/mcp/index.ts +80 -0
- package/src/mcp/processManager.ts +299 -0
- package/src/mcp/rateLimiter.ts +50 -0
- package/src/mcp/registry.ts +151 -0
- package/src/mcp/schemaConverter.ts +100 -0
- package/src/mcp/servers/navigation.ts +854 -0
- package/src/mcp/toolCatalog.ts +169 -0
- package/src/mcp/types.ts +95 -0
- package/src/utils/approvalBridge.ts +17 -5
- package/src/utils/commands/compact.ts +30 -0
- package/src/utils/commands/echo.ts +1 -1
- package/src/utils/commands/index.ts +4 -6
- package/src/utils/commands/new.ts +15 -0
- package/src/utils/commands/types.ts +3 -0
- package/src/utils/config.ts +3 -1
- package/src/utils/diffRendering.tsx +1 -3
- package/src/utils/exploreBridge.ts +10 -0
- package/src/utils/markdown.tsx +163 -99
- package/src/utils/models.ts +31 -9
- package/src/utils/questionBridge.ts +36 -1
- package/src/utils/tokenEstimator.ts +32 -0
- package/src/utils/toolFormatting.ts +268 -7
- package/src/web/app.tsx +72 -72
- package/src/web/components/HomePage.tsx +7 -7
- package/src/web/components/MessageItem.tsx +22 -22
- package/src/web/components/QuestionPanel.tsx +72 -12
- package/src/web/components/Sidebar.tsx +0 -2
- package/src/web/components/ThinkingIndicator.tsx +1 -0
- package/src/web/server.tsx +767 -683
- package/src/utils/commands/redo.ts +0 -74
- package/src/utils/commands/sessions.ts +0 -129
- package/src/utils/commands/undo.ts +0 -75
- package/src/utils/undoRedo.ts +0 -429
- package/src/utils/undoRedoBridge.ts +0 -45
- package/src/utils/undoRedoDb.ts +0 -338
|
@@ -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 {
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
|
495
|
-
for (const
|
|
496
|
-
results.push(...
|
|
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
|
-
|
|
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<
|
|
502
|
+
async function listFilesRecursive(dirPath: string, workspace: string, filterPattern?: string, includeHidden = false): Promise<WalkOutput> {
|
|
507
503
|
const fullPath = resolve(workspace, dirPath);
|
|
508
|
-
const
|
|
504
|
+
const { results, errors } = await walkDirectory(fullPath, filterPattern, includeHidden);
|
|
509
505
|
const separator = workspace.endsWith(sep) ? '' : sep;
|
|
510
506
|
|
|
511
|
-
return
|
|
512
|
-
|
|
513
|
-
|
|
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();
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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 (
|
|
1003
|
-
|
|
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 (
|
|
1026
|
-
|
|
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();
|
|
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 =
|
|
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:
|
|
249
|
+
system: systemPrompt,
|
|
252
250
|
tools,
|
|
253
251
|
maxSteps: MAX_STEPS,
|
|
254
252
|
abortSignal,
|
package/src/agent/tools/grep.ts
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
});
|
package/src/agent/types.ts
CHANGED
|
@@ -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;
|
package/src/components/App.tsx
CHANGED
|
@@ -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
|
|
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
|
|