From 2e3bc7bbb40429c30ef1cce162b3cf63826c3a33 Mon Sep 17 00:00:00 2001 From: Cameron Gagnon Date: Thu, 9 Jul 2026 22:26:36 +0000 Subject: [PATCH] fix(app): default review tab to the branch diff on feature branches The review tab defaulted to git mode (working tree vs HEAD), so a session on a pushed feature branch showed only uncommitted drift (e.g. opencode rewriting .opencode/package.json to pin @opencode-ai/plugin on startup) instead of the branch's committed work. Prefer the branch diff (branch vs default branch) as the auto-default when a branch option is available, while preserving an explicit user selection. --- packages/app/src/pages/session.tsx | 40 ++++++++------- .../app/src/pages/session/helpers.test.ts | 50 +++++++++++++++++++ packages/app/src/pages/session/helpers.ts | 34 +++++++++++++ 3 files changed, 106 insertions(+), 18 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 1c9391e0fdcf..b2c563c75429 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -56,6 +56,9 @@ import { createSessionTabs, createSizing, focusTerminalById, + resolveReviewChangeMode, + reviewChangeOptions, + type ReviewChangeMode, shouldFocusTerminalOnKeyDown, shouldShowFileTree, } from "@/pages/session/helpers" @@ -82,13 +85,17 @@ type FollowupItem = FollowupDraft & { id: string } type FollowupEdit = Pick const emptyFollowups: FollowupItem[] = [] -type ChangeMode = "git" | "branch" | "turn" +type ChangeMode = ReviewChangeMode type VcsMode = "git" | "branch" const sessionViewState = () => ({ messageId: undefined as string | undefined, mobileTab: "session" as "session" | "changes", changes: "git" as ChangeMode, + // Until the user explicitly picks a review mode, the default is auto-selected + // from changesOptions() so feature-branch sessions open on the full branch diff + // rather than working-tree-only ("git") changes. + changesTouched: false, }) async function runPromptRollbackMutation(input: { @@ -358,17 +365,13 @@ export default function Page() { const project = sync().project return !!project && project.vcs !== "git" }) - const changesOptions = createMemo(() => { - const list: ChangeMode[] = [] - const project = sync().project - const vcs = sync().data.vcs - if (project?.vcs === "git") list.push("git") - if (project?.vcs === "git" && vcs?.branch && vcs?.default_branch && vcs.branch !== vcs.default_branch) { - list.push("branch") - } - list.push("turn") - return list - }) + const changesOptions = createMemo(() => + reviewChangeOptions({ + vcs: sync().project?.vcs, + branch: sync().data.vcs?.branch, + defaultBranch: sync().data.vcs?.default_branch, + }), + ) const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes") const wantsReview = createMemo(() => isDesktop() @@ -736,11 +739,12 @@ export default function Page() { createEffect(() => { if (!sync().project) return - const list = changesOptions() - if (list.includes(store.changes)) return - const next = list[0] - if (!next) return - setStore("changes", next) + const next = resolveReviewChangeMode({ + options: changesOptions(), + current: store.changes, + touched: store.changesTouched, + }) + if (store.changes !== next) setStore("changes", next) }) createEffect( @@ -820,7 +824,7 @@ export default function Page() { options={changesOptions()} current={store.changes} label={label} - onSelect={(option) => option && setStore("changes", option)} + onSelect={(option) => option && setStore({ changes: option, changesTouched: true })} variant="ghost" size="small" valueClass="text-14-medium" diff --git a/packages/app/src/pages/session/helpers.test.ts b/packages/app/src/pages/session/helpers.test.ts index ad6961af3635..4b66e5f4ccc2 100644 --- a/packages/app/src/pages/session/helpers.test.ts +++ b/packages/app/src/pages/session/helpers.test.ts @@ -7,6 +7,8 @@ import { createSessionTabs, focusTerminalById, getTabReorderIndex, + resolveReviewChangeMode, + reviewChangeOptions, shouldFocusTerminalOnKeyDown, shouldShowFileTree, } from "./helpers" @@ -125,6 +127,54 @@ describe("getTabReorderIndex", () => { }) }) +describe("reviewChangeOptions", () => { + test("prefers the branch diff on a feature-branch git session", () => { + expect(reviewChangeOptions({ vcs: "git", branch: "forge-task--abc", defaultBranch: "main" })).toEqual([ + "branch", + "git", + "turn", + ]) + }) + + test("omits branch when on the default branch", () => { + expect(reviewChangeOptions({ vcs: "git", branch: "main", defaultBranch: "main" })).toEqual(["git", "turn"]) + }) + + test("omits branch when branch metadata is missing", () => { + expect(reviewChangeOptions({ vcs: "git" })).toEqual(["git", "turn"]) + }) + + test("offers only turn diffs without git", () => { + expect(reviewChangeOptions({ vcs: "none" })).toEqual(["turn"]) + expect(reviewChangeOptions({})).toEqual(["turn"]) + }) +}) + +describe("resolveReviewChangeMode", () => { + const options = reviewChangeOptions({ vcs: "git", branch: "forge-task--abc", defaultBranch: "main" }) + + test("auto-selects the branch diff when the user has not chosen a mode", () => { + expect(resolveReviewChangeMode({ options, current: "git", touched: false })).toBe("branch") + }) + + test("re-tracks the default even after an unrelated store default", () => { + expect(resolveReviewChangeMode({ options, current: "turn", touched: false })).toBe("branch") + }) + + test("keeps an explicit user choice", () => { + expect(resolveReviewChangeMode({ options, current: "git", touched: true })).toBe("git") + expect(resolveReviewChangeMode({ options, current: "turn", touched: true })).toBe("turn") + }) + + test("falls back to the default when the explicit choice is unavailable", () => { + expect(resolveReviewChangeMode({ options: ["git", "turn"], current: "branch", touched: true })).toBe("git") + }) + + test("returns the current mode when no options exist", () => { + expect(resolveReviewChangeMode({ options: [], current: "git", touched: false })).toBe("git") + }) +}) + describe("createSessionTabs", () => { test("normalizes the effective file tab", () => { createRoot((dispose) => { diff --git a/packages/app/src/pages/session/helpers.ts b/packages/app/src/pages/session/helpers.ts index 53558a95528e..f456713a1a13 100644 --- a/packages/app/src/pages/session/helpers.ts +++ b/packages/app/src/pages/session/helpers.ts @@ -20,6 +20,40 @@ type TabsInput = { export const getSessionKey = (dir: string | undefined, id: string | undefined) => `${dir ?? ""}${id ? `/${id}` : ""}` +export type ReviewChangeMode = "git" | "branch" | "turn" + +// Available review-tab change modes, in auto-default priority order. "branch" +// (current branch vs default branch = the session's committed work) comes first +// so a feature-branch session opens on the full task diff instead of +// working-tree-only ("git") changes, which otherwise surface unrelated startup +// drift (e.g. tooling that rewrites .opencode/package.json) as the "only" change. +export function reviewChangeOptions(input: { + vcs?: string + branch?: string + defaultBranch?: string +}): ReviewChangeMode[] { + const list: ReviewChangeMode[] = [] + const isGit = input.vcs === "git" + if (isGit && input.branch && input.defaultBranch && input.branch !== input.defaultBranch) list.push("branch") + if (isGit) list.push("git") + list.push("turn") + return list +} + +// Resolve the review mode to display. Until the user explicitly picks one +// (`touched`), track the best default (options[0]); after that, keep their +// choice and only fall back to the default when it is no longer available. +export function resolveReviewChangeMode(input: { + options: ReviewChangeMode[] + current: ReviewChangeMode + touched: boolean +}): ReviewChangeMode { + const next = input.options[0] + if (!next) return input.current + if (input.touched) return input.options.includes(input.current) ? input.current : next + return next +} + export function shouldShowFileTree(input: { visible: boolean; opened: boolean }) { return input.opened && input.visible }