Skip to content

Commit 04d236c

Browse files
authored
fix(app): keep v2 review pane mounted across session tab switches (#35074)
1 parent 5455fed commit 04d236c

2 files changed

Lines changed: 135 additions & 3 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { base64Encode } from "@opencode-ai/core/util/encode"
2+
import { expect, test, type Page } from "@playwright/test"
3+
import { mockOpenCodeServer } from "../utils/mock-server"
4+
import { expectAppVisible, expectSessionTitle } from "../utils/waits"
5+
6+
const directory = "C:/OpenCode/ReviewTabSwitch"
7+
const projectID = "proj_review_tab_switch"
8+
const sessionA = "ses_review_tab_a"
9+
const sessionB = "ses_review_tab_b"
10+
const titleA = "Alpha session"
11+
const titleB = "Beta session"
12+
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
13+
// Marks the review pane DOM node so a remount (fresh node) is detectable.
14+
const PROBE = "original"
15+
16+
test.use({ viewport: { width: 1440, height: 900 } })
17+
18+
// The v2 review pane's diff data is workspace-scoped: switching between session
19+
// tabs in the same workspace must update its parameters reactively instead of
20+
// tearing the pane down and remounting it (which flickers).
21+
test("keeps the v2 review pane mounted when switching session tabs in a workspace", async ({ page }) => {
22+
await setup(page)
23+
24+
await page.goto(sessionHref(sessionA))
25+
await expectSessionTitle(page, titleA)
26+
27+
await page.getByRole("button", { name: "Toggle review" }).click()
28+
const review = page.locator('#review-panel [data-component="session-review-v2"]')
29+
await expectAppVisible(review)
30+
await expectAppVisible(page.getByRole("button", { name: /example\.ts/ }))
31+
await writeProbe(page)
32+
33+
await switchTab(page, titleB)
34+
await expectSessionTitle(page, titleB)
35+
await expectAppVisible(review)
36+
expect(await readProbe(page)).toBe(PROBE)
37+
38+
await switchTab(page, titleA)
39+
await expectSessionTitle(page, titleA)
40+
await expectAppVisible(review)
41+
expect(await readProbe(page)).toBe(PROBE)
42+
})
43+
44+
type Probed = HTMLElement & { __e2eProbe?: string }
45+
46+
async function switchTab(page: Page, title: string) {
47+
await page.locator("[data-titlebar-tab-slot]", { hasText: title }).click()
48+
}
49+
50+
async function writeProbe(page: Page) {
51+
await page.locator('#review-panel [data-component="session-review-v2"]').evaluate((el, probe) => {
52+
;(el as Probed).__e2eProbe = probe
53+
}, PROBE)
54+
}
55+
56+
async function readProbe(page: Page) {
57+
return page.locator('#review-panel [data-component="session-review-v2"]').evaluate((el) => (el as Probed).__e2eProbe)
58+
}
59+
60+
async function setup(page: Page) {
61+
await mockOpenCodeServer(page, {
62+
directory,
63+
project: {
64+
id: projectID,
65+
worktree: directory,
66+
vcs: "git",
67+
name: "review-tab-switch",
68+
time: { created: 1700000000000, updated: 1700000000000 },
69+
sandboxes: [],
70+
},
71+
provider: {
72+
all: [
73+
{
74+
id: "opencode",
75+
name: "OpenCode",
76+
models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } },
77+
},
78+
],
79+
connected: ["opencode"],
80+
default: { providerID: "opencode", modelID: "test" },
81+
},
82+
sessions: [session(sessionA, titleA, 1700000000000), session(sessionB, titleB, 1700000001000)],
83+
vcsDiff: [
84+
{
85+
file: "src/example.ts",
86+
additions: 1,
87+
deletions: 1,
88+
status: "modified",
89+
patch:
90+
"diff --git a/src/example.ts b/src/example.ts\n--- a/src/example.ts\n+++ b/src/example.ts\n@@ -1 +1 @@\n-export const value = 'before'\n+export const value = 'after'\n",
91+
},
92+
],
93+
pageMessages: () => ({ items: [] }),
94+
})
95+
96+
await page.addInitScript(
97+
({ directory, server, sessions }) => {
98+
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
99+
localStorage.setItem(
100+
"opencode.global.dat:server",
101+
JSON.stringify({
102+
projects: { local: [{ worktree: directory, expanded: true }] },
103+
lastProject: { local: directory },
104+
}),
105+
)
106+
localStorage.setItem(
107+
"opencode.window.browser.dat:tabs",
108+
JSON.stringify(sessions.map((sessionId: string) => ({ type: "session", server, sessionId }))),
109+
)
110+
},
111+
{ directory, server, sessions: [sessionA, sessionB] },
112+
)
113+
}
114+
115+
function session(id: string, title: string, created: number) {
116+
return {
117+
id,
118+
slug: id,
119+
projectID,
120+
directory,
121+
title,
122+
version: "dev",
123+
time: { created, updated: created },
124+
}
125+
}
126+
127+
function sessionHref(sessionID: string) {
128+
return `/server/${base64Encode(server)}/session/${sessionID}`
129+
}

packages/app/src/pages/session.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,11 +1210,14 @@ export default function Page() {
12101210
},
12111211
})
12121212

1213+
// Latch: defer only the first diff render off the mount critical path. This Page
1214+
// stays mounted across same-workspace session tab switches, so gating on every
1215+
// deferRender flip tore down and remounted the whole review pane on tab switch.
1216+
const reviewPanelV2Rendered = createMemo<boolean>((prev) => prev || !store.deferRender, false)
1217+
12131218
const reviewPanelV2 = () => (
12141219
<div class="flex flex-col h-full overflow-hidden bg-background-stronger contain-strict">
1215-
{/* The route remounts per session; defer the diff render off the switch critical path
1216-
like the legacy review tab does. */}
1217-
<Show when={!store.deferRender}>
1220+
<Show when={reviewPanelV2Rendered()}>
12181221
<ReviewPanelV2 {...reviewPanelV2Props()} />
12191222
</Show>
12201223
</div>

0 commit comments

Comments
 (0)