fix(coding-agent): keep grep context lines aligned with ripgrep when a file has a lone CR (#11465)

When `context` is set, the grep tool reads the file again to print the
lines around each match. It turned CRLF into LF and also turned every
lone CR into LF before splitting the text into lines. ripgrep, which
supplies the match line numbers, counts lines only at LF. A lone CR
therefore added an extra line on pi's side, so every later line was off
by one.

For example, in a file containing `before\rprogress\nneedle\nafter`, a
search for `needle` with `context: 1` showed `progress` as the line-2
match and left out `after`.

The fix keeps the CRLF normalization and splits only on LF, so pi
numbers lines the same way ripgrep does. Displayed lines already have
`\r` removed, so the line before the match now shows as
`beforeprogress`.

**Testing**
- Added a test in `packages/coding-agent/test/tools.test.ts` for a file
with a lone CR searched with `context: 1`. It fails without the fix and
passes with it.
- `test/tools.test.ts`: all 87 tests pass.
- `npm run check` passes.
- Reproduction from the issue. Before the fix:
  ```
  f.txt-1- before
  f.txt:2: progress
  f.txt-3- needle
  ```
  After the fix:
  ```
  f.txt-1- beforeprogress
  f.txt:2: needle
  f.txt-3- after
  ```

Fixes #11398

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
This commit is contained in:
robopipi[bot] 2026-10-27 09:07:23 +01:00 • committed by GitHub
parent 6c384aa30f
commit fecc19e4e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 18 additions and 1 deletions

View file

@ -150,7 +150,7 @@ export function createGrepToolDefinition(
if (!lines) {
try {
const content = await ops.readFile(filePath);
lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
lines = content.replace(/\r\n/g, "\n").split("\n");
} catch {
lines = [];
}

View file

@ -908,6 +908,23 @@ describe("Coding Agent Tools", () => {
expect(output).not.toContain("match two");
});
// https://github.com/earendil-works/pi/issues/11398
it("should keep context line numbers aligned with ripgrep when a file contains a lone CR", async () => {
const testFile = join(testDir, "lone-cr.txt");
writeFileSync(testFile, "before\rprogress\nneedle\nafter");
const result = await grepTool.execute("test-call-lone-cr", {
pattern: "needle",
path: testFile,
context: 1,
});
const output = getTextOutput(result);
expect(output).toContain("lone-cr.txt-1- beforeprogress");
expect(output).toContain("lone-cr.txt:2: needle");
expect(output).toContain("lone-cr.txt-3- after");
});
// https://github.com/earendil-works/pi/issues/11203
it("should ignore RIPGREP_CONFIG_PATH", async () => {
const configFile = join(testDir, "rg-config");