diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 618fdffa0..6750f385d 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -35,6 +35,7 @@ ### Fixed - Fixed the `pi_read` range translation padding the slice it asks for. `piReadPath` composed a plain `:N+K` selector, which the local `read` tool expands by one leading and three trailing context line — so a frame naming offset 5/limit 20 received lines 4-27. Ranged Pi reads now compose `:raw:N+K`; the wire result is an opaque output string, so the line-number gutter `raw` also drops carries nothing the contract needs. +- Fixed ranged legacy Cursor reads reporting the returned window byte length as the full file size. - Fixed four Cursor exec frames answering with a result whose oneof was never set. In proto3 that is not an empty result — the server reads it as "the tool ran and produced nothing", indistinguishable from real success. `listMcpResourcesExecResult`, `readMcpResourceExecResult`, `recordScreenResult` and `computerUseResult` now send `ListMcpResourcesSuccess{resources: []}`, `ReadMcpResourceNotFound{uri}`, `RecordScreenFailure` and `ComputerUseError` respectively. - The MCP resource frames now answer from the host instead of a fixed verdict. `CursorExecHandlers` gained `listMcpResources`/`readMcpResource`, so a host holding live MCP connections advertises them; the empty catalog and `not_found` above remain the answer when no handler is supplied. A handler that throws surfaces as `ListMcpResourcesError`/`ReadMcpResourceError` rather than collapsing into "none exist", which the model cannot retry. A read carrying `download_path` forwards it and answers with `ReadMcpResourceSuccess.download_path` and no content, which is what that mode means. - Fixed Cursor `connect_scm` calls losing their repository and settling on a fabricated verdict. The target rides in the `ConnectScmArgs.target` oneof, so reading a flat `github` property always saw `undefined`; and the authoritative `success`/`error`/`rejected` result only arrives on the completion frame, so answering at the announcement persisted a fixed failure for every call — including the ones the server went on to accept. The block now opens on the start frame and settles from the completion's decoded result. diff --git a/packages/ai/src/providers/cursor.ts b/packages/ai/src/providers/cursor.ts index 94511e0c3..4160141f8 100644 --- a/packages/ai/src/providers/cursor.ts +++ b/packages/ai/src/providers/cursor.ts @@ -2439,6 +2439,13 @@ function readTotalLinesFromDetails(toolResult: ToolResultMessage): number | unde return typeof totalLines === "number" && Number.isFinite(totalLines) ? totalLines : undefined; } +function readFileSizeFromDetails(toolResult: ToolResultMessage): number | undefined { + const details = toolResult.details; + if (!details || typeof details !== "object" || !("fileSize" in details)) return undefined; + const { fileSize } = details; + return typeof fileSize === "number" && Number.isSafeInteger(fileSize) && fileSize >= 0 ? fileSize : undefined; +} + function buildReadResultFromToolResult(path: string, toolResult: ToolResultMessage, rangeApplied = false) { const text = toolResultToText(toolResult); if (toolResult.isError) { @@ -2455,7 +2462,7 @@ function buildReadResultFromToolResult(path: string, toolResult: ToolResultMessa value: create(ReadSuccessSchema, { path, totalLines, - fileSize: BigInt(Buffer.byteLength(text, "utf-8")), + fileSize: BigInt(readFileSizeFromDetails(toolResult) ?? Buffer.byteLength(text, "utf-8")), truncated: toolResultWasTruncated(toolResult), output: { case: "content", value: text }, // Set when this client composed the frame's window onto the read, diff --git a/packages/ai/test/cursor-exec-modern.test.ts b/packages/ai/test/cursor-exec-modern.test.ts index 8d5836838..68624164c 100644 --- a/packages/ai/test/cursor-exec-modern.test.ts +++ b/packages/ai/test/cursor-exec-modern.test.ts @@ -1899,6 +1899,7 @@ describe("Cursor exec answers: what the result claims about the work", () => { details: { truncation: { truncated: true, totalLines: 97 }, meta: { truncation: { totalLines: 101 } }, + fileSize: 4096, }, }); }, @@ -1910,6 +1911,7 @@ describe("Cursor exec answers: what the result claims about the work", () => { if (answer.value.result.case !== "success") throw new Error(`got ${answer.value.result.case}`); expect(answer.value.result.value.totalLines).toBe(101); expect(answer.value.result.value.rangeApplied).toBe(true); + expect(answer.value.result.value.fileSize).toBe(4096n); }); it("counts the payload when the read returned the file whole", async () => { diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 74b9e1521..7f4924b21 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -22,6 +22,7 @@ ### Fixed +- Fixed Cursor ranged-read results losing the full file byte size after applying the requested window. - Fixed `/tan` agents being unable to read parent-session `local://` attachments by correctly resolving local protocol options against the parent session's artifacts. - Fixed Codex web search silently returning plain completions when the hosted web search tool was skipped. - Fixed TUI collaboration guest loader not starting when joining or reconnecting mid-turn. diff --git a/packages/coding-agent/src/tools/read.ts b/packages/coding-agent/src/tools/read.ts index d6f0e0bee..95cd116a7 100644 --- a/packages/coding-agent/src/tools/read.ts +++ b/packages/coding-agent/src/tools/read.ts @@ -735,6 +735,8 @@ export interface ReadToolDetails { method?: string; notes?: string[]; meta?: OutputMeta; + /** Full on-disk byte size recorded before applying a file range. */ + fileSize?: number; /** Raw text + start line for user-visible TUI rendering, set when content is text-like. * Mirrors the same lines the model receives but without hashline/line-number prefixes, * so the TUI can render the file content with its own gutter without re-parsing the formatted text. */ @@ -2959,6 +2961,7 @@ export class ReadTool implements AgentTool { } } + details.fileSize = fileSize; this.#markMarkdownContentType(details, absolutePath); if (suffixResolution) { details.suffixResolution = suffixResolution;