diff --git a/bun.lock b/bun.lock index 897ca4fa93..df1485f400 100644 --- a/bun.lock +++ b/bun.lock @@ -533,6 +533,7 @@ "@solid-primitives/resize-observer": "2.1.3", "@solidjs/meta": "catalog:", "@solidjs/router": "catalog:", + "diff": "catalog:", "dompurify": "3.3.1", "fuzzysort": "catalog:", "katex": "0.16.27", diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts index 4af6365535..01248e20e8 100644 --- a/packages/app/src/context/global-sync/event-reducer.ts +++ b/packages/app/src/context/global-sync/event-reducer.ts @@ -1,7 +1,6 @@ import { Binary } from "@opencode-ai/util/binary" import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store" import type { - FileDiff, Message, Part, PermissionRequest, @@ -9,6 +8,7 @@ import type { QuestionRequest, Session, SessionStatus, + SnapshotFileDiff, Todo, } from "@opencode-ai/sdk/v2/client" import type { State, VcsCache } from "./types" @@ -161,7 +161,7 @@ export function applyDirectoryEvent(input: { break } case "session.diff": { - const props = event.properties as { sessionID: string; diff: FileDiff[] } + const props = event.properties as { sessionID: string; diff: SnapshotFileDiff[] } input.setStore("session_diff", props.sessionID, reconcile(props.diff, { key: "file" })) break } diff --git a/packages/app/src/context/global-sync/session-cache.test.ts b/packages/app/src/context/global-sync/session-cache.test.ts index 8e11110e3d..472ac219e9 100644 --- a/packages/app/src/context/global-sync/session-cache.test.ts +++ b/packages/app/src/context/global-sync/session-cache.test.ts @@ -1,11 +1,11 @@ import { describe, expect, test } from "bun:test" import type { - FileDiff, Message, Part, PermissionRequest, QuestionRequest, SessionStatus, + SnapshotFileDiff, Todo, } from "@opencode-ai/sdk/v2/client" import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache" @@ -33,7 +33,7 @@ describe("app session cache", () => { test("dropSessionCaches clears orphaned parts without message rows", () => { const store: { session_status: Record - session_diff: Record + session_diff: Record todo: Record message: Record part: Record @@ -64,7 +64,7 @@ describe("app session cache", () => { const m = msg("msg_1", "ses_1") const store: { session_status: Record - session_diff: Record + session_diff: Record todo: Record message: Record part: Record diff --git a/packages/app/src/context/global-sync/session-cache.ts b/packages/app/src/context/global-sync/session-cache.ts index 0177ebbe13..6f4d81062b 100644 --- a/packages/app/src/context/global-sync/session-cache.ts +++ b/packages/app/src/context/global-sync/session-cache.ts @@ -1,10 +1,10 @@ import type { - FileDiff, Message, Part, PermissionRequest, QuestionRequest, SessionStatus, + SnapshotFileDiff, Todo, } from "@opencode-ai/sdk/v2/client" @@ -12,7 +12,7 @@ export const SESSION_CACHE_LIMIT = 40 type SessionCache = { session_status: Record - session_diff: Record + session_diff: Record todo: Record message: Record part: Record diff --git a/packages/app/src/context/global-sync/types.ts b/packages/app/src/context/global-sync/types.ts index 1d6e550f8e..b0f340a902 100644 --- a/packages/app/src/context/global-sync/types.ts +++ b/packages/app/src/context/global-sync/types.ts @@ -2,7 +2,6 @@ import type { Agent, Command, Config, - FileDiff, LspStatus, McpStatus, Message, @@ -14,6 +13,7 @@ import type { QuestionRequest, Session, SessionStatus, + SnapshotFileDiff, Todo, VcsInfo, } from "@opencode-ai/sdk/v2/client" @@ -48,7 +48,7 @@ export type State = { [sessionID: string]: SessionStatus } session_diff: { - [sessionID: string]: FileDiff[] + [sessionID: string]: SnapshotFileDiff[] } todo: { [sessionID: string]: Todo[] diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 0c67647261..cf50fbe908 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1,4 +1,4 @@ -import type { FileDiff, Project, UserMessage } from "@opencode-ai/sdk/v2" +import type { Project, UserMessage, VcsFileDiff } from "@opencode-ai/sdk/v2" import { useDialog } from "@opencode-ai/ui/context/dialog" import { useMutation } from "@tanstack/solid-query" import { @@ -68,7 +68,7 @@ type FollowupItem = FollowupDraft & { id: string } type FollowupEdit = Pick const emptyFollowups: FollowupItem[] = [] -type ChangeMode = "git" | "branch" | "session" | "turn" +type ChangeMode = "git" | "branch" | "turn" type VcsMode = "git" | "branch" type SessionHistoryWindowInput = { @@ -463,13 +463,6 @@ export default function Page() { if (!id) return false return sync.session.history.loading(id) }) - const diffsReady = createMemo(() => { - const id = params.id - if (!id) return true - if (!hasSessionReview()) return true - return sync.data.session_diff[id] !== undefined - }) - const userMessages = createMemo( () => messages().filter((m) => m.role === "user") as UserMessage[], emptyUserMessages, @@ -527,10 +520,19 @@ export default function Page() { deferRender: false, }) - const [vcs, setVcs] = createStore({ + const [vcs, setVcs] = createStore<{ diff: { - git: [] as FileDiff[], - branch: [] as FileDiff[], + git: VcsFileDiff[] + branch: VcsFileDiff[] + } + ready: { + git: boolean + branch: boolean + } + }>({ + diff: { + git: [] as VcsFileDiff[], + branch: [] as VcsFileDiff[], }, ready: { git: false, @@ -648,6 +650,7 @@ export default function Page() { }, desktopReviewOpen()) const turnDiffs = createMemo(() => lastUserMessage()?.summary?.diffs ?? []) + const nogit = createMemo(() => !!sync.project && sync.project.vcs !== "git") const changesOptions = createMemo(() => { const list: ChangeMode[] = [] if (sync.project?.vcs === "git") list.push("git") @@ -659,7 +662,7 @@ export default function Page() { ) { list.push("branch") } - list.push("session", "turn") + list.push("turn") return list }) const vcsMode = createMemo(() => { @@ -668,20 +671,17 @@ export default function Page() { const reviewDiffs = createMemo(() => { if (store.changes === "git") return vcs.diff.git if (store.changes === "branch") return vcs.diff.branch - if (store.changes === "session") return diffs() return turnDiffs() }) const reviewCount = createMemo(() => { if (store.changes === "git") return vcs.diff.git.length if (store.changes === "branch") return vcs.diff.branch.length - if (store.changes === "session") return sessionCount() return turnDiffs().length }) const hasReview = createMemo(() => reviewCount() > 0) const reviewReady = createMemo(() => { if (store.changes === "git") return vcs.ready.git if (store.changes === "branch") return vcs.ready.branch - if (store.changes === "session") return !hasSessionReview() || diffsReady() return true }) @@ -749,13 +749,6 @@ export default function Page() { scrollToMessage(msgs[targetIndex], "auto") } - const sessionEmptyKey = createMemo(() => { - const project = sync.project - if (project && !project.vcs) return "session.review.noVcs" - if (sync.data.config.snapshot === false) return "session.review.noSnapshot" - return "session.review.empty" - }) - function upsert(next: Project) { const list = globalSync.data.project sync.set("project", next.id) @@ -1156,7 +1149,6 @@ export default function Page() { const label = (option: ChangeMode) => { if (option === "git") return language.t("ui.sessionReview.title.git") if (option === "branch") return language.t("ui.sessionReview.title.branch") - if (option === "session") return language.t("ui.sessionReview.title") return language.t("ui.sessionReview.title.lastTurn") } @@ -1179,11 +1171,26 @@ export default function Page() { ) + const createGit = (input: { emptyClass: string }) => ( +
+
+
{language.t("session.review.noVcs.createGit.title")}
+
+ {language.t("session.review.noVcs.createGit.description")} +
+
+ +
+ ) + const reviewEmptyText = createMemo(() => { if (store.changes === "git") return language.t("session.review.noUncommittedChanges") if (store.changes === "branch") return language.t("session.review.noBranchChanges") - if (store.changes === "turn") return language.t("session.review.noChanges") - return language.t(sessionEmptyKey()) + return language.t("session.review.noChanges") }) const reviewEmpty = (input: { loadingClass: string; emptyClass: string }) => { @@ -1193,31 +1200,10 @@ export default function Page() { } if (store.changes === "turn") { + if (nogit()) return createGit(input) return empty(reviewEmptyText()) } - if (hasSessionReview() && !diffsReady()) { - return
{language.t("session.review.loadingChanges")}
- } - - if (sessionEmptyKey() === "session.review.noVcs") { - return ( -
-
-
{language.t("session.review.noVcs.createGit.title")}
-
- {language.t("session.review.noVcs.createGit.description")} -
-
- -
- ) - } - return (
{reviewEmptyText()}
diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx index b681286450..71dfe375e0 100644 --- a/packages/app/src/pages/session/review-tab.tsx +++ b/packages/app/src/pages/session/review-tab.tsx @@ -1,6 +1,6 @@ import { createEffect, createSignal, onCleanup, type JSX } from "solid-js" import { makeEventListener } from "@solid-primitives/event-listener" -import type { FileDiff } from "@opencode-ai/sdk/v2" +import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" import { SessionReview } from "@opencode-ai/ui/session-review" import type { SessionReviewCommentActions, @@ -14,10 +14,12 @@ import type { LineComment } from "@/context/comments" export type DiffStyle = "unified" | "split" +type ReviewDiff = SnapshotFileDiff | VcsFileDiff + export interface SessionReviewTabProps { title?: JSX.Element empty?: JSX.Element - diffs: () => FileDiff[] + diffs: () => ReviewDiff[] view: () => ReturnType["view"]> diffStyle: DiffStyle onDiffStyleChange?: (style: DiffStyle) => void diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 86f932ea25..cddbea84d6 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -8,7 +8,7 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Mark } from "@opencode-ai/ui/logo" import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd" import type { DragEvent } from "@thisbeyond/solid-dnd" -import type { FileDiff } from "@opencode-ai/sdk/v2" +import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" import { useDialog } from "@opencode-ai/ui/context/dialog" @@ -27,7 +27,7 @@ import { useSessionLayout } from "@/pages/session/session-layout" export function SessionSidePanel(props: { canReview: () => boolean - diffs: () => FileDiff[] + diffs: () => (SnapshotFileDiff | VcsFileDiff)[] diffsReady: () => boolean empty: () => string hasReview: () => boolean diff --git a/packages/enterprise/src/core/share.ts b/packages/enterprise/src/core/share.ts index c6291b75d2..18fcd7a071 100644 --- a/packages/enterprise/src/core/share.ts +++ b/packages/enterprise/src/core/share.ts @@ -1,4 +1,4 @@ -import { FileDiff, Message, Model, Part, Session } from "@opencode-ai/sdk/v2" +import { Message, Model, Part, Session, SnapshotFileDiff } from "@opencode-ai/sdk/v2" import { fn } from "@opencode-ai/util/fn" import { iife } from "@opencode-ai/util/iife" import z from "zod" @@ -27,7 +27,7 @@ export namespace Share { }), z.object({ type: z.literal("session_diff"), - data: z.custom(), + data: z.custom(), }), z.object({ type: z.literal("model"), diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx index e755ea75a1..edeeaf1ad5 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -1,4 +1,4 @@ -import { FileDiff, Message, Model, Part, Session, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2" +import { Message, Model, Part, Session, SessionStatus, SnapshotFileDiff, UserMessage } from "@opencode-ai/sdk/v2" import { SessionTurn } from "@opencode-ai/ui/session-turn" import { SessionReview } from "@opencode-ai/ui/session-review" import { DataProvider } from "@opencode-ai/ui/context" @@ -51,7 +51,7 @@ const getData = query(async (shareID) => { shareID: string session: Session[] session_diff: { - [sessionID: string]: FileDiff[] + [sessionID: string]: SnapshotFileDiff[] } session_status: { [sessionID: string]: SessionStatus diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 91baca52a2..396d756301 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -2124,7 +2124,7 @@ function ApplyPatch(props: ToolProps) { } > - + diff --git a/packages/opencode/src/project/vcs.ts b/packages/opencode/src/project/vcs.ts index 5142079b1d..ec6e415c82 100644 --- a/packages/opencode/src/project/vcs.ts +++ b/packages/opencode/src/project/vcs.ts @@ -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, ) { 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 + 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 + export interface Interface { readonly init: () => Effect.Effect readonly branch: () => Effect.Effect readonly defaultBranch: () => Effect.Effect - readonly diff: (mode: Mode) => Effect.Effect + readonly diff: (mode: Mode) => Effect.Effect } interface State { diff --git a/packages/opencode/src/server/instance.ts b/packages/opencode/src/server/instance.ts index 7cc7886b04..65ea2fac2e 100644 --- a/packages/opencode/src/server/instance.ts +++ b/packages/opencode/src/server/instance.ts @@ -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()), }, }, }, diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index 2eb9887ea4..0cd0055c85 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -59,7 +59,7 @@ export namespace ShareNext { } | { type: "session_diff" - data: SDK.FileDiff[] + data: SDK.SnapshotFileDiff[] } | { type: "model" diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 2db67695ff..569c834bf4 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -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 @@ -521,8 +520,6 @@ export namespace Snapshot { const map = new Map() 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, diff --git a/packages/opencode/src/tool/apply_patch.ts b/packages/opencode/src/tool/apply_patch.ts index c23c0dd3d0..30b2e91ace 100644 --- a/packages/opencode/src/tool/apply_patch.ts +++ b/packages/opencode/src/tool/apply_patch.ts @@ -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, diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index 554d547d05..9505dd9eab 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -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, } diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts index 12d71f19a0..6619b3c605 100644 --- a/packages/opencode/test/share/share-next.test.ts +++ b/packages/opencode/test/share/share-next.test.ts @@ -272,8 +272,8 @@ describe("ShareNext", () => { diff: [ { file: "a.ts", - before: "one", - after: "two", + patch: + "Index: a.ts\n===================================================================\n--- a.ts\t\n+++ a.ts\t\n@@ -1,1 +1,1 @@\n-one\n\\ No newline at end of file\n+two\n\\ No newline at end of file\n", additions: 1, deletions: 1, status: "modified", @@ -285,8 +285,8 @@ describe("ShareNext", () => { diff: [ { file: "b.ts", - before: "old", - after: "new", + patch: + "Index: b.ts\n===================================================================\n--- b.ts\t\n+++ b.ts\t\n@@ -1,1 +1,1 @@\n-old\n\\ No newline at end of file\n+new\n\\ No newline at end of file\n", additions: 2, deletions: 0, status: "modified", @@ -304,8 +304,7 @@ describe("ShareNext", () => { type: string data: Array<{ file: string - before: string - after: string + patch: string additions: number deletions: number status?: string @@ -318,8 +317,8 @@ describe("ShareNext", () => { expect(body.data[0].data).toEqual([ { file: "b.ts", - before: "old", - after: "new", + patch: + "Index: b.ts\n===================================================================\n--- b.ts\t\n+++ b.ts\t\n@@ -1,1 +1,1 @@\n-old\n\\ No newline at end of file\n+new\n\\ No newline at end of file\n", additions: 2, deletions: 0, status: "modified", diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index f53f1e8111..3cedfb941d 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -974,8 +974,7 @@ test("diffFull with new file additions", async () => { const newFileDiff = diffs[0] expect(newFileDiff.file).toBe("new.txt") - expect(newFileDiff.before).toBe("") - expect(newFileDiff.after).toBe("new content") + expect(newFileDiff.patch).toContain("+new content") expect(newFileDiff.additions).toBe(1) expect(newFileDiff.deletions).toBe(0) }, @@ -1020,26 +1019,23 @@ test("diffFull with a large interleaved mixed diff", async () => { for (let i = 0; i < ids.length; i++) { const m = map.get(fwd("mix", `${ids[i]}-mod.txt`)) expect(m).toBeDefined() - expect(m!.before).toBe(`before-${ids[i]}-é\n🙂\nline`) - expect(m!.after).toBe(`after-${ids[i]}-é\n🚀\nline`) + expect(m!.patch).toContain(`-before-${ids[i]}-é`) + expect(m!.patch).toContain(`+after-${ids[i]}-é`) expect(m!.status).toBe("modified") const d = map.get(fwd("mix", `${ids[i]}-del.txt`)) expect(d).toBeDefined() - expect(d!.before).toBe(`gone-${ids[i]}\n你好`) - expect(d!.after).toBe("") + expect(d!.patch).toContain(`-gone-${ids[i]}`) expect(d!.status).toBe("deleted") const a = map.get(fwd("mix", `${ids[i]}-add.txt`)) expect(a).toBeDefined() - expect(a!.before).toBe("") - expect(a!.after).toBe(`new-${ids[i]}\nこんにちは`) + expect(a!.patch).toContain(`+new-${ids[i]}`) expect(a!.status).toBe("added") const b = map.get(fwd("mix", `${ids[i]}-bin.bin`)) expect(b).toBeDefined() - expect(b!.before).toBe("") - expect(b!.after).toBe("") + expect(b!.patch).toBe("") expect(b!.additions).toBe(0) expect(b!.deletions).toBe(0) expect(b!.status).toBe("modified") @@ -1092,8 +1088,8 @@ test("diffFull with file modifications", async () => { const modifiedFileDiff = diffs[0] expect(modifiedFileDiff.file).toBe("b.txt") - expect(modifiedFileDiff.before).toBe(tmp.extra.bContent) - expect(modifiedFileDiff.after).toBe("modified content") + expect(modifiedFileDiff.patch).toContain(`-${tmp.extra.bContent}`) + expect(modifiedFileDiff.patch).toContain("+modified content") expect(modifiedFileDiff.additions).toBeGreaterThan(0) expect(modifiedFileDiff.deletions).toBeGreaterThan(0) }, @@ -1118,8 +1114,7 @@ test("diffFull with file deletions", async () => { const removedFileDiff = diffs[0] expect(removedFileDiff.file).toBe("a.txt") - expect(removedFileDiff.before).toBe(tmp.extra.aContent) - expect(removedFileDiff.after).toBe("") + expect(removedFileDiff.patch).toContain(`-${tmp.extra.aContent}`) expect(removedFileDiff.additions).toBe(0) expect(removedFileDiff.deletions).toBe(1) }, @@ -1144,8 +1139,8 @@ test("diffFull with multiple line additions", async () => { const multiDiff = diffs[0] expect(multiDiff.file).toBe("multi.txt") - expect(multiDiff.before).toBe("") - expect(multiDiff.after).toBe("line1\nline2\nline3") + expect(multiDiff.patch).toContain("+line1") + expect(multiDiff.patch).toContain("+line3") expect(multiDiff.additions).toBe(3) expect(multiDiff.deletions).toBe(0) }, @@ -1171,15 +1166,13 @@ test("diffFull with addition and deletion", async () => { const addedFileDiff = diffs.find((d) => d.file === "added.txt") expect(addedFileDiff).toBeDefined() - expect(addedFileDiff!.before).toBe("") - expect(addedFileDiff!.after).toBe("added content") + expect(addedFileDiff!.patch).toContain("+added content") expect(addedFileDiff!.additions).toBe(1) expect(addedFileDiff!.deletions).toBe(0) const removedFileDiff = diffs.find((d) => d.file === "a.txt") expect(removedFileDiff).toBeDefined() - expect(removedFileDiff!.before).toBe(tmp.extra.aContent) - expect(removedFileDiff!.after).toBe("") + expect(removedFileDiff!.patch).toContain(`-${tmp.extra.aContent}`) expect(removedFileDiff!.additions).toBe(0) expect(removedFileDiff!.deletions).toBe(1) }, @@ -1263,7 +1256,7 @@ test("diffFull with binary file changes", async () => { const binaryDiff = diffs[0] expect(binaryDiff.file).toBe("binary.bin") - expect(binaryDiff.before).toBe("") + expect(binaryDiff.patch).toBe("") }, }) }) diff --git a/packages/opencode/test/tool/apply_patch.test.ts b/packages/opencode/test/tool/apply_patch.test.ts index 4e276517f1..19c8cfefd0 100644 --- a/packages/opencode/test/tool/apply_patch.test.ts +++ b/packages/opencode/test/tool/apply_patch.test.ts @@ -27,9 +27,7 @@ type AskInput = { filePath: string relativePath: string type: "add" | "update" | "delete" | "move" - diff: string - before: string - after: string + patch: string additions: number deletions: number movePath?: string @@ -112,12 +110,12 @@ describe("tool.apply_patch freeform", () => { const addFile = permissionCall.metadata.files.find((f) => f.type === "add") expect(addFile).toBeDefined() expect(addFile!.relativePath).toBe("nested/new.txt") - expect(addFile!.after).toBe("created\n") + expect(addFile!.patch).toContain("+created") const updateFile = permissionCall.metadata.files.find((f) => f.type === "update") expect(updateFile).toBeDefined() - expect(updateFile!.before).toContain("line2") - expect(updateFile!.after).toContain("changed") + expect(updateFile!.patch).toContain("-line2") + expect(updateFile!.patch).toContain("+changed") const added = await fs.readFile(path.join(fixture.path, "nested", "new.txt"), "utf-8") expect(added).toBe("created\n") @@ -151,8 +149,8 @@ describe("tool.apply_patch freeform", () => { expect(moveFile.type).toBe("move") expect(moveFile.relativePath).toBe("renamed/dir/name.txt") expect(moveFile.movePath).toBe(path.join(fixture.path, "renamed/dir/name.txt")) - expect(moveFile.before).toBe("old content\n") - expect(moveFile.after).toBe("new content\n") + expect(moveFile.patch).toContain("-old content") + expect(moveFile.patch).toContain("+new content") }, }) }) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index fc1616c4fd..0a9aa4358e 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -347,10 +347,9 @@ export type EventCommandExecuted = { } } -export type FileDiff = { +export type SnapshotFileDiff = { file: string - before: string - after: string + patch: string additions: number deletions: number status?: "added" | "deleted" | "modified" @@ -360,7 +359,7 @@ export type EventSessionDiff = { type: "session.diff" properties: { sessionID: string - diff: Array + diff: Array } } @@ -542,7 +541,7 @@ export type UserMessage = { summary?: { title?: string body?: string - diffs: Array + diffs: Array } agent: string model: { @@ -917,7 +916,7 @@ export type Session = { additions: number deletions: number files: number - diffs?: Array + diffs?: Array } share?: { url: string @@ -1078,7 +1077,7 @@ export type SyncEventSessionUpdated = { additions: number deletions: number files: number - diffs?: Array + diffs?: Array } | null share?: { url: string | null @@ -1803,7 +1802,7 @@ export type GlobalSession = { additions: number deletions: number files: number - diffs?: Array + diffs?: Array } share?: { url: string @@ -2009,6 +2008,14 @@ export type VcsInfo = { default_branch?: string } +export type VcsFileDiff = { + file: string + patch: string + additions: number + deletions: number + status?: "added" | "deleted" | "modified" +} + export type Command = { name: string description?: string @@ -3503,7 +3510,7 @@ export type SessionDiffResponses = { /** * Successfully retrieved diff */ - 200: Array + 200: Array } export type SessionDiffResponse = SessionDiffResponses[keyof SessionDiffResponses] @@ -5159,7 +5166,7 @@ export type VcsDiffResponses = { /** * VCS diff */ - 200: Array + 200: Array } export type VcsDiffResponse = VcsDiffResponses[keyof VcsDiffResponses] diff --git a/packages/ui/package.json b/packages/ui/package.json index 64520f707c..d3e1cc9429 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -53,6 +53,7 @@ "@solid-primitives/resize-observer": "2.1.3", "@solidjs/meta": "catalog:", "@solidjs/router": "catalog:", + "diff": "catalog:", "dompurify": "3.3.1", "fuzzysort": "catalog:", "katex": "0.16.27", diff --git a/packages/ui/src/components/file-media.tsx b/packages/ui/src/components/file-media.tsx index 2fd54588a3..f066019d72 100644 --- a/packages/ui/src/components/file-media.tsx +++ b/packages/ui/src/components/file-media.tsx @@ -16,6 +16,7 @@ export type FileMediaOptions = { current?: unknown before?: unknown after?: unknown + deleted?: boolean readFile?: (path: string) => Promise onLoad?: () => void onError?: (ctx: { kind: "image" | "audio" | "svg" }) => void @@ -49,6 +50,7 @@ export function FileMedia(props: { media?: FileMediaOptions; fallback: () => JSX const media = cfg() const k = kind() if (!media || !k) return false + if (media.deleted) return true if (k === "svg") return false if (media.current !== undefined) return false return !hasMediaValue(media.after as any) && hasMediaValue(media.before as any) diff --git a/packages/ui/src/components/file-ssr.tsx b/packages/ui/src/components/file-ssr.tsx index 9526907831..fed5c89315 100644 --- a/packages/ui/src/components/file-ssr.tsx +++ b/packages/ui/src/components/file-ssr.tsx @@ -1,5 +1,5 @@ import { DIFFS_TAG_NAME, FileDiff, VirtualizedFileDiff } from "@pierre/diffs" -import { type PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" +import { type PreloadFileDiffResult, type PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" import { createEffect, onCleanup, onMount, Show, splitProps } from "solid-js" import { Dynamic, isServer } from "solid-js/web" import { useWorkerPool } from "../context/worker-pool" @@ -16,8 +16,10 @@ import { import { acquireVirtualizer, virtualMetrics } from "../pierre/virtualizer" import { File, type DiffFileProps, type FileProps } from "./file" +type DiffPreload = PreloadMultiFileDiffResult | PreloadFileDiffResult + type SSRDiffFileProps = DiffFileProps & { - preloadedDiff: PreloadMultiFileDiffResult + preloadedDiff: DiffPreload } function DiffSSRViewer(props: SSRDiffFileProps) { @@ -32,6 +34,7 @@ function DiffSSRViewer(props: SSRDiffFileProps) { const [local, others] = splitProps(props, [ "mode", "media", + "fileDiff", "before", "after", "class", @@ -90,12 +93,13 @@ function DiffSSRViewer(props: SSRDiffFileProps) { onCleanup(observeViewerScheme(() => fileDiffRef)) const virtualizer = getVirtualizer() + const annotations = local.annotations ?? local.preloadedDiff.annotations ?? [] fileDiffInstance = virtualizer ? new VirtualizedFileDiff( { ...createDefaultOptions(props.diffStyle), ...others, - ...local.preloadedDiff, + ...(local.preloadedDiff.options ?? {}), }, virtualizer, virtualMetrics, @@ -105,7 +109,7 @@ function DiffSSRViewer(props: SSRDiffFileProps) { { ...createDefaultOptions(props.diffStyle), ...others, - ...local.preloadedDiff, + ...(local.preloadedDiff.options ?? {}), }, workerPool, ) @@ -114,13 +118,24 @@ function DiffSSRViewer(props: SSRDiffFileProps) { // @ts-expect-error private field required for hydration fileDiffInstance.fileContainer = fileDiffRef - fileDiffInstance.hydrate({ - oldFile: local.before, - newFile: local.after, - lineAnnotations: local.annotations ?? [], - fileContainer: fileDiffRef, - containerWrapper: container, - }) + fileDiffInstance.hydrate( + local.fileDiff + ? { + fileDiff: local.fileDiff, + lineAnnotations: annotations, + fileContainer: fileDiffRef, + containerWrapper: container, + prerenderedHTML: local.preloadedDiff.prerenderedHTML, + } + : { + oldFile: local.before, + newFile: local.after, + lineAnnotations: annotations, + fileContainer: fileDiffRef, + containerWrapper: container, + prerenderedHTML: local.preloadedDiff.prerenderedHTML, + }, + ) notifyRendered() }) diff --git a/packages/ui/src/components/file.tsx b/packages/ui/src/components/file.tsx index fb488729e2..b78f0bae44 100644 --- a/packages/ui/src/components/file.tsx +++ b/packages/ui/src/components/file.tsx @@ -3,6 +3,7 @@ import { DEFAULT_VIRTUAL_FILE_METRICS, type DiffLineAnnotation, type FileContents, + type FileDiffMetadata, File as PierreFile, type FileDiffOptions, FileDiff, @@ -14,7 +15,7 @@ import { VirtualizedFileDiff, Virtualizer, } from "@pierre/diffs" -import { type PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" +import { type PreloadFileDiffResult, type PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" import { createMediaQuery } from "@solid-primitives/media" import { makeEventListener } from "@solid-primitives/event-listener" import { ComponentProps, createEffect, createMemo, createSignal, onCleanup, onMount, Show, splitProps } from "solid-js" @@ -80,15 +81,29 @@ export type TextFileProps = FileOptions & preloadedDiff?: PreloadMultiFileDiffResult } -export type DiffFileProps = FileDiffOptions & +type DiffPreload = PreloadMultiFileDiffResult | PreloadFileDiffResult + +type DiffBaseProps = FileDiffOptions & SharedProps & { mode: "diff" - before: FileContents - after: FileContents annotations?: DiffLineAnnotation[] - preloadedDiff?: PreloadMultiFileDiffResult + preloadedDiff?: DiffPreload } +type DiffPairProps = DiffBaseProps & { + before: FileContents + after: FileContents + fileDiff?: undefined +} + +type DiffPatchProps = DiffBaseProps & { + fileDiff: FileDiffMetadata + before?: undefined + after?: undefined +} + +export type DiffFileProps = DiffPairProps | DiffPatchProps + export type FileProps = TextFileProps | DiffFileProps const sharedKeys = [ @@ -108,7 +123,7 @@ const sharedKeys = [ ] as const const textKeys = ["file", ...sharedKeys] as const -const diffKeys = ["before", "after", ...sharedKeys] as const +const diffKeys = ["fileDiff", "before", "after", ...sharedKeys] as const // --------------------------------------------------------------------------- // Shared viewer hook @@ -976,6 +991,12 @@ function DiffViewer(props: DiffFileProps) { const virtuals = createSharedVirtualStrategy(() => viewer.container) const large = createMemo(() => { + if (local.fileDiff) { + const before = local.fileDiff.deletionLines.join("") + const after = local.fileDiff.additionLines.join("") + return Math.max(before.length, after.length) > 500_000 + } + const before = typeof local.before?.contents === "string" ? local.before.contents : "" const after = typeof local.after?.contents === "string" ? local.after.contents : "" return Math.max(before.length, after.length) > 500_000 @@ -1054,6 +1075,17 @@ function DiffViewer(props: DiffFileProps) { instance = value }, draw: (value) => { + if (local.fileDiff) { + value.render({ + fileDiff: local.fileDiff, + lineAnnotations: [], + containerWrapper: viewer.container, + }) + return + } + + if (!local.before || !local.after) return + value.render({ oldFile: { ...local.before, contents: beforeContents, cacheKey: cacheKey(beforeContents) }, newFile: { ...local.after, contents: afterContents, cacheKey: cacheKey(afterContents) }, diff --git a/packages/ui/src/components/session-diff.test.ts b/packages/ui/src/components/session-diff.test.ts new file mode 100644 index 0000000000..463a729778 --- /dev/null +++ b/packages/ui/src/components/session-diff.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, test } from "bun:test" +import { normalize, text } from "./session-diff" + +describe("session diff", () => { + test("keeps unified patch content", () => { + const diff = { + file: "a.ts", + patch: + "Index: a.ts\n===================================================================\n--- a.ts\t\n+++ a.ts\t\n@@ -1,2 +1,2 @@\n one\n-two\n+three\n", + additions: 1, + deletions: 1, + status: "modified" as const, + } + const view = normalize(diff) + + expect(view.patch).toBe(diff.patch) + expect(view.fileDiff.name).toBe("a.ts") + expect(text(view, "deletions")).toBe("one\ntwo\n") + expect(text(view, "additions")).toBe("one\nthree\n") + }) + + test("converts legacy content into a patch", () => { + const diff = { + file: "a.ts", + before: "one\n", + after: "two\n", + additions: 1, + deletions: 1, + status: "modified" as const, + } + const view = normalize(diff) + + expect(view.patch).toContain("@@ -1,1 +1,1 @@") + expect(text(view, "deletions")).toBe("one\n") + expect(text(view, "additions")).toBe("two\n") + }) +}) diff --git a/packages/ui/src/components/session-diff.ts b/packages/ui/src/components/session-diff.ts new file mode 100644 index 0000000000..cc2b1ce527 --- /dev/null +++ b/packages/ui/src/components/session-diff.ts @@ -0,0 +1,83 @@ +import { parsePatchFiles, type FileDiffMetadata } from "@pierre/diffs" +import { sampledChecksum } from "@opencode-ai/util/encode" +import { formatPatch, structuredPatch } from "diff" +import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" + +type LegacyDiff = { + file: string + patch?: string + before?: string + after?: string + additions: number + deletions: number + status?: "added" | "deleted" | "modified" +} + +type ReviewDiff = SnapshotFileDiff | VcsFileDiff | LegacyDiff + +export type ViewDiff = { + file: string + patch: string + additions: number + deletions: number + status?: "added" | "deleted" | "modified" + fileDiff: FileDiffMetadata +} + +const cache = new Map() + +function empty(file: string, key: string) { + return { + name: file, + type: "change", + hunks: [], + splitLineCount: 0, + unifiedLineCount: 0, + isPartial: true, + deletionLines: [], + additionLines: [], + cacheKey: key, + } satisfies FileDiffMetadata +} + +function patch(diff: ReviewDiff) { + if (typeof diff.patch === "string") return diff.patch + return formatPatch( + structuredPatch( + diff.file, + diff.file, + "before" in diff && typeof diff.before === "string" ? diff.before : "", + "after" in diff && typeof diff.after === "string" ? diff.after : "", + "", + "", + { context: Number.MAX_SAFE_INTEGER }, + ), + ) +} + +function file(file: string, patch: string) { + const hit = cache.get(patch) + if (hit) return hit + + const key = sampledChecksum(patch) ?? file + const value = parsePatchFiles(patch, key).flatMap((item) => item.files)[0] ?? empty(file, key) + cache.set(patch, value) + return value +} + +export function normalize(diff: ReviewDiff): ViewDiff { + const next = patch(diff) + return { + file: diff.file, + patch: next, + additions: diff.additions, + deletions: diff.deletions, + status: diff.status, + fileDiff: file(diff.file, next), + } +} + +export function text(diff: ViewDiff, side: "deletions" | "additions") { + if (side === "deletions") return diff.fileDiff.deletionLines.join("") + return diff.fileDiff.additionLines.join("") +} diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 3b582d66f9..90da853efc 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -15,7 +15,7 @@ import { getDirectory, getFilename } from "@opencode-ai/util/path" import { checksum } from "@opencode-ai/util/encode" import { createEffect, createMemo, For, Match, onCleanup, Show, Switch, untrack, type JSX } from "solid-js" import { createStore } from "solid-js/store" -import { type FileContent, type FileDiff } from "@opencode-ai/sdk/v2" +import { type FileContent, type SnapshotFileDiff, type VcsFileDiff } from "@opencode-ai/sdk/v2" import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" import { type SelectedLineRange } from "@pierre/diffs" import { Dynamic } from "solid-js/web" @@ -23,6 +23,7 @@ import { mediaKindFromPath } from "../pierre/media" import { cloneSelectedLineRange, previewSelectedLines } from "../pierre/selection-bridge" import { createLineCommentController } from "./line-comment-annotations" import type { LineCommentEditorProps } from "./line-comment" +import { normalize, text, type ViewDiff } from "./session-diff" const MAX_DIFF_CHANGED_LINES = 500 const REVIEW_MOUNT_MARGIN = 300 @@ -61,7 +62,8 @@ export type SessionReviewCommentActions = { export type SessionReviewFocus = { file: string; id: string } -type ReviewDiff = FileDiff & { preloaded?: PreloadMultiFileDiffResult } +type ReviewDiff = (SnapshotFileDiff | VcsFileDiff) & { preloaded?: PreloadMultiFileDiffResult } +type Item = ViewDiff & { preloaded?: PreloadMultiFileDiffResult } export interface SessionReviewProps { title?: JSX.Element @@ -155,8 +157,8 @@ export const SessionReview = (props: SessionReviewProps) => { const opened = () => store.opened const open = () => props.open ?? store.open - const files = createMemo(() => props.diffs.map((diff) => diff.file)) - const diffs = createMemo(() => new Map(props.diffs.map((diff) => [diff.file, diff] as const))) + const items = createMemo(() => props.diffs.map((diff) => ({ ...normalize(diff), preloaded: diff.preloaded }))) + const files = createMemo(() => items().map((diff) => diff.file)) const grouped = createMemo(() => { const next = new Map() for (const comment of props.comments ?? []) { @@ -246,10 +248,10 @@ export const SessionReview = (props: SessionReviewProps) => { const selectionSide = (range: SelectedLineRange) => range.endSide ?? range.side ?? "additions" - const selectionPreview = (diff: FileDiff, range: SelectedLineRange) => { + const selectionPreview = (diff: ViewDiff, range: SelectedLineRange) => { const side = selectionSide(range) - const contents = side === "deletions" ? diff.before : diff.after - if (typeof contents !== "string" || contents.length === 0) return undefined + const contents = text(diff, side) + if (contents.length === 0) return undefined return previewSelectedLines(contents, range) } @@ -359,7 +361,7 @@ export const SessionReview = (props: SessionReviewProps) => {
- + {(diff) => { let wrapper: HTMLDivElement | undefined const file = diff.file @@ -371,8 +373,8 @@ export const SessionReview = (props: SessionReviewProps) => { const comments = createMemo(() => grouped().get(file) ?? []) const commentedLines = createMemo(() => comments().map((c) => c.selection)) - const beforeText = () => (typeof diff.before === "string" ? diff.before : "") - const afterText = () => (typeof diff.after === "string" ? diff.after : "") + const beforeText = () => text(diff, "deletions") + const afterText = () => text(diff, "additions") const changedLines = () => diff.additions + diff.deletions const mediaKind = createMemo(() => mediaKindFromPath(file)) @@ -581,6 +583,7 @@ export const SessionReview = (props: SessionReviewProps) => { { @@ -596,20 +599,11 @@ export const SessionReview = (props: SessionReviewProps) => { renderHoverUtility={props.onLineComment ? commentsUi.renderHoverUtility : undefined} selectedLines={selectedLines()} commentedLines={commentedLines()} - before={{ - name: file, - contents: typeof diff.before === "string" ? diff.before : "", - }} - after={{ - name: file, - contents: typeof diff.after === "string" ? diff.after : "", - }} media={{ mode: "auto", path: file, - before: diff.before, - after: diff.after, - readFile: props.readFile, + deleted: diff.status === "deleted", + readFile: diff.status === "deleted" ? undefined : props.readFile, }} /> diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index c20e5fb1ce..bb699a77e2 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -1,4 +1,9 @@ -import { AssistantMessage, type FileDiff, Message as MessageType, Part as PartType } from "@opencode-ai/sdk/v2/client" +import { + AssistantMessage, + type SnapshotFileDiff, + Message as MessageType, + Part as PartType, +} from "@opencode-ai/sdk/v2/client" import type { SessionStatus } from "@opencode-ai/sdk/v2" import { useData } from "../context" import { useFileComponent } from "../context/file" @@ -19,6 +24,7 @@ import { SessionRetry } from "./session-retry" import { TextReveal } from "./text-reveal" import { createAutoScroll } from "../hooks" import { useI18n } from "../context/i18n" +import { normalize } from "./session-diff" function record(value: unknown): value is Record { return !!value && typeof value === "object" && !Array.isArray(value) @@ -163,7 +169,7 @@ export function SessionTurn( const emptyMessages: MessageType[] = [] const emptyParts: PartType[] = [] const emptyAssistant: AssistantMessage[] = [] - const emptyDiffs: FileDiff[] = [] + const emptyDiffs: SnapshotFileDiff[] = [] const idle = { type: "idle" as const } const allMessages = createMemo(() => props.messages ?? list(data.store.message?.[props.sessionID], emptyMessages)) @@ -232,7 +238,7 @@ export function SessionTurn( const seen = new Set() return files - .reduceRight((result, diff) => { + .reduceRight((result, diff) => { if (seen.has(diff.file)) return result seen.add(diff.file) result.push(diff) @@ -447,6 +453,7 @@ export function SessionTurn( > {(diff) => { + const view = normalize(diff) const active = createMemo(() => expanded().includes(diff.file)) const [shown, setShown] = createSignal(false) @@ -495,12 +502,7 @@ export function SessionTurn(
- +
diff --git a/packages/ui/src/context/data.tsx b/packages/ui/src/context/data.tsx index 93368c2a05..632bed0cfa 100644 --- a/packages/ui/src/context/data.tsx +++ b/packages/ui/src/context/data.tsx @@ -1,4 +1,4 @@ -import type { Message, Session, Part, FileDiff, SessionStatus, ProviderListResponse } from "@opencode-ai/sdk/v2" +import type { Message, Session, Part, SnapshotFileDiff, SessionStatus, ProviderListResponse } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "./helper" import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" @@ -13,7 +13,7 @@ type Data = { [sessionID: string]: SessionStatus } session_diff: { - [sessionID: string]: FileDiff[] + [sessionID: string]: SnapshotFileDiff[] } session_diff_preload?: { [sessionID: string]: PreloadMultiFileDiffResult[]