refactor(snapshot): store unified patches in file diffs (#21244)

Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
This commit is contained in:
Dax
2026-04-07 19:48:23 -04:00
committed by GitHub
parent 463318486f
commit b7fab49b64
30 changed files with 343 additions and 183 deletions

View File

@@ -2124,7 +2124,7 @@ function ApplyPatch(props: ToolProps<typeof ApplyPatchTool>) {
</text>
}
>
<Diff diff={file.diff} filePath={file.filePath} />
<Diff diff={file.patch} filePath={file.filePath} />
<Diagnostics diagnostics={props.metadata.diagnostics} filePath={file.movePath ?? file.filePath} />
</Show>
</BlockTool>

View File

@@ -1,4 +1,5 @@
import { Effect, Layer, ServiceMap, Stream } from "effect"
import { formatPatch, structuredPatch } from "diff"
import path from "path"
import { Bus } from "@/bus"
import { BusEvent } from "@/bus/bus-event"
@@ -7,7 +8,6 @@ import { makeRuntime } from "@/effect/run-service"
import { AppFileSystem } from "@/filesystem"
import { FileWatcher } from "@/file/watcher"
import { Git } from "@/git"
import { Snapshot } from "@/snapshot"
import { Log } from "@/util/log"
import { Instance } from "./instance"
import z from "zod"
@@ -49,6 +49,8 @@ export namespace Vcs {
map: Map<string, { additions: number; deletions: number }>,
) {
const base = ref ? yield* git.prefix(cwd) : ""
const patch = (file: string, before: string, after: string) =>
formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER }))
const next = yield* Effect.forEach(
list,
(item) =>
@@ -58,12 +60,11 @@ export namespace Vcs {
const stat = map.get(item.file)
return {
file: item.file,
before,
after,
patch: patch(item.file, before, after),
additions: stat?.additions ?? (item.status === "added" ? count(after) : 0),
deletions: stat?.deletions ?? (item.status === "deleted" ? count(before) : 0),
status: item.status,
} satisfies Snapshot.FileDiff
} satisfies FileDiff
}),
{ concurrency: 8 },
)
@@ -125,11 +126,24 @@ export namespace Vcs {
})
export type Info = z.infer<typeof Info>
export const FileDiff = z
.object({
file: z.string(),
patch: z.string(),
additions: z.number(),
deletions: z.number(),
status: z.enum(["added", "deleted", "modified"]).optional(),
})
.meta({
ref: "VcsFileDiff",
})
export type FileDiff = z.infer<typeof FileDiff>
export interface Interface {
readonly init: () => Effect.Effect<void>
readonly branch: () => Effect.Effect<string | undefined>
readonly defaultBranch: () => Effect.Effect<string | undefined>
readonly diff: (mode: Mode) => Effect.Effect<Snapshot.FileDiff[]>
readonly diff: (mode: Mode) => Effect.Effect<FileDiff[]>
}
interface State {

View File

@@ -154,7 +154,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket, app: Hono = new Hono()
description: "VCS diff",
content: {
"application/json": {
schema: resolver(Snapshot.FileDiff.array()),
schema: resolver(Vcs.FileDiff.array()),
},
},
},

View File

@@ -59,7 +59,7 @@ export namespace ShareNext {
}
| {
type: "session_diff"
data: SDK.FileDiff[]
data: SDK.SnapshotFileDiff[]
}
| {
type: "model"

View File

@@ -1,6 +1,6 @@
import { NodeFileSystem, NodePath } from "@effect/platform-node"
import { Cause, Duration, Effect, Layer, Schedule, Semaphore, ServiceMap, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { formatPatch, structuredPatch } from "diff"
import path from "path"
import z from "zod"
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
@@ -22,14 +22,13 @@ export namespace Snapshot {
export const FileDiff = z
.object({
file: z.string(),
before: z.string(),
after: z.string(),
patch: z.string(),
additions: z.number(),
deletions: z.number(),
status: z.enum(["added", "deleted", "modified"]).optional(),
})
.meta({
ref: "FileDiff",
ref: "SnapshotFileDiff",
})
export type FileDiff = z.infer<typeof FileDiff>
@@ -521,8 +520,6 @@ export namespace Snapshot {
const map = new Map<string, { before: string; after: string }>()
const dec = new TextDecoder()
let i = 0
// Parse the default `git cat-file --batch` stream: one header line,
// then exactly `size` bytes of blob content, then a trailing newline.
for (const ref of refs) {
let end = i
while (end < out.length && out[end] !== 10) end += 1
@@ -620,8 +617,9 @@ export namespace Snapshot {
]
})
const step = 100
const patch = (file: string, before: string, after: string) =>
formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER }))
// Keep batches bounded so a large diff does not buffer every blob at once.
for (let i = 0; i < rows.length; i += step) {
const run = rows.slice(i, i + step)
const text = yield* load(run)
@@ -631,8 +629,7 @@ export namespace Snapshot {
const [before, after] = row.binary ? ["", ""] : text ? [hit.before, hit.after] : yield* show(row)
result.push({
file: row.file,
before,
after,
patch: row.binary ? "" : patch(row.file, before, after),
additions: row.additions,
deletions: row.deletions,
status: row.status,

View File

@@ -164,9 +164,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
filePath: change.filePath,
relativePath: path.relative(Instance.worktree, change.movePath ?? change.filePath).replaceAll("\\", "/"),
type: change.type,
diff: change.diff,
before: change.oldContent,
after: change.newContent,
patch: change.diff,
additions: change.additions,
deletions: change.deletions,
movePath: change.movePath,

View File

@@ -123,8 +123,7 @@ export const EditTool = Tool.define("edit", {
const filediff: Snapshot.FileDiff = {
file: filePath,
before: contentOld,
after: contentNew,
patch: diff,
additions: 0,
deletions: 0,
}