Преглед изворни кода

revert: roll back git-backed review modes (#19295)

Shoubhit Dash пре 4 месеци
родитељ
комит
1b028d0632

+ 1 - 1
packages/app/src/context/global-sync/bootstrap.ts

@@ -190,7 +190,7 @@ export async function bootstrapDirectory(input: {
         input.sdk.vcs.get().then((x) => {
           const next = x.data ?? input.store.vcs
           input.setStore("vcs", next)
-          if (next) input.vcsCache.setStore("value", next)
+          if (next?.branch) input.vcsCache.setStore("value", next)
         }),
       ),
     () => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))),

+ 4 - 6
packages/app/src/context/global-sync/event-reducer.test.ts

@@ -494,10 +494,8 @@ describe("applyDirectoryEvent", () => {
   })
 
   test("updates vcs branch in store and cache", () => {
-    const [store, setStore] = createStore(baseState({ vcs: { branch: "main", default_branch: "main" } }))
-    const [cacheStore, setCacheStore] = createStore({
-      value: { branch: "main", default_branch: "main" } as State["vcs"],
-    })
+    const [store, setStore] = createStore(baseState())
+    const [cacheStore, setCacheStore] = createStore({ value: undefined as State["vcs"] })
 
     applyDirectoryEvent({
       event: { type: "vcs.branch.updated", properties: { branch: "feature/test" } },
@@ -513,8 +511,8 @@ describe("applyDirectoryEvent", () => {
       },
     })
 
-    expect(store.vcs).toEqual({ branch: "feature/test", default_branch: "main" })
-    expect(cacheStore.value).toEqual({ branch: "feature/test", default_branch: "main" })
+    expect(store.vcs).toEqual({ branch: "feature/test" })
+    expect(cacheStore.value).toEqual({ branch: "feature/test" })
   })
 
   test("routes disposal and lsp events to side-effect handlers", () => {

+ 2 - 2
packages/app/src/context/global-sync/event-reducer.ts

@@ -271,9 +271,9 @@ export function applyDirectoryEvent(input: {
       break
     }
     case "vcs.branch.updated": {
-      const props = event.properties as { branch?: string }
+      const props = event.properties as { branch: string }
       if (input.store.vcs?.branch === props.branch) break
-      const next = { ...input.store.vcs, branch: props.branch }
+      const next = { branch: props.branch }
       input.setStore("vcs", next)
       if (input.vcsCache) input.vcsCache.setStore("value", next)
       break

+ 0 - 2
packages/app/src/i18n/en.ts

@@ -535,8 +535,6 @@ export const dict = {
   "session.review.noVcs.createGit.action": "Create Git repository",
   "session.review.noSnapshot": "Snapshot tracking is disabled in config, so session changes are unavailable",
   "session.review.noChanges": "No changes",
-  "session.review.noUncommittedChanges": "No uncommitted changes yet",
-  "session.review.noBranchChanges": "No branch changes yet",
 
   "session.files.selectToOpen": "Select a file to open",
   "session.files.all": "All files",

+ 40 - 232
packages/app/src/pages/session.tsx

@@ -1,4 +1,4 @@
-import type { FileDiff, Project, UserMessage } from "@opencode-ai/sdk/v2"
+import type { Project, UserMessage } from "@opencode-ai/sdk/v2"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { useMutation } from "@tanstack/solid-query"
 import {
@@ -64,9 +64,6 @@ import { formatServerError } from "@/utils/server-errors"
 const emptyUserMessages: UserMessage[] = []
 const emptyFollowups: (FollowupDraft & { id: string })[] = []
 
-type ChangeMode = "git" | "branch" | "session" | "turn"
-type VcsMode = "git" | "branch"
-
 type SessionHistoryWindowInput = {
   sessionID: () => string | undefined
   messagesReady: () => boolean
@@ -427,16 +424,15 @@ export default function Page() {
 
   const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
   const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
-  const sessionCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
-  const hasSessionReview = createMemo(() => sessionCount() > 0)
-  const canReview = createMemo(() => !!params.id)
+  const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
+  const hasReview = createMemo(() => reviewCount() > 0)
   const reviewTab = createMemo(() => isDesktop())
   const tabState = createSessionTabs({
     tabs,
     pathFromTab: file.pathFromTab,
     normalizeTab,
     review: reviewTab,
-    hasReview: canReview,
+    hasReview,
   })
   const contextOpen = tabState.contextOpen
   const openedTabs = tabState.openedTabs
@@ -459,12 +455,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[],
@@ -518,22 +508,11 @@ export default function Page() {
   const [store, setStore] = createStore({
     messageId: undefined as string | undefined,
     mobileTab: "session" as "session" | "changes",
-    changes: "git" as ChangeMode,
+    changes: "session" as "session" | "turn",
     newSessionWorktree: "main",
     deferRender: false,
   })
 
-  const [vcs, setVcs] = createStore({
-    diff: {
-      git: [] as FileDiff[],
-      branch: [] as FileDiff[],
-    },
-    ready: {
-      git: false,
-      branch: false,
-    },
-  })
-
   const [followup, setFollowup] = createStore({
     items: {} as Record<string, (FollowupDraft & { id: string })[] | undefined>,
     failed: {} as Record<string, string | undefined>,
@@ -560,68 +539,6 @@ export default function Page() {
   let refreshTimer: number | undefined
   let diffFrame: number | undefined
   let diffTimer: number | undefined
-  const vcsTask = new Map<VcsMode, Promise<void>>()
-  const vcsRun = new Map<VcsMode, number>()
-
-  const bumpVcs = (mode: VcsMode) => {
-    const next = (vcsRun.get(mode) ?? 0) + 1
-    vcsRun.set(mode, next)
-    return next
-  }
-
-  const resetVcs = (mode?: VcsMode) => {
-    const list = mode ? [mode] : (["git", "branch"] as const)
-    list.forEach((item) => {
-      bumpVcs(item)
-      vcsTask.delete(item)
-      setVcs("diff", item, [])
-      setVcs("ready", item, false)
-    })
-  }
-
-  const loadVcs = (mode: VcsMode, force = false) => {
-    if (sync.project?.vcs !== "git") return Promise.resolve()
-    if (!force && vcs.ready[mode]) return Promise.resolve()
-
-    if (force) {
-      if (vcsTask.has(mode)) bumpVcs(mode)
-      vcsTask.delete(mode)
-      setVcs("ready", mode, false)
-    }
-
-    const current = vcsTask.get(mode)
-    if (current) return current
-
-    const run = bumpVcs(mode)
-
-    const task = sdk.client.vcs
-      .diff({ mode })
-      .then((result) => {
-        if (vcsRun.get(mode) !== run) return
-        setVcs("diff", mode, result.data ?? [])
-        setVcs("ready", mode, true)
-      })
-      .catch((error) => {
-        if (vcsRun.get(mode) !== run) return
-        console.debug("[session-review] failed to load vcs diff", { mode, error })
-        setVcs("diff", mode, [])
-        setVcs("ready", mode, true)
-      })
-      .finally(() => {
-        if (vcsTask.get(mode) === task) vcsTask.delete(mode)
-      })
-
-    vcsTask.set(mode, task)
-    return task
-  }
-
-  const refreshVcs = () => {
-    resetVcs()
-    const mode = untrack(vcsMode)
-    if (!mode) return
-    if (!untrack(wantsReview)) return
-    void loadVcs(mode, true)
-  }
 
   createComputed((prev) => {
     const open = desktopReviewOpen()
@@ -637,42 +554,7 @@ export default function Page() {
   }, desktopReviewOpen())
 
   const turnDiffs = createMemo(() => lastUserMessage()?.summary?.diffs ?? [])
-  const changesOptions = createMemo<ChangeMode[]>(() => {
-    const list: ChangeMode[] = []
-    if (sync.project?.vcs === "git") list.push("git")
-    if (
-      sync.project?.vcs === "git" &&
-      sync.data.vcs?.branch &&
-      sync.data.vcs?.default_branch &&
-      sync.data.vcs.branch !== sync.data.vcs.default_branch
-    ) {
-      list.push("branch")
-    }
-    list.push("session", "turn")
-    return list
-  })
-  const vcsMode = createMemo<VcsMode | undefined>(() => {
-    if (store.changes === "git" || store.changes === "branch") return store.changes
-  })
-  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
-  })
+  const reviewDiffs = createMemo(() => (store.changes === "session" ? diffs() : turnDiffs()))
 
   const newSessionWorktree = createMemo(() => {
     if (store.newSessionWorktree === "create") return "create"
@@ -738,7 +620,13 @@ export default function Page() {
     scrollToMessage(msgs[targetIndex], "auto")
   }
 
-  const sessionEmptyKey = createMemo(() => {
+  const diffsReady = createMemo(() => {
+    const id = params.id
+    if (!id) return true
+    if (!hasReview()) return true
+    return sync.data.session_diff[id] !== undefined
+  })
+  const reviewEmptyKey = createMemo(() => {
     const project = sync.project
     if (project && !project.vcs) return "session.review.noVcs"
     if (sync.data.config.snapshot === false) return "session.review.noSnapshot"
@@ -860,46 +748,13 @@ export default function Page() {
       sessionKey,
       () => {
         setStore("messageId", undefined)
-        setStore("changes", "git")
+        setStore("changes", "session")
         setUi("pendingMessage", undefined)
       },
       { defer: true },
     ),
   )
 
-  createEffect(
-    on(
-      () => sdk.directory,
-      () => {
-        resetVcs()
-      },
-      { defer: true },
-    ),
-  )
-
-  createEffect(
-    on(
-      () => [sync.data.vcs?.branch, sync.data.vcs?.default_branch] as const,
-      (next, prev) => {
-        if (prev === undefined || same(next, prev)) return
-        refreshVcs()
-      },
-      { defer: true },
-    ),
-  )
-
-  const stopVcs = sdk.event.listen((evt) => {
-    if (evt.details.type !== "file.watcher.updated") return
-    const props =
-      typeof evt.details.properties === "object" && evt.details.properties
-        ? (evt.details.properties as Record<string, unknown>)
-        : undefined
-    const file = typeof props?.file === "string" ? props.file : undefined
-    if (!file || file.startsWith(".git/")) return
-    refreshVcs()
-  })
-  onCleanup(stopVcs)
-
   createEffect(
     on(
       () => params.dir,
@@ -1022,40 +877,6 @@ export default function Page() {
   }
 
   const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
-  const wantsReview = createMemo(() =>
-    isDesktop()
-      ? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
-      : store.mobileTab === "changes",
-  )
-
-  createEffect(() => {
-    const list = changesOptions()
-    if (list.includes(store.changes)) return
-    const next = list[0]
-    if (!next) return
-    setStore("changes", next)
-  })
-
-  createEffect(() => {
-    const mode = vcsMode()
-    if (!mode) return
-    if (!wantsReview()) return
-    void loadVcs(mode)
-  })
-
-  createEffect(
-    on(
-      () => sync.data.session_status[params.id ?? ""]?.type,
-      (next, prev) => {
-        const mode = vcsMode()
-        if (!mode) return
-        if (!wantsReview()) return
-        if (next !== "idle" || prev === undefined || prev === "idle") return
-        void loadVcs(mode, true)
-      },
-      { defer: true },
-    ),
-  )
 
   const fileTreeTab = () => layout.fileTree.tab()
   const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value)
@@ -1102,23 +923,21 @@ export default function Page() {
     loadFile: file.load,
   })
 
+  const changesOptions = ["session", "turn"] as const
+  const changesOptionsList = [...changesOptions]
+
   const changesTitle = () => {
-    if (!canReview()) {
+    if (!hasReview()) {
       return null
     }
 
-    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")
-    }
-
     return (
       <Select
-        options={changesOptions()}
+        options={changesOptionsList}
         current={store.changes}
-        label={label}
+        label={(option) =>
+          option === "session" ? language.t("ui.sessionReview.title") : language.t("ui.sessionReview.title.lastTurn")
+        }
         onSelect={(option) => option && setStore("changes", option)}
         variant="ghost"
         size="small"
@@ -1127,34 +946,20 @@ export default function Page() {
     )
   }
 
-  const empty = (text: string) => (
+  const emptyTurn = () => (
     <div class="h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6">
-      <div class="text-14-regular text-text-weak max-w-56">{text}</div>
+      <div class="text-14-regular text-text-weak max-w-56">{language.t("session.review.noChanges")}</div>
     </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())
-  })
-
   const reviewEmpty = (input: { loadingClass: string; emptyClass: string }) => {
-    if (store.changes === "git" || store.changes === "branch") {
-      if (!reviewReady()) return <div class={input.loadingClass}>{language.t("session.review.loadingChanges")}</div>
-      return empty(reviewEmptyText())
-    }
+    if (store.changes === "turn") return emptyTurn()
 
-    if (store.changes === "turn") {
-      return empty(reviewEmptyText())
-    }
-
-    if (hasSessionReview() && !diffsReady()) {
+    if (hasReview() && !diffsReady()) {
       return <div class={input.loadingClass}>{language.t("session.review.loadingChanges")}</div>
     }
 
-    if (sessionEmptyKey() === "session.review.noVcs") {
+    if (reviewEmptyKey() === "session.review.noVcs") {
       return (
         <div class={input.emptyClass}>
           <div class="flex flex-col gap-3">
@@ -1174,7 +979,7 @@ export default function Page() {
 
     return (
       <div class={input.emptyClass}>
-        <div class="text-14-regular text-text-weak max-w-56">{reviewEmptyText()}</div>
+        <div class="text-14-regular text-text-weak max-w-56">{language.t(reviewEmptyKey())}</div>
       </div>
     )
   }
@@ -1278,7 +1083,7 @@ export default function Page() {
     const pending = tree.pendingDiff
     if (!pending) return
     if (!tree.reviewScroll) return
-    if (!reviewReady()) return
+    if (!diffsReady()) return
 
     const attempt = (count: number) => {
       if (tree.pendingDiff !== pending) return
@@ -1319,7 +1124,10 @@ export default function Page() {
     const id = params.id
     if (!id) return
 
-    if (!wantsReview()) return
+    const wants = isDesktop()
+      ? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
+      : store.mobileTab === "changes"
+    if (!wants) return
     if (sync.data.session_diff[id] !== undefined) return
     if (sync.status === "loading") return
 
@@ -1328,7 +1136,13 @@ export default function Page() {
 
   createEffect(
     on(
-      () => [sessionKey(), wantsReview()] as const,
+      () =>
+        [
+          sessionKey(),
+          isDesktop()
+            ? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
+            : store.mobileTab === "changes",
+        ] as const,
       ([key, wants]) => {
         if (diffFrame !== undefined) cancelAnimationFrame(diffFrame)
         if (diffTimer !== undefined) window.clearTimeout(diffTimer)
@@ -2014,12 +1828,6 @@ export default function Page() {
         </div>
 
         <SessionSidePanel
-          canReview={canReview}
-          diffs={reviewDiffs}
-          diffsReady={reviewReady}
-          empty={reviewEmptyText}
-          hasReview={hasReview}
-          reviewCount={reviewCount}
           reviewPanel={reviewPanel}
           activeDiff={tree.activeDiff}
           focusReviewDiff={focusReviewDiff}

+ 36 - 22
packages/app/src/pages/session/session-side-panel.tsx

@@ -8,7 +8,6 @@ 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 { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 
@@ -20,6 +19,7 @@ import { useCommand } from "@/context/command"
 import { useFile, type SelectedLineRange } from "@/context/file"
 import { useLanguage } from "@/context/language"
 import { useLayout } from "@/context/layout"
+import { useSync } from "@/context/sync"
 import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
 import { FileTabContent } from "@/pages/session/file-tabs"
 import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
@@ -27,12 +27,6 @@ import { setSessionHandoff } from "@/pages/session/handoff"
 import { useSessionLayout } from "@/pages/session/session-layout"
 
 export function SessionSidePanel(props: {
-  canReview: () => boolean
-  diffs: () => FileDiff[]
-  diffsReady: () => boolean
-  empty: () => string
-  hasReview: () => boolean
-  reviewCount: () => number
   reviewPanel: () => JSX.Element
   activeDiff?: string
   focusReviewDiff: (path: string) => void
@@ -40,11 +34,12 @@ export function SessionSidePanel(props: {
   size: Sizing
 }) {
   const layout = useLayout()
+  const sync = useSync()
   const file = useFile()
   const language = useLanguage()
   const command = useCommand()
   const dialog = useDialog()
-  const { sessionKey, tabs, view } = useSessionLayout()
+  const { params, sessionKey, tabs, view } = useSessionLayout()
 
   const isDesktop = createMediaQuery("(min-width: 768px)")
 
@@ -59,7 +54,24 @@ export function SessionSidePanel(props: {
   })
   const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px"))
 
-  const diffFiles = createMemo(() => props.diffs().map((d) => d.file))
+  const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
+  const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
+  const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
+  const hasReview = createMemo(() => reviewCount() > 0)
+  const diffsReady = createMemo(() => {
+    const id = params.id
+    if (!id) return true
+    if (!hasReview()) return true
+    return sync.data.session_diff[id] !== undefined
+  })
+
+  const reviewEmptyKey = createMemo(() => {
+    if (sync.project && !sync.project.vcs) return "session.review.noVcs"
+    if (sync.data.config.snapshot === false) return "session.review.noSnapshot"
+    return "session.review.noChanges"
+  })
+
+  const diffFiles = createMemo(() => diffs().map((d) => d.file))
   const kinds = createMemo(() => {
     const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
       if (!a) return b
@@ -70,7 +82,7 @@ export function SessionSidePanel(props: {
     const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
 
     const out = new Map<string, "add" | "del" | "mix">()
-    for (const diff of props.diffs()) {
+    for (const diff of diffs()) {
       const file = normalize(diff.file)
       const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
 
@@ -124,7 +136,7 @@ export function SessionSidePanel(props: {
     pathFromTab: file.pathFromTab,
     normalizeTab,
     review: reviewTab,
-    hasReview: props.canReview,
+    hasReview,
   })
   const contextOpen = tabState.contextOpen
   const openedTabs = tabState.openedTabs
@@ -229,12 +241,12 @@ export function SessionSidePanel(props: {
                         onCleanup(stop)
                       }}
                     >
-                      <Show when={reviewTab() && props.canReview()}>
+                      <Show when={reviewTab()}>
                         <Tabs.Trigger value="review">
                           <div class="flex items-center gap-1.5">
                             <div>{language.t("session.tab.review")}</div>
-                            <Show when={props.hasReview()}>
-                              <div>{props.reviewCount()}</div>
+                            <Show when={hasReview()}>
+                              <div>{reviewCount()}</div>
                             </Show>
                           </div>
                         </Tabs.Trigger>
@@ -291,7 +303,7 @@ export function SessionSidePanel(props: {
                     </Tabs.List>
                   </div>
 
-                  <Show when={reviewTab() && props.canReview()}>
+                  <Show when={reviewTab()}>
                     <Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
                       <Show when={activeTab() === "review"}>{props.reviewPanel()}</Show>
                     </Tabs.Content>
@@ -365,10 +377,8 @@ export function SessionSidePanel(props: {
               >
                 <Tabs.List>
                   <Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
-                    {props.reviewCount()}{" "}
-                    {language.t(
-                      props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
-                    )}
+                    {reviewCount()}{" "}
+                    {language.t(reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other")}
                   </Tabs.Trigger>
                   <Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
                     {language.t("session.files.all")}
@@ -376,9 +386,9 @@ export function SessionSidePanel(props: {
                 </Tabs.List>
                 <Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
                   <Switch>
-                    <Match when={props.hasReview() || !props.diffsReady()}>
+                    <Match when={hasReview()}>
                       <Show
-                        when={props.diffsReady()}
+                        when={diffsReady()}
                         fallback={
                           <div class="px-2 py-2 text-12-regular text-text-weak">
                             {language.t("common.loading")}
@@ -397,7 +407,11 @@ export function SessionSidePanel(props: {
                         />
                       </Show>
                     </Match>
-                    <Match when={true}>{empty(props.empty())}</Match>
+                    <Match when={true}>
+                      {empty(
+                        language.t(sync.project && !sync.project.vcs ? "session.review.noChanges" : reviewEmptyKey()),
+                      )}
+                    </Match>
                   </Switch>
                 </Tabs.Content>
                 <Tabs.Content value="all" class="bg-background-stronger px-3 py-0">

+ 5 - 1
packages/app/src/pages/session/use-session-commands.tsx

@@ -56,7 +56,11 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
     if (!id) return
     return sync.session.get(id)
   }
-  const hasReview = () => !!params.id
+  const hasReview = () => {
+    const id = params.id
+    if (!id) return false
+    return Math.max(info()?.summary?.files ?? 0, (sync.data.session_diff[id] ?? []).length) > 0
+  }
   const normalizeTab = (tab: string) => {
     if (!tab.startsWith("file://")) return tab
     return file.tab(tab)

+ 5 - 5
packages/opencode/src/cli/cmd/github.ts

@@ -28,9 +28,9 @@ import { Provider } from "../../provider/provider"
 import { Bus } from "../../bus"
 import { MessageV2 } from "../../session/message-v2"
 import { SessionPrompt } from "@/session/prompt"
-import { Git } from "@/git"
 import { setTimeout as sleep } from "node:timers/promises"
 import { Process } from "@/util/process"
+import { git } from "@/util/git"
 
 type GitHubAuthor = {
   login: string
@@ -257,7 +257,7 @@ export const GithubInstallCommand = cmd({
             }
 
             // Get repo info
-            const info = (await Git.run(["remote", "get-url", "origin"], { cwd: Instance.worktree })).text().trim()
+            const info = (await git(["remote", "get-url", "origin"], { cwd: Instance.worktree })).text().trim()
             const parsed = parseGitHubRemote(info)
             if (!parsed) {
               prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
@@ -496,20 +496,20 @@ export const GithubRunCommand = cmd({
           : "issue"
         : undefined
       const gitText = async (args: string[]) => {
-        const result = await Git.run(args, { cwd: Instance.worktree })
+        const result = await git(args, { cwd: Instance.worktree })
         if (result.exitCode !== 0) {
           throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
         }
         return result.text().trim()
       }
       const gitRun = async (args: string[]) => {
-        const result = await Git.run(args, { cwd: Instance.worktree })
+        const result = await git(args, { cwd: Instance.worktree })
         if (result.exitCode !== 0) {
           throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
         }
         return result
       }
-      const gitStatus = (args: string[]) => Git.run(args, { cwd: Instance.worktree })
+      const gitStatus = (args: string[]) => git(args, { cwd: Instance.worktree })
       const commitChanges = async (summary: string, actor?: string) => {
         const args = ["commit", "-m", summary]
         if (actor) args.push("-m", `Co-authored-by: ${actor} <${actor}@users.noreply.github.com>`)

+ 4 - 4
packages/opencode/src/cli/cmd/pr.ts

@@ -1,8 +1,8 @@
 import { UI } from "../ui"
 import { cmd } from "./cmd"
-import { Git } from "@/git"
 import { Instance } from "@/project/instance"
 import { Process } from "@/util/process"
+import { git } from "@/util/git"
 
 export const PrCommand = cmd({
   command: "pr <number>",
@@ -67,9 +67,9 @@ export const PrCommand = cmd({
               const remoteName = forkOwner
 
               // Check if remote already exists
-              const remotes = (await Git.run(["remote"], { cwd: Instance.worktree })).text().trim()
+              const remotes = (await git(["remote"], { cwd: Instance.worktree })).text().trim()
               if (!remotes.split("\n").includes(remoteName)) {
-                await Git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
+                await git(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
                   cwd: Instance.worktree,
                 })
                 UI.println(`Added fork remote: ${remoteName}`)
@@ -77,7 +77,7 @@ export const PrCommand = cmd({
 
               // Set upstream to the fork so pushes go there
               const headRefName = prInfo.headRefName
-              await Git.run(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
+              await git(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
                 cwd: Instance.worktree,
               })
             }

+ 7 - 7
packages/opencode/src/file/index.ts

@@ -1,7 +1,7 @@
 import { BusEvent } from "@/bus/bus-event"
 import { InstanceState } from "@/effect/instance-state"
 import { makeRuntime } from "@/effect/run-service"
-import { Git } from "@/git"
+import { git } from "@/util/git"
 import { Effect, Layer, ServiceMap } from "effect"
 import { formatPatch, structuredPatch } from "diff"
 import fs from "fs"
@@ -420,7 +420,7 @@ export namespace File {
 
         return yield* Effect.promise(async () => {
           const diffOutput = (
-            await Git.run(["-c", "core.fsmonitor=false", "-c", "core.quotepath=false", "diff", "--numstat", "HEAD"], {
+            await git(["-c", "core.fsmonitor=false", "-c", "core.quotepath=false", "diff", "--numstat", "HEAD"], {
               cwd: Instance.directory,
             })
           ).text()
@@ -440,7 +440,7 @@ export namespace File {
           }
 
           const untrackedOutput = (
-            await Git.run(
+            await git(
               [
                 "-c",
                 "core.fsmonitor=false",
@@ -473,7 +473,7 @@ export namespace File {
           }
 
           const deletedOutput = (
-            await Git.run(
+            await git(
               [
                 "-c",
                 "core.fsmonitor=false",
@@ -564,17 +564,17 @@ export namespace File {
 
           if (Instance.project.vcs === "git") {
             let diff = (
-              await Git.run(["-c", "core.fsmonitor=false", "diff", "--", file], { cwd: Instance.directory })
+              await git(["-c", "core.fsmonitor=false", "diff", "--", file], { cwd: Instance.directory })
             ).text()
             if (!diff.trim()) {
               diff = (
-                await Git.run(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file], {
+                await git(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file], {
                   cwd: Instance.directory,
                 })
               ).text()
             }
             if (diff.trim()) {
-              const original = (await Git.run(["show", `HEAD:${file}`], { cwd: Instance.directory })).text()
+              const original = (await git(["show", `HEAD:${file}`], { cwd: Instance.directory })).text()
               const patch = structuredPatch(file, file, original, content, "old", "new", {
                 context: Infinity,
                 ignoreWhitespace: true,

+ 2 - 2
packages/opencode/src/file/watcher.ts

@@ -10,8 +10,8 @@ import { BusEvent } from "@/bus/bus-event"
 import { InstanceState } from "@/effect/instance-state"
 import { makeRuntime } from "@/effect/run-service"
 import { Flag } from "@/flag/flag"
-import { Git } from "@/git"
 import { Instance } from "@/project/instance"
+import { git } from "@/util/git"
 import { lazy } from "@/util/lazy"
 import { Config } from "../config/config"
 import { FileIgnore } from "./ignore"
@@ -130,7 +130,7 @@ export namespace FileWatcher {
 
             if (Instance.project.vcs === "git") {
               const result = yield* Effect.promise(() =>
-                Git.run(["rev-parse", "--git-dir"], {
+                git(["rev-parse", "--git-dir"], {
                   cwd: Instance.project.worktree,
                 }),
               )

+ 0 - 308
packages/opencode/src/git/index.ts

@@ -1,308 +0,0 @@
-import { NodeFileSystem, NodePath } from "@effect/platform-node"
-import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
-import { Effect, Layer, ServiceMap, Stream } from "effect"
-import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
-import { makeRuntime } from "@/effect/run-service"
-
-export namespace Git {
-  const cfg = [
-    "--no-optional-locks",
-    "-c",
-    "core.autocrlf=false",
-    "-c",
-    "core.fsmonitor=false",
-    "-c",
-    "core.longpaths=true",
-    "-c",
-    "core.symlinks=true",
-    "-c",
-    "core.quotepath=false",
-  ] as const
-
-  const out = (result: { text(): string }) => result.text().trim()
-  const nuls = (text: string) => text.split("\0").filter(Boolean)
-  const fail = (err: unknown) =>
-    ({
-      exitCode: 1,
-      text: () => "",
-      stdout: Buffer.alloc(0),
-      stderr: Buffer.from(err instanceof Error ? err.message : String(err)),
-    }) satisfies Result
-
-  export type Kind = "added" | "deleted" | "modified"
-
-  export type Base = {
-    readonly name: string
-    readonly ref: string
-  }
-
-  export type Item = {
-    readonly file: string
-    readonly code: string
-    readonly status: Kind
-  }
-
-  export type Stat = {
-    readonly file: string
-    readonly additions: number
-    readonly deletions: number
-  }
-
-  export interface Result {
-    readonly exitCode: number
-    readonly text: () => string
-    readonly stdout: Buffer
-    readonly stderr: Buffer
-  }
-
-  export interface Options {
-    readonly cwd: string
-    readonly env?: Record<string, string>
-  }
-
-  export interface Interface {
-    readonly run: (args: string[], opts: Options) => Effect.Effect<Result>
-    readonly branch: (cwd: string) => Effect.Effect<string | undefined>
-    readonly prefix: (cwd: string) => Effect.Effect<string>
-    readonly defaultBranch: (cwd: string) => Effect.Effect<Base | undefined>
-    readonly hasHead: (cwd: string) => Effect.Effect<boolean>
-    readonly mergeBase: (cwd: string, base: string, head?: string) => Effect.Effect<string | undefined>
-    readonly show: (cwd: string, ref: string, file: string, prefix?: string) => Effect.Effect<string>
-    readonly status: (cwd: string) => Effect.Effect<Item[]>
-    readonly diff: (cwd: string, ref: string) => Effect.Effect<Item[]>
-    readonly stats: (cwd: string, ref: string) => Effect.Effect<Stat[]>
-  }
-
-  const kind = (code: string): Kind => {
-    if (code === "??") return "added"
-    if (code.includes("U")) return "modified"
-    if (code.includes("A") && !code.includes("D")) return "added"
-    if (code.includes("D") && !code.includes("A")) return "deleted"
-    return "modified"
-  }
-
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Git") {}
-
-  export const layer = Layer.effect(
-    Service,
-    Effect.gen(function* () {
-      const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
-
-      const run = Effect.fn("Git.run")(
-        function* (args: string[], opts: Options) {
-          const proc = ChildProcess.make("git", [...cfg, ...args], {
-            cwd: opts.cwd,
-            env: opts.env,
-            extendEnv: true,
-            stdin: "ignore",
-            stdout: "pipe",
-            stderr: "pipe",
-          })
-          const handle = yield* spawner.spawn(proc)
-          const [stdout, stderr] = yield* Effect.all(
-            [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))],
-            { concurrency: 2 },
-          )
-          return {
-            exitCode: yield* handle.exitCode,
-            text: () => stdout,
-            stdout: Buffer.from(stdout),
-            stderr: Buffer.from(stderr),
-          } satisfies Result
-        },
-        Effect.scoped,
-        Effect.catch((err) => Effect.succeed(fail(err))),
-      )
-
-      const text = Effect.fn("Git.text")(function* (args: string[], opts: Options) {
-        return (yield* run(args, opts)).text()
-      })
-
-      const lines = Effect.fn("Git.lines")(function* (args: string[], opts: Options) {
-        return (yield* text(args, opts))
-          .split(/\r?\n/)
-          .map((item) => item.trim())
-          .filter(Boolean)
-      })
-
-      const refs = Effect.fnUntraced(function* (cwd: string) {
-        return yield* lines(["for-each-ref", "--format=%(refname:short)", "refs/heads"], { cwd })
-      })
-
-      const configured = Effect.fnUntraced(function* (cwd: string, list: string[]) {
-        const result = yield* run(["config", "init.defaultBranch"], { cwd })
-        const name = out(result)
-        if (!name || !list.includes(name)) return
-        return { name, ref: name } satisfies Base
-      })
-
-      const primary = Effect.fnUntraced(function* (cwd: string) {
-        const list = yield* lines(["remote"], { cwd })
-        if (list.includes("origin")) return "origin"
-        if (list.length === 1) return list[0]
-        if (list.includes("upstream")) return "upstream"
-        return list[0]
-      })
-
-      const branch = Effect.fn("Git.branch")(function* (cwd: string) {
-        const result = yield* run(["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd })
-        if (result.exitCode !== 0) return
-        const text = out(result)
-        return text || undefined
-      })
-
-      const prefix = Effect.fn("Git.prefix")(function* (cwd: string) {
-        const result = yield* run(["rev-parse", "--show-prefix"], { cwd })
-        if (result.exitCode !== 0) return ""
-        return out(result)
-      })
-
-      const defaultBranch = Effect.fn("Git.defaultBranch")(function* (cwd: string) {
-        const remote = yield* primary(cwd)
-        if (remote) {
-          const head = yield* run(["symbolic-ref", `refs/remotes/${remote}/HEAD`], { cwd })
-          if (head.exitCode === 0) {
-            const ref = out(head).replace(/^refs\/remotes\//, "")
-            const name = ref.startsWith(`${remote}/`) ? ref.slice(`${remote}/`.length) : ""
-            if (name) return { name, ref } satisfies Base
-          }
-        }
-
-        const list = yield* refs(cwd)
-        const next = yield* configured(cwd, list)
-        if (next) return next
-        if (list.includes("main")) return { name: "main", ref: "main" } satisfies Base
-        if (list.includes("master")) return { name: "master", ref: "master" } satisfies Base
-      })
-
-      const hasHead = Effect.fn("Git.hasHead")(function* (cwd: string) {
-        const result = yield* run(["rev-parse", "--verify", "HEAD"], { cwd })
-        return result.exitCode === 0
-      })
-
-      const mergeBase = Effect.fn("Git.mergeBase")(function* (cwd: string, base: string, head = "HEAD") {
-        const result = yield* run(["merge-base", base, head], { cwd })
-        if (result.exitCode !== 0) return
-        const text = out(result)
-        return text || undefined
-      })
-
-      const show = Effect.fn("Git.show")(function* (cwd: string, ref: string, file: string, prefix = "") {
-        const target = prefix ? `${prefix}${file}` : file
-        const result = yield* run(["show", `${ref}:${target}`], { cwd })
-        if (result.exitCode !== 0) return ""
-        if (result.stdout.includes(0)) return ""
-        return result.text()
-      })
-
-      const status = Effect.fn("Git.status")(function* (cwd: string) {
-        return nuls(
-          yield* text(["status", "--porcelain=v1", "--untracked-files=all", "--no-renames", "-z", "--", "."], {
-            cwd,
-          }),
-        ).flatMap((item) => {
-          const file = item.slice(3)
-          if (!file) return []
-          const code = item.slice(0, 2)
-          return [{ file, code, status: kind(code) } satisfies Item]
-        })
-      })
-
-      const diff = Effect.fn("Git.diff")(function* (cwd: string, ref: string) {
-        const list = nuls(
-          yield* text(["diff", "--no-ext-diff", "--no-renames", "--name-status", "-z", ref, "--", "."], { cwd }),
-        )
-        return list.flatMap((code, idx) => {
-          if (idx % 2 !== 0) return []
-          const file = list[idx + 1]
-          if (!code || !file) return []
-          return [{ file, code, status: kind(code) } satisfies Item]
-        })
-      })
-
-      const stats = Effect.fn("Git.stats")(function* (cwd: string, ref: string) {
-        return nuls(
-          yield* text(["diff", "--no-ext-diff", "--no-renames", "--numstat", "-z", ref, "--", "."], { cwd }),
-        ).flatMap((item) => {
-          const a = item.indexOf("\t")
-          const b = item.indexOf("\t", a + 1)
-          if (a === -1 || b === -1) return []
-          const file = item.slice(b + 1)
-          if (!file) return []
-          const adds = item.slice(0, a)
-          const dels = item.slice(a + 1, b)
-          const additions = adds === "-" ? 0 : Number.parseInt(adds || "0", 10)
-          const deletions = dels === "-" ? 0 : Number.parseInt(dels || "0", 10)
-          return [
-            {
-              file,
-              additions: Number.isFinite(additions) ? additions : 0,
-              deletions: Number.isFinite(deletions) ? deletions : 0,
-            } satisfies Stat,
-          ]
-        })
-      })
-
-      return Service.of({
-        run,
-        branch,
-        prefix,
-        defaultBranch,
-        hasHead,
-        mergeBase,
-        show,
-        status,
-        diff,
-        stats,
-      })
-    }),
-  )
-
-  export const defaultLayer = layer.pipe(
-    Layer.provide(CrossSpawnSpawner.layer),
-    Layer.provide(NodeFileSystem.layer),
-    Layer.provide(NodePath.layer),
-  )
-
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export function run(args: string[], opts: Options) {
-    return runPromise((git) => git.run(args, opts))
-  }
-
-  export function branch(cwd: string) {
-    return runPromise((git) => git.branch(cwd))
-  }
-
-  export function prefix(cwd: string) {
-    return runPromise((git) => git.prefix(cwd))
-  }
-
-  export function defaultBranch(cwd: string) {
-    return runPromise((git) => git.defaultBranch(cwd))
-  }
-
-  export function hasHead(cwd: string) {
-    return runPromise((git) => git.hasHead(cwd))
-  }
-
-  export function mergeBase(cwd: string, base: string, head?: string) {
-    return runPromise((git) => git.mergeBase(cwd, base, head))
-  }
-
-  export function show(cwd: string, ref: string, file: string, prefix?: string) {
-    return runPromise((git) => git.show(cwd, ref, file, prefix))
-  }
-
-  export function status(cwd: string) {
-    return runPromise((git) => git.status(cwd))
-  }
-
-  export function diff(cwd: string, ref: string) {
-    return runPromise((git) => git.diff(cwd, ref))
-  }
-
-  export function stats(cwd: string, ref: string) {
-    return runPromise((git) => git.stats(cwd, ref))
-  }
-}

+ 18 - 144
packages/opencode/src/project/vcs.ts

@@ -1,111 +1,17 @@
 import { Effect, Layer, ServiceMap, Stream } from "effect"
-import path from "path"
 import { Bus } from "@/bus"
 import { BusEvent } from "@/bus/bus-event"
 import { InstanceState } from "@/effect/instance-state"
 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 { git } from "@/util/git"
 import { Instance } from "./instance"
 import z from "zod"
 
 export namespace Vcs {
   const log = Log.create({ service: "vcs" })
 
-  const count = (text: string) => {
-    if (!text) return 0
-    if (!text.endsWith("\n")) return text.split("\n").length
-    return text.slice(0, -1).split("\n").length
-  }
-
-  const work = Effect.fnUntraced(function* (fs: AppFileSystem.Interface, cwd: string, file: string) {
-    const full = path.join(cwd, file)
-    if (!(yield* fs.exists(full).pipe(Effect.orDie))) return ""
-    const buf = yield* fs.readFile(full).pipe(Effect.catch(() => Effect.succeed(new Uint8Array())))
-    if (Buffer.from(buf).includes(0)) return ""
-    return Buffer.from(buf).toString("utf8")
-  })
-
-  const nums = (list: Git.Stat[]) =>
-    new Map(list.map((item) => [item.file, { additions: item.additions, deletions: item.deletions }] as const))
-
-  const merge = (...lists: Git.Item[][]) => {
-    const out = new Map<string, Git.Item>()
-    lists.flat().forEach((item) => {
-      if (!out.has(item.file)) out.set(item.file, item)
-    })
-    return [...out.values()]
-  }
-
-  const files = Effect.fnUntraced(function* (
-    fs: AppFileSystem.Interface,
-    git: Git.Interface,
-    cwd: string,
-    ref: string | undefined,
-    list: Git.Item[],
-    map: Map<string, { additions: number; deletions: number }>,
-  ) {
-    const base = ref ? yield* git.prefix(cwd) : ""
-    const next = yield* Effect.forEach(
-      list,
-      (item) =>
-        Effect.gen(function* () {
-          const before = item.status === "added" || !ref ? "" : yield* git.show(cwd, ref, item.file, base)
-          const after = item.status === "deleted" ? "" : yield* work(fs, cwd, item.file)
-          const stat = map.get(item.file)
-          return {
-            file: 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
-        }),
-      { concurrency: 8 },
-    )
-    return next.toSorted((a, b) => a.file.localeCompare(b.file))
-  })
-
-  const track = Effect.fnUntraced(function* (
-    fs: AppFileSystem.Interface,
-    git: Git.Interface,
-    cwd: string,
-    ref: string | undefined,
-  ) {
-    if (!ref) return yield* files(fs, git, cwd, ref, yield* git.status(cwd), new Map())
-    const [list, stats] = yield* Effect.all([git.status(cwd), git.stats(cwd, ref)], { concurrency: 2 })
-    return yield* files(fs, git, cwd, ref, list, nums(stats))
-  })
-
-  const compare = Effect.fnUntraced(function* (
-    fs: AppFileSystem.Interface,
-    git: Git.Interface,
-    cwd: string,
-    ref: string,
-  ) {
-    const [list, stats, extra] = yield* Effect.all([git.diff(cwd, ref), git.stats(cwd, ref), git.status(cwd)], {
-      concurrency: 3,
-    })
-    return yield* files(
-      fs,
-      git,
-      cwd,
-      ref,
-      merge(
-        list,
-        extra.filter((item) => item.code === "??"),
-      ),
-      nums(stats),
-    )
-  })
-
-  export const Mode = z.enum(["git", "branch"])
-  export type Mode = z.infer<typeof Mode>
-
   export const Event = {
     BranchUpdated: BusEvent.define(
       "vcs.branch.updated",
@@ -118,7 +24,6 @@ export namespace Vcs {
   export const Info = z
     .object({
       branch: z.string().optional(),
-      default_branch: z.string().optional(),
     })
     .meta({
       ref: "VcsInfo",
@@ -128,40 +33,42 @@ export namespace Vcs {
   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[]>
   }
 
   interface State {
     current: string | undefined
-    root: Git.Base | undefined
   }
 
   export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Vcs") {}
 
-  export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Git.Service | Bus.Service> = Layer.effect(
+  export const layer: Layer.Layer<Service, never, Bus.Service> = Layer.effect(
     Service,
     Effect.gen(function* () {
-      const fs = yield* AppFileSystem.Service
-      const git = yield* Git.Service
       const bus = yield* Bus.Service
       const state = yield* InstanceState.make<State>(
         Effect.fn("Vcs.state")((ctx) =>
           Effect.gen(function* () {
             if (ctx.project.vcs !== "git") {
-              return { current: undefined, root: undefined }
+              return { current: undefined }
+            }
+
+            const get = async () => {
+              const result = await git(["rev-parse", "--abbrev-ref", "HEAD"], {
+                cwd: ctx.worktree,
+              })
+              if (result.exitCode !== 0) return undefined
+              const text = result.text().trim()
+              return text || undefined
             }
 
-            const get = () => Effect.runPromise(git.branch(ctx.directory))
-            const [current, root] = yield* Effect.all([git.branch(ctx.directory), git.defaultBranch(ctx.directory)], {
-              concurrency: 2,
-            })
-            const value = { current, root }
-            log.info("initialized", { branch: value.current, default_branch: value.root?.name })
+            const value = {
+              current: yield* Effect.promise(() => get()),
+            }
+            log.info("initialized", { branch: value.current })
 
             yield* bus.subscribe(FileWatcher.Event.Updated).pipe(
               Stream.filter((evt) => evt.properties.file.endsWith("HEAD")),
-              Stream.runForEach((_evt) =>
+              Stream.runForEach(() =>
                 Effect.gen(function* () {
                   const next = yield* Effect.promise(() => get())
                   if (next !== value.current) {
@@ -186,36 +93,11 @@ export namespace Vcs {
         branch: Effect.fn("Vcs.branch")(function* () {
           return yield* InstanceState.use(state, (x) => x.current)
         }),
-        defaultBranch: Effect.fn("Vcs.defaultBranch")(function* () {
-          return yield* InstanceState.use(state, (x) => x.root?.name)
-        }),
-        diff: Effect.fn("Vcs.diff")(function* (mode: Mode) {
-          const value = yield* InstanceState.get(state)
-          if (Instance.project.vcs !== "git") return []
-          if (mode === "git") {
-            return yield* track(
-              fs,
-              git,
-              Instance.directory,
-              (yield* git.hasHead(Instance.directory)) ? "HEAD" : undefined,
-            )
-          }
-
-          if (!value.root) return []
-          if (value.current && value.current === value.root.name) return []
-          const ref = yield* git.mergeBase(Instance.directory, value.root.ref)
-          if (!ref) return []
-          return yield* compare(fs, git, Instance.directory, ref)
-        }),
       })
     }),
   )
 
-  export const defaultLayer = layer.pipe(
-    Layer.provide(Git.defaultLayer),
-    Layer.provide(AppFileSystem.defaultLayer),
-    Layer.provide(Bus.layer),
-  )
+  export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
 
   const { runPromise } = makeRuntime(Service, defaultLayer)
 
@@ -226,12 +108,4 @@ export namespace Vcs {
   export function branch() {
     return runPromise((svc) => svc.branch())
   }
-
-  export function defaultBranch() {
-    return runPromise((svc) => svc.defaultBranch())
-  }
-
-  export function diff(mode: Mode) {
-    return runPromise((svc) => svc.diff(mode))
-  }
 }

+ 1 - 30
packages/opencode/src/server/server.ts

@@ -39,7 +39,6 @@ import { websocket } from "hono/bun"
 import { HTTPException } from "hono/http-exception"
 import { errors } from "./error"
 import { Filesystem } from "@/util/filesystem"
-import { Snapshot } from "@/snapshot"
 import { QuestionRoutes } from "./routes/question"
 import { PermissionRoutes } from "./routes/permission"
 import { GlobalRoutes } from "./routes/global"
@@ -338,40 +337,12 @@ export namespace Server {
           },
         }),
         async (c) => {
-          const [branch, default_branch] = await Promise.all([Vcs.branch(), Vcs.defaultBranch()])
+          const branch = await Vcs.branch()
           return c.json({
             branch,
-            default_branch,
           })
         },
       )
-      .get(
-        "/vcs/diff",
-        describeRoute({
-          summary: "Get VCS diff",
-          description: "Retrieve the current git diff for the working tree or against the default branch.",
-          operationId: "vcs.diff",
-          responses: {
-            200: {
-              description: "VCS diff",
-              content: {
-                "application/json": {
-                  schema: resolver(Snapshot.FileDiff.array()),
-                },
-              },
-            },
-          },
-        }),
-        validator(
-          "query",
-          z.object({
-            mode: Vcs.Mode,
-          }),
-        ),
-        async (c) => {
-          return c.json(await Vcs.diff(c.req.valid("query").mode))
-        },
-      )
       .get(
         "/command",
         describeRoute({

+ 2 - 2
packages/opencode/src/storage/storage.ts

@@ -7,8 +7,8 @@ import { lazy } from "../util/lazy"
 import { Lock } from "../util/lock"
 import { NamedError } from "@opencode-ai/util/error"
 import z from "zod"
-import { Git } from "@/git"
 import { Glob } from "../util/glob"
+import { git } from "@/util/git"
 
 export namespace Storage {
   const log = Log.create({ service: "storage" })
@@ -49,7 +49,7 @@ export namespace Storage {
           }
           if (!worktree) continue
           if (!(await Filesystem.isDir(worktree))) continue
-          const result = await Git.run(["rev-list", "--max-parents=0", "--all"], {
+          const result = await git(["rev-list", "--max-parents=0", "--all"], {
             cwd: worktree,
           })
           const [id] = result

+ 35 - 0
packages/opencode/src/util/git.ts

@@ -0,0 +1,35 @@
+import { Process } from "./process"
+
+export interface GitResult {
+  exitCode: number
+  text(): string
+  stdout: Buffer
+  stderr: Buffer
+}
+
+/**
+ * Run a git command.
+ *
+ * Uses Process helpers with stdin ignored to avoid protocol pipe inheritance
+ * issues in embedded/client environments.
+ */
+export async function git(args: string[], opts: { cwd: string; env?: Record<string, string> }): Promise<GitResult> {
+  return Process.run(["git", ...args], {
+    cwd: opts.cwd,
+    env: opts.env,
+    stdin: "ignore",
+    nothrow: true,
+  })
+    .then((result) => ({
+      exitCode: result.code,
+      text: () => result.stdout.toString(),
+      stdout: result.stdout,
+      stderr: result.stderr,
+    }))
+    .catch((error) => ({
+      exitCode: 1,
+      text: () => "",
+      stdout: Buffer.alloc(0),
+      stderr: Buffer.from(error instanceof Error ? error.message : String(error)),
+    }))
+}

+ 41 - 10
packages/opencode/src/worktree/index.ts

@@ -11,7 +11,6 @@ import { Log } from "../util/log"
 import { Slug } from "@opencode-ai/util/slug"
 import { BusEvent } from "@/bus/bus-event"
 import { GlobalBus } from "@/bus/global"
-import { Git } from "@/git"
 import { Effect, FileSystem, Layer, Path, Scope, ServiceMap, Stream } from "effect"
 import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
 import { NodeFileSystem, NodePath } from "@effect/platform-node"
@@ -505,24 +504,56 @@ export namespace Worktree {
 
         const worktreePath = entry.path
 
-        const base = yield* Effect.promise(() => Git.defaultBranch(Instance.worktree))
-        if (!base) {
+        const remoteList = yield* git(["remote"], { cwd: Instance.worktree })
+        if (remoteList.code !== 0) {
+          throw new ResetFailedError({ message: remoteList.stderr || remoteList.text || "Failed to list git remotes" })
+        }
+
+        const remotes = remoteList.text
+          .split("\n")
+          .map((l) => l.trim())
+          .filter(Boolean)
+        const remote = remotes.includes("origin")
+          ? "origin"
+          : remotes.length === 1
+            ? remotes[0]
+            : remotes.includes("upstream")
+              ? "upstream"
+              : ""
+
+        const remoteHead = remote
+          ? yield* git(["symbolic-ref", `refs/remotes/${remote}/HEAD`], { cwd: Instance.worktree })
+          : { code: 1, text: "", stderr: "" }
+
+        const remoteRef = remoteHead.code === 0 ? remoteHead.text.trim() : ""
+        const remoteTarget = remoteRef ? remoteRef.replace(/^refs\/remotes\//, "") : ""
+        const remoteBranch =
+          remote && remoteTarget.startsWith(`${remote}/`) ? remoteTarget.slice(`${remote}/`.length) : ""
+
+        const [mainCheck, masterCheck] = yield* Effect.all(
+          [
+            git(["show-ref", "--verify", "--quiet", "refs/heads/main"], { cwd: Instance.worktree }),
+            git(["show-ref", "--verify", "--quiet", "refs/heads/master"], { cwd: Instance.worktree }),
+          ],
+          { concurrency: 2 },
+        )
+        const localBranch = mainCheck.code === 0 ? "main" : masterCheck.code === 0 ? "master" : ""
+
+        const target = remoteBranch ? `${remote}/${remoteBranch}` : localBranch
+        if (!target) {
           throw new ResetFailedError({ message: "Default branch not found" })
         }
 
-        const sep = base.ref.indexOf("/")
-        if (base.ref !== base.name && sep > 0) {
-          const remote = base.ref.slice(0, sep)
-          const branch = base.ref.slice(sep + 1)
+        if (remoteBranch) {
           yield* gitExpect(
-            ["fetch", remote, branch],
+            ["fetch", remote, remoteBranch],
             { cwd: Instance.worktree },
-            (r) => new ResetFailedError({ message: r.stderr || r.text || `Failed to fetch ${base.ref}` }),
+            (r) => new ResetFailedError({ message: r.stderr || r.text || `Failed to fetch ${target}` }),
           )
         }
 
         yield* gitExpect(
-          ["reset", "--hard", base.ref],
+          ["reset", "--hard", target],
           { cwd: worktreePath },
           (r) => new ResetFailedError({ message: r.stderr || r.text || "Failed to reset worktree to target" }),
         )

+ 0 - 128
packages/opencode/test/git/git.test.ts

@@ -1,128 +0,0 @@
-import { $ } from "bun"
-import { describe, expect, test } from "bun:test"
-import fs from "fs/promises"
-import path from "path"
-import { ManagedRuntime } from "effect"
-import { Git } from "../../src/git"
-import { tmpdir } from "../fixture/fixture"
-
-const weird = process.platform === "win32" ? "space file.txt" : "tab\tfile.txt"
-
-async function withGit<T>(body: (rt: ManagedRuntime.ManagedRuntime<Git.Service, never>) => Promise<T>) {
-  const rt = ManagedRuntime.make(Git.defaultLayer)
-  try {
-    return await body(rt)
-  } finally {
-    await rt.dispose()
-  }
-}
-
-describe("Git", () => {
-  test("branch() returns current branch name", async () => {
-    await using tmp = await tmpdir({ git: true })
-
-    await withGit(async (rt) => {
-      const branch = await rt.runPromise(Git.Service.use((git) => git.branch(tmp.path)))
-      expect(branch).toBeDefined()
-      expect(typeof branch).toBe("string")
-    })
-  })
-
-  test("branch() returns undefined for non-git directories", async () => {
-    await using tmp = await tmpdir()
-
-    await withGit(async (rt) => {
-      const branch = await rt.runPromise(Git.Service.use((git) => git.branch(tmp.path)))
-      expect(branch).toBeUndefined()
-    })
-  })
-
-  test("branch() returns undefined for detached HEAD", async () => {
-    await using tmp = await tmpdir({ git: true })
-    const hash = (await $`git rev-parse HEAD`.cwd(tmp.path).quiet().text()).trim()
-    await $`git checkout --detach ${hash}`.cwd(tmp.path).quiet()
-
-    await withGit(async (rt) => {
-      const branch = await rt.runPromise(Git.Service.use((git) => git.branch(tmp.path)))
-      expect(branch).toBeUndefined()
-    })
-  })
-
-  test("defaultBranch() uses init.defaultBranch when available", async () => {
-    await using tmp = await tmpdir({ git: true })
-    await $`git branch -M trunk`.cwd(tmp.path).quiet()
-    await $`git config init.defaultBranch trunk`.cwd(tmp.path).quiet()
-
-    await withGit(async (rt) => {
-      const branch = await rt.runPromise(Git.Service.use((git) => git.defaultBranch(tmp.path)))
-      expect(branch?.name).toBe("trunk")
-      expect(branch?.ref).toBe("trunk")
-    })
-  })
-
-  test("status() handles special filenames", async () => {
-    await using tmp = await tmpdir({ git: true })
-    await fs.writeFile(path.join(tmp.path, weird), "hello\n", "utf-8")
-
-    await withGit(async (rt) => {
-      const status = await rt.runPromise(Git.Service.use((git) => git.status(tmp.path)))
-      expect(status).toEqual(
-        expect.arrayContaining([
-          expect.objectContaining({
-            file: weird,
-            status: "added",
-          }),
-        ]),
-      )
-    })
-  })
-
-  test("diff(), stats(), and mergeBase() parse tracked changes", async () => {
-    await using tmp = await tmpdir({ git: true })
-    await $`git branch -M main`.cwd(tmp.path).quiet()
-    await fs.writeFile(path.join(tmp.path, weird), "before\n", "utf-8")
-    await $`git add .`.cwd(tmp.path).quiet()
-    await $`git commit --no-gpg-sign -m "add file"`.cwd(tmp.path).quiet()
-    await $`git checkout -b feature/test`.cwd(tmp.path).quiet()
-    await fs.writeFile(path.join(tmp.path, weird), "after\n", "utf-8")
-
-    await withGit(async (rt) => {
-      const [base, diff, stats] = await Promise.all([
-        rt.runPromise(Git.Service.use((git) => git.mergeBase(tmp.path, "main"))),
-        rt.runPromise(Git.Service.use((git) => git.diff(tmp.path, "HEAD"))),
-        rt.runPromise(Git.Service.use((git) => git.stats(tmp.path, "HEAD"))),
-      ])
-
-      expect(base).toBeTruthy()
-      expect(diff).toEqual(
-        expect.arrayContaining([
-          expect.objectContaining({
-            file: weird,
-            status: "modified",
-          }),
-        ]),
-      )
-      expect(stats).toEqual(
-        expect.arrayContaining([
-          expect.objectContaining({
-            file: weird,
-            additions: 1,
-            deletions: 1,
-          }),
-        ]),
-      )
-    })
-  })
-
-  test("show() returns empty text for binary blobs", async () => {
-    await using tmp = await tmpdir({ git: true })
-    await fs.writeFile(path.join(tmp.path, "bin.dat"), new Uint8Array([0, 1, 2, 3]))
-    await $`git add .`.cwd(tmp.path).quiet()
-    await $`git commit --no-gpg-sign -m "add binary"`.cwd(tmp.path).quiet()
-
-    await withGit(async (rt) => {
-      const text = await rt.runPromise(Git.Service.use((git) => git.show(tmp.path, "HEAD", "bin.dat")))
-      expect(text).toBe("")
-    })
-  })
-})

+ 4 - 122
packages/opencode/test/project/vcs.test.ts

@@ -8,13 +8,8 @@ import { Instance } from "../../src/project/instance"
 import { GlobalBus } from "../../src/bus/global"
 import { Vcs } from "../../src/project/vcs"
 
-// Skip in CI — native @parcel/watcher binding needed
 const describeVcs = FileWatcher.hasNativeBinding() && !process.env.CI ? describe : describe.skip
 
-// ---------------------------------------------------------------------------
-// Helpers
-// ---------------------------------------------------------------------------
-
 async function withVcs(directory: string, body: () => Promise<void>) {
   return Instance.provide({
     directory,
@@ -27,20 +22,8 @@ async function withVcs(directory: string, body: () => Promise<void>) {
   })
 }
 
-function withVcsOnly(directory: string, body: () => Promise<void>) {
-  return Instance.provide({
-    directory,
-    fn: async () => {
-      Vcs.init()
-      await body()
-    },
-  })
-}
-
 type BranchEvent = { directory?: string; payload: { type: string; properties: { branch?: string } } }
-const weird = process.platform === "win32" ? "space file.txt" : "tab\tfile.txt"
 
-/** Wait for a Vcs.Event.BranchUpdated event on GlobalBus, with retry polling as fallback */
 function nextBranchUpdate(directory: string, timeout = 10_000) {
   return new Promise<string | undefined>((resolve, reject) => {
     let settled = false
@@ -66,10 +49,6 @@ function nextBranchUpdate(directory: string, timeout = 10_000) {
   })
 }
 
-// ---------------------------------------------------------------------------
-// Tests
-// ---------------------------------------------------------------------------
-
 describeVcs("Vcs", () => {
   afterEach(async () => {
     await Instance.disposeAll()
@@ -103,7 +82,8 @@ describeVcs("Vcs", () => {
       const pending = nextBranchUpdate(tmp.path)
 
       const head = path.join(tmp.path, ".git", "HEAD")
-      await fs.writeFile(head, `ref: refs/heads/${branch}\n`)
+      await fs.writeFile(head, `ref: refs/heads/${branch}
+`)
 
       const updated = await pending
       expect(updated).toBe(branch)
@@ -119,7 +99,8 @@ describeVcs("Vcs", () => {
       const pending = nextBranchUpdate(tmp.path)
 
       const head = path.join(tmp.path, ".git", "HEAD")
-      await fs.writeFile(head, `ref: refs/heads/${branch}\n`)
+      await fs.writeFile(head, `ref: refs/heads/${branch}
+`)
 
       await pending
       const current = await Vcs.branch()
@@ -127,102 +108,3 @@ describeVcs("Vcs", () => {
     })
   })
 })
-
-describe("Vcs diff", () => {
-  afterEach(async () => {
-    await Instance.disposeAll()
-  })
-
-  test("defaultBranch() falls back to main", async () => {
-    await using tmp = await tmpdir({ git: true })
-    await $`git branch -M main`.cwd(tmp.path).quiet()
-
-    await withVcsOnly(tmp.path, async () => {
-      const branch = await Vcs.defaultBranch()
-      expect(branch).toBe("main")
-    })
-  })
-
-  test("defaultBranch() uses init.defaultBranch when available", async () => {
-    await using tmp = await tmpdir({ git: true })
-    await $`git branch -M trunk`.cwd(tmp.path).quiet()
-    await $`git config init.defaultBranch trunk`.cwd(tmp.path).quiet()
-
-    await withVcsOnly(tmp.path, async () => {
-      const branch = await Vcs.defaultBranch()
-      expect(branch).toBe("trunk")
-    })
-  })
-
-  test("detects current branch from the active worktree", async () => {
-    await using tmp = await tmpdir({ git: true })
-    await using wt = await tmpdir()
-    await $`git branch -M main`.cwd(tmp.path).quiet()
-    const dir = path.join(wt.path, "feature")
-    await $`git worktree add -b feature/test ${dir} HEAD`.cwd(tmp.path).quiet()
-
-    await withVcsOnly(dir, async () => {
-      const [branch, base] = await Promise.all([Vcs.branch(), Vcs.defaultBranch()])
-      expect(branch).toBe("feature/test")
-      expect(base).toBe("main")
-    })
-  })
-
-  test("diff('git') returns uncommitted changes", async () => {
-    await using tmp = await tmpdir({ git: true })
-    await fs.writeFile(path.join(tmp.path, "file.txt"), "original\n", "utf-8")
-    await $`git add .`.cwd(tmp.path).quiet()
-    await $`git commit --no-gpg-sign -m "add file"`.cwd(tmp.path).quiet()
-    await fs.writeFile(path.join(tmp.path, "file.txt"), "changed\n", "utf-8")
-
-    await withVcsOnly(tmp.path, async () => {
-      const diff = await Vcs.diff("git")
-      expect(diff).toEqual(
-        expect.arrayContaining([
-          expect.objectContaining({
-            file: "file.txt",
-            status: "modified",
-          }),
-        ]),
-      )
-    })
-  })
-
-  test("diff('git') handles special filenames", async () => {
-    await using tmp = await tmpdir({ git: true })
-    await fs.writeFile(path.join(tmp.path, weird), "hello\n", "utf-8")
-
-    await withVcsOnly(tmp.path, async () => {
-      const diff = await Vcs.diff("git")
-      expect(diff).toEqual(
-        expect.arrayContaining([
-          expect.objectContaining({
-            file: weird,
-            status: "added",
-          }),
-        ]),
-      )
-    })
-  })
-
-  test("diff('branch') returns changes against default branch", async () => {
-    await using tmp = await tmpdir({ git: true })
-    await $`git branch -M main`.cwd(tmp.path).quiet()
-    await $`git checkout -b feature/test`.cwd(tmp.path).quiet()
-    await fs.writeFile(path.join(tmp.path, "branch.txt"), "hello\n", "utf-8")
-    await $`git add .`.cwd(tmp.path).quiet()
-    await $`git commit --no-gpg-sign -m "branch file"`.cwd(tmp.path).quiet()
-
-    await withVcsOnly(tmp.path, async () => {
-      const diff = await Vcs.diff("branch")
-      expect(diff).toEqual(
-        expect.arrayContaining([
-          expect.objectContaining({
-            file: "branch.txt",
-            status: "added",
-          }),
-        ]),
-      )
-    })
-  })
-})

+ 0 - 33
packages/sdk/js/src/v2/gen/sdk.gen.ts

@@ -175,7 +175,6 @@ import type {
   TuiSelectSessionResponses,
   TuiShowToastResponses,
   TuiSubmitPromptResponses,
-  VcsDiffResponses,
   VcsGetResponses,
   WorktreeCreateErrors,
   WorktreeCreateInput,
@@ -3740,38 +3739,6 @@ export class Vcs extends HeyApiClient {
       ...params,
     })
   }
-
-  /**
-   * Get VCS diff
-   *
-   * Retrieve the current git diff for the working tree or against the default branch.
-   */
-  public diff<ThrowOnError extends boolean = false>(
-    parameters: {
-      directory?: string
-      workspace?: string
-      mode: "git" | "branch"
-    },
-    options?: Options<never, ThrowOnError>,
-  ) {
-    const params = buildClientParams(
-      [parameters],
-      [
-        {
-          args: [
-            { in: "query", key: "directory" },
-            { in: "query", key: "workspace" },
-            { in: "query", key: "mode" },
-          ],
-        },
-      ],
-    )
-    return (options?.client ?? this.client).get<VcsDiffResponses, unknown, ThrowOnError>({
-      url: "/vcs/diff",
-      ...options,
-      ...params,
-    })
-  }
 }
 
 export class Command extends HeyApiClient {

+ 1 - 22
packages/sdk/js/src/v2/gen/types.gen.ts

@@ -1994,8 +1994,7 @@ export type Path = {
 }
 
 export type VcsInfo = {
-  branch?: string
-  default_branch?: string
+  branch: string
 }
 
 export type Command = {
@@ -5010,26 +5009,6 @@ export type VcsGetResponses = {
 
 export type VcsGetResponse = VcsGetResponses[keyof VcsGetResponses]
 
-export type VcsDiffData = {
-  body?: never
-  path?: never
-  query: {
-    directory?: string
-    workspace?: string
-    mode: "git" | "branch"
-  }
-  url: "/vcs/diff"
-}
-
-export type VcsDiffResponses = {
-  /**
-   * VCS diff
-   */
-  200: Array<FileDiff>
-}
-
-export type VcsDiffResponse = VcsDiffResponses[keyof VcsDiffResponses]
-
 export type CommandListData = {
   body?: never
   path?: never

+ 19 - 17
packages/ui/src/components/session-review.tsx

@@ -151,6 +151,7 @@ export const SessionReview = (props: SessionReviewProps) => {
 
   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 diffStyle = () => props.diffStyle ?? (props.split ? "split" : "unified")
   const hasDiffs = () => files().length > 0
 
@@ -281,10 +282,11 @@ export const SessionReview = (props: SessionReviewProps) => {
           <Show when={hasDiffs()} fallback={props.empty}>
             <div class="pb-6">
               <Accordion multiple value={open()} onChange={handleChange}>
-                <For each={props.diffs}>
-                  {(diff) => {
+                <For each={files()}>
+                  {(file) => {
                     let wrapper: HTMLDivElement | undefined
-                    const file = diff.file
+
+                    const item = createMemo(() => diffs().get(file)!)
 
                     const expanded = createMemo(() => open().includes(file))
                     const force = () => !!store.force[file]
@@ -292,9 +294,9 @@ export const SessionReview = (props: SessionReviewProps) => {
                     const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === 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 changedLines = () => diff.additions + diff.deletions
+                    const beforeText = () => (typeof item().before === "string" ? item().before : "")
+                    const afterText = () => (typeof item().after === "string" ? item().after : "")
+                    const changedLines = () => item().additions + item().deletions
                     const mediaKind = createMemo(() => mediaKindFromPath(file))
 
                     const tooLarge = createMemo(() => {
@@ -305,9 +307,9 @@ export const SessionReview = (props: SessionReviewProps) => {
                     })
 
                     const isAdded = () =>
-                      diff.status === "added" || (beforeText().length === 0 && afterText().length > 0)
+                      item().status === "added" || (beforeText().length === 0 && afterText().length > 0)
                     const isDeleted = () =>
-                      diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0)
+                      item().status === "deleted" || (afterText().length === 0 && beforeText().length > 0)
 
                     const selectedLines = createMemo(() => {
                       const current = selection()
@@ -344,7 +346,7 @@ export const SessionReview = (props: SessionReviewProps) => {
                           file,
                           selection,
                           comment,
-                          preview: selectionPreview(diff, selection),
+                          preview: selectionPreview(item(), selection),
                         })
                       },
                       onUpdate: ({ id, comment, selection }) => {
@@ -353,7 +355,7 @@ export const SessionReview = (props: SessionReviewProps) => {
                           file,
                           selection,
                           comment,
-                          preview: selectionPreview(diff, selection),
+                          preview: selectionPreview(item(), selection),
                         })
                       },
                       onDelete: (comment) => {
@@ -430,7 +432,7 @@ export const SessionReview = (props: SessionReviewProps) => {
                                       <span data-slot="session-review-change" data-type="added">
                                         {i18n.t("ui.sessionReview.change.added")}
                                       </span>
-                                      <DiffChanges changes={diff} />
+                                      <DiffChanges changes={item()} />
                                     </div>
                                   </Match>
                                   <Match when={isDeleted()}>
@@ -444,7 +446,7 @@ export const SessionReview = (props: SessionReviewProps) => {
                                     </span>
                                   </Match>
                                   <Match when={true}>
-                                    <DiffChanges changes={diff} />
+                                    <DiffChanges changes={item()} />
                                   </Match>
                                 </Switch>
                                 <span data-slot="session-review-diff-chevron">
@@ -490,7 +492,7 @@ export const SessionReview = (props: SessionReviewProps) => {
                                   <Dynamic
                                     component={fileComponent}
                                     mode="diff"
-                                    preloadedDiff={diff.preloaded}
+                                    preloadedDiff={item().preloaded}
                                     diffStyle={diffStyle()}
                                     onRendered={() => {
                                       props.onDiffRendered?.()
@@ -507,17 +509,17 @@ export const SessionReview = (props: SessionReviewProps) => {
                                     commentedLines={commentedLines()}
                                     before={{
                                       name: file,
-                                      contents: typeof diff.before === "string" ? diff.before : "",
+                                      contents: typeof item().before === "string" ? item().before : "",
                                     }}
                                     after={{
                                       name: file,
-                                      contents: typeof diff.after === "string" ? diff.after : "",
+                                      contents: typeof item().after === "string" ? item().after : "",
                                     }}
                                     media={{
                                       mode: "auto",
                                       path: file,
-                                      before: diff.before,
-                                      after: diff.after,
+                                      before: item().before,
+                                      after: item().after,
                                       readFile: props.readFile,
                                     }}
                                   />

+ 0 - 2
packages/ui/src/i18n/en.ts

@@ -1,7 +1,5 @@
 export const dict: Record<string, string> = {
   "ui.sessionReview.title": "Session changes",
-  "ui.sessionReview.title.git": "Git changes",
-  "ui.sessionReview.title.branch": "Branch changes",
   "ui.sessionReview.title.lastTurn": "Last turn changes",
   "ui.sessionReview.diffStyle.unified": "Unified",
   "ui.sessionReview.diffStyle.split": "Split",