feat: enable hashline by default

pull/13854/merge
Shoubhit Dash 2026-02-22 22:31:33 +05:30
parent ce5c827a6e
commit 9ef803be82
9 changed files with 50 additions and 33 deletions

View File

@ -1186,11 +1186,16 @@ export namespace Config {
.object({
disable_paste_summary: z.boolean().optional(),
batch_tool: z.boolean().optional().describe("Enable the batch tool"),
hashline_edit: z.boolean().optional().describe("Enable hashline-backed edit/read tool behavior"),
hashline_edit: z
.boolean()
.optional()
.describe("Enable hashline-backed edit/read tool behavior (default true, set false to disable)"),
hashline_autocorrect: z
.boolean()
.optional()
.describe("Enable hashline autocorrect cleanup for copied prefixes and formatting artifacts"),
.describe(
"Enable hashline autocorrect cleanup for copied prefixes and formatting artifacts (default true)",
),
openTelemetry: z
.boolean()
.optional()

View File

@ -467,9 +467,9 @@ export const EditTool = Tool.define("edit", {
}
const config = await Config.get()
if (config.experimental?.hashline_edit !== true) {
if (config.experimental?.hashline_edit === false) {
throw new Error(
"Hashline edit payload is disabled. Enable experimental.hashline_edit to use hashline operations.",
"Hashline edit payload is disabled. Set experimental.hashline_edit to true to use hashline operations.",
)
}
@ -483,7 +483,7 @@ export const EditTool = Tool.define("edit", {
return executeHashline(
hashlineParams,
ctx,
config.experimental?.hashline_autocorrect === true || Bun.env.OPENCODE_HL_AUTOCORRECT === "1",
config.experimental?.hashline_autocorrect !== false || Bun.env.OPENCODE_HL_AUTOCORRECT === "1",
)
},
})

View File

@ -12,11 +12,12 @@ Legacy schema (always supported):
- The edit fails if `oldString` matches multiple locations and `replaceAll` is not true.
- Use `replaceAll: true` for global replacements.
Hashline schema (requires `experimental.hashline_edit: true`):
Hashline schema (default behavior):
- `{ filePath, edits, delete?, rename? }`
- Do not mix legacy fields (`oldString/newString/replaceAll`) with hashline fields (`edits/delete/rename`) in one call.
- Use strict anchor references from `Read` output: `LINE#ID`.
- Optional cleanup behavior can be enabled with `experimental.hashline_autocorrect: true`.
- Hashline mode can be turned off with `experimental.hashline_edit: false`.
- Autocorrect cleanup is on by default and can be turned off with `experimental.hashline_autocorrect: false`.
- When `Read` returns `LINE#ID:<content>`, prefer hashline operations.
- Operations:
- `set_line { line, text }`

View File

@ -194,7 +194,7 @@ export const ReadTool = Tool.define("read", {
throw new Error(`Offset ${offset} is out of range for this file (${lines} lines)`)
}
const useHashline = (await Config.get()).experimental?.hashline_edit === true
const useHashline = (await Config.get()).experimental?.hashline_edit !== false
const content = raw.map((line, index) => {
const lineNumber = index + offset
if (useHashline) return `${hashlineRef(lineNumber, full[index])}:${line}`

View File

@ -8,8 +8,8 @@ Usage:
- Use the grep tool to find specific content in large files or files with long lines.
- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.
- Contents are returned with a line prefix.
- Default format: `<line>: <content>` (example: `1: foo`).
- When `experimental.hashline_edit` is enabled: `LINE#ID:<content>` (example: `1#AB:foo`). Use these anchors for hashline edits.
- Default format: `LINE#ID:<content>` (example: `1#AB:foo`). Use these anchors for hashline edits.
- Legacy format can be restored with `experimental.hashline_edit: false`: `<line>: <content>` (example: `1: foo`).
- For directories, entries are returned one per line (without line numbers) with a trailing `/` for subdirectories.
- Any line longer than 2000 characters is truncated.
- Call this tool in parallel when you know there are multiple files you want to read.

View File

@ -143,7 +143,7 @@ export namespace ToolRegistry {
return model.providerID === "opencode" || Flag.OPENCODE_ENABLE_EXA
}
if (config.experimental?.hashline_edit === true) {
if (config.experimental?.hashline_edit !== false) {
if (t.id === "apply_patch") return false
return true
}

View File

@ -744,6 +744,11 @@ describe("tool.edit", () => {
test("rejects hashline payload when experimental mode is disabled", async () => {
await using tmp = await tmpdir({
config: {
experimental: {
hashline_edit: false,
},
},
init: async (dir) => {
await fs.writeFile(path.join(dir, "file.txt"), "a", "utf-8")
},

View File

@ -270,10 +270,10 @@ describe("tool.read truncation", () => {
fn: async () => {
const read = await ReadTool.init()
const result = await read.execute({ filePath: path.join(tmp.path, "offset.txt"), offset: 10, limit: 5 }, ctx)
expect(result.output).toContain("10: line10")
expect(result.output).toContain("14: line14")
expect(result.output).not.toContain("9: line10")
expect(result.output).not.toContain("15: line15")
expect(result.output).toContain(hashlineLine(10, "line10"))
expect(result.output).toContain(hashlineLine(14, "line14"))
expect(result.output).not.toContain(hashlineLine(9, "line9"))
expect(result.output).not.toContain(hashlineLine(15, "line15"))
expect(result.output).toContain("line10")
expect(result.output).toContain("line14")
expect(result.output).not.toContain("line0")
@ -445,13 +445,8 @@ root_type Monster;`
})
describe("tool.read hashline output", () => {
test("returns LINE#ID prefixes when hashline mode is enabled", async () => {
test("returns LINE#ID prefixes by default", async () => {
await using tmp = await tmpdir({
config: {
experimental: {
hashline_edit: true,
},
},
init: async (dir) => {
await Bun.write(path.join(dir, "hashline.txt"), "foo\nbar")
},
@ -471,6 +466,11 @@ describe("tool.read hashline output", () => {
test("keeps legacy line prefixes when hashline mode is disabled", async () => {
await using tmp = await tmpdir({
config: {
experimental: {
hashline_edit: false,
},
},
init: async (dir) => {
await Bun.write(path.join(dir, "legacy.txt"), "foo\nbar")
},

View File

@ -7,14 +7,8 @@ describe("tool.registry hashline routing", () => {
test.each([
{ providerID: "openai", modelID: "gpt-5" },
{ providerID: "anthropic", modelID: "claude-3-7-sonnet" },
])("disables apply_patch and enables edit when experimental hashline is on (%o)", async (model) => {
await using tmp = await tmpdir({
config: {
experimental: {
hashline_edit: true,
},
},
})
])("disables apply_patch and enables edit by default (%o)", async (model) => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
@ -28,8 +22,14 @@ describe("tool.registry hashline routing", () => {
})
})
test("keeps existing GPT apply_patch routing when experimental hashline is off", async () => {
await using tmp = await tmpdir()
test("keeps existing GPT apply_patch routing when hashline is explicitly disabled", async () => {
await using tmp = await tmpdir({
config: {
experimental: {
hashline_edit: false,
},
},
})
await Instance.provide({
directory: tmp.path,
@ -45,8 +45,14 @@ describe("tool.registry hashline routing", () => {
})
})
test("keeps existing non-GPT routing when experimental hashline is off", async () => {
await using tmp = await tmpdir()
test("keeps existing non-GPT routing when hashline is explicitly disabled", async () => {
await using tmp = await tmpdir({
config: {
experimental: {
hashline_edit: false,
},
},
})
await Instance.provide({
directory: tmp.path,