From 5647cf83cd960e26e3ca8cd35a0098343b00b10c Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 19 Feb 2026 15:54:15 +0900 Subject: [PATCH] feat(hashline-read-enhancer): add write tool support and fix early termination - Support write tool in addition to read tool - Fix early termination when encountering non-matching lines Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- src/hooks/hashline-read-enhancer/hook.ts | 14 +++- .../hashline-read-enhancer/index.test.ts | 78 ++++++++++++------- 2 files changed, 63 insertions(+), 29 deletions(-) diff --git a/src/hooks/hashline-read-enhancer/hook.ts b/src/hooks/hashline-read-enhancer/hook.ts index 18dfabe2e..285fe737b 100644 --- a/src/hooks/hashline-read-enhancer/hook.ts +++ b/src/hooks/hashline-read-enhancer/hook.ts @@ -8,7 +8,8 @@ interface HashlineReadEnhancerConfig { const READ_LINE_PATTERN = /^(\d+): (.*)$/ function isReadTool(toolName: string): boolean { - return toolName.toLowerCase() === "read" + const lower = toolName.toLowerCase() + return lower === "read" || lower === "write" } function shouldProcess(config: HashlineReadEnhancerConfig): boolean { @@ -39,7 +40,16 @@ function transformOutput(output: string): string { return output } const lines = output.split("\n") - return lines.map(transformLine).join("\n") + const result: string[] = [] + for (const line of lines) { + if (!READ_LINE_PATTERN.test(line)) { + result.push(line) + result.push(...lines.slice(result.length)) + break + } + result.push(transformLine(line)) + } + return result.join("\n") } export function createHashlineReadEnhancerHook( diff --git a/src/hooks/hashline-read-enhancer/index.test.ts b/src/hooks/hashline-read-enhancer/index.test.ts index b660d39aa..5d6571ab1 100644 --- a/src/hooks/hashline-read-enhancer/index.test.ts +++ b/src/hooks/hashline-read-enhancer/index.test.ts @@ -71,6 +71,48 @@ describe("createHashlineReadEnhancerHook", () => { expect(output.output).toContain("|") }) + + it("should process 'write' tool (lowercase)", async () => { + //#given + const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true)) + const input = { tool: "write", sessionID, callID: "call-1" } + const output = { title: "Write", output: "1: hello\n2: world", metadata: {} } + + //#when + await hook["tool.execute.after"](input, output) + + //#then + expect(output.output).toContain("|") + }) + + it("should process 'Write' tool (mixed case)", async () => { + //#given + const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true)) + const input = { tool: "Write", sessionID, callID: "call-1" } + const output = { title: "Write", output: "1: hello\n2: world", metadata: {} } + + //#when + await hook["tool.execute.after"](input, output) + + //#then + expect(output.output).toContain("|") + }) + + it("should transform write tool output to LINE:HASH|content format", async () => { + //#given + const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true)) + const input = { tool: "write", sessionID, callID: "call-1" } + const output = { title: "Write", output: "1: const x = 1\n2: const y = 2", metadata: {} } + + //#when + await hook["tool.execute.after"](input, output) + + //#then + const lines = output.output.split("\n") + expect(lines[0]).toMatch(/^1:[a-f0-9]{2}\|const x = 1$/) + expect(lines[1]).toMatch(/^2:[a-f0-9]{2}\|const y = 2$/) + }) + it("should skip non-read tools", async () => { //#given const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true)) @@ -209,22 +251,20 @@ describe("createHashlineReadEnhancerHook", () => { const hook = createHashlineReadEnhancerHook(mockCtx, createMockConfig(true)) const input = { tool: "read", sessionID, callID: "call-1" } const fileLines = ["import { foo } from './bar'", " const x = 1", "", "export default x"] - const readOutput = fileLines.map((line, i) => `${i + 1}: ${line}`).join(" -") + const readOutput = fileLines.map((line, i) => `${i + 1}: ${line}`).join("\n") const output = { title: "Read", output: readOutput, metadata: {} } //#when await hook["tool.execute.after"](input, output) //#then - each hash in Read output must satisfy validateLineRef - const transformedLines = output.output.split(" -") + const transformedLines = output.output.split("\n") for (let i = 0; i < fileLines.length; i++) { const lineNum = i + 1 const expectedHash = computeLineHash(lineNum, fileLines[i]) expect(transformedLines[i]).toBe(`${lineNum}:${expectedHash}|${fileLines[i]}`) // Must not throw when used with validateLineRef - expect(() => validateLineRef({ line: lineNum, hash: expectedHash }, fileLines)).not.toThrow() + expect(() => validateLineRef(fileLines, `${lineNum}:${expectedHash}`)).not.toThrow() } }) }) @@ -236,11 +276,7 @@ describe("createHashlineReadEnhancerHook", () => { const input = { tool: "read", sessionID, callID: "call-1" } const output = { title: "Read", - output: "1: const x = 1 -2: const y = 2 -[Project README] -1: First item -2: Second item", + output: ["1: const x = 1", "2: const y = 2", "[Project README]", "1: First item", "2: Second item"].join("\n"), metadata: {}, } @@ -248,8 +284,7 @@ describe("createHashlineReadEnhancerHook", () => { await hook["tool.execute.after"](input, output) //#then - only original file content gets hashified - const lines = output.output.split(" -") + const lines = output.output.split("\n") expect(lines[0]).toMatch(/^1:[a-f0-9]{2}\|const x = 1$/) expect(lines[1]).toMatch(/^2:[a-f0-9]{2}\|const y = 2$/) expect(lines[2]).toBe("[Project README]") @@ -263,10 +298,7 @@ describe("createHashlineReadEnhancerHook", () => { const input = { tool: "read", sessionID, callID: "call-1" } const output = { title: "Read", - output: "1: const x = 1 -2: const y = 2 - -(File has more lines. Use 'offset' parameter to read beyond line 2)", + output: ["1: const x = 1", "2: const y = 2", "", "(File has more lines. Use 'offset' parameter to read beyond line 2)"].join("\n"), metadata: {}, } @@ -274,8 +306,7 @@ describe("createHashlineReadEnhancerHook", () => { await hook["tool.execute.after"](input, output) //#then - footer passes through unchanged - const lines = output.output.split(" -") + const lines = output.output.split("\n") expect(lines[0]).toMatch(/^1:[a-f0-9]{2}\|const x = 1$/) expect(lines[1]).toMatch(/^2:[a-f0-9]{2}\|const y = 2$/) expect(lines[2]).toBe("") @@ -288,13 +319,7 @@ describe("createHashlineReadEnhancerHook", () => { const input = { tool: "read", sessionID, callID: "call-1" } const output = { title: "Read", - output: "1: export function hello() { -2: return 42 -3: } - -[System Reminder] -1: Do not forget X -2: Always do Y", + output: ["1: export function hello() {", "2: return 42", "3: }", "", "[System Reminder]", "1: Do not forget X", "2: Always do Y"].join("\n"), metadata: {}, } @@ -302,8 +327,7 @@ describe("createHashlineReadEnhancerHook", () => { await hook["tool.execute.after"](input, output) //#then - const lines = output.output.split(" -") + const lines = output.output.split("\n") expect(lines[0]).toMatch(/^1:[a-f0-9]{2}\|export function hello\(\) \{$/) expect(lines[1]).toMatch(/^2:[a-f0-9]{2}\| return 42$/) expect(lines[2]).toMatch(/^3:[a-f0-9]{2}\|}$/)