Skip to content

Commit c6977a8

Browse files
authored
fix(core): preserve trailing blank lines in read pages
1 parent 1dcc655 commit c6977a8

2 files changed

Lines changed: 94 additions & 1 deletion

File tree

packages/core/src/tool/plugin/read.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,9 @@ export const toModelContent = (path: string, offset: number | undefined, output:
192192
}
193193

194194
const start = output.type === "text-page" ? output.offset : 1
195-
const lines = output.content === "" ? [] : output.content.replace(/\n$/, "").split("\n")
195+
// Pages already join selected lines; a trailing newline represents a selected blank line.
196+
const text = output.type === "file" ? output.content.replace(/\n$/, "") : output.content
197+
const lines = output.content === "" ? [] : text.split("\n")
196198
const content = [
197199
lines.length === 0 ? `Read file ${path}, 0 lines` : `Read file ${path}, lines ${start}-${start + lines.length - 1}`,
198200
]

packages/core/test/tool-read-filesystem.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fs from "fs/promises"
33
import path from "path"
44
import { Environment } from "@opencode/core/environment/index"
55
import { AbsolutePath } from "@opencode/core/schema"
6+
import { ReadTool } from "@opencode/core/tool/plugin/read"
67
import { ReadToolFileSystem } from "@opencode/core/tool/read-filesystem"
78
import { CrossSpawnSpawner } from "@opencode/util/cross-spawn-spawner"
89
import { LayerNodePlatform } from "@opencode/util/effect/app-node-platform"
@@ -20,6 +21,96 @@ const fixture = Effect.gen(function* () {
2021
})
2122
const absolute = (value: string) => AbsolutePath.make(value)
2223

24+
describe("ReadTool text serialization", () => {
25+
const cases = [
26+
{
27+
name: "preserves a selected trailing blank line before continuation",
28+
content: "alpha\n\nomega\n",
29+
page: { offset: 1, limit: 2 },
30+
output: { type: "text-page", content: "alpha\n", offset: 1, truncated: true, next: 3 },
31+
model: "Read file lines.txt, lines 1-2\n1: alpha\n2: \n[Output truncated. Continue reading with offset: 3]",
32+
},
33+
{
34+
name: "preserves multiple selected trailing blank lines at a noninitial offset",
35+
content: "before\nalpha\n\n\nomega\n",
36+
page: { offset: 2, limit: 3 },
37+
output: { type: "text-page", content: "alpha\n\n", offset: 2, truncated: true, next: 5 },
38+
model: "Read file lines.txt, lines 2-4\n2: alpha\n3: \n4: \n[Output truncated. Continue reading with offset: 5]",
39+
},
40+
{
41+
name: "preserves a selected trailing blank line at EOF",
42+
content: "alpha\n\n",
43+
page: { limit: 2 },
44+
output: { type: "text-page", content: "alpha\n", offset: 1, truncated: false },
45+
model: "Read file lines.txt, lines 1-2\n1: alpha\n2: ",
46+
},
47+
{
48+
name: "preserves internal blank lines in a page",
49+
content: "alpha\n\nomega\n",
50+
page: { limit: 3 },
51+
output: { type: "text-page", content: "alpha\n\nomega", offset: 1, truncated: false },
52+
model: "Read file lines.txt, lines 1-3\n1: alpha\n2: \n3: omega",
53+
},
54+
{
55+
name: "preserves continuation for a nonblank page",
56+
content: "alpha\n\nomega\n",
57+
page: { limit: 1 },
58+
output: { type: "text-page", content: "alpha", offset: 1, truncated: true, next: 2 },
59+
model: "Read file lines.txt, lines 1-1\n1: alpha\n[Output truncated. Continue reading with offset: 2]",
60+
},
61+
{
62+
name: "strips only the terminal file newline in a whole-file read",
63+
content: "alpha\n\n",
64+
page: {},
65+
output: { type: "file", content: "alpha\n\n", encoding: "utf8" },
66+
model: "Read file lines.txt, lines 1-2\n1: alpha\n2: ",
67+
},
68+
{
69+
name: "does not add a line for a whole-file terminal newline",
70+
content: "alpha\n",
71+
page: {},
72+
output: { type: "file", content: "alpha\n", encoding: "utf8" },
73+
model: "Read file lines.txt, lines 1-1\n1: alpha",
74+
},
75+
{
76+
name: "preserves a whole-file read without a terminal newline",
77+
content: "alpha",
78+
page: {},
79+
output: { type: "file", content: "alpha", encoding: "utf8" },
80+
model: "Read file lines.txt, lines 1-1\n1: alpha",
81+
},
82+
{
83+
name: "preserves empty whole-file output",
84+
content: "",
85+
page: {},
86+
output: { type: "file", content: "", encoding: "utf8" },
87+
model: "Read file lines.txt, 0 lines",
88+
},
89+
{
90+
name: "preserves empty-file page output",
91+
content: "",
92+
page: { limit: 2 },
93+
output: { type: "text-page", content: "", offset: 1, truncated: false },
94+
model: "Read file lines.txt, 0 lines",
95+
},
96+
]
97+
98+
cases.forEach((input) => {
99+
it.live(input.name, () =>
100+
Effect.gen(function* () {
101+
const current = yield* fixture
102+
const file = absolute(path.join(current.directory, "lines.txt"))
103+
yield* current.files.writeFileString(file, input.content)
104+
105+
const result = yield* ReadToolFileSystem.read(current.environment, file, "lines.txt", input.page)
106+
107+
expect(result).toMatchObject(input.output)
108+
expect(ReadTool.toModelContent("lines.txt", undefined, result)).toBe(input.model)
109+
}),
110+
)
111+
})
112+
})
113+
23114
describe("ReadToolFileSystem", () => {
24115
it.effect("preserves the environment not-found error", () =>
25116
Effect.gen(function* () {

0 commit comments

Comments
 (0)