Compare commits

...
Author SHA1 Message Date
Brendonovich 95c9c96598 fix(app): interleave repeated file patch hunks 2026-08-25 03:42:37 +00:00
2 changed files with 55 additions and 7 deletions
@@ -71,13 +71,36 @@ describe("apply patch files", () => {
expect(groups[0]?.deletions).toBe(2)
})
test("keeps sequential partial patches under one file", () => {
test("merges sequential partial patches under one file", () => {
const groups = patchFileGroups([
{ file: "src/a.ts", patch: "@@ -1 +1 @@\n-a\n+b", additions: 1, deletions: 1, status: "modified" },
{ file: "src/a.ts", patch: "@@ -2 +2 @@\n-c\n+d", additions: 1, deletions: 1, status: "modified" },
])
expect(groups).toHaveLength(1)
expect(groups[0]?.views).toHaveLength(2)
expect(groups[0]?.views).toHaveLength(1)
})
test("orders partial patch hunks by their position in the file", () => {
const groups = patchFileGroups([
{
file: "src/a.ts",
patch: "@@ -10 +10 @@\n-old 10\n+new 10\n@@ -200 +200 @@\n-old 200\n+new 200",
additions: 2,
deletions: 2,
status: "modified",
},
{
file: "src/a.ts",
patch: "@@ -50 +50 @@\n-old 50\n+new 50\n@@ -300 +300 @@\n-old 300\n+new 300",
additions: 2,
deletions: 2,
status: "modified",
},
])
expect(groups[0]?.views.flatMap((view) => view.fileDiff.hunks.map((hunk) => hunk.additionStart))).toEqual([
10, 50, 200, 300,
])
})
})
@@ -1,5 +1,5 @@
import type { FileDiffInfo } from "@opencode-ai/client/promise"
import { diffLines } from "diff"
import { diffLines, formatPatch, parsePatch } from "diff"
import { completePatchContents, normalize, type ViewDiff } from "./session-diff"
type Kind = "add" | "update" | "delete"
@@ -9,11 +9,12 @@ export type ApplyPatchFile = {
type: Kind
additions: number
deletions: number
patch: string
view: ViewDiff
contents?: { before: string; after: string }
}
export type ApplyPatchFileGroup = Omit<ApplyPatchFile, "view" | "contents"> & { views: ViewDiff[] }
export type ApplyPatchFileGroup = Omit<ApplyPatchFile, "patch" | "view" | "contents"> & { views: ViewDiff[] }
export function changedFileDiff(value: unknown): value is FileDiffInfo {
if (!value || typeof value !== "object") return false
@@ -33,6 +34,7 @@ export function patchFile(value: unknown): ApplyPatchFile | undefined {
type: value.status === "added" ? "add" : value.status === "deleted" ? "delete" : "update",
additions: value.additions,
deletions: value.deletions,
patch: value.patch,
view: normalize(value),
contents: completePatchContents(value.patch),
}
@@ -58,12 +60,14 @@ export function patchFileGroups(value: unknown): ApplyPatchFileGroup[] {
(file, index) => !!file.contents && (index === 0 || files[index - 1]?.contents?.after === file.contents.before),
)
if (!chained) {
const additions = files.reduce((total, file) => total + file.additions, 0)
const deletions = files.reduce((total, file) => total + file.deletions, 0)
return {
path,
type,
additions: files.reduce((total, file) => total + file.additions, 0),
deletions: files.reduce((total, file) => total + file.deletions, 0),
views: files.map((file) => file.view),
additions,
deletions,
views: mergePartialViews(path, type, additions, deletions, files),
}
}
@@ -92,3 +96,24 @@ export function patchFileGroups(value: unknown): ApplyPatchFileGroup[] {
}
})
}
function mergePartialViews(path: string, type: Kind, additions: number, deletions: number, files: ApplyPatchFile[]) {
try {
const patches = files.map((file) => parsePatch(file.patch)[0])
if (patches.some((patch) => !patch?.hunks.length)) return files.map((file) => file.view)
const hunks = patches
.flatMap((patch) => patch?.hunks ?? [])
.toSorted((a, b) => Math.min(a.oldStart, a.newStart) - Math.min(b.oldStart, b.newStart))
return [
normalize({
file: path,
patch: formatPatch({ oldFileName: path, newFileName: path, oldHeader: "", newHeader: "", hunks }),
status: type === "add" ? "added" : type === "delete" ? "deleted" : "modified",
additions,
deletions,
}),
]
} catch {
return files.map((file) => file.view)
}
}