diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index bce6eaf01..f1c172fef 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog ## [Unreleased] +### Added + +- Support for `move` parameter in `computeHashlineDiff` to enable file move operations alongside content edits + +### Changed + +- Modified no-op detection logic to allow move-only operations when file content remains unchanged ## [13.3.1] - 2026-02-26 diff --git a/packages/coding-agent/src/modes/components/tool-execution.ts b/packages/coding-agent/src/modes/components/tool-execution.ts index 1ae25d2e1..e71645129 100644 --- a/packages/coding-agent/src/modes/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/components/tool-execution.ts @@ -204,12 +204,13 @@ export class ToolExecutionComponent extends Container { return; } const edits = this.#args?.edits; + const move = this.#args?.move; if (path && Array.isArray(edits)) { - const argsKey = JSON.stringify({ path, edits }); + const argsKey = JSON.stringify({ path, edits, move }); if (this.#editDiffArgsKey === argsKey) return; this.#editDiffArgsKey = argsKey; - computeHashlineDiff({ path, edits }, this.#cwd).then(result => { + computeHashlineDiff({ path, edits, move }, this.#cwd).then(result => { if (this.#editDiffArgsKey === argsKey) { this.#editDiffPreview = result; this.#updateDisplay(); diff --git a/packages/coding-agent/src/patch/diff.ts b/packages/coding-agent/src/patch/diff.ts index 6757cff17..456c597f5 100644 --- a/packages/coding-agent/src/patch/diff.ts +++ b/packages/coding-agent/src/patch/diff.ts @@ -386,10 +386,10 @@ export async function computePatchDiff( * Used for preview rendering in the TUI before hashline-mode edits execute. */ export async function computeHashlineDiff( - input: { path: string; edits: HashlineEdit[] }, + input: { path: string; edits: HashlineEdit[]; move?: string }, cwd: string, ): Promise { - const { path, edits } = input; + const { path, edits, move } = input; const absolutePath = resolveToCwd(path, cwd); try { @@ -414,7 +414,7 @@ export async function computeHashlineDiff( const normalizedContent = normalizeToLF(content); const result = applyHashlineEdits(normalizedContent, edits); - if (normalizedContent === result.lines) { + if (normalizedContent === result.lines && !move) { return { error: `No changes would be made to ${path}. The edits produce identical content.` }; } diff --git a/packages/coding-agent/test/edit-diff.test.ts b/packages/coding-agent/test/edit-diff.test.ts index 3b75f4f54..b681d36ad 100644 --- a/packages/coding-agent/test/edit-diff.test.ts +++ b/packages/coding-agent/test/edit-diff.test.ts @@ -1,5 +1,13 @@ -import { describe, expect, test } from "bun:test"; -import { adjustIndentation, DEFAULT_FUZZY_THRESHOLD, findEditMatch } from "@oh-my-pi/pi-coding-agent/patch"; +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import * as fs from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; +import { + adjustIndentation, + computeHashlineDiff, + DEFAULT_FUZZY_THRESHOLD, + findEditMatch, +} from "@oh-my-pi/pi-coding-agent/patch"; describe("findEditMatch", () => { describe("exact matching", () => { @@ -207,3 +215,44 @@ describe("adjustIndentation", () => { expect(result).toBe("bar"); }); }); + +describe("computeHashlineDiff", () => { + let tempDir = ""; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "edit-diff-hashline-")); + }); + + afterEach(async () => { + if (tempDir) { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + test("returns no-op error for unchanged content when move is absent", async () => { + const sourcePath = path.join(tempDir, "source.txt"); + await Bun.write(sourcePath, "unchanged content\n"); + + const result = await computeHashlineDiff({ path: sourcePath, edits: [] }, tempDir); + expect("error" in result).toBe(true); + if ("error" in result) { + expect(result.error).toContain("No changes would be made"); + } + }); + + test("allows move-only operation when content is unchanged", async () => { + const sourcePath = path.join(tempDir, "source.txt"); + await Bun.write(sourcePath, "unchanged content\n"); + + const result = await computeHashlineDiff( + { path: sourcePath, edits: [], move: path.join(tempDir, "moved", "target.txt") }, + tempDir, + ); + + expect("error" in result).toBe(false); + if ("diff" in result) { + expect(result.diff).toBe(""); + expect(result.firstChangedLine).toBeUndefined(); + } + }); +});