Skip to content

Commit 006d673

Browse files
authored
tweak: make read tool offset 1 indexed instead of 0 to avoid confusion that could be caused by line #s being 1 based (anomalyco#13198)
1 parent 6b4d617 commit 006d673

4 files changed

Lines changed: 38 additions & 13 deletions

File tree

packages/opencode/src/session/prompt.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,9 +1022,9 @@ export namespace SessionPrompt {
10221022
}
10231023
}
10241024
}
1025-
offset = Math.max(start - 1, 0)
1025+
offset = Math.max(start, 1)
10261026
if (end) {
1027-
limit = end - offset
1027+
limit = end - (offset - 1)
10281028
}
10291029
}
10301030
const args = { filePath: filepath, offset, limit }

packages/opencode/src/tool/read.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ export const ReadTool = Tool.define("read", {
1818
description: DESCRIPTION,
1919
parameters: z.object({
2020
filePath: z.string().describe("The absolute path to the file or directory to read"),
21-
offset: z.coerce.number().describe("The 0-based line offset to start reading from").optional(),
21+
offset: z.coerce.number().describe("The line number to start reading from (1-indexed)").optional(),
2222
limit: z.coerce.number().describe("The maximum number of lines to read (defaults to 2000)").optional(),
2323
}),
2424
async execute(params, ctx) {
25+
if (params.offset !== undefined && params.offset < 1) {
26+
throw new Error("offset must be greater than or equal to 1")
27+
}
2528
let filepath = params.filePath
2629
if (!path.isAbsolute(filepath)) {
2730
filepath = path.resolve(Instance.directory, filepath)
@@ -78,9 +81,10 @@ export const ReadTool = Tool.define("read", {
7881
entries.sort((a, b) => a.localeCompare(b))
7982

8083
const limit = params.limit ?? DEFAULT_READ_LIMIT
81-
const offset = params.offset || 0
82-
const sliced = entries.slice(offset, offset + limit)
83-
const truncated = offset + sliced.length < entries.length
84+
const offset = params.offset ?? 1
85+
const start = offset - 1
86+
const sliced = entries.slice(start, start + limit)
87+
const truncated = start + sliced.length < entries.length
8488

8589
const output = [
8690
`<path>${filepath}</path>`,
@@ -138,13 +142,15 @@ export const ReadTool = Tool.define("read", {
138142
if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`)
139143

140144
const limit = params.limit ?? DEFAULT_READ_LIMIT
141-
const offset = params.offset || 0
145+
const offset = params.offset ?? 1
146+
const start = offset - 1
142147
const lines = await file.text().then((text) => text.split("\n"))
148+
if (start >= lines.length) throw new Error(`Offset ${offset} is out of range for this file (${lines.length} lines)`)
143149

144150
const raw: string[] = []
145151
let bytes = 0
146152
let truncatedByBytes = false
147-
for (let i = offset; i < Math.min(lines.length, offset + limit); i++) {
153+
for (let i = start; i < Math.min(lines.length, start + limit); i++) {
148154
const line = lines[i].length > MAX_LINE_LENGTH ? lines[i].substring(0, MAX_LINE_LENGTH) + "..." : lines[i]
149155
const size = Buffer.byteLength(line, "utf-8") + (raw.length > 0 ? 1 : 0)
150156
if (bytes + size > MAX_BYTES) {
@@ -156,15 +162,15 @@ export const ReadTool = Tool.define("read", {
156162
}
157163

158164
const content = raw.map((line, index) => {
159-
return `${index + offset + 1}: ${line}`
165+
return `${index + offset}: ${line}`
160166
})
161167
const preview = raw.slice(0, 20).join("\n")
162168

163169
let output = [`<path>${filepath}</path>`, `<type>file</type>`, "<content>"].join("\n")
164170
output += content.join("\n")
165171

166172
const totalLines = lines.length
167-
const lastReadLine = offset + raw.length
173+
const lastReadLine = offset + raw.length - 1
168174
const hasMoreLines = totalLines > lastReadLine
169175
const truncated = hasMoreLines || truncatedByBytes
170176

packages/opencode/src/tool/read.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ Read a file or directory from the local filesystem. If the path does not exist,
33
Usage:
44
- The filePath parameter should be an absolute path.
55
- By default, this tool returns up to 2000 lines from the start of the file.
6+
- The offset parameter is the line number to start from (1-indexed).
67
- To read later sections, call this tool again with a larger offset.
78
- Use the grep tool to find specific content in large files or files with long lines.
89
- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.

packages/opencode/test/tool/read.test.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ describe("tool.read truncation", () => {
258258
test("respects offset parameter", async () => {
259259
await using tmp = await tmpdir({
260260
init: async (dir) => {
261-
const lines = Array.from({ length: 20 }, (_, i) => `line${i}`).join("\n")
261+
const lines = Array.from({ length: 20 }, (_, i) => `line${i + 1}`).join("\n")
262262
await Bun.write(path.join(dir, "offset.txt"), lines)
263263
},
264264
})
@@ -275,19 +275,37 @@ describe("tool.read truncation", () => {
275275
})
276276
})
277277

278+
test("throws when offset is beyond end of file", async () => {
279+
await using tmp = await tmpdir({
280+
init: async (dir) => {
281+
const lines = Array.from({ length: 3 }, (_, i) => `line${i + 1}`).join("\n")
282+
await Bun.write(path.join(dir, "short.txt"), lines)
283+
},
284+
})
285+
await Instance.provide({
286+
directory: tmp.path,
287+
fn: async () => {
288+
const read = await ReadTool.init()
289+
await expect(
290+
read.execute({ filePath: path.join(tmp.path, "short.txt"), offset: 4, limit: 5 }, ctx),
291+
).rejects.toThrow("Offset 4 is out of range for this file (3 lines)")
292+
},
293+
})
294+
})
295+
278296
test("does not mark final directory page as truncated", async () => {
279297
await using tmp = await tmpdir({
280298
init: async (dir) => {
281299
await Promise.all(
282-
Array.from({ length: 10 }, (_, i) => Bun.write(path.join(dir, "dir", `file-${i}.txt`), `line${i}`)),
300+
Array.from({ length: 10 }, (_, i) => Bun.write(path.join(dir, "dir", `file-${i + 1}.txt`), `line${i}`)),
283301
)
284302
},
285303
})
286304
await Instance.provide({
287305
directory: tmp.path,
288306
fn: async () => {
289307
const read = await ReadTool.init()
290-
const result = await read.execute({ filePath: path.join(tmp.path, "dir"), offset: 5, limit: 5 }, ctx)
308+
const result = await read.execute({ filePath: path.join(tmp.path, "dir"), offset: 6, limit: 5 }, ctx)
291309
expect(result.metadata.truncated).toBe(false)
292310
expect(result.output).not.toContain("Showing 5 of 10 entries")
293311
},

0 commit comments

Comments
 (0)