From 74ea770fdc0abe14dedb9f0ab4cc6c27e2eb8605 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 26 Mar 2026 19:34:32 +0530 Subject: [PATCH] revert: roll back git-backed review modes The hosted web app now depends on VCS diff endpoints that are not available in released CLI versions, which breaks opencode web for prod users. Revert the rollout until the web app is bundled with the CLI or the contract is shipped together. --- .../app/src/context/global-sync/bootstrap.ts | 2 +- .../context/global-sync/event-reducer.test.ts | 10 +- .../src/context/global-sync/event-reducer.ts | 4 +- packages/app/src/i18n/en.ts | 2 - packages/app/src/pages/session.tsx | 272 +++------------- .../src/pages/session/session-side-panel.tsx | 58 ++-- .../pages/session/use-session-commands.tsx | 6 +- packages/opencode/src/cli/cmd/github.ts | 10 +- packages/opencode/src/cli/cmd/pr.ts | 8 +- packages/opencode/src/file/index.ts | 14 +- packages/opencode/src/file/watcher.ts | 4 +- packages/opencode/src/git/index.ts | 308 ------------------ packages/opencode/src/project/vcs.ts | 162 +-------- packages/opencode/src/server/server.ts | 31 +- packages/opencode/src/storage/storage.ts | 4 +- packages/opencode/src/util/git.ts | 35 ++ packages/opencode/src/worktree/index.ts | 51 ++- packages/opencode/test/git/git.test.ts | 128 -------- packages/opencode/test/project/vcs.test.ts | 126 +------ packages/sdk/js/src/v2/gen/sdk.gen.ts | 33 -- packages/sdk/js/src/v2/gen/types.gen.ts | 23 +- packages/ui/src/components/session-review.tsx | 36 +- packages/ui/src/i18n/en.ts | 2 - 23 files changed, 227 insertions(+), 1102 deletions(-) delete mode 100644 packages/opencode/src/git/index.ts create mode 100644 packages/opencode/src/util/git.ts delete mode 100644 packages/opencode/test/git/git.test.ts diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 4790011a53e1..6eec688b7487 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/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 ?? []))), diff --git a/packages/app/src/context/global-sync/event-reducer.test.ts b/packages/app/src/context/global-sync/event-reducer.test.ts index 892129788e6c..cf2da135cbbc 100644 --- a/packages/app/src/context/global-sync/event-reducer.test.ts +++ b/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", () => { diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts index 4af636553526..5d8b7c4e3d8e 100644 --- a/packages/app/src/context/global-sync/event-reducer.ts +++ b/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 diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index bdf97ec0fea5..dde09251191c 100644 --- a/packages/app/src/i18n/en.ts +++ b/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", diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 8c32a7237f58..2d3e31355ae6 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 } 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, failed: {} as Record, @@ -560,68 +539,6 @@ export default function Page() { let refreshTimer: number | undefined let diffFrame: number | undefined let diffTimer: number | undefined - const vcsTask = new Map>() - const vcsRun = new Map() - - 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(() => { - 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(() => { - 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) - : 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 (