From 8cb2bfe995608ff9f4e6577d0b2431a0d08f9817 Mon Sep 17 00:00:00 2001 From: Christian Gunderman Date: Wed, 18 Feb 2026 05:45:59 +0000 Subject: [PATCH] Fixes --- packages/core/src/tools/edit.ts | 58 +++++++++++++++++++++++++ packages/core/src/tools/read-file.ts | 41 +++++++++++++++--- packages/core/src/tools/ripGrep.ts | 63 ++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 6 deletions(-) diff --git a/packages/core/src/tools/edit.ts b/packages/core/src/tools/edit.ts index 8a48161662..1680ec16b8 100644 --- a/packages/core/src/tools/edit.ts +++ b/packages/core/src/tools/edit.ts @@ -410,6 +410,57 @@ interface CalculatedEdit { matchRanges?: Array<{ start: number; end: number }>; } +function getDiffContextSnippet( + originalContent: string, + newContent: string, + contextLines = 5, +): string { + if (!originalContent) return newContent; + const changes = Diff.diffLines(originalContent, newContent); + const newLines = newContent.split(/\r?\n/); + const ranges: Array<{ start: number; end: number }> = []; + let newLineIdx = 0; + for (const change of changes) { + if (change.added) { + ranges.push({ start: newLineIdx, end: newLineIdx + (change.count ?? 0) }); + newLineIdx += change.count ?? 0; + } else if (change.removed) { + ranges.push({ start: newLineIdx, end: newLineIdx }); + } else { + newLineIdx += change.count ?? 0; + } + } + if (ranges.length === 0) return newContent; + const expandedRanges = ranges.map((r) => ({ + start: Math.max(0, r.start - contextLines), + end: Math.min(newLines.length, r.end + contextLines), + })); + expandedRanges.sort((a, b) => a.start - b.start); + const mergedRanges: Array<{ start: number; end: number }> = []; + if (expandedRanges.length > 0) { + let current = expandedRanges[0]; + for (let i = 1; i < expandedRanges.length; i++) { + const next = expandedRanges[i]; + if (next.start <= current.end) { + current.end = Math.max(current.end, next.end); + } else { + mergedRanges.push(current); + current = next; + } + } + mergedRanges.push(current); + } + const outputParts: string[] = []; + let lastEnd = 0; + for (const range of mergedRanges) { + if (range.start > lastEnd) outputParts.push('...'); + outputParts.push(newLines.slice(range.start, range.end).join('\n')); + lastEnd = range.end; + } + if (lastEnd < newLines.length) outputParts.push('...'); + return outputParts.join('\n'); +} + class EditToolInvocation extends BaseToolInvocation implements ToolInvocation @@ -876,6 +927,13 @@ class EditToolInvocation ? `Created new file: ${this.params.file_path} with provided content.` : `Successfully modified file: ${this.params.file_path} (${editData.occurrences} replacements).`, ]; + const snippet = getDiffContextSnippet( + originalContent, + finalContent, + 5, + ); + llmSuccessMessageParts.push(`Here is the updated code: +${snippet}`); const fuzzyFeedback = getFuzzyMatchFeedback(editData); if (fuzzyFeedback) { llmSuccessMessageParts.push(fuzzyFeedback); diff --git a/packages/core/src/tools/read-file.ts b/packages/core/src/tools/read-file.ts index c43f79ded0..badb8f12c7 100644 --- a/packages/core/src/tools/read-file.ts +++ b/packages/core/src/tools/read-file.ts @@ -160,6 +160,8 @@ ${result.llmContent}`; /** * Implementation of the ReadFile tool logic */ +const readCache = new Map(); + export class ReadFileTool extends BaseDeclarativeTool< ReadFileToolParams, ToolResult @@ -185,6 +187,12 @@ export class ReadFileTool extends BaseDeclarativeTool< config.getTargetDir(), config.getFileFilteringOptions(), ); + // Clear cache on any potential file modification + messageBus.on('tool_call_confirm', (details) => { + if (details.type === 'edit' || details.title.toLowerCase().includes('write')) { + readCache.clear(); + } + }); } protected override validateToolParamValues( @@ -233,13 +241,34 @@ export class ReadFileTool extends BaseDeclarativeTool< _toolName?: string, _toolDisplayName?: string, ): ToolInvocation { - return new ReadFileToolInvocation( - this.config, + const key = JSON.stringify({ + path: path.resolve(this.config.getTargetDir(), params.file_path), + offset: params.offset, + limit: params.limit, + }); + + return { + toolLocations: () => [{ path: path.resolve(this.config.getTargetDir(), params.file_path), line: params.offset }], + getDescription: () => shortenPath(makeRelative(path.resolve(this.config.getTargetDir(), params.file_path), this.config.getTargetDir())), params, - messageBus, - _toolName, - _toolDisplayName, - ); + execute: async () => { + if (readCache.has(key)) { + return readCache.get(key)!; + } + const invocation = new ReadFileToolInvocation( + this.config, + params, + messageBus, + _toolName, + _toolDisplayName, + ); + const result = await invocation.execute(); + if (!result.error) { + readCache.set(key, result); + } + return result; + }, + } as unknown as ToolInvocation; } override getSchema(modelId?: string) { diff --git a/packages/core/src/tools/ripGrep.ts b/packages/core/src/tools/ripGrep.ts index d4478fffee..fcdff05ea5 100644 --- a/packages/core/src/tools/ripGrep.ts +++ b/packages/core/src/tools/ripGrep.ts @@ -160,6 +160,8 @@ export interface RipGrepToolParams { */ interface GrepMatch { filePath: string; + absolutePath: string; + absolutePath: string; lineNumber: number; line: string; isContext?: boolean; @@ -310,6 +312,66 @@ class GrepToolInvocation extends BaseToolInvocation< const matchCount = matchesOnly.length; const matchTerm = matchCount === 1 ? 'match' : 'matches'; + // Greedy Grep: If match count is low and no context was requested, automatically return context. + if ( + matchCount <= 3 && + matchCount > 0 && + !this.params.names_only && + !this.params.context && + !this.params.before && + !this.params.after + ) { + for (const filePath in matchesByFile) { + const fileMatches = matchesByFile[filePath]; + let fileLines: string[] | null = null; + try { + const content = await fsPromises.readFile( + fileMatches[0].absolutePath, + 'utf8', + ); + fileLines = content.split(/ ? +/); + } catch (err) { + debugLogger.warn( + \`Failed to read file for context: \${fileMatches[0].absolutePath}\`, + err, + ); + } + + if (fileLines) { + const newFileMatches: GrepMatch[] = []; + const seenLines = new Set(); + for (const match of fileMatches) { + const startLine = Math.max(0, match.lineNumber - 1 - 50); + const endLine = Math.min( + fileLines.length, + match.lineNumber - 1 + 50 + 1, + ); + for (let i = startLine; i < endLine; i++) { + if (!seenLines.has(i + 1)) { + newFileMatches.push({ + absolutePath: match.absolutePath, + filePath: match.filePath, + lineNumber: i + 1, + line: fileLines[i], + isContext: i + 1 !== match.lineNumber, + }); + seenLines.add(i + 1); + } else if (i + 1 === match.lineNumber) { + const index = newFileMatches.findIndex(m => m.lineNumber === i + 1); + if (index !== -1) { + newFileMatches[index].isContext = false; + } + } + } + } + matchesByFile[filePath] = newFileMatches.sort( + (a, b) => a.lineNumber - b.lineNumber, + ); + } + } + } + const wasTruncated = matchCount >= totalMaxMatches; if (this.params.names_only) { @@ -500,6 +562,7 @@ class GrepToolInvocation extends BaseToolInvocation< const relativeFilePath = path.relative(basePath, absoluteFilePath); return { + absolutePath: absoluteFilePath, filePath: relativeFilePath || path.basename(absoluteFilePath), lineNumber: data.line_number, line: data.lines.text.trimEnd(),