feat(tui): add main branch source to diff mode (#30942)
This commit is contained in:
parent
0016dfd9b1
commit
6eec98371a
@ -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
|
||||||
|
|||||||
@ -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"),
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
@ -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>,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user