From 5382c546898dbfb0bcc94f97e9098f0f4dc60104 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Tue, 21 Apr 2026 16:09:26 +0800 Subject: [PATCH 1/3] fix: polish review git changes UI --- packages/app/src/pages/session.tsx | 9 +- .../app/src/pages/session/review-tab.test.tsx | 84 +++++++++++++++++++ packages/app/src/pages/session/review-tab.tsx | 8 +- .../pages/session/session-side-panel.test.tsx | 10 +++ .../src/pages/session/session-side-panel.tsx | 38 +++++---- .../ui/src/i18n/session-review-title.test.ts | 10 +++ packages/ui/src/i18n/zh.ts | 2 + packages/ui/src/i18n/zht.ts | 2 + 8 files changed, 132 insertions(+), 31 deletions(-) create mode 100644 packages/app/src/pages/session/review-tab.test.tsx create mode 100644 packages/ui/src/i18n/session-review-title.test.ts diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index b52a1cf5d..536ac2e4a 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -51,7 +51,7 @@ import { shouldFocusTerminalOnKeyDown, } from "@/pages/session/helpers" import { MessageTimeline } from "@/pages/session/message-timeline" -import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab" +import { SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab" import { useSessionLayout } from "@/pages/session/session-layout" import { syncSessionModel } from "@/pages/session/session-model-helpers" import { SessionSidePanel } from "@/pages/session/session-side-panel" @@ -1239,8 +1239,6 @@ export default function Page() { } const reviewContent = (input: { - diffStyle: DiffStyle - onDiffStyleChange?: (style: DiffStyle) => void classes?: SessionReviewTabProps["classes"] loadingClass: string emptyClass: string @@ -1251,8 +1249,6 @@ export default function Page() { empty={reviewEmpty(input)} diffs={reviewDiffs} view={view} - diffStyle={input.diffStyle} - onDiffStyleChange={input.onDiffStyleChange} onScrollRef={(el) => setTree("reviewScroll", el)} focusedFile={tree.activeDiff} onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })} @@ -1275,8 +1271,6 @@ export default function Page() {
{reviewContent({ - diffStyle: layout.review.diffStyle(), - onDiffStyleChange: layout.review.setDiffStyle, loadingClass: "px-6 py-4 text-text-weak", emptyClass: "h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6", })} @@ -1998,7 +1992,6 @@ export default function Page() { { + mock.module("@opencode-ai/ui/session-review", () => ({ + SessionReview: (props: any) => { + capturedProps.push(props) + return null + }, + })) + + mock.module("@/context/sdk", () => ({ + useSDK: () => ({ + client: { + file: { + read: async () => ({ data: "" }), + }, + }, + }), + })) + + mock.module("@/context/layout", () => ({ + useLayout: () => ({ + ready: () => true, + }), + })) + + SessionReviewTab = (await import("./review-tab")).SessionReviewTab +}) + +beforeEach(() => { + capturedProps.length = 0 + document.body.innerHTML = "" + ;(globalThis as any).React = { + createElement: (component: unknown, props: Record | null, ...children: unknown[]) => { + if (typeof component === "function") return component({ ...(props ?? {}), children }) + return null + }, + } +}) + +afterAll(() => { + mock.restore() +}) + +describe("SessionReviewTab", () => { + test("keeps PawWork review diffs in unified mode without exposing style switching", () => { + const root = document.createElement("div") + document.body.append(root) + + const dispose = createRoot((dispose) => { + SessionReviewTab({ + diffs: () => [ + { + file: "src/demo.ts", + patch: "@@ -1 +1 @@\n-old\n+new\n", + additions: 1, + deletions: 1, + status: "modified", + }, + ], + view: () => + ({ + review: { + open: () => [], + setOpen: () => undefined, + }, + scroll: () => undefined, + setScroll: () => undefined, + }) as any, + }) + return dispose + }) + + expect(capturedProps).toHaveLength(1) + expect(capturedProps[0].diffStyle).toBe("unified") + expect(capturedProps[0].onDiffStyleChange).toBeUndefined() + + dispose() + }) +}) diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx index 71dfe375e..1bf56ddf0 100644 --- a/packages/app/src/pages/session/review-tab.tsx +++ b/packages/app/src/pages/session/review-tab.tsx @@ -12,8 +12,6 @@ import { useSDK } from "@/context/sdk" import { useLayout } from "@/context/layout" import type { LineComment } from "@/context/comments" -export type DiffStyle = "unified" | "split" - type ReviewDiff = SnapshotFileDiff | VcsFileDiff export interface SessionReviewTabProps { @@ -21,8 +19,6 @@ export interface SessionReviewTabProps { empty?: JSX.Element diffs: () => ReviewDiff[] view: () => ReturnType["view"]> - diffStyle: DiffStyle - onDiffStyleChange?: (style: DiffStyle) => void onViewFile?: (file: string) => void onLineComment?: (comment: { file: string; selection: SelectedLineRange; comment: string; preview?: string }) => void onLineCommentUpdate?: (comment: SessionReviewCommentUpdate) => void @@ -119,7 +115,6 @@ export function SessionReviewTab(props: SessionReviewTabProps) { createEffect(() => { props.diffs().length - props.diffStyle if (!layout.ready()) return queueRestore() }) @@ -152,8 +147,7 @@ export function SessionReviewTab(props: SessionReviewTabProps) { container: props.classes?.container ?? "pl-3", }} diffs={props.diffs()} - diffStyle={props.diffStyle} - onDiffStyleChange={props.onDiffStyleChange} + diffStyle="unified" onViewFile={props.onViewFile} focusedFile={props.focusedFile} readFile={readFile} diff --git a/packages/app/src/pages/session/session-side-panel.test.tsx b/packages/app/src/pages/session/session-side-panel.test.tsx index c86965f25..83b331afb 100644 --- a/packages/app/src/pages/session/session-side-panel.test.tsx +++ b/packages/app/src/pages/session/session-side-panel.test.tsx @@ -121,6 +121,16 @@ describe("formatRightPanelWidth", () => { }) }) +describe("shouldShowReviewFileOpenButton", () => { + test("hides the standalone file-open button on the main review view", async () => { + const { shouldShowReviewFileOpenButton } = await import("./session-side-panel") + + expect(shouldShowReviewFileOpenButton("review", false)).toBe(false) + expect(shouldShowReviewFileOpenButton("context", false)).toBe(true) + expect(shouldShowReviewFileOpenButton("review", true)).toBe(true) + }) +}) + describe("makeRightPanelResizeHandler", () => { test("calls size.touch() then layout.rightPanel.resize(width) in order", async () => { const { makeRightPanelResizeHandler } = await import("./session-side-panel") diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 6ed3ce42b..c434bbbcc 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -43,6 +43,10 @@ export function makeRightPanelResizeHandler( } } +export function shouldShowReviewFileOpenButton(activeTab: string | undefined, hasSecondaryTabs: boolean): boolean { + return hasSecondaryTabs || activeTab !== "review" +} + type RightPanelShellIconName = "status" | "folder" | "review" | "terminal" function RightPanelShellIcon(props: { icon: RightPanelShellIconName }) { @@ -292,22 +296,24 @@ export function SessionSidePanel(props: { - - openFilePicker(showAllFiles)} - aria-label={language.t("command.file.open")} - /> - -
+ +
+ + openFilePicker(showAllFiles)} + aria-label={language.t("command.file.open")} + /> + +
+
} > { + test("localizes Git changes for Chinese users", () => { + expect(zh["ui.sessionReview.title.git"]).toBe("文件变更") + expect(zht["ui.sessionReview.title.git"]).toBe("檔案變更") + }) +}) diff --git a/packages/ui/src/i18n/zh.ts b/packages/ui/src/i18n/zh.ts index 577975437..4b1e9336f 100644 --- a/packages/ui/src/i18n/zh.ts +++ b/packages/ui/src/i18n/zh.ts @@ -4,6 +4,8 @@ type Keys = keyof typeof en export const dict = { "ui.sessionReview.title": "会话变更", + "ui.sessionReview.title.git": "文件变更", + "ui.sessionReview.title.branch": "分支变更", "ui.sessionReview.title.lastTurn": "上一轮变更", "ui.sessionReview.diffStyle.unified": "统一", "ui.sessionReview.diffStyle.split": "拆分", diff --git a/packages/ui/src/i18n/zht.ts b/packages/ui/src/i18n/zht.ts index 2d433b57a..f0369d39a 100644 --- a/packages/ui/src/i18n/zht.ts +++ b/packages/ui/src/i18n/zht.ts @@ -4,6 +4,8 @@ type Keys = keyof typeof en export const dict = { "ui.sessionReview.title": "工作階段變更", + "ui.sessionReview.title.git": "檔案變更", + "ui.sessionReview.title.branch": "分支變更", "ui.sessionReview.title.lastTurn": "上一輪變更", "ui.sessionReview.diffStyle.unified": "整合", "ui.sessionReview.diffStyle.split": "拆分", From 6175eb4179f3cce083defeb9ea74b1cc42083fa7 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Tue, 21 Apr 2026 16:53:55 +0800 Subject: [PATCH 2/3] test: address review feedback --- packages/app/src/pages/session/review-tab.test.tsx | 7 ++++--- packages/app/src/pages/session/session-side-panel.tsx | 1 + packages/ui/src/i18n/session-review-title.test.ts | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/app/src/pages/session/review-tab.test.tsx b/packages/app/src/pages/session/review-tab.test.tsx index 2ee2acf1a..a47ab8641 100644 --- a/packages/app/src/pages/session/review-tab.test.tsx +++ b/packages/app/src/pages/session/review-tab.test.tsx @@ -3,6 +3,7 @@ import { createRoot } from "solid-js" let SessionReviewTab: typeof import("./review-tab").SessionReviewTab const capturedProps: any[] = [] +const originalReact = (globalThis as any).React beforeAll(async () => { mock.module("@opencode-ai/ui/session-review", () => ({ @@ -34,6 +35,7 @@ beforeAll(async () => { beforeEach(() => { capturedProps.length = 0 document.body.innerHTML = "" + // Bun compiles the imported TSX through React.createElement in this direct component-call test. ;(globalThis as any).React = { createElement: (component: unknown, props: Record | null, ...children: unknown[]) => { if (typeof component === "function") return component({ ...(props ?? {}), children }) @@ -44,13 +46,12 @@ beforeEach(() => { afterAll(() => { mock.restore() + if (originalReact === undefined) delete (globalThis as any).React + else (globalThis as any).React = originalReact }) describe("SessionReviewTab", () => { test("keeps PawWork review diffs in unified mode without exposing style switching", () => { - const root = document.createElement("div") - document.body.append(root) - const dispose = createRoot((dispose) => { SessionReviewTab({ diffs: () => [ diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index c434bbbcc..c65e85b08 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -43,6 +43,7 @@ export function makeRightPanelResizeHandler( } } +/** Returns whether the Review inner tab row should expose the file-open shortcut. */ export function shouldShowReviewFileOpenButton(activeTab: string | undefined, hasSecondaryTabs: boolean): boolean { return hasSecondaryTabs || activeTab !== "review" } diff --git a/packages/ui/src/i18n/session-review-title.test.ts b/packages/ui/src/i18n/session-review-title.test.ts index 1bcbca82b..eac49cde8 100644 --- a/packages/ui/src/i18n/session-review-title.test.ts +++ b/packages/ui/src/i18n/session-review-title.test.ts @@ -3,8 +3,10 @@ import { dict as zh } from "./zh" import { dict as zht } from "./zht" describe("session review title translations", () => { - test("localizes Git changes for Chinese users", () => { + test("localizes Review change titles for Chinese users", () => { expect(zh["ui.sessionReview.title.git"]).toBe("文件变更") expect(zht["ui.sessionReview.title.git"]).toBe("檔案變更") + expect(zh["ui.sessionReview.title.branch"]).toBe("分支变更") + expect(zht["ui.sessionReview.title.branch"]).toBe("分支變更") }) }) From f796e1d285f017a3fa74a25d6e00eb999d087495 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Tue, 21 Apr 2026 17:01:15 +0800 Subject: [PATCH 3/3] test: resolve review cleanup comments --- packages/app/src/pages/session/review-tab.test.tsx | 12 +++++++----- packages/app/src/pages/session/review-tab.tsx | 1 + .../app/src/pages/session/session-side-panel.tsx | 4 ++++ 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/app/src/pages/session/review-tab.test.tsx b/packages/app/src/pages/session/review-tab.test.tsx index a47ab8641..21dfdc7b6 100644 --- a/packages/app/src/pages/session/review-tab.test.tsx +++ b/packages/app/src/pages/session/review-tab.test.tsx @@ -76,10 +76,12 @@ describe("SessionReviewTab", () => { return dispose }) - expect(capturedProps).toHaveLength(1) - expect(capturedProps[0].diffStyle).toBe("unified") - expect(capturedProps[0].onDiffStyleChange).toBeUndefined() - - dispose() + try { + expect(capturedProps).toHaveLength(1) + expect(capturedProps[0].diffStyle).toBe("unified") + expect(capturedProps[0].onDiffStyleChange).toBeUndefined() + } finally { + dispose() + } }) }) diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx index 1bf56ddf0..de3651a01 100644 --- a/packages/app/src/pages/session/review-tab.tsx +++ b/packages/app/src/pages/session/review-tab.tsx @@ -39,6 +39,7 @@ export interface SessionReviewTabProps { } } +/** Renders the session Review panel with a unified diff view and persisted scroll position. */ export function SessionReviewTab(props: SessionReviewTabProps) { let scroll: HTMLDivElement | undefined let restoreFrame: number | undefined diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index c65e85b08..adb5ed6ec 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -29,10 +29,12 @@ import { setSessionHandoff } from "@/pages/session/handoff" import type { RightPanelTab } from "@/pages/session/right-panel-tabs" import { useSessionLayout } from "@/pages/session/session-layout" +/** Converts right-panel state into the CSS width applied to the shell. */ export function formatRightPanelWidth(open: boolean, width: number): string { return open ? `${width}px` : "0px" } +/** Creates a resize callback that marks user sizing before delegating width storage to layout state. */ export function makeRightPanelResizeHandler( size: { touch: () => void }, layout: { rightPanel: { resize: (width: number) => void } }, @@ -50,6 +52,7 @@ export function shouldShowReviewFileOpenButton(activeTab: string | undefined, ha type RightPanelShellIconName = "status" | "folder" | "review" | "terminal" +/** Maps right-panel tab names to their shell icon components. */ function RightPanelShellIcon(props: { icon: RightPanelShellIconName }) { return ( @@ -69,6 +72,7 @@ function RightPanelShellIcon(props: { icon: RightPanelShellIconName }) { ) } +/** Hosts the session right panel tabs, resize behavior, and active panel content. */ export function SessionSidePanel(props: { canReview: () => boolean diffs: () => (SnapshotFileDiff | VcsFileDiff)[]