feat(core): Include 50 lines of context in grep_search when matches <= 3

This commit is contained in:
Christian Gunderman
2026-02-17 19:36:59 -08:00
parent 047a57dee8
commit 259c4d57b5
2 changed files with 92 additions and 12 deletions

View File

@@ -493,10 +493,12 @@ describe('GrepTool', () => {
// sub/fileC.txt has 1 world, so total matches = 2.
expect(result.llmContent).toContain('Found 2 matches');
expect(result.llmContent).toContain('File: fileA.txt');
expect(result.llmContent).toContain('L1: hello world');
expect(result.llmContent).not.toContain('L2: second line with world');
// Should be a match
expect(result.llmContent).toContain('> L1: hello world');
// Should NOT be a match (but might be in context)
expect(result.llmContent).not.toContain('> L2: second line with world');
expect(result.llmContent).toContain('File: sub/fileC.txt');
expect(result.llmContent).toContain('L1: another world in sub dir');
expect(result.llmContent).toContain('> L1: another world in sub dir');
});
it('should return only file paths when names_only is true', async () => {
@@ -530,8 +532,36 @@ describe('GrepTool', () => {
expect(result.llmContent).toContain('Found 1 match');
expect(result.llmContent).toContain('copyright.txt');
expect(result.llmContent).toContain('Copyright 2025 Google LLC');
expect(result.llmContent).not.toContain('Copyright 2026 Google LLC');
// Should be a match
expect(result.llmContent).toContain('> L1: Copyright 2025 Google LLC');
// Should NOT be a match (but might be in context)
expect(result.llmContent).not.toContain(
'> L2: Copyright 2026 Google LLC',
);
});
it('should include context when matches are <= 3', async () => {
const lines = Array.from({ length: 100 }, (_, i) => `Line ${i + 1}`);
lines[50] = 'Target match';
await fs.writeFile(
path.join(tempRootDir, 'context.txt'),
lines.join('\n'),
);
const params: GrepToolParams = { pattern: 'Target match' };
const invocation = grepTool.build(params);
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toContain(
'Found 1 match for pattern "Target match"',
);
expect(result.llmContent).toContain('Match at line 51:');
// Verify context before
expect(result.llmContent).toContain(' L40: Line 40');
// Verify match line
expect(result.llmContent).toContain('> L51: Target match');
// Verify context after
expect(result.llmContent).toContain(' L60: Line 60');
});
});

View File

@@ -75,6 +75,7 @@ export interface GrepToolParams {
*/
interface GrepMatch {
filePath: string;
absolutePath: string;
lineNumber: number;
line: string;
}
@@ -130,6 +131,7 @@ class GrepToolInvocation extends BaseToolInvocation<
return {
filePath: relativeFilePath || path.basename(absoluteFilePath),
absolutePath: absoluteFilePath,
lineNumber,
line: lineContent,
};
@@ -309,14 +311,61 @@ class GrepToolInvocation extends BaseToolInvocation<
llmContent += `:\n---\n`;
for (const filePath in matchesByFile) {
llmContent += `File: ${filePath}
if (matchCount <= 3 && matchCount > 0 && !this.params.names_only) {
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(/\r?\n/);
} catch (err) {
debugLogger.warn(
`Failed to read file for context: ${fileMatches[0].absolutePath}`,
err,
);
}
llmContent += `File: ${filePath}\n`;
for (const match of fileMatches) {
if (fileLines) {
const startLine = Math.max(0, match.lineNumber - 1 - 50);
const endLine = Math.min(
fileLines.length,
match.lineNumber - 1 + 50 + 1,
);
const contextLines = fileLines.slice(startLine, endLine);
llmContent += `Match at line ${match.lineNumber}:\n`;
contextLines.forEach((line, index) => {
const currentLineNumber = startLine + index + 1;
if (currentLineNumber === match.lineNumber) {
llmContent += `> L${currentLineNumber}: ${line}\n`;
} else {
llmContent += ` L${currentLineNumber}: ${line}\n`;
}
});
llmContent += '\n';
} else {
const trimmedLine = match.line.trim();
llmContent += `L${match.lineNumber}: ${trimmedLine}\n`;
}
}
llmContent += '---\n';
}
} else {
for (const filePath in matchesByFile) {
llmContent += `File: ${filePath}
`;
matchesByFile[filePath].forEach((match) => {
const trimmedLine = match.line.trim();
llmContent += `L${match.lineNumber}: ${trimmedLine}\n`;
});
llmContent += '---\n';
matchesByFile[filePath].forEach((match) => {
const trimmedLine = match.line.trim();
llmContent += `L${match.lineNumber}: ${trimmedLine}\n`;
});
llmContent += '---\n';
}
}
return {
@@ -569,6 +618,7 @@ class GrepToolInvocation extends BaseToolInvocation<
filePath:
path.relative(absolutePath, fileAbsolutePath) ||
path.basename(fileAbsolutePath),
absolutePath: fileAbsolutePath,
lineNumber: index + 1,
line,
});