snow-ai 0.2.18 → 0.2.19

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.
@@ -16,6 +16,31 @@ export class FilesystemMCPService {
16
16
  writable: true,
17
17
  value: void 0
18
18
  });
19
+ /**
20
+ * File extensions supported by Prettier for automatic formatting
21
+ */
22
+ Object.defineProperty(this, "prettierSupportedExtensions", {
23
+ enumerable: true,
24
+ configurable: true,
25
+ writable: true,
26
+ value: [
27
+ '.js',
28
+ '.jsx',
29
+ '.ts',
30
+ '.tsx',
31
+ '.json',
32
+ '.css',
33
+ '.scss',
34
+ '.less',
35
+ '.html',
36
+ '.vue',
37
+ '.yaml',
38
+ '.yml',
39
+ '.md',
40
+ '.graphql',
41
+ '.gql',
42
+ ]
43
+ });
19
44
  this.basePath = resolve(basePath);
20
45
  }
21
46
  /**
@@ -403,6 +428,234 @@ export class FilesystemMCPService {
403
428
  throw new Error(`Failed to get file info for ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
404
429
  }
405
430
  }
431
+ /**
432
+ * Edit a file by searching for exact content and replacing it
433
+ * This method is SAFER than line-based editing as it automatically handles code boundaries.
434
+ *
435
+ * @param filePath - Path to the file to edit
436
+ * @param searchContent - Exact content to search for (must match precisely, including whitespace)
437
+ * @param replaceContent - New content to replace the search content with
438
+ * @param occurrence - Which occurrence to replace (1-indexed, default: 1, use -1 for all)
439
+ * @param contextLines - Number of context lines to return before and after the edit (default: 8)
440
+ * @returns Object containing success message, before/after comparison, and diagnostics
441
+ * @throws Error if search content is not found or multiple matches exist
442
+ */
443
+ async editFileBySearch(filePath, searchContent, replaceContent, occurrence = 1, contextLines = 8) {
444
+ try {
445
+ const fullPath = this.resolvePath(filePath);
446
+ // For absolute paths, skip validation to allow access outside base path
447
+ if (!isAbsolute(filePath)) {
448
+ await this.validatePath(fullPath);
449
+ }
450
+ // Read the entire file
451
+ const content = await fs.readFile(fullPath, 'utf-8');
452
+ const lines = content.split('\n');
453
+ // Normalize search content (handle different line ending styles)
454
+ const normalizedSearch = searchContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
455
+ const normalizedContent = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
456
+ // Find all matches
457
+ const matches = [];
458
+ let searchIndex = 0;
459
+ while (true) {
460
+ const matchIndex = normalizedContent.indexOf(normalizedSearch, searchIndex);
461
+ if (matchIndex === -1)
462
+ break;
463
+ // Calculate line numbers for this match
464
+ const beforeMatch = normalizedContent.substring(0, matchIndex);
465
+ const startLine = (beforeMatch.match(/\n/g) || []).length + 1;
466
+ const matchLines = (normalizedSearch.match(/\n/g) || []).length;
467
+ const endLine = startLine + matchLines;
468
+ matches.push({ index: matchIndex, line: startLine, endLine });
469
+ searchIndex = matchIndex + normalizedSearch.length;
470
+ }
471
+ // Handle no matches
472
+ if (matches.length === 0) {
473
+ throw new Error(`Search content not found in file. Please verify the exact content including whitespace and indentation.`);
474
+ }
475
+ // Handle occurrence selection
476
+ let selectedMatch;
477
+ if (occurrence === -1) {
478
+ // Replace all occurrences
479
+ if (matches.length === 1) {
480
+ selectedMatch = matches[0];
481
+ }
482
+ else {
483
+ throw new Error(`Found ${matches.length} matches. Please specify which occurrence to replace (1-${matches.length}), or use occurrence=-1 to replace all (not yet implemented for safety).`);
484
+ }
485
+ }
486
+ else if (occurrence < 1 || occurrence > matches.length) {
487
+ throw new Error(`Invalid occurrence ${occurrence}. Found ${matches.length} match(es) at lines: ${matches
488
+ .map(m => m.line)
489
+ .join(', ')}`);
490
+ }
491
+ else {
492
+ selectedMatch = matches[occurrence - 1];
493
+ }
494
+ const { line: startLine, endLine } = selectedMatch;
495
+ // Backup file before editing
496
+ await incrementalSnapshotManager.backupFile(fullPath);
497
+ // Perform the replacement
498
+ const normalizedReplace = replaceContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
499
+ const beforeContent = normalizedContent.substring(0, selectedMatch.index);
500
+ const afterContent = normalizedContent.substring(selectedMatch.index + normalizedSearch.length);
501
+ const modifiedContent = beforeContent + normalizedReplace + afterContent;
502
+ // Calculate replaced content for display
503
+ const replacedLines = lines.slice(startLine - 1, endLine);
504
+ const maxLineNumWidth = String(endLine).length;
505
+ const replacedContent = replacedLines
506
+ .map((line, idx) => {
507
+ const lineNum = startLine + idx;
508
+ const paddedNum = String(lineNum).padStart(maxLineNumWidth, ' ');
509
+ return `${paddedNum}→${line}`;
510
+ })
511
+ .join('\n');
512
+ // Calculate context boundaries
513
+ const modifiedLines = modifiedContent.split('\n');
514
+ const replaceLines = normalizedReplace.split('\n');
515
+ const lineDifference = replaceLines.length - (endLine - startLine);
516
+ const smartBoundaries = this.findSmartContextBoundaries(lines, startLine, endLine, contextLines);
517
+ const contextStart = smartBoundaries.start;
518
+ const contextEnd = smartBoundaries.end;
519
+ // Extract old content for context
520
+ const oldContextLines = lines.slice(contextStart - 1, contextEnd);
521
+ const oldContent = oldContextLines
522
+ .map((line, idx) => {
523
+ const lineNum = contextStart + idx;
524
+ const paddedNum = String(lineNum).padStart(String(contextEnd).length, ' ');
525
+ return `${paddedNum}→${line}`;
526
+ })
527
+ .join('\n');
528
+ // Write the modified content
529
+ await fs.writeFile(fullPath, modifiedContent, 'utf-8');
530
+ // Format with Prettier if applicable
531
+ let finalContent = modifiedContent;
532
+ let finalLines = modifiedLines;
533
+ let finalTotalLines = modifiedLines.length;
534
+ let finalContextEnd = Math.min(finalTotalLines, contextEnd + lineDifference);
535
+ // Check if Prettier supports this file type
536
+ const fileExtension = path.extname(fullPath).toLowerCase();
537
+ const shouldFormat = this.prettierSupportedExtensions.includes(fileExtension);
538
+ if (shouldFormat) {
539
+ try {
540
+ execSync(`npx prettier --write "${fullPath}"`, {
541
+ stdio: 'pipe',
542
+ encoding: 'utf-8',
543
+ });
544
+ // Re-read the file after formatting
545
+ finalContent = await fs.readFile(fullPath, 'utf-8');
546
+ finalLines = finalContent.split('\n');
547
+ finalTotalLines = finalLines.length;
548
+ finalContextEnd = Math.min(finalTotalLines, contextStart + (contextEnd - contextStart) + lineDifference);
549
+ }
550
+ catch (formatError) {
551
+ // Continue with unformatted content
552
+ }
553
+ }
554
+ // Extract new content for context
555
+ const newContextLines = finalLines.slice(contextStart - 1, finalContextEnd);
556
+ const newContextContent = newContextLines
557
+ .map((line, idx) => {
558
+ const lineNum = contextStart + idx;
559
+ const paddedNum = String(lineNum).padStart(String(finalContextEnd).length, ' ');
560
+ return `${paddedNum}→${line}`;
561
+ })
562
+ .join('\n');
563
+ // Analyze code structure
564
+ const editedContentLines = replaceLines;
565
+ const structureAnalysis = this.analyzeCodeStructure(finalContent, filePath, editedContentLines);
566
+ // Get diagnostics from VS Code
567
+ let diagnostics = [];
568
+ try {
569
+ await new Promise(resolve => setTimeout(resolve, 500));
570
+ diagnostics = await vscodeConnection.requestDiagnostics(fullPath);
571
+ }
572
+ catch (error) {
573
+ // Ignore diagnostics errors
574
+ }
575
+ // Build result
576
+ const result = {
577
+ message: `✅ File edited successfully using search-replace (safer boundary detection): ${filePath}\n` +
578
+ ` Matched: lines ${startLine}-${endLine} (occurrence ${occurrence}/${matches.length})\n` +
579
+ ` Result: ${replaceLines.length} new lines` +
580
+ (smartBoundaries.extended
581
+ ? `\n 📍 Context auto-extended to show complete code block (lines ${contextStart}-${finalContextEnd})`
582
+ : ''),
583
+ oldContent,
584
+ newContent: newContextContent,
585
+ replacedContent,
586
+ matchLocation: { startLine, endLine },
587
+ contextStartLine: contextStart,
588
+ contextEndLine: finalContextEnd,
589
+ totalLines: finalTotalLines,
590
+ structureAnalysis,
591
+ completeOldContent: content,
592
+ completeNewContent: finalContent,
593
+ diagnostics: undefined,
594
+ };
595
+ // Add diagnostics if found
596
+ if (diagnostics.length > 0) {
597
+ result.diagnostics = diagnostics;
598
+ const errorCount = diagnostics.filter(d => d.severity === 'error').length;
599
+ const warningCount = diagnostics.filter(d => d.severity === 'warning').length;
600
+ if (errorCount > 0 || warningCount > 0) {
601
+ result.message += `\n\n⚠️ Diagnostics detected: ${errorCount} error(s), ${warningCount} warning(s)`;
602
+ const formattedDiagnostics = diagnostics
603
+ .filter(d => d.severity === 'error' || d.severity === 'warning')
604
+ .map(d => {
605
+ const icon = d.severity === 'error' ? '❌' : '⚠️';
606
+ const location = `${filePath}:${d.line}:${d.character}`;
607
+ return ` ${icon} [${d.source || 'unknown'}] ${location}\n ${d.message}`;
608
+ })
609
+ .join('\n\n');
610
+ result.message += `\n\n📋 Diagnostic Details:\n${formattedDiagnostics}`;
611
+ result.message += `\n\n ⚡ TIP: Review the errors above and make another edit to fix them`;
612
+ }
613
+ }
614
+ // Add structure analysis warnings
615
+ const structureWarnings = [];
616
+ if (!structureAnalysis.bracketBalance.curly.balanced) {
617
+ const diff = structureAnalysis.bracketBalance.curly.open -
618
+ structureAnalysis.bracketBalance.curly.close;
619
+ structureWarnings.push(`Curly brackets: ${diff > 0 ? `${diff} unclosed {` : `${Math.abs(diff)} extra }`}`);
620
+ }
621
+ if (!structureAnalysis.bracketBalance.round.balanced) {
622
+ const diff = structureAnalysis.bracketBalance.round.open -
623
+ structureAnalysis.bracketBalance.round.close;
624
+ structureWarnings.push(`Round brackets: ${diff > 0 ? `${diff} unclosed (` : `${Math.abs(diff)} extra )`}`);
625
+ }
626
+ if (!structureAnalysis.bracketBalance.square.balanced) {
627
+ const diff = structureAnalysis.bracketBalance.square.open -
628
+ structureAnalysis.bracketBalance.square.close;
629
+ structureWarnings.push(`Square brackets: ${diff > 0 ? `${diff} unclosed [` : `${Math.abs(diff)} extra ]`}`);
630
+ }
631
+ if (structureAnalysis.htmlTags && !structureAnalysis.htmlTags.balanced) {
632
+ if (structureAnalysis.htmlTags.unclosedTags.length > 0) {
633
+ structureWarnings.push(`Unclosed HTML tags: ${structureAnalysis.htmlTags.unclosedTags.join(', ')}`);
634
+ }
635
+ if (structureAnalysis.htmlTags.unopenedTags.length > 0) {
636
+ structureWarnings.push(`Unopened closing tags: ${structureAnalysis.htmlTags.unopenedTags.join(', ')}`);
637
+ }
638
+ }
639
+ if (structureAnalysis.indentationWarnings.length > 0) {
640
+ structureWarnings.push(...structureAnalysis.indentationWarnings.map(w => `Indentation: ${w}`));
641
+ }
642
+ if (structureAnalysis.codeBlockBoundary &&
643
+ structureAnalysis.codeBlockBoundary.suggestion) {
644
+ structureWarnings.push(`Boundary: ${structureAnalysis.codeBlockBoundary.suggestion}`);
645
+ }
646
+ if (structureWarnings.length > 0) {
647
+ result.message += `\n\n🔍 Structure Analysis:\n`;
648
+ structureWarnings.forEach(warning => {
649
+ result.message += ` ⚠️ ${warning}\n`;
650
+ });
651
+ result.message += `\n 💡 TIP: These warnings help identify potential issues. If intentional (e.g., opening a block), you can ignore them.`;
652
+ }
653
+ return result;
654
+ }
655
+ catch (error) {
656
+ throw new Error(`Failed to edit file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
657
+ }
658
+ }
406
659
  /**
407
660
  * Edit a file by replacing lines within a specified range
408
661
  * BEST PRACTICE: Keep edits small and focused (≤15 lines recommended) for better accuracy.
@@ -490,12 +743,8 @@ export class FilesystemMCPService {
490
743
  let finalContextEnd = newContextEnd;
491
744
  let finalContextContent = newContextContent;
492
745
  // Check if Prettier supports this file type
493
- const prettierSupportedExtensions = [
494
- '.js', '.jsx', '.ts', '.tsx', '.json', '.css', '.scss', '.less',
495
- '.html', '.vue', '.yaml', '.yml', '.md', '.graphql', '.gql'
496
- ];
497
746
  const fileExtension = path.extname(fullPath).toLowerCase();
498
- const shouldFormat = prettierSupportedExtensions.includes(fileExtension);
747
+ const shouldFormat = this.prettierSupportedExtensions.includes(fileExtension);
499
748
  if (shouldFormat) {
500
749
  try {
501
750
  execSync(`npx prettier --write "${fullPath}"`, {
@@ -628,102 +877,6 @@ export class FilesystemMCPService {
628
877
  throw new Error(`Failed to edit file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
629
878
  }
630
879
  }
631
- /**
632
- * Search for code keywords in files within a directory
633
- * @param query - Search keyword or pattern
634
- * @param dirPath - Directory to search in (default: current directory)
635
- * @param fileExtensions - Array of file extensions to search (e.g., ['.ts', '.tsx', '.js']). If empty, search all files.
636
- * @param caseSensitive - Whether the search should be case-sensitive (default: false)
637
- * @param maxResults - Maximum number of results to return (default: 100)
638
- * @returns Search results with file paths, line numbers, and matched content
639
- */
640
- async searchCode(query, dirPath = '.', fileExtensions = [], caseSensitive = false, maxResults = 100, searchMode = 'text') {
641
- const matches = [];
642
- let searchedFiles = 0;
643
- const fullDirPath = this.resolvePath(dirPath);
644
- // Prepare search regex based on mode
645
- const flags = caseSensitive ? 'g' : 'gi';
646
- let searchRegex = null;
647
- if (searchMode === 'text') {
648
- // Escape special regex characters for literal text search
649
- searchRegex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags);
650
- }
651
- else if (searchMode === 'regex') {
652
- // Use query as-is for regex search
653
- searchRegex = new RegExp(query, flags);
654
- }
655
- // Recursively search files
656
- const searchInDirectory = async (currentPath) => {
657
- try {
658
- const entries = await fs.readdir(currentPath, { withFileTypes: true });
659
- for (const entry of entries) {
660
- if (matches.length >= maxResults) {
661
- return;
662
- }
663
- const fullPath = path.join(currentPath, entry.name);
664
- // Skip common directories that should be ignored
665
- if (entry.isDirectory()) {
666
- const dirName = entry.name;
667
- if (dirName === 'node_modules' ||
668
- dirName === '.git' ||
669
- dirName === 'dist' ||
670
- dirName === 'build' ||
671
- dirName.startsWith('.')) {
672
- continue;
673
- }
674
- await searchInDirectory(fullPath);
675
- }
676
- else if (entry.isFile()) {
677
- // Filter by file extension if specified
678
- if (fileExtensions.length > 0) {
679
- const ext = path.extname(entry.name);
680
- if (!fileExtensions.includes(ext)) {
681
- continue;
682
- }
683
- }
684
- searchedFiles++;
685
- try {
686
- const content = await fs.readFile(fullPath, 'utf-8');
687
- // Text or Regex search mode
688
- if (searchRegex) {
689
- const lines = content.split('\n');
690
- lines.forEach((line, index) => {
691
- if (matches.length >= maxResults) {
692
- return;
693
- }
694
- // Reset regex for each line
695
- searchRegex.lastIndex = 0;
696
- const match = searchRegex.exec(line);
697
- if (match) {
698
- matches.push({
699
- filePath: path.relative(this.basePath, fullPath),
700
- lineNumber: index + 1,
701
- lineContent: line.trim(),
702
- column: match.index + 1,
703
- matchedText: match[0],
704
- });
705
- }
706
- });
707
- }
708
- }
709
- catch (error) {
710
- // Skip files that cannot be read (binary files, permission issues, etc.)
711
- }
712
- }
713
- }
714
- }
715
- catch (error) {
716
- // Skip directories that cannot be accessed
717
- }
718
- };
719
- await searchInDirectory(fullDirPath);
720
- return {
721
- query,
722
- totalMatches: matches.length,
723
- matches,
724
- searchedFiles,
725
- };
726
- }
727
880
  /**
728
881
  * Resolve path relative to base path and normalize it
729
882
  * @private
@@ -846,77 +999,66 @@ export const mcpTools = [
846
999
  },
847
1000
  },
848
1001
  {
849
- name: 'filesystem_edit',
850
- description: '🎯 PREFERRED tool for precise file editing with intelligent feedback. **BEST PRACTICES**: (1) Use SMALL, INCREMENTAL edits (recommended ≤15 lines per edit) - SAFER and MORE ACCURATE, preventing syntax errors. (2) For large changes, make MULTIPLE PARALLEL edits to different sections instead of one large edit. (3) Must use exact line numbers Code boundaries should not be redundant or missing, such as `{}` or HTML tags causing syntax errors. **WORKFLOW**: (1) Read target section with filesystem_read, (2) Edit small sections, (3) Review auto-generated structure analysis and diagnostics, (4) Make parallel edits to non-overlapping ranges if needed. **SMART FEATURES**: Auto-detects bracket/tag mismatches, indentation issues, and code block boundaries. Context auto-extends to show complete functions/classes when detected.',
1002
+ name: 'filesystem_edit_search',
1003
+ description: '🎯 **RECOMMENDED** for most edits: Search-and-replace editing with AUTOMATIC boundary detection. **WHY USE THIS**: (1) NO need to count line numbers - just copy the exact code you want to change, (2) SAFER - automatically handles code boundaries to prevent bracket/tag mismatches, (3) CLEARER intent - shows exactly what you\'re changing. **BEST FOR**: Modifying existing functions, fixing bugs, updating logic. **WORKFLOW**: (1) Read file with filesystem_read, (2) Copy exact code block to change (including whitespace!), (3) Provide the replacement. **HANDLES**: Multiple occurrences, whitespace normalization, structure validation. **TIP**: For new code additions, use filesystem_edit with line numbers instead.',
851
1004
  inputSchema: {
852
1005
  type: 'object',
853
1006
  properties: {
854
1007
  filePath: {
855
1008
  type: 'string',
856
- description: 'Path to the file to edit (absolute or relative)',
1009
+ description: 'Path to the file to edit',
857
1010
  },
858
- startLine: {
859
- type: 'number',
860
- description: '⚠️ CRITICAL: Starting line number (1-indexed, inclusive). MUST match exact line number from filesystem_read output. Double-check this value!',
861
- },
862
- endLine: {
863
- type: 'number',
864
- description: '⚠️ CRITICAL: Ending line number (1-indexed, inclusive). MUST match exact line number from filesystem_read output. 💡 TIP: Keep edits small (≤15 lines recommended) for better accuracy.',
1011
+ searchContent: {
1012
+ type: 'string',
1013
+ description: '⚠️ EXACT content to find and replace. MUST match precisely including indentation and whitespace. Copy directly from filesystem_read output (WITHOUT line numbers like "123→").',
865
1014
  },
866
- newContent: {
1015
+ replaceContent: {
867
1016
  type: 'string',
868
- description: 'New content to replace specified lines. ⚠️ Do NOT include line numbers. ⚠️ Ensure proper indentation and bracket closure. Keep changes MINIMAL and FOCUSED.',
1017
+ description: 'New content to replace the search content. Should maintain consistent indentation with surrounding code.',
1018
+ },
1019
+ occurrence: {
1020
+ type: 'number',
1021
+ description: 'Which occurrence to replace if multiple matches found (1-indexed). Default: 1 (first match). Use -1 for all occurrences (not yet supported).',
1022
+ default: 1,
869
1023
  },
870
1024
  contextLines: {
871
1025
  type: 'number',
872
- description: 'Number of context lines to show before/after edit for verification (default: 8)',
1026
+ description: 'Number of context lines to show before/after edit (default: 8)',
873
1027
  default: 8,
874
1028
  },
875
1029
  },
876
- required: ['filePath', 'startLine', 'endLine', 'newContent'],
1030
+ required: ['filePath', 'searchContent', 'replaceContent'],
877
1031
  },
878
1032
  },
879
1033
  {
880
- name: 'filesystem_search',
881
- description: "Search for code keywords across files in a directory. Useful for finding function definitions, variable usages, or any code patterns. Similar to VS Code's global search feature.",
1034
+ name: 'filesystem_edit',
1035
+ description: '🔧 Line-based editing for precise line range replacements. **WHEN TO USE**: (1) Adding completely new code sections, (2) Deleting specific line ranges, (3) When you need exact line-number control. **LIMITATIONS**: Requires manual line number tracking, higher risk of boundary errors. **RECOMMENDATION**: For most edits, use filesystem_edit_search instead - it\'s safer and easier. **BEST PRACTICES**: (1) Keep edits small (≤15 lines), (2) Always read file first to get exact line numbers, (3) Double-check boundaries for brackets/tags. **SMART FEATURES**: Auto-detects bracket/tag mismatches, indentation issues, context auto-extends to show complete code blocks.',
882
1036
  inputSchema: {
883
1037
  type: 'object',
884
1038
  properties: {
885
- query: {
886
- type: 'string',
887
- description: 'The keyword or text to search for (e.g., function name, variable name, or any code pattern)',
888
- },
889
- dirPath: {
1039
+ filePath: {
890
1040
  type: 'string',
891
- description: 'Directory to search in (relative to base path or absolute). Defaults to current directory.',
892
- default: '.',
893
- },
894
- fileExtensions: {
895
- type: 'array',
896
- items: {
897
- type: 'string',
898
- },
899
- description: 'Array of file extensions to search (e.g., [".ts", ".tsx", ".js"]). If empty, searches all text files.',
900
- default: [],
1041
+ description: 'Path to the file to edit (absolute or relative)',
901
1042
  },
902
- caseSensitive: {
903
- type: 'boolean',
904
- description: 'Whether the search should be case-sensitive',
905
- default: false,
1043
+ startLine: {
1044
+ type: 'number',
1045
+ description: '⚠️ CRITICAL: Starting line number (1-indexed, inclusive). MUST match exact line number from filesystem_read output. Double-check this value!',
906
1046
  },
907
- maxResults: {
1047
+ endLine: {
908
1048
  type: 'number',
909
- description: 'Maximum number of results to return',
910
- default: 100,
1049
+ description: '⚠️ CRITICAL: Ending line number (1-indexed, inclusive). MUST match exact line number from filesystem_read output. 💡 TIP: Keep edits small (≤15 lines recommended) for better accuracy.',
911
1050
  },
912
- searchMode: {
1051
+ newContent: {
913
1052
  type: 'string',
914
- enum: ['text', 'regex'],
915
- description: 'Search mode: "text" for literal text search (default), "regex" for regular expression search',
916
- default: 'text',
1053
+ description: 'New content to replace specified lines. ⚠️ Do NOT include line numbers. ⚠️ Ensure proper indentation and bracket closure. Keep changes MINIMAL and FOCUSED.',
1054
+ },
1055
+ contextLines: {
1056
+ type: 'number',
1057
+ description: 'Number of context lines to show before/after edit for verification (default: 8)',
1058
+ default: 8,
917
1059
  },
918
1060
  },
919
- required: ['query'],
1061
+ required: ['filePath', 'startLine', 'endLine', 'newContent'],
920
1062
  },
921
1063
  },
922
1064
  ];
@@ -5,6 +5,7 @@ interface Props {
5
5
  filename?: string;
6
6
  completeOldContent?: string;
7
7
  completeNewContent?: string;
8
+ startLineNumber?: number;
8
9
  }
9
- export default function DiffViewer({ oldContent, newContent, filename, completeOldContent, completeNewContent, }: Props): React.JSX.Element;
10
+ export default function DiffViewer({ oldContent, newContent, filename, completeOldContent, completeNewContent, startLineNumber, }: Props): React.JSX.Element;
10
11
  export {};
@@ -12,7 +12,7 @@ function stripLineNumbers(content) {
12
12
  })
13
13
  .join('\n');
14
14
  }
15
- export default function DiffViewer({ oldContent = '', newContent, filename, completeOldContent, completeNewContent, }) {
15
+ export default function DiffViewer({ oldContent = '', newContent, filename, completeOldContent, completeNewContent, startLineNumber = 1, }) {
16
16
  // If complete file contents are provided, use them for intelligent diff
17
17
  const useCompleteContent = completeOldContent && completeNewContent;
18
18
  const diffOldContent = useCompleteContent
@@ -38,8 +38,8 @@ export default function DiffViewer({ oldContent = '', newContent, filename, comp
38
38
  // Generate line-by-line diff
39
39
  const diffResult = Diff.diffLines(diffOldContent, diffNewContent);
40
40
  const allChanges = [];
41
- let oldLineNum = 1;
42
- let newLineNum = 1;
41
+ let oldLineNum = startLineNumber;
42
+ let newLineNum = startLineNumber;
43
43
  diffResult.forEach((part) => {
44
44
  const lines = part.value.replace(/\n$/, '').split('\n');
45
45
  lines.forEach((line) => {
@@ -127,20 +127,37 @@ export default function DiffViewer({ oldContent = '', newContent, filename, comp
127
127
  ' ',
128
128
  filename))),
129
129
  React.createElement(Box, { flexDirection: "column" },
130
- hunks.map((hunk, hunkIndex) => (React.createElement(Box, { key: hunkIndex, flexDirection: "column", marginBottom: 1 }, hunk.changes.map((change, changeIndex) => {
131
- if (change.type === 'added') {
132
- return (React.createElement(Text, { key: changeIndex, color: "white", backgroundColor: "#006400" },
133
- "+ ",
134
- change.content));
135
- }
136
- if (change.type === 'removed') {
137
- return (React.createElement(Text, { key: changeIndex, color: "white", backgroundColor: "#8B0000" },
138
- "- ",
130
+ hunks.map((hunk, hunkIndex) => (React.createElement(Box, { key: hunkIndex, flexDirection: "column", marginBottom: 1 },
131
+ React.createElement(Text, { color: "cyan", dimColor: true },
132
+ "@@ Lines ",
133
+ hunk.startLine,
134
+ "-",
135
+ hunk.endLine,
136
+ " @@"),
137
+ hunk.changes.map((change, changeIndex) => {
138
+ // Calculate line number to display
139
+ const lineNum = change.type === 'added'
140
+ ? change.newLineNum
141
+ : change.oldLineNum;
142
+ const lineNumStr = lineNum ? String(lineNum).padStart(4, ' ') : ' ';
143
+ if (change.type === 'added') {
144
+ return (React.createElement(Text, { key: changeIndex, color: "white", backgroundColor: "#006400" },
145
+ lineNumStr,
146
+ " + ",
147
+ change.content));
148
+ }
149
+ if (change.type === 'removed') {
150
+ return (React.createElement(Text, { key: changeIndex, color: "white", backgroundColor: "#8B0000" },
151
+ lineNumStr,
152
+ " - ",
153
+ change.content));
154
+ }
155
+ // Unchanged lines (context)
156
+ return (React.createElement(Text, { key: changeIndex, dimColor: true },
157
+ lineNumStr,
158
+ " ",
139
159
  change.content));
140
- }
141
- // Unchanged lines (context)
142
- return (React.createElement(Text, { key: changeIndex, dimColor: true }, change.content));
143
- })))),
160
+ })))),
144
161
  hunks.length > 1 && (React.createElement(Box, { marginTop: 1 },
145
162
  React.createElement(Text, { color: "gray", dimColor: true },
146
163
  "Total: ",
@@ -22,6 +22,15 @@ function formatArgumentsAsTree(args, toolName) {
22
22
  if (toolName === 'filesystem-edit') {
23
23
  excludeFields.add('newContent');
24
24
  }
25
+ if (toolName === 'filesystem-edit_search') {
26
+ excludeFields.add('searchContent');
27
+ excludeFields.add('replaceContent');
28
+ }
29
+ // For ACE tools, exclude large result fields that may contain extensive code
30
+ if (toolName?.startsWith('ace-')) {
31
+ excludeFields.add('context'); // ACE tools may return large context strings
32
+ excludeFields.add('signature'); // Function signatures can be verbose
33
+ }
25
34
  const keys = Object.keys(args).filter(key => !excludeFields.has(key));
26
35
  return keys.map((key, index) => ({
27
36
  key,