Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions packages/app/src/pages/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -1239,8 +1239,6 @@ export default function Page() {
}

const reviewContent = (input: {
diffStyle: DiffStyle
onDiffStyleChange?: (style: DiffStyle) => void
classes?: SessionReviewTabProps["classes"]
loadingClass: string
emptyClass: string
Expand All @@ -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" })}
Expand All @@ -1275,8 +1271,6 @@ export default function Page() {
<div class="flex flex-col h-full overflow-hidden bg-background-stronger contain-strict">
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
{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",
})}
Expand Down Expand Up @@ -1998,7 +1992,6 @@ export default function Page() {
<MessageTimeline
mobileChanges={mobileChanges()}
mobileFallback={reviewContent({
diffStyle: "unified",
classes: {
root: "pb-8",
header: "px-4",
Expand Down
87 changes: 87 additions & 0 deletions packages/app/src/pages/session/review-tab.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { afterAll, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test"
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", () => ({
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 = ""
// Bun compiles the imported TSX through React.createElement in this direct component-call test.
;(globalThis as any).React = {
createElement: (component: unknown, props: Record<string, unknown> | null, ...children: unknown[]) => {
if (typeof component === "function") return component({ ...(props ?? {}), children })
return null
},
}
Comment thread
Astro-Han marked this conversation as resolved.
})

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 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
})

try {
expect(capturedProps).toHaveLength(1)
expect(capturedProps[0].diffStyle).toBe("unified")
expect(capturedProps[0].onDiffStyleChange).toBeUndefined()
} finally {
dispose()
}
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
9 changes: 2 additions & 7 deletions packages/app/src/pages/session/review-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,13 @@ 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 {
title?: JSX.Element
empty?: JSX.Element
diffs: () => ReviewDiff[]
view: () => ReturnType<ReturnType<typeof useLayout>["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
Expand All @@ -43,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
Expand Down Expand Up @@ -119,7 +116,6 @@ export function SessionReviewTab(props: SessionReviewTabProps) {

createEffect(() => {
props.diffs().length
props.diffStyle
if (!layout.ready()) return
queueRestore()
})
Expand Down Expand Up @@ -152,8 +148,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}
Expand Down
10 changes: 10 additions & 0 deletions packages/app/src/pages/session/session-side-panel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
43 changes: 27 additions & 16 deletions packages/app/src/pages/session/session-side-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 } },
Expand All @@ -43,8 +45,14 @@ 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"
}

type RightPanelShellIconName = "status" | "folder" | "review" | "terminal"

/** Maps right-panel tab names to their shell icon components. */
function RightPanelShellIcon(props: { icon: RightPanelShellIconName }) {
return (
<Switch>
Expand All @@ -64,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)[]
Expand Down Expand Up @@ -292,22 +301,24 @@ export function SessionSidePanel(props: {
<Show
when={showSecondaryReviewTabs()}
fallback={
<div class="w-full bg-background-stronger flex items-center justify-end px-3 py-1.5">
<TooltipKeybind
title={language.t("command.file.open")}
keybind={command.keybind("file.open")}
class="flex items-center"
>
<IconButton
icon="plus-small"
variant="ghost"
iconSize="large"
class="!rounded-md"
onClick={() => openFilePicker(showAllFiles)}
aria-label={language.t("command.file.open")}
/>
</TooltipKeybind>
</div>
<Show when={shouldShowReviewFileOpenButton(activeTab(), false)}>
<div class="w-full bg-background-stronger flex items-center justify-end px-3 py-1.5">
<TooltipKeybind
title={language.t("command.file.open")}
keybind={command.keybind("file.open")}
class="flex items-center"
>
<IconButton
icon="plus-small"
variant="ghost"
iconSize="large"
class="!rounded-md"
onClick={() => openFilePicker(showAllFiles)}
aria-label={language.t("command.file.open")}
/>
</TooltipKeybind>
</div>
</Show>
}
>
<Tabs.List
Expand Down
12 changes: 12 additions & 0 deletions packages/ui/src/i18n/session-review-title.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { describe, expect, test } from "bun:test"
import { dict as zh } from "./zh"
import { dict as zht } from "./zht"

describe("session review title translations", () => {
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("分支變更")
})
})
2 changes: 2 additions & 0 deletions packages/ui/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": "拆分",
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/i18n/zht.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": "拆分",
Expand Down
Loading