Pārlūkot izejas kodu

feat(app): review panel improvements (#36716)

Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
Aarav Sareen 1 mēnesi atpakaļ
vecāks
revīzija
3ba3954aa0

+ 5 - 1
packages/app/src/pages/session.tsx

@@ -668,7 +668,8 @@ export default function Page() {
   const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
   const wantsReview = createMemo(() =>
     isDesktop()
-      ? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
+      ? desktopFileTreeOpen() ||
+        (desktopReviewOpen() && (activeTab() === "review" || (newSessionDesign() && !!activeFileTab())))
       : store.mobileTab === "changes",
   )
   const vcsMode = createMemo<VcsMode | undefined>(() => {
@@ -2366,6 +2367,9 @@ export default function Page() {
                     reviewHasFocusableContent={() => hasReview() || reviewV2State.sidebarOpened()}
                     reviewCount={reviewCount}
                     reviewPanel={reviewPanelV2}
+                    diffVersion={vcsQuery.dataUpdatedAt}
+                    loadDiff={loadReviewDiff}
+                    expandUnchanged={reviewV2State.expandMode() === "expand"}
                     reviewSidebarToggle={(disabled) => (
                       <SessionReviewV2SidebarToggle
                         opened={reviewV2State.sidebarOpened()}

+ 66 - 16
packages/app/src/pages/session/file-tabs.tsx

@@ -1,4 +1,4 @@
-import { createEffect, createMemo, createSignal, Match, on, onCleanup, Show, Switch } from "solid-js"
+import { createEffect, createMemo, createResource, createSignal, Match, on, onCleanup, Show, Switch } from "solid-js"
 import { createStore } from "solid-js/store"
 import { Dynamic } from "solid-js/web"
 import { makeEventListener } from "@solid-primitives/event-listener"
@@ -8,6 +8,7 @@ import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/sessi
 import { createLineCommentController } from "@opencode-ai/session-ui/line-comment-annotations"
 import { createLineCommentControllerV2 } from "@opencode-ai/session-ui/v2/line-comment-annotations-v2"
 import { sampledChecksum } from "@opencode-ai/core/util/encode"
+import { normalize, text } from "@opencode-ai/session-ui/session-diff"
 import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
 import { IconButton } from "@opencode-ai/ui/icon-button"
 import { LineCommentV2OverflowIcon } from "@opencode-ai/ui/v2/line-comment-v2"
@@ -18,11 +19,23 @@ import { showToast } from "@/utils/toast"
 import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
 import { useComments } from "@/context/comments"
 import { useLanguage } from "@/context/language"
+import { useLayout } from "@/context/layout"
 import { usePrompt } from "@/context/prompt"
 import { useSettings } from "@/context/settings"
 import { getSessionHandoff } from "@/pages/session/handoff"
 import { useSessionLayout } from "@/pages/session/session-layout"
 import { createSessionTabs } from "@/pages/session/helpers"
+import { reviewDiffNeedsLoad, type RenderDiff } from "@/pages/session/v2/review-diff-kinds"
+
+type SessionFileViewProps = {
+  tab: string
+  diff?: RenderDiff
+  diffVersion?: number
+  loadDiff?: (path: string, version?: number) => Promise<RenderDiff | undefined>
+  expandUnchanged?: boolean
+}
+
+const selectionSide = (range: SelectedLineRange) => range.endSide ?? range.side ?? "additions"
 
 function FileCommentMenu(props: {
   moreLabel: string
@@ -207,11 +220,27 @@ export function FileTabContent(props: { tab: string }) {
   )
 }
 
-export function SessionFileView(props: { tab: string }) {
+export function SessionFileView(props: SessionFileViewProps) {
   const settings = useSettings()
+  const detailSource = createMemo(() => {
+    if (!props.diff || !props.loadDiff || !reviewDiffNeedsLoad(props.diff)) return
+    return { diff: props.diff, load: props.loadDiff, version: props.diffVersion }
+  })
+  const [loadedDiff] = createResource(detailSource, async ({ diff, load, version }) => {
+    const value = await load(diff.file, version)
+    if (value?.file !== diff.file) return
+    return { source: diff, version, value }
+  })
+  const diff = createMemo(() => {
+    const source = props.diff
+    if (!source) return
+    const loaded = loadedDiff()
+    return normalize(loaded?.source === source && loaded.version === props.diffVersion ? loaded.value : source)
+  })
+
   return (
     <Show when={settings.general.newLayoutDesigns()} fallback={<SessionFileViewV1 tab={props.tab} />}>
-      <SessionFileViewV2 tab={props.tab} />
+      <SessionFileViewV2 tab={props.tab} diff={diff()} expandUnchanged={props.expandUnchanged} />
     </Show>
   )
 }
@@ -501,11 +530,16 @@ function SessionFileViewV1(props: { tab: string }) {
   return content()
 }
 
-function SessionFileViewV2(props: { tab: string }) {
+function SessionFileViewV2(props: {
+  tab: string
+  diff?: ReturnType<typeof normalize>
+  expandUnchanged?: boolean
+}) {
   const file = useFile()
   const comments = useComments()
   const language = useLanguage()
   const prompt = usePrompt()
+  const layout = useLayout()
   const fileComponent = useFileComponent()
   const { sessionKey, tabs, view } = useSessionLayout()
   const activeFileTab = createSessionTabs({
@@ -548,10 +582,15 @@ function SessionFileViewV2(props: { tab: string }) {
     })
   }
 
-  const buildPreview = (filePath: string, selection: FileSelection) => {
-    const source = filePath === path() ? contents() : file.get(filePath)?.content?.content
+  const buildPreview = (filePath: string, lines: SelectedLineRange) => {
+    const source =
+      filePath === path()
+        ? props.diff
+          ? text(props.diff, selectionSide(lines))
+          : contents()
+        : file.get(filePath)?.content?.content
     if (!source) return undefined
-    return selectionPreview(source, selection)
+    return selectionPreview(source, selectionFromLines(lines))
   }
 
   const addCommentToContext = (input: {
@@ -562,7 +601,7 @@ function SessionFileViewV2(props: { tab: string }) {
     origin?: "review" | "file"
   }) => {
     const selection = selectionFromLines(input.selection)
-    const preview = input.preview ?? buildPreview(input.file, selection)
+    const preview = input.preview ?? buildPreview(input.file, input.selection)
 
     const saved = comments.add({
       file: input.file,
@@ -587,7 +626,7 @@ function SessionFileViewV2(props: { tab: string }) {
     comment: string
   }) => {
     comments.update(input.file, input.id, input.comment)
-    const preview = input.file === path() ? buildPreview(input.file, selectionFromLines(input.selection)) : undefined
+    const preview = input.file === path() ? buildPreview(input.file, input.selection) : undefined
     prompt.context.updateComment(input.file, input.id, {
       comment: input.comment,
       ...(preview ? { preview } : {}),
@@ -628,7 +667,7 @@ function SessionFileViewV2(props: { tab: string }) {
     mention: {
       items: file.searchFilesAndDirectories,
     },
-    getSide: (range) => range.endSide ?? range.side ?? "additions",
+    getSide: selectionSide,
     state: {
       opened: () => note.openedComment,
       setOpened: (id) => setNote("openedComment", id),
@@ -726,12 +765,21 @@ function SessionFileViewV2(props: { tab: string }) {
     <div class="relative overflow-hidden pb-40">
       <Dynamic
         component={fileComponent}
-        mode="text"
-        file={{
-          name: path() ?? "",
-          contents: source,
-          cacheKey: cacheKey(),
-        }}
+        {...(props.diff
+          ? {
+              mode: "diff" as const,
+              fileDiff: props.diff.fileDiff,
+              diffStyle: layout.review.diffStyle(),
+              expandUnchanged: props.expandUnchanged,
+            }
+          : {
+              mode: "text" as const,
+              file: {
+                name: path() ?? "",
+                contents: source,
+                cacheKey: cacheKey(),
+              },
+            })}
         enableLineSelection
         enableGutterUtility
         selectedLines={activeSelection()}
@@ -761,6 +809,7 @@ function SessionFileViewV2(props: { tab: string }) {
         media={{
           mode: "auto",
           path: path(),
+          deleted: props.diff?.status === "deleted",
           current: state()?.content,
           onLoad: scrollSync.queueRestore,
           onError: (args: { kind: "image" | "audio" | "svg" }) => {
@@ -779,6 +828,7 @@ function SessionFileViewV2(props: { tab: string }) {
     <div class="mt-3 relative h-full min-h-0">
       <ScrollView class="h-full" viewportRef={scrollSync.setViewport} onScroll={scrollSync.handleScroll as any}>
         <Switch>
+          <Match when={props.diff}>{renderFile(contents())}</Match>
           <Match when={state()?.loaded}>{renderFile(contents())}</Match>
           <Match when={state()?.loading}>
             <div class="px-6 py-4 text-text-weak">{language.t("common.loading")}...</div>

+ 12 - 0
packages/app/src/pages/session/session-side-panel.tsx

@@ -71,6 +71,9 @@ export function SessionSidePanel(props: {
   reviewHasFocusableContent: () => boolean
   reviewCount: () => number
   reviewPanel: () => JSX.Element
+  diffVersion?: number
+  loadDiff?: (path: string, version?: number) => Promise<RenderDiff | undefined>
+  expandUnchanged?: boolean
   reviewSidebarToggle?: (disabled: boolean) => JSX.Element
   fileBrowserState?: SessionFileBrowserState
   activeDiff?: string
@@ -88,6 +91,11 @@ export function SessionSidePanel(props: {
   const sdk = useSDK()
   const { sessionKey, tabs, view, params } = useSessionLayout()
   const projectDirectory = createMemo(() => sdk().directory)
+  const diffForTab = (tab: string) => {
+    const path = file.pathFromTab(tab)
+    if (!path) return
+    return props.diffs().find((diff): diff is RenderDiff => renderDiff(diff) && diff.file === path)
+  }
 
   const isDesktop = createMediaQuery("(min-width: 768px)")
   const shown = settings.visibility.fileTree
@@ -739,6 +747,10 @@ export function SessionSidePanel(props: {
                               active={file.pathFromTab(browserTab() ?? activeFileTab() ?? "")}
                               kinds={kinds()}
                               state={props.fileBrowserState!}
+                              diff={diffForTab(browserTab() ?? activeFileTab() ?? "")}
+                              diffVersion={props.diffVersion}
+                              loadDiff={props.loadDiff}
+                              expandUnchanged={props.expandUnchanged}
                               onSelect={(path) => previewTab(file.tab(path))}
                               onSelectPermanent={(path) => openTab(file.tab(path))}
                               filterRef={(element) => (fileFilter = element)}

+ 14 - 1
packages/app/src/pages/session/v2/session-file-browser-tab.tsx

@@ -11,6 +11,7 @@ import { useSDK } from "@/context/sdk"
 import { displayName } from "@/pages/layout/helpers"
 import { useSessionLayout } from "@/pages/session/session-layout"
 import { SessionFileView } from "@/pages/session/file-tabs"
+import type { RenderDiff } from "@/pages/session/v2/review-diff-kinds"
 import { applyFileListKeyDown, SessionFileListV2 } from "@/pages/session/v2/session-file-list-v2"
 import { pathKey } from "@/utils/path-key"
 
@@ -30,6 +31,10 @@ export function SessionFileBrowserTab(props: {
   active?: string
   kinds: ReadonlyMap<string, Kind>
   state: SessionFileBrowserState
+  diff?: RenderDiff
+  diffVersion?: number
+  loadDiff?: (path: string, version?: number) => Promise<RenderDiff | undefined>
+  expandUnchanged?: boolean
   onSelect: (path: string) => void
   onSelectPermanent: (path: string) => void
   filterRef?: (element: HTMLInputElement) => void
@@ -172,7 +177,15 @@ export function SessionFileBrowserTab(props: {
       >
         <div class="min-h-0 flex-1">
           <Show when={props.tab} keyed>
-            {(tab) => <SessionFileView tab={tab} />}
+            {(tab) => (
+              <SessionFileView
+                tab={tab}
+                diff={props.diff}
+                diffVersion={props.diffVersion}
+                loadDiff={props.loadDiff}
+                expandUnchanged={props.expandUnchanged}
+              />
+            )}
           </Show>
         </div>
       </Show>

+ 1 - 9
packages/session-ui/src/v2/components/session-review-v2.css

@@ -243,15 +243,7 @@
   color: var(--v2-icon-icon-base);
 }
 
-[data-component="icon-button-v2"][data-variant="ghost"].session-review-v2-sidebar-toggle[aria-expanded="true"]:not(
-    :disabled
-  ),
-[data-component="icon-button-v2"][data-variant="ghost"].session-review-v2-sidebar-toggle[aria-expanded="true"]:is(
-    :hover,
-    [data-state="hover"],
-    :active,
-    [data-state="pressed"]
-  ):not(:disabled) {
+[data-component="icon-button-v2"][data-variant="ghost"].session-review-v2-sidebar-toggle[aria-expanded="true"] {
   background-color: var(--v2-overlay-simple-overlay-pressed);
 }