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 (