fix(coding-agent): hide line range for full-file read calls with null offset/limit

Strict tool schemas make models send null for omitted optional fields.
The read renderer only treated undefined as missing, so full-file reads
rendered as "path:1".

fixes #9996
This commit is contained in:
David Brailovsky 2026-09-25 11:35:36 +02:00
parent 92e8d4f02a
commit 49681e1b71
3 changed files with 20 additions and 2 deletions

View file

@ -16,6 +16,7 @@
- Prevented managed git packages from automatically installing Pi peer dependencies and added warnings for extension packages that list host-provided modules in `dependencies` ([#9863](https://github.com/earendil-works/pi/issues/9863)). - Prevented managed git packages from automatically installing Pi peer dependencies and added warnings for extension packages that list host-provided modules in `dependencies` ([#9863](https://github.com/earendil-works/pi/issues/9863)).
- Fixed pinned git extensions loaded with `-e` continuing to use the first downloaded commit after the ref changes ([#9982](https://github.com/earendil-works/pi/issues/9982)). - Fixed pinned git extensions loaded with `-e` continuing to use the first downloaded commit after the ref changes ([#9982](https://github.com/earendil-works/pi/issues/9982)).
- Fixed `RpcClient` skipping the next event listener when a listener unsubscribes while handling an event, which could make `waitForIdle()` time out after `collectEvents()` ([#9990](https://github.com/earendil-works/pi/issues/9990)). - Fixed `RpcClient` skipping the next event listener when a listener unsubscribes while handling an event, which could make `waitForIdle()` time out after `collectEvents()` ([#9990](https://github.com/earendil-works/pi/issues/9990)).
- Fixed full-file `read` calls rendering as `:1` when models send `null` for omitted `offset` and `limit` ([#9996](https://github.com/earendil-works/pi/issues/9996)).
## [0.87.1] - 2026-09-22 ## [0.87.1] - 2026-09-22

View file

@ -26,9 +26,10 @@ interface CompactReadClassification {
const COMPACT_RESOURCE_FILE_NAMES = new Set(["AGENTS.override.md", "AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]); const COMPACT_RESOURCE_FILE_NAMES = new Set(["AGENTS.override.md", "AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]);
type ReadRenderArgs = { path?: string; file_path?: string; offset?: number; limit?: number }; type ReadRenderArgs = { path?: string; file_path?: string; offset?: number; limit?: number };
function formatReadLineRange(args: ReadRenderArgs | undefined, theme: Theme): string { function formatReadLineRange(args: ReadRenderArgs | undefined, theme: Theme): string {
if (args?.offset === undefined && args?.limit === undefined) return ""; // Strict tool schemas make models send null for omitted optional fields.
if (args?.offset == null && args?.limit == null) return "";
const startLine = args.offset ?? 1; const startLine = args.offset ?? 1;
const endLine = args.limit !== undefined ? startLine + args.limit - 1 : ""; const endLine = args.limit != null ? startLine + args.limit - 1 : "";
return theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`); return theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
} }
function formatReadCall(args: ReadRenderArgs | undefined, theme: Theme, cwd: string): string { function formatReadCall(args: ReadRenderArgs | undefined, theme: Theme, cwd: string): string {

View file

@ -294,6 +294,22 @@ describe("ToolExecutionComponent parity", () => {
expect(rendered.match(/\bread\b/g)?.length ?? 0).toBe(1); expect(rendered.match(/\bread\b/g)?.length ?? 0).toBe(1);
}); });
// Issue #9996: strict tool schemas make models send null for omitted optional fields.
test("renders read calls with null offset and limit as full-file reads", () => {
const component = new ToolExecutionComponent(
"read",
"tool-read-null-range",
{ path: "src/example.ts", offset: null, limit: null },
{},
createReadToolDefinition(process.cwd()),
createFakeTui(),
process.cwd(),
);
const rendered = stripAnsi(component.render(120).join("\n"));
expect(rendered).toContain("read src/example.ts");
expect(rendered).not.toContain("src/example.ts:");
});
test("inherits missing built-in result renderer slot from the built-in tool", () => { test("inherits missing built-in result renderer slot from the built-in tool", () => {
const overrideDefinition: ToolDefinition = { const overrideDefinition: ToolDefinition = {
...createBaseToolDefinition("read"), ...createBaseToolDefinition("read"),