Skip to content

Commit 482c59b

Browse files
fix: recover moved fileUpdate diff from new file
1 parent a639237 commit 482c59b

2 files changed

Lines changed: 78 additions & 11 deletions

File tree

src/CodexToolCallMapper.ts

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ToolCallContent } from "@agentclientprotocol/sdk";
2-
import { applyPatch, parsePatch } from "diff";
2+
import { applyPatch, parsePatch, reversePatch } from "diff";
33
import { readFile } from "node:fs/promises";
44
import path from "node:path";
55
import type { UpdateSessionEvent } from "./ACPSessionConnection";
@@ -293,18 +293,44 @@ async function createAddFileContent(change: FileUpdateChange): Promise<ToolCallC
293293
}
294294

295295
async function createUpdateFileContent(change: FileUpdateChange): Promise<ToolCallContent | null> {
296-
const oldContent = await readFile(change.path, { encoding: "utf8" }).catch(() => null);
297-
if (oldContent === null) return null;
296+
if (change.kind.type !== "update") return null;
298297

299298
const unifiedDiff = recoverCorruptedDiff(change.diff);
300-
const newContent = applyPatch(oldContent, unifiedDiff);
301-
if (!newContent) return null;
299+
const movePath = change.kind.move_path;
302300

301+
const oldContent = await readFileContent(change.path);
302+
if (oldContent !== null) {
303+
const patchedContent = applyPatch(oldContent, unifiedDiff);
304+
if (patchedContent === false) return null;
305+
return createUpdateDiffContent(movePath ?? change.path, oldContent, patchedContent);
306+
}
307+
308+
if (!movePath) return null;
309+
const newContent = await readFileContent(movePath);
310+
if (newContent === null) return null;
311+
312+
const revertedPatch = revertPatch(unifiedDiff);
313+
if (!revertedPatch) return null;
314+
315+
const revertedContent = applyPatch(newContent, revertedPatch);
316+
if (revertedContent === false) return null;
317+
318+
return createUpdateDiffContent(movePath, revertedContent, newContent);
319+
}
320+
321+
function revertPatch(unifiedDiff: string) {
322+
const [patch] = parsePatch(unifiedDiff);
323+
if (!patch) return null;
324+
325+
return reversePatch(patch);
326+
}
327+
328+
function createUpdateDiffContent(path: string, oldText: string, newText: string): ToolCallContent {
303329
return {
304330
type: "diff",
305-
oldText: oldContent,
306-
newText: newContent,
307-
path: change.path,
331+
oldText,
332+
newText,
333+
path,
308334
_meta: {
309335
kind: "update",
310336
},
@@ -313,9 +339,8 @@ async function createUpdateFileContent(change: FileUpdateChange): Promise<ToolCa
313339

314340
async function createDeleteFileContent(change: FileUpdateChange): Promise<ToolCallContent> {
315341
// If the patch deletes a file, the old content may be only available from the diff.
316-
const oldContent = await readFile(change.path, { encoding: "utf8"} ).catch(() =>
317-
isUnifiedDiff(change.diff) ? patchToDeletedContent(change.diff) : change.diff
318-
);
342+
const fileContent = await readFileContent(change.path);
343+
const oldContent = fileContent ?? (isUnifiedDiff(change.diff) ? patchToDeletedContent(change.diff) : change.diff);
319344

320345
return {
321346
type: "diff",
@@ -328,6 +353,10 @@ async function createDeleteFileContent(change: FileUpdateChange): Promise<ToolCa
328353
}
329354
}
330355

356+
async function readFileContent(filePath: string): Promise<string | null> {
357+
return await readFile(filePath, { encoding: "utf8" }).catch(() => null);
358+
}
359+
331360
/**
332361
* Fix unified diff content corrupted by codex agent.
333362
* Removes synthetic "Moved to" from the end.

src/__tests__/CodexACPAgent/file-change-events.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,44 @@ Moved to: /test/project/NewFile.kt`,
330330
{
331331
oldText: 'old code line\n',
332332
newText: 'new code line\n',
333+
path: '/test/project/NewFile.kt',
334+
},
335+
],
336+
});
337+
});
338+
339+
it('should parse update diffs when the original file was moved already', async () => {
340+
mockFileContent('/test/project/NewFile.kt', 'new code line\n');
341+
342+
const fileChange: ThreadItem = {
343+
type: 'fileChange',
344+
id: 'file-change-moved-file-exists',
345+
changes: [
346+
{
347+
path: '/test/project/OriginalFile.kt',
348+
kind: {
349+
type: 'update',
350+
move_path: '/test/project/NewFile.kt',
351+
},
352+
diff:
353+
`@@ -1 +1 @@
354+
-old code line
355+
+new code line
356+
357+
358+
Moved to: /test/project/NewFile.kt`,
359+
},
360+
],
361+
status: 'inProgress',
362+
};
363+
364+
const updateEvent = await createFileChangeUpdate(fileChange);
365+
expect(updateEvent).toMatchObject({
366+
content: [
367+
{
368+
oldText: 'old code line\n',
369+
newText: 'new code line\n',
370+
path: '/test/project/NewFile.kt',
333371
},
334372
],
335373
});

0 commit comments

Comments
 (0)