fix(cursor): honor legacy read range and grep offset

Modern Cursor builds paginate the legacy `read` and `grep` frames with
fields this branch modeled in the proto but never wired.

`read` composed no range, so every page returned the whole file (or its
own truncation) and a model walking a large file never advanced past the
first window. It now goes through `piReadPath`, the same helper the Pi
frame uses, so both translate a range identically - including the
`limit: 0` case, which asks for zero lines and has no selector. The
answer reports `range_applied`, left false for an unranged read since
that is precisely the server's "this is the whole file".

`grep` dropped its `offset`. The local tool paginates by file through
`skip` and advertises exactly that unit in its own "use skip=N" advice,
so an unforwarded offset re-ran the identical search and answered page
one forever. A present `0` stays unset: it means "start at the
beginning", which is the un-skipped search.

(cherry picked from commit 4f647d86b6e4a507d57fb4d247f19db8240e51cf)
This commit is contained in:
Diogo Soares Rodrigues
2026-07-27 16:57:30 -03:00
committed by can1357
parent d9bc1e80b6
commit 4414ee4b0d
5 changed files with 98 additions and 4 deletions
@@ -1533,3 +1533,41 @@ describe("Cursor modern exec frames: server-resolved tool calls leave a paired b
expect(output.content.filter(block => block.type === "toolCall")).toHaveLength(0);
});
});
describe("Cursor legacy read frame: range reporting", () => {
it("reports rangeApplied only when the frame asked for a window", async () => {
// `range_applied: false` tells the server "this is the whole file", which
// is true for an unranged read and a lie for a paginated one — a model
// walking a large file would stop after the first window believing it had
// seen everything.
const handlers: CursorExecHandlers = {
async read() {
return toolResult("line1\nline2");
},
};
const ranged = await dispatchExec(
buildExecMessage({
case: "readArgs",
value: create(ReadArgsSchema, { path: "/repo/big.ts", toolCallId: "c1", offset: 5, limit: 20 }),
}),
{ execHandlers: handlers },
);
const rangedAnswer = soleResult(ranged.frames);
if (rangedAnswer.case !== "readResult") throw new Error(`got ${rangedAnswer.case}`);
if (rangedAnswer.value.result.case !== "success") throw new Error(`got ${rangedAnswer.value.result.case}`);
expect(rangedAnswer.value.result.value.rangeApplied).toBe(true);
const whole = await dispatchExec(
buildExecMessage({
case: "readArgs",
value: create(ReadArgsSchema, { path: "/repo/big.ts", toolCallId: "c2" }),
}),
{ execHandlers: handlers },
);
const wholeAnswer = soleResult(whole.frames);
if (wholeAnswer.case !== "readResult") throw new Error(`got ${wholeAnswer.case}`);
if (wholeAnswer.value.result.case !== "success") throw new Error(`got ${wholeAnswer.value.result.case}`);
expect(wholeAnswer.value.result.value.rangeApplied).toBe(false);
});
});