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

Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
This commit is contained in:
Aarav Sareen
2026-07-20 12:19:23 +05:30
committed by GitHub
parent 3fc5af6dd1
commit 3ba3954aa0
5 changed files with 98 additions and 27 deletions
+5 -1
View File
@@ -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()}
+63 -13
View File
@@ -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={{
{...(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>
@@ -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)}
@@ -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>
@@ -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);
}