summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src
diff options
context:
space:
mode:
authorDax <[email protected]>2026-04-07 19:48:23 -0400
committerGitHub <[email protected]>2026-04-07 19:48:23 -0400
commitb7fab49b64275b83bcec8200d7492fc5d15ffe06 (patch)
treed9dadf65ca69eb4b8fe75654eb15666ee2b23774 /packages/app/src
parent463318486f94fa20e8d864d77708a347fa8423e3 (diff)
downloadopencode-b7fab49b64275b83bcec8200d7492fc5d15ffe06.tar.gz
opencode-b7fab49b64275b83bcec8200d7492fc5d15ffe06.zip
refactor(snapshot): store unified patches in file diffs (#21244)
Co-authored-by: Adam <[email protected]>
Diffstat (limited to 'packages/app/src')
-rw-r--r--packages/app/src/context/global-sync/event-reducer.ts4
-rw-r--r--packages/app/src/context/global-sync/session-cache.test.ts6
-rw-r--r--packages/app/src/context/global-sync/session-cache.ts4
-rw-r--r--packages/app/src/context/global-sync/types.ts4
-rw-r--r--packages/app/src/pages/session.tsx82
-rw-r--r--packages/app/src/pages/session/review-tab.tsx6
-rw-r--r--packages/app/src/pages/session/session-side-panel.tsx4
7 files changed, 49 insertions, 61 deletions
diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts
index 4af636553..01248e20e 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 8e11110e3..472ac219e 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<string, SessionStatus | undefined>
- session_diff: Record<string, FileDiff[] | undefined>
+ session_diff: Record<string, SnapshotFileDiff[] | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
@@ -64,7 +64,7 @@ describe("app session cache", () => {
const m = msg("msg_1", "ses_1")
const store: {
session_status: Record<string, SessionStatus | undefined>
- session_diff: Record<string, FileDiff[] | undefined>
+ session_diff: Record<string, SnapshotFileDiff[] | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
diff --git a/packages/app/src/context/global-sync/session-cache.ts b/packages/app/src/context/global-sync/session-cache.ts
index 0177ebbe1..6f4d81062 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<string, SessionStatus | undefined>
- session_diff: Record<string, FileDiff[] | undefined>
+ session_diff: Record<string, SnapshotFileDiff[] | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
diff --git a/packages/app/src/context/global-sync/types.ts b/packages/app/src/context/global-sync/types.ts
index 1d6e550f8..b0f340a90 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 0c6764726..cf50fbe90 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<FollowupItem, "id" | "prompt" | "context">
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: VcsFileDiff[]
+ branch: VcsFileDiff[]
+ }
+ ready: {
+ git: boolean
+ branch: boolean
+ }
+ }>({
diff: {
- git: [] as FileDiff[],
- branch: [] as FileDiff[],
+ 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<ChangeMode[]>(() => {
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<VcsMode | undefined>(() => {
@@ -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() {
</div>
)
+ const createGit = (input: { emptyClass: string }) => (
+ <div class={input.emptyClass}>
+ <div class="flex flex-col gap-3">
+ <div class="text-14-medium text-text-strong">{language.t("session.review.noVcs.createGit.title")}</div>
+ <div class="text-14-regular text-text-base max-w-md" style={{ "line-height": "var(--line-height-normal)" }}>
+ {language.t("session.review.noVcs.createGit.description")}
+ </div>
+ </div>
+ <Button size="large" disabled={gitMutation.isPending} onClick={initGit}>
+ {gitMutation.isPending
+ ? language.t("session.review.noVcs.createGit.actionLoading")
+ : language.t("session.review.noVcs.createGit.action")}
+ </Button>
+ </div>
+ )
+
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 <div class={input.loadingClass}>{language.t("session.review.loadingChanges")}</div>
- }
-
- if (sessionEmptyKey() === "session.review.noVcs") {
- return (
- <div class={input.emptyClass}>
- <div class="flex flex-col gap-3">
- <div class="text-14-medium text-text-strong">{language.t("session.review.noVcs.createGit.title")}</div>
- <div class="text-14-regular text-text-base max-w-md" style={{ "line-height": "var(--line-height-normal)" }}>
- {language.t("session.review.noVcs.createGit.description")}
- </div>
- </div>
- <Button size="large" disabled={gitMutation.isPending} onClick={initGit}>
- {gitMutation.isPending
- ? language.t("session.review.noVcs.createGit.actionLoading")
- : language.t("session.review.noVcs.createGit.action")}
- </Button>
- </div>
- )
- }
-
return (
<div class={input.emptyClass}>
<div class="text-14-regular text-text-weak max-w-56">{reviewEmptyText()}</div>
diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx
index b68128645..71dfe375e 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<ReturnType<typeof useLayout>["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 86f932ea2..cddbea84d 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