feat(tui): add main branch source to diff mode (#30942)

This commit is contained in:
Victor Navarro 2026-06-24 12:35:37 +02:00 committed by GitHub
parent 0016dfd9b1
commit 6eec98371a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 82 additions and 22 deletions

View File

@ -382,7 +382,7 @@ export type TuiState = {
worktree: string worktree: string
directory: string directory: string
} }
readonly vcs: { branch?: string } | undefined readonly vcs: { branch?: string; default_branch?: string } | undefined
session: { session: {
count: () => number count: () => number
get: (sessionID: string) => Session | undefined get: (sessionID: string) => Session | undefined

View File

@ -39,11 +39,11 @@ const ROUTE = "diff"
const MIN_SPLIT_WIDTH = 100 const MIN_SPLIT_WIDTH = 100
const FILE_TREE_WIDTH = 32 const FILE_TREE_WIDTH = 32
const PLAIN_TEXT_FILETYPE = "opencode-plain-text" 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_SHOW_FILE_TREE = "diff_viewer_show_file_tree"
const KV_SINGLE_PATCH = "diff_viewer_single_patch" const KV_SINGLE_PATCH = "diff_viewer_single_patch"
const KV_VIEW = "diff_viewer_view" const KV_VIEW = "diff_viewer_view"
type DiffMode = "git" | "last-turn" type DiffMode = "git" | "branch" | "last-turn"
type DiffViewerFocus = "patches" | "files" type DiffViewerFocus = "patches" | "files"
type DiffView = "split" | "unified" type DiffView = "split" | "unified"
type SelectedHunk = { readonly fileIndex: number; readonly hunkIndex: number; readonly scrollTop: number } 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 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 }) { function DiffViewer(props: { api: TuiPluginApi }) {
const dimensions = useTerminalDimensions() const dimensions = useTerminalDimensions()
const themeState = useTheme() const themeState = useTheme()
@ -117,7 +123,7 @@ function DiffViewer(props: { api: TuiPluginApi }) {
} }
const result = await props.api.client.vcs.diff( 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 }, { throwOnError: true },
) )
return normalizeDiffs(result.data ?? []) return normalizeDiffs(result.data ?? [])
@ -675,18 +681,30 @@ function DiffViewer(props: { api: TuiPluginApi }) {
}, },
] ]
const switchDiffOptions = createMemo(() => [ const switchDiffOptions = createMemo(() => {
{ const vcs = props.api.state.vcs
title: "Working tree", return [
value: "git" as const, {
description: "Show current git changes", title: "Working tree",
}, value: "git" as const,
{ description: "Show current git changes",
title: "Last turn", },
value: "last-turn" as const, ...(vcs?.branch && vcs.default_branch && vcs.branch !== vcs.default_branch
description: "Show changes from the last assistant turn", ? [
}, {
]) 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 = () => { const openSwitchDiffDialog = () => {
props.api.ui.dialog.replace(() => ( props.api.ui.dialog.replace(() => (
@ -736,7 +754,7 @@ function DiffViewer(props: { api: TuiPluginApi }) {
<PanelGroup axis="y" width="100%" height="100%"> <PanelGroup axis="y" width="100%" height="100%">
<Panel border="none" flexShrink={0} padding={0} paddingLeft={1}> <Panel border="none" flexShrink={0} padding={0} paddingLeft={1}>
<text fg={theme().text}>Diff </text> <text fg={theme().text}>Diff </text>
<text fg={theme().textMuted}>{mode() === "last-turn" ? "last turn" : "working tree"}</text> <text fg={theme().textMuted}>{diffSourceLabel(mode())}</text>
<box flexGrow={1} /> <box flexGrow={1} />
<text fg={theme().textMuted}> <text fg={theme().textMuted}>
{files().length} {files().length === 1 ? "file" : "files"} {files().length} {files().length === 1 ? "file" : "files"}
@ -971,7 +989,7 @@ function DiffViewerHelpDialog() {
{ {
shortcut: useCommandShortcut("diff.switch_source"), shortcut: useCommandShortcut("diff.switch_source"),
action: "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"), shortcut: useCommandShortcut("diff.toggle_view"),

View File

@ -113,6 +113,7 @@ function stateApi(sync: ReturnType<typeof useSync>): TuiPluginApi["state"] {
if (!sync.data.vcs) return if (!sync.data.vcs) return
return { return {
branch: sync.data.vcs.branch, branch: sync.data.vcs.branch,
default_branch: sync.data.vcs.default_branch,
} }
}, },
session: { session: {

View File

@ -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< const commands = new Map<
string, string,
NonNullable<Parameters<TuiPluginApi["keymap"]["registerLayer"]>[0]["commands"]>[number] NonNullable<Parameters<TuiPluginApi["keymap"]["registerLayer"]>[0]["commands"]>[number]
>() >()
let current = startRoute let current = initialRoute ?? startRoute
let renderDiff: TuiRouteDefinition["render"] | undefined let renderDiff: TuiRouteDefinition["render"] | undefined
let vcsDiffInput: unknown let vcsDiffInput: unknown
let sessionDiffInput: unknown
const config = createTuiResolvedConfig() const config = createTuiResolvedConfig()
function Harness() { function Harness() {
const renderer = useRenderer() const renderer = useRenderer()
@ -124,7 +125,12 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20) {
return { data: vcsDiff } return { data: vcsDiff }
}, },
}, },
session: { diff: async () => ({ data: [] }) }, session: {
diff: async (input: unknown) => {
sessionDiffInput = input
return { data: [] }
},
},
} as unknown as TuiPluginApi["client"], } as unknown as TuiPluginApi["client"],
state: { state: {
session: { session: {
@ -149,7 +155,7 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20) {
} satisfies TuiPluginApi } satisfies TuiPluginApi
void diffViewerPlugin.tui(api, undefined, pluginMeta) void diffViewerPlugin.tui(api, undefined, pluginMeta)
commands.get("diff.open")?.run?.({} as never) if (!initialRoute) commands.get("diff.open")?.run?.({} as never)
return ( return (
<TestTuiContexts> <TestTuiContexts>
@ -173,6 +179,7 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20) {
commands, commands,
current: () => current, current: () => current,
vcsDiffInput: () => vcsDiffInput, vcsDiffInput: () => vcsDiffInput,
sessionDiffInput: () => sessionDiffInput,
} }
} }
@ -201,6 +208,40 @@ const session = {
}, },
} satisfies 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( async function waitForCommand(
app: Awaited<ReturnType<typeof testRender>>, app: Awaited<ReturnType<typeof testRender>>,
commands: Map<string, unknown>, commands: Map<string, unknown>,