From c99ccb7e90ff5415623d583766a656d932c46f51 Mon Sep 17 00:00:00 2001 From: vimtor Date: Fri, 5 Jun 2026 13:00:31 +0200 Subject: [PATCH] tui: let users review branch changes against main --- .../feature-plugins/system/diff-viewer.tsx | 21 ++++-- .../test/cli/tui/diff-viewer.test.tsx | 68 +++++++++++++++++++ 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/diff-viewer.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/diff-viewer.tsx index 614408bcec..7555a55c51 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/diff-viewer.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/diff-viewer.tsx @@ -33,11 +33,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" @@ -75,6 +75,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() @@ -106,7 +112,7 @@ function DiffViewer(props: { api: TuiPluginApi }) { } const result = await props.api.client.vcs.diff( - { mode: "git", context: WORKING_TREE_DIFF_CONTEXT_LINES }, + { mode: input.mode, context: VCS_DIFF_CONTEXT_LINES }, { throwOnError: true }, ) return normalizeDiffs(result.data ?? []) @@ -609,6 +615,11 @@ function DiffViewer(props: { api: TuiPluginApi }) { value: "git" as const, description: "Show current git changes", }, + { + title: "Main branch", + value: "branch" as const, + description: "Show changes compared to main branch", + }, { title: "Last turn", value: "last-turn" as const, @@ -664,7 +675,7 @@ function DiffViewer(props: { api: TuiPluginApi }) { Diff - {mode() === "last-turn" ? "last turn" : "working tree"} + {diffSourceLabel(mode())} {files().length} {files().length === 1 ? "file" : "files"} @@ -874,7 +885,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/opencode/test/cli/tui/diff-viewer.test.tsx b/packages/opencode/test/cli/tui/diff-viewer.test.tsx index e4649a448a..e55f2ba814 100644 --- a/packages/opencode/test/cli/tui/diff-viewer.test.tsx +++ b/packages/opencode/test/cli/tui/diff-viewer.test.tsx @@ -85,6 +85,66 @@ test("closing the diff viewer returns to the route it opened from", async () => } }) +test("branch diff source requests branch VCS diff", async () => { + const calls: Parameters[0][] = [] + const current: TuiRouteCurrent = { name: "diff", params: { mode: "branch" } } + let renderDiff: TuiRouteDefinition["render"] | undefined + await mkdir(Global.Path.state, { recursive: true }) + await Bun.write(path.join(Global.Path.state, "kv.json"), "{}") + + function Harness() { + const renderer = useRenderer() + const keymap = createDefaultOpenTuiKeymap(renderer) + const base = createTuiPluginApi({ + keymap, + client: { + vcs: { + diff: async (parameters: Parameters[0]) => { + calls.push(parameters) + return { data: [] } + }, + }, + session: { diff: async () => ({ data: [] }) }, + } as unknown as TuiPluginApi["client"], + }) + const api = { + ...base, + route: { + register(routes) { + renderDiff = routes.find((route) => route.name === "diff")?.render + return () => {} + }, + navigate() {}, + get current() { + return current + }, + }, + } satisfies TuiPluginApi + + void diffViewerPlugin.tui(api, undefined, pluginMeta) + + return ( + + + + + {renderDiff?.({ params: "params" in current ? current.params : undefined })} + + + + + ) + } + + const app = await testRender(() => , { width: 80, height: 20 }) + try { + await waitForCall(app, calls) + expect(calls[0]).toEqual({ mode: "branch", context: 12 }) + } finally { + app.renderer.destroy() + } +}) + async function waitForCommand( app: Awaited>, commands: Map, @@ -97,6 +157,14 @@ async function waitForCommand( } } +async function waitForCall(app: Awaited>, calls: unknown[]) { + for (let attempt = 0; attempt < 10; attempt++) { + await app.renderOnce() + if (calls.length > 0) return + await new Promise((resolve) => setTimeout(resolve, 25)) + } +} + const pluginMeta = { id: "diff-viewer", source: "internal",