From 6eec98371ac491ca37d8f3aea1c3a362263dd5f9 Mon Sep 17 00:00:00 2001 From: Victor Navarro Date: Wed, 24 Jun 2026 12:35:37 +0200 Subject: [PATCH] feat(tui): add main branch source to diff mode (#30942) --- packages/plugin/src/tui.ts | 2 +- .../feature-plugins/system/diff-viewer.tsx | 52 +++++++++++++------ packages/tui/src/plugin/adapters.tsx | 1 + .../tui/test/cli/tui/diff-viewer.test.tsx | 49 +++++++++++++++-- 4 files changed, 82 insertions(+), 22 deletions(-) diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index e36d91381..678121694 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -382,7 +382,7 @@ export type TuiState = { worktree: string directory: string } - readonly vcs: { branch?: string } | undefined + readonly vcs: { branch?: string; default_branch?: string } | undefined session: { count: () => number get: (sessionID: string) => Session | undefined diff --git a/packages/tui/src/feature-plugins/system/diff-viewer.tsx b/packages/tui/src/feature-plugins/system/diff-viewer.tsx index 23e7a1019..ed88a1107 100644 --- a/packages/tui/src/feature-plugins/system/diff-viewer.tsx +++ b/packages/tui/src/feature-plugins/system/diff-viewer.tsx @@ -39,11 +39,11 @@ const ROUTE = "diff" const MIN_SPLIT_WIDTH = 100 const FILE_TREE_WIDTH = 32 const PLAIN_TEXT_FILETYPE = "opencode-plain-text" -const WORKING_TREE_DIFF_CONTEXT_LINES = 12 +const VCS_DIFF_CONTEXT_LINES = 12 const KV_SHOW_FILE_TREE = "diff_viewer_show_file_tree" const KV_SINGLE_PATCH = "diff_viewer_single_patch" const KV_VIEW = "diff_viewer_view" -type DiffMode = "git" | "last-turn" +type DiffMode = "git" | "branch" | "last-turn" type DiffViewerFocus = "patches" | "files" type DiffView = "split" | "unified" type SelectedHunk = { readonly fileIndex: number; readonly hunkIndex: number; readonly scrollTop: number } @@ -82,6 +82,12 @@ function storedView(value: unknown): DiffView | undefined { if (value === "split" || value === "unified") return value } +function diffSourceLabel(mode: DiffMode) { + if (mode === "last-turn") return "last turn" + if (mode === "branch") return "main branch" + return "working tree" +} + function DiffViewer(props: { api: TuiPluginApi }) { const dimensions = useTerminalDimensions() const themeState = useTheme() @@ -117,7 +123,7 @@ function DiffViewer(props: { api: TuiPluginApi }) { } const result = await props.api.client.vcs.diff( - { directory: input.directory, mode: "git", context: WORKING_TREE_DIFF_CONTEXT_LINES }, + { directory: input.directory, mode: input.mode, context: VCS_DIFF_CONTEXT_LINES }, { throwOnError: true }, ) return normalizeDiffs(result.data ?? []) @@ -675,18 +681,30 @@ function DiffViewer(props: { api: TuiPluginApi }) { }, ] - const switchDiffOptions = createMemo(() => [ - { - title: "Working tree", - value: "git" as const, - description: "Show current git changes", - }, - { - title: "Last turn", - value: "last-turn" as const, - description: "Show changes from the last assistant turn", - }, - ]) + const switchDiffOptions = createMemo(() => { + const vcs = props.api.state.vcs + return [ + { + title: "Working tree", + value: "git" as const, + description: "Show current git changes", + }, + ...(vcs?.branch && vcs.default_branch && vcs.branch !== vcs.default_branch + ? [ + { + title: "Main branch", + value: "branch" as const, + description: "Show changes compared to main branch", + }, + ] + : []), + { + title: "Last turn", + value: "last-turn" as const, + description: "Show changes from the last assistant turn", + }, + ] + }) const openSwitchDiffDialog = () => { props.api.ui.dialog.replace(() => ( @@ -736,7 +754,7 @@ function DiffViewer(props: { api: TuiPluginApi }) { Diff - {mode() === "last-turn" ? "last turn" : "working tree"} + {diffSourceLabel(mode())} {files().length} {files().length === 1 ? "file" : "files"} @@ -971,7 +989,7 @@ function DiffViewerHelpDialog() { { shortcut: useCommandShortcut("diff.switch_source"), action: "Switch source", - description: "Choose working tree or last-turn changes", + description: "Choose working tree, main branch, or last-turn changes", }, { shortcut: useCommandShortcut("diff.toggle_view"), diff --git a/packages/tui/src/plugin/adapters.tsx b/packages/tui/src/plugin/adapters.tsx index 216190a1d..fef0ec8eb 100644 --- a/packages/tui/src/plugin/adapters.tsx +++ b/packages/tui/src/plugin/adapters.tsx @@ -113,6 +113,7 @@ function stateApi(sync: ReturnType): TuiPluginApi["state"] { if (!sync.data.vcs) return return { branch: sync.data.vcs.branch, + default_branch: sync.data.vcs.default_branch, } }, session: { diff --git a/packages/tui/test/cli/tui/diff-viewer.test.tsx b/packages/tui/test/cli/tui/diff-viewer.test.tsx index 9af37dbc9..f7fc9c0a5 100644 --- a/packages/tui/test/cli/tui/diff-viewer.test.tsx +++ b/packages/tui/test/cli/tui/diff-viewer.test.tsx @@ -98,14 +98,15 @@ test("brackets navigate diff hunks", async () => { } }) -async function renderDiffViewer(vcsDiff: unknown[], height = 20) { +async function renderDiffViewer(vcsDiff: unknown[], height = 20, initialRoute?: TuiRouteCurrent) { const commands = new Map< string, NonNullable[0]["commands"]>[number] >() - let current = startRoute + let current = initialRoute ?? startRoute let renderDiff: TuiRouteDefinition["render"] | undefined let vcsDiffInput: unknown + let sessionDiffInput: unknown const config = createTuiResolvedConfig() function Harness() { const renderer = useRenderer() @@ -124,7 +125,12 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20) { return { data: vcsDiff } }, }, - session: { diff: async () => ({ data: [] }) }, + session: { + diff: async (input: unknown) => { + sessionDiffInput = input + return { data: [] } + }, + }, } as unknown as TuiPluginApi["client"], state: { session: { @@ -149,7 +155,7 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20) { } satisfies TuiPluginApi void diffViewerPlugin.tui(api, undefined, pluginMeta) - commands.get("diff.open")?.run?.({} as never) + if (!initialRoute) commands.get("diff.open")?.run?.({} as never) return ( @@ -173,6 +179,7 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20) { commands, current: () => current, vcsDiffInput: () => vcsDiffInput, + sessionDiffInput: () => sessionDiffInput, } } @@ -201,6 +208,40 @@ const session = { }, } satisfies Session +test("branch diff source requests branch VCS diff", async () => { + const viewer = await renderDiffViewer([], 20, { + name: "diff", + params: { mode: "branch", sessionID: "session-1", returnRoute: startRoute }, + }) + try { + expect(viewer.current()).toEqual({ + name: "diff", + params: { mode: "branch", sessionID: "session-1", returnRoute: startRoute }, + }) + expect(viewer.vcsDiffInput()).toEqual({ directory: "/repo/session", mode: "branch", context: 12 }) + expect(viewer.sessionDiffInput()).toBeUndefined() + } finally { + viewer.app.renderer.destroy() + } +}) + +test("last-turn diff source requests session diff", async () => { + const viewer = await renderDiffViewer([], 20, { + name: "diff", + params: { mode: "last-turn", sessionID: "session-1", messageID: "message-1", returnRoute: startRoute }, + }) + try { + expect(viewer.current()).toEqual({ + name: "diff", + params: { mode: "last-turn", sessionID: "session-1", messageID: "message-1", returnRoute: startRoute }, + }) + expect(viewer.sessionDiffInput()).toEqual({ sessionID: "session-1", messageID: "message-1" }) + expect(viewer.vcsDiffInput()).toBeUndefined() + } finally { + viewer.app.renderer.destroy() + } +}) + async function waitForCommand( app: Awaited>, commands: Map,