Bladeren bron

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

Victor Navarro 1 maand geleden
bovenliggende
commit
6eec98371a

+ 1 - 1
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

+ 35 - 17
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 }) {
       <PanelGroup axis="y" width="100%" height="100%">
         <Panel border="none" flexShrink={0} padding={0} paddingLeft={1}>
           <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} />
           <text fg={theme().textMuted}>
             {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"),

+ 1 - 0
packages/tui/src/plugin/adapters.tsx

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

+ 45 - 4
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<Parameters<TuiPluginApi["keymap"]["registerLayer"]>[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 (
       <TestTuiContexts>
@@ -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<ReturnType<typeof testRender>>,
   commands: Map<string, unknown>,