From fb0346fe459f28564971dbbc0dcd49d47f77f46d Mon Sep 17 00:00:00 2001 From: qingminlong <0668001063@xydigit.com> Date: Fri, 17 Jul 2026 09:38:27 +0800 Subject: [PATCH] fix(read): normalize displayed line ranges for limits (#105105) Co-authored-by: Peter Steinberger --- src/agents/sessions/tools/read.test.ts | 25 ++++++++++++++++++++++++- src/agents/sessions/tools/read.ts | 4 +++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/agents/sessions/tools/read.test.ts b/src/agents/sessions/tools/read.test.ts index 29545681da0a..cc2e3a8b83d3 100644 --- a/src/agents/sessions/tools/read.test.ts +++ b/src/agents/sessions/tools/read.test.ts @@ -9,7 +9,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useAutoCleanupTempDirTracker } from "../../../../test/helpers/temp-dir.js"; import { withEnvAsync } from "../../../test-utils/env.js"; import { createReadToolDefinition } from "./read.js"; -import { DEFAULT_MAX_BYTES } from "./truncate.js"; +import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "./truncate.js"; const decodeWindowsTextFileBufferMock = vi.hoisted(() => vi.fn(() => "")); const tempDirs = useAutoCleanupTempDirTracker(afterEach); @@ -43,6 +43,21 @@ function textContent( return first?.type === "text" ? (first.text ?? "") : ""; } +const plainTheme = { + fg: (_token: string, text: string) => text, + bold: (text: string) => text, +} as never; + +function renderReadCall(args: { path: string; offset?: number; limit?: number }): string { + const tool = createReadToolDefinition("/workspace"); + const component = tool.renderCall?.(args, plainTheme, { + lastComponent: undefined, + expanded: true, + cwd: "/workspace", + } as never); + return component?.render(120).join("\n").trimEnd() ?? ""; +} + describe("read tool", () => { beforeEach(() => { decodeWindowsTextFileBufferMock.mockReset(); @@ -151,6 +166,14 @@ describe("read tool", () => { expect(textContent(result)).toBe("alpha\n\n[2 more lines in file. Use offset=2 to continue.]"); }); + it.each([ + { limit: -1, range: ":1-1" }, + { limit: 1.5, range: ":1-1" }, + { limit: Number.POSITIVE_INFINITY, range: `:1-${DEFAULT_MAX_LINES}` }, + ])("normalizes read call line ranges for limit $limit", ({ limit, range }) => { + expect(renderReadCall({ path: "notes.txt", limit })).toBe(`read notes.txt${range}`); + }); + it.each([0, -1, 1.5])("rejects invalid offset %s before accessing the file", async (offset) => { const access = vi.fn(async () => {}); const detectImageMimeType = vi.fn(async () => null); diff --git a/src/agents/sessions/tools/read.ts b/src/agents/sessions/tools/read.ts index 9e94e10dbe39..8dc8e90b3ea6 100644 --- a/src/agents/sessions/tools/read.ts +++ b/src/agents/sessions/tools/read.ts @@ -87,7 +87,9 @@ function formatReadLineRange(args: ReadRenderArgs | undefined, theme: Theme): st return ""; } const startLine = args.offset ?? 1; - const endLine = args.limit !== undefined ? startLine + args.limit - 1 : ""; + const normalizedLimit = + args.limit !== undefined ? normalizePositiveLimit(args.limit, DEFAULT_MAX_LINES) : undefined; + const endLine = normalizedLimit !== undefined ? startLine + normalizedLimit - 1 : ""; return theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`); }