From db8d3c6590909eff5f00a16a709b5d7cb0f67f2b Mon Sep 17 00:00:00 2001 From: Justin Carper Date: Tue, 16 Jun 2026 15:19:21 -0400 Subject: [PATCH] feat(read): surface lines-read/total in Read transcript label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cursor ReadArgs = { path } only — no offset/limit (cursor-sdk-shared ReadArgsSchema is a strip object). Two same-path reads are re-reads of identical content, never different sections. Derive per-call detail from result instead: count content lines and compare to totalLines. Title becomes e.g. "handlers.go (2000/5000 lines)" so a reader can spot redundant re-reads and see truncation at a glance. Also stores linesReturned + fileSize in block metadata, and documents the no-range constraint in a comment above the adapter so the question does not recur. --- src/provider/stream-map.ts | 19 ++++++++++++++++++- test/stream-map.test.ts | 25 +++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/provider/stream-map.ts b/src/provider/stream-map.ts index 768b485..8f0b488 100644 --- a/src/provider/stream-map.ts +++ b/src/provider/stream-map.ts @@ -313,6 +313,15 @@ const NATIVE_ADAPTERS: Record = { }, }, // Cursor `read` → opencode `read`. + // + // Cursor's read tool takes ONLY `{ path }` — there is no `offset`/`limit`/ + // line-range arg (see `@cursor/sdk` `ReadArgsSchema`, a `{ path: string }` + // "strip" object). The model therefore cannot request a sub-range, so two + // same-path reads in a transcript are RE-READS of the same content, never + // "different sections". The only per-call detail available is in the result + // `value` (`content`, `totalLines`, `fileSize`); we derive the lines-read + // count from `content` and surface `linesReturned/totalLines` in the title + // so a reader can see how much was read and spot redundant re-reads. read: { tool: "read", input: (args) => ({ filePath: strField(args, "path") ?? "" }), @@ -321,12 +330,20 @@ const NATIVE_ADAPTERS: Record = { if (content === undefined) return null; const filePath = strField(args, "path") ?? ""; const totalLines = numField(value, "totalLines"); + const fileSize = numField(value, "fileSize"); + const linesReturned = content.split("\n").length; + const lineLabel = + totalLines !== undefined + ? `${linesReturned}/${totalLines} lines` + : `${linesReturned} lines`; return { - title: filePath, + title: `${filePath} (${lineLabel})`, metadata: { preview: content.split("\n").slice(0, 20).join("\n"), loaded: [] as string[], + linesReturned, ...(totalLines !== undefined ? { totalLines } : {}), + ...(fileSize !== undefined ? { fileSize } : {}), }, output: content, }; diff --git a/test/stream-map.test.ts b/test/stream-map.test.ts index f1cf927..d18ef36 100644 --- a/test/stream-map.test.ts +++ b/test/stream-map.test.ts @@ -894,9 +894,30 @@ describe("native tool mapping (blocks)", () => { input: JSON.stringify({ filePath: "/a.ts" }), }); expect(foldedResult(result)).toMatchObject({ - title: "/a.ts", + title: "/a.ts (2/2 lines)", output: "l1\nl2", - metadata: { preview: "l1\nl2", totalLines: 2 }, + metadata: { + preview: "l1\nl2", + totalLines: 2, + linesReturned: 2, + fileSize: 6, + }, + }); + }); + + it("read title shows lines-read/total when Cursor truncates the file", async () => { + const { result } = await mapTool( + "read", + { path: "/a.ts" }, + { + status: "success", + value: { content: "l1", totalLines: 10, fileSize: 99 }, + }, + ); + expect(foldedResult(result)).toMatchObject({ + title: "/a.ts (1/10 lines)", + output: "l1", + metadata: { linesReturned: 1, totalLines: 10, fileSize: 99 }, }); });