This commit is contained in:
Christian Gunderman
2026-02-18 05:45:59 +00:00
parent 5a1f0cbc49
commit 8cb2bfe995
3 changed files with 156 additions and 6 deletions

View File

@@ -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<EditToolParams, ToolResult>
implements ToolInvocation<EditToolParams, ToolResult>
@@ -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);

View File

@@ -160,6 +160,8 @@ ${result.llmContent}`;
/**
* Implementation of the ReadFile tool logic
*/
const readCache = new Map<string, ToolResult>();
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<ReadFileToolParams, ToolResult> {
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<ReadFileToolParams, ToolResult>;
}
override getSchema(modelId?: string) {

View File

@@ -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<number>();
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;
@@ -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,