fix(coding-agent): added gap separators between noncontiguous search matches

- SearchTool now tracked the last emitted line and inserted ellipsis markers when noncontiguous match blocks were output.
- Display output gap markers were padded to align with code-frame gutters.
- Added a regression test that verified a no-context search emits an ellipsis between separated matches in the same file.
This commit is contained in:
can1357
2026-05-15 23:15:29 +02:00
parent feb6c74c84
commit 85515f35a6
2 changed files with 25 additions and 0 deletions
@@ -328,11 +328,18 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
return nextWidth; return nextWidth;
}, 0); }, 0);
const cacheEntries: Array<readonly [number, string]> = []; const cacheEntries: Array<readonly [number, string]> = [];
let lastEmittedLine: number | undefined;
const gutterPad = " ".repeat(lineNumberWidth + 1);
for (const match of fileMatches) { for (const match of fileMatches) {
const pushLine = (lineNumber: number, line: string, isMatch: boolean, recordable: boolean) => { const pushLine = (lineNumber: number, line: string, isMatch: boolean, recordable: boolean) => {
if (lastEmittedLine !== undefined && lineNumber > lastEmittedLine + 1) {
modelOut.push("...");
displayOut.push(`${gutterPad}│...`);
}
modelOut.push(formatMatchLine(lineNumber, line, isMatch, { useHashLines })); modelOut.push(formatMatchLine(lineNumber, line, isMatch, { useHashLines }));
displayOut.push(formatCodeFrameLine(isMatch ? "*" : " ", lineNumber, line, lineNumberWidth)); displayOut.push(formatCodeFrameLine(isMatch ? "*" : " ", lineNumber, line, lineNumberWidth));
if (recordable) cacheEntries.push([lineNumber, line] as const); if (recordable) cacheEntries.push([lineNumber, line] as const);
lastEmittedLine = lineNumber;
}; };
if (match.contextBefore) { if (match.contextBefore) {
for (const ctx of match.contextBefore) { for (const ctx of match.contextBefore) {
+18
View File
@@ -1419,6 +1419,24 @@ function b() {
expect(output).toMatch(/\*5\|match two/); expect(output).toMatch(/\*5\|match two/);
}); });
it("inserts a gap separator between non-contiguous match blocks", async () => {
const testFile = path.join(testDir, "gaps.txt");
const lines = Array.from({ length: 10 }, (_, idx) => (idx === 0 || idx === 5 ? "match" : `filler ${idx}`));
fs.writeFileSync(testFile, lines.join("\n"));
const noContextSettings = Settings.isolated({ "search.contextBefore": 0, "search.contextAfter": 0 });
const noContextSearchTool = wrapToolWithMetaNotice(
new SearchTool(createTestToolSession(testDir, noContextSettings)),
);
const result = await noContextSearchTool.execute("test-call-12-gap", {
pattern: "match",
paths: [testFile],
});
const output = getTextOutput(result);
expect(output).toMatch(/\*1\|match\n\.\.\.\n\*6\|match/);
});
it("should paginate files via the skip parameter", async () => { it("should paginate files via the skip parameter", async () => {
const skipDir = path.join(testDir, "skip-dir"); const skipDir = path.join(testDir, "skip-dir");
fs.mkdirSync(skipDir, { recursive: true }); fs.mkdirSync(skipDir, { recursive: true });