From 9ef803be82a3940f97815be6a2d7bd9792d52163 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Sun, 22 Feb 2026 22:31:33 +0530 Subject: [PATCH] feat: enable hashline by default --- packages/opencode/src/config/config.ts | 9 ++++-- packages/opencode/src/tool/edit.ts | 6 ++-- packages/opencode/src/tool/edit.txt | 5 ++-- packages/opencode/src/tool/read.ts | 2 +- packages/opencode/src/tool/read.txt | 4 +-- packages/opencode/src/tool/registry.ts | 2 +- packages/opencode/test/tool/edit.test.ts | 5 ++++ packages/opencode/test/tool/read.test.ts | 20 ++++++------- .../test/tool/registry-hashline.test.ts | 30 +++++++++++-------- 9 files changed, 50 insertions(+), 33 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index aad60764d2..b312503f5a 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -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() diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index 577fbdceb0..a97189a8b6 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -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", ) }, }) diff --git a/packages/opencode/src/tool/edit.txt b/packages/opencode/src/tool/edit.txt index b16af91a84..eaee266337 100644 --- a/packages/opencode/src/tool/edit.txt +++ b/packages/opencode/src/tool/edit.txt @@ -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:`, prefer hashline operations. - Operations: - `set_line { line, text }` diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 847c8a4c52..544bfa2429 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -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}` diff --git a/packages/opencode/src/tool/read.txt b/packages/opencode/src/tool/read.txt index cfa401f532..921b2384ef 100644 --- a/packages/opencode/src/tool/read.txt +++ b/packages/opencode/src/tool/read.txt @@ -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: `: ` (example: `1: foo`). -- When `experimental.hashline_edit` is enabled: `LINE#ID:` (example: `1#AB:foo`). Use these anchors for hashline edits. +- Default format: `LINE#ID:` (example: `1#AB:foo`). Use these anchors for hashline edits. +- Legacy format can be restored with `experimental.hashline_edit: false`: `: ` (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. diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 7e0481b6d7..94fb4b0a0d 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -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 } diff --git a/packages/opencode/test/tool/edit.test.ts b/packages/opencode/test/tool/edit.test.ts index 82159b3b1c..cf20c11aee 100644 --- a/packages/opencode/test/tool/edit.test.ts +++ b/packages/opencode/test/tool/edit.test.ts @@ -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") }, diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index 6af64fd5c8..f9e8a355ac 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -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") }, diff --git a/packages/opencode/test/tool/registry-hashline.test.ts b/packages/opencode/test/tool/registry-hashline.test.ts index e7cf8a334b..bd620702f8 100644 --- a/packages/opencode/test/tool/registry-hashline.test.ts +++ b/packages/opencode/test/tool/registry-hashline.test.ts @@ -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,