feat(app): v2 review panel overhaul (#31882)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
This commit is contained in:
co-authored by
LukeParkerDev
parent
fbb95a6ee3
commit
7d2618637f
@@ -24,8 +24,10 @@ import { debounce } from "@solid-primitives/scheduled"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { FileProvider, selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
|
||||
import { createStore } from "solid-js/store"
|
||||
import type { SessionReviewLineComment } from "@opencode-ai/session-ui/session-review"
|
||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
|
||||
import { isScrollKeyTarget, scrollKey, scrollKeyOwner } from "@opencode-ai/ui/scroll-view"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||
@@ -77,6 +79,10 @@ import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { syncSessionModel } from "@/pages/session/session-model-helpers"
|
||||
import { SessionSidePanel } from "@/pages/session/session-side-panel"
|
||||
import { SessionReviewEmptyChangesV2 } from "@opencode-ai/session-ui/v2/session-review-empty-changes-v2"
|
||||
import { SessionReviewEmptyNoGitV2 } from "@opencode-ai/session-ui/v2/session-review-empty-no-git-v2"
|
||||
import { ReviewPanelV2 } from "@/pages/session/v2/review-panel-v2"
|
||||
import { createReviewPanelV2State } from "@/pages/session/v2/review-panel-v2-state"
|
||||
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
||||
import { useComposerCommands } from "@/pages/session/use-composer-commands"
|
||||
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
||||
@@ -1052,22 +1058,22 @@ export default function Page() {
|
||||
loadFile: file.load,
|
||||
})
|
||||
|
||||
const changesLabel = (option: ChangeMode) => {
|
||||
if (option === "git") return language.t("ui.sessionReview.title.git")
|
||||
if (option === "branch") return language.t("ui.sessionReview.title.branch")
|
||||
return language.t("ui.sessionReview.title.lastTurn")
|
||||
}
|
||||
|
||||
const changesTitle = () => {
|
||||
if (!canReview()) {
|
||||
return null
|
||||
}
|
||||
|
||||
const label = (option: ChangeMode) => {
|
||||
if (option === "git") return language.t("ui.sessionReview.title.git")
|
||||
if (option === "branch") return language.t("ui.sessionReview.title.branch")
|
||||
return language.t("ui.sessionReview.title.lastTurn")
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={changesOptions()}
|
||||
current={store.changes}
|
||||
label={label}
|
||||
label={changesLabel}
|
||||
onSelect={(option) => option && setStore("changes", option)}
|
||||
variant="ghost"
|
||||
size="small"
|
||||
@@ -1076,6 +1082,24 @@ export default function Page() {
|
||||
)
|
||||
}
|
||||
|
||||
const changesTitleV2 = () => {
|
||||
if (!canReview()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectV2
|
||||
appearance="inline"
|
||||
options={changesOptions()}
|
||||
current={store.changes}
|
||||
label={changesLabel}
|
||||
placement="bottom-start"
|
||||
gutter={6}
|
||||
onSelect={(option) => option && setStore("changes", option)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const empty = (text: string) => (
|
||||
<div class="h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6">
|
||||
<div class="text-14-regular text-text-weak max-w-56">{text}</div>
|
||||
@@ -1122,6 +1146,16 @@ export default function Page() {
|
||||
)
|
||||
}
|
||||
|
||||
const reviewEmptyV2 = () => {
|
||||
if ((store.changes === "git" || store.changes === "branch") && !reviewReady()) {
|
||||
return <div class="px-6 py-4 text-text-weak">{language.t("session.review.loadingChanges")}</div>
|
||||
}
|
||||
if (store.changes === "turn" && nogit()) {
|
||||
return <SessionReviewEmptyNoGitV2 pending={gitMutation.isPending} onInitGit={initGit} />
|
||||
}
|
||||
return <SessionReviewEmptyChangesV2 />
|
||||
}
|
||||
|
||||
const reviewContent = (input: {
|
||||
diffStyle: DiffStyle
|
||||
onDiffStyleChange?: (style: DiffStyle) => void
|
||||
@@ -1155,6 +1189,63 @@ export default function Page() {
|
||||
</Show>
|
||||
)
|
||||
|
||||
const reviewV2State = createReviewPanelV2State()
|
||||
|
||||
// Getters defer reactive reads to the consuming scope. Eager reads here ran inside
|
||||
// the side panel's Show children and remounted the whole review panel on unrelated
|
||||
// updates such as session switches.
|
||||
const reviewPanelV2Props = () => ({
|
||||
get title() {
|
||||
return changesTitleV2()
|
||||
},
|
||||
get empty() {
|
||||
return reviewEmptyV2()
|
||||
},
|
||||
diffs: reviewDiffs,
|
||||
diffsReady: reviewReady,
|
||||
get activeFile() {
|
||||
return tree.activeDiff
|
||||
},
|
||||
onSelectFile: focusReviewDiff,
|
||||
get diffStyle() {
|
||||
return layout.review.diffStyle()
|
||||
},
|
||||
onDiffStyleChange: layout.review.setDiffStyle,
|
||||
state: reviewV2State,
|
||||
onLineComment: (comment: SessionReviewLineComment) => addCommentToContext({ ...comment, origin: "review" }),
|
||||
onLineCommentUpdate: updateCommentInContext,
|
||||
onLineCommentDelete: removeCommentFromContext,
|
||||
get lineCommentActions() {
|
||||
return reviewCommentActions()
|
||||
},
|
||||
get comments() {
|
||||
return comments.all()
|
||||
},
|
||||
get focusedComment() {
|
||||
return comments.focus()
|
||||
},
|
||||
onFocusedCommentChange: (focus: { file: string; id: string } | null) => {
|
||||
// The preview clears the focus once it has opened the comment; persist the
|
||||
// focused file as the active selection so the preview stays on it. Skip
|
||||
// files outside the current diff set (their focus is cleared unhandled).
|
||||
if (!focus) {
|
||||
const current = comments.focus()
|
||||
if (current && reviewDiffs().some((diff) => diff.file === current.file)) focusReviewDiff(current.file)
|
||||
}
|
||||
comments.setFocus(focus)
|
||||
},
|
||||
})
|
||||
|
||||
const reviewPanelV2 = () => (
|
||||
<div class="flex flex-col h-full overflow-hidden bg-background-stronger contain-strict">
|
||||
{/* The route remounts per session; defer the diff render off the switch critical path
|
||||
like the legacy review tab does. */}
|
||||
<Show when={!store.deferRender}>
|
||||
<ReviewPanelV2 {...reviewPanelV2Props()} />
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
|
||||
const reviewPanel = () => (
|
||||
<div
|
||||
classList={{
|
||||
@@ -2078,7 +2169,7 @@ export default function Page() {
|
||||
empty={reviewEmptyText}
|
||||
hasReview={hasReview}
|
||||
reviewCount={reviewCount}
|
||||
reviewPanel={reviewPanel}
|
||||
reviewPanel={() => (newSessionDesign() ? reviewPanelV2() : reviewPanel())}
|
||||
activeDiff={tree.activeDiff}
|
||||
focusReviewDiff={focusReviewDiff}
|
||||
reviewSnap={ui.reviewSnap}
|
||||
|
||||
Reference in New Issue
Block a user