Skip to content

Commit 7d26186

Browse files
arvsrnHona
andauthored
feat(app): v2 review panel overhaul (#31882)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
1 parent fbb95a6 commit 7d26186

35 files changed

Lines changed: 3438 additions & 214 deletions

packages/app/e2e/performance/timeline/session-tab-switch-benchmark.spec.ts

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import type { Page } from "@playwright/test"
22
import { expectSessionTitle } from "../../utils/waits"
33
import { benchmark, expect, withBenchmarkPage } from "../benchmark"
44
import { fixture } from "./session-timeline-stress.fixture"
5-
import { installStressSessionTabs, mockStressTimeline, stressSessionHref } from "./timeline-test-helpers"
5+
import {
6+
createReviewDiffs,
7+
installStressSessionTabs,
8+
installTimelineSettings,
9+
mockStressTimeline,
10+
stressSessionHref,
11+
} from "./timeline-test-helpers"
612
import { measureSessionSwitch, waitForStableTimeline } from "./session-tab-switch-probe"
713

814
type Result = Awaited<ReturnType<typeof measureSessionSwitch>>
@@ -20,8 +26,41 @@ benchmark("benchmarks cold and hot session tab switching", async ({ browser, rep
2026
report({ results, summary: summarize(results) })
2127
})
2228

23-
async function trial(page: Page, mode: "cold" | "hot") {
24-
await mockStressTimeline(page)
29+
benchmark(
30+
"benchmarks v2 session tab switching with and without the review pane",
31+
async ({ browser, report }, testInfo) => {
32+
benchmark.setTimeout(360_000)
33+
const runs = Number(process.env.SESSION_TAB_SWITCH_RUNS ?? 5)
34+
const results = {
35+
closed: { cold: [] as Result[], hot: [] as Result[] },
36+
open: { cold: [] as Result[], hot: [] as Result[] },
37+
}
38+
for (const reviewPane of ["closed", "open"] as const) {
39+
for (const mode of ["cold", "hot"] as const) {
40+
for (let run = 0; run < runs; run++) {
41+
results[reviewPane][mode].push(
42+
await withBenchmarkPage(
43+
browser,
44+
`session-tab-switch-v2-${reviewPane}-${mode}-${run}`,
45+
(page) => trial(page, mode, { newLayoutDesigns: true, reviewPane }),
46+
testInfo,
47+
),
48+
)
49+
}
50+
}
51+
}
52+
report({ results, summary: summarizeReviewPane(results) }, { runs, reviewDiffs: createReviewDiffs().length })
53+
},
54+
)
55+
56+
async function trial(
57+
page: Page,
58+
mode: "cold" | "hot",
59+
options?: { newLayoutDesigns?: boolean; reviewPane?: "closed" | "open" },
60+
) {
61+
const reviewDiffs = options?.newLayoutDesigns ? createReviewDiffs() : undefined
62+
await mockStressTimeline(page, { vcsDiff: reviewDiffs })
63+
if (options?.newLayoutDesigns) await installTimelineSettings(page)
2564
await installStressSessionTabs(page)
2665
if (mode === "hot") {
2766
await page.goto(stressSessionHref(fixture.targetID))
@@ -33,6 +72,10 @@ async function trial(page: Page, mode: "cold" | "hot") {
3372
await expectSessionTitle(page, fixture.expected.sourceTitle)
3473
}
3574
await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!)
75+
if (options?.reviewPane === "open") {
76+
await openReviewPane(page)
77+
await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!)
78+
}
3679

3780
const destinationIDs = fixture.messages[fixture.targetID].map((message) => message.info.id)
3881
const sourceIDs = fixture.messages[fixture.sourceID].map((message) => message.info.id)
@@ -70,10 +113,32 @@ function summarize(results: Record<"cold" | "hot", Result[]>) {
70113
)
71114
}
72115

116+
function summarizeReviewPane(results: Record<"closed" | "open", Record<"cold" | "hot", Result[]>>) {
117+
return Object.fromEntries(
118+
Object.entries(results).map(([reviewPane, values]) => [
119+
reviewPane,
120+
summarize(values as Record<"cold" | "hot", Result[]>),
121+
]),
122+
)
123+
}
124+
73125
async function switchSession(page: Page, sessionID: string, title: string) {
74126
const href = stressSessionHref(sessionID)
75127
const tab = page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`).first()
76128
await expect(tab).toBeVisible()
77129
await tab.click()
78130
await expectSessionTitle(page, title)
79131
}
132+
133+
async function openReviewPane(page: Page) {
134+
await page.getByRole("button", { name: "Toggle review" }).click()
135+
const panel = page.locator("#review-panel")
136+
await expect(panel).toBeVisible()
137+
// Text-based readiness works across review implementations; the legacy list mounts
138+
// diff viewers lazily while V2 mounts the active preview eagerly.
139+
await page.waitForFunction(() => {
140+
const panel = document.querySelector<HTMLElement>("#review-panel")
141+
const text = panel?.textContent ?? ""
142+
return text.includes("generated-000.ts") && text.includes("+3")
143+
})
144+
}

packages/app/e2e/performance/timeline/session-tab-switch-metrics.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ export type SessionSwitchSample = {
55
hasVisibleRows: boolean
66
last: boolean
77
bottomErrorPx?: number
8+
review?: {
9+
fileHost: boolean
10+
fileHostReplaced: boolean
11+
header: string
12+
replacedLevels: string[]
13+
}
814
}
915

1016
export function classifySessionSwitch(samples: SessionSwitchSample[]) {
@@ -23,6 +29,10 @@ export function classifySessionSwitch(samples: SessionSwitchSample[]) {
2329
(sample) => sample.hasVisibleRows && sample.destination.length === 0 && sample.source.length === 0,
2430
).length,
2531
sourceSamples: samples.filter((sample) => sample.source.length > 0).length,
32+
reviewFileHostMissingSamples: samples.filter((sample) => sample.review && !sample.review.fileHost).length,
33+
reviewFileHostReplacedSamples: samples.filter((sample) => sample.review?.fileHostReplaced).length,
34+
reviewHeaders: [...new Set(samples.flatMap((sample) => (sample.review ? [sample.review.header] : [])))],
35+
reviewReplacedLevels: [...new Set(samples.flatMap((sample) => sample.review?.replacedLevels ?? []))],
2636
}
2737
}
2838

packages/app/e2e/performance/timeline/session-tab-switch-probe.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,41 @@ async function installSessionSwitchProbe(
1616
const samples: SessionSwitchSample[] = []
1717
let started: number | undefined
1818
let running = true
19+
const reviewLevels: Record<string, string> = {
20+
panel: "#review-panel",
21+
tabs: '#review-panel [data-component="tabs"]',
22+
body: '#review-panel [data-slot="session-review-v2-body"]',
23+
review: '#review-panel [data-component="session-review-v2"]',
24+
preview: '#review-panel [data-slot="session-review-v2-preview"]',
25+
scroll: '#review-panel [data-slot="session-review-v2-diff-scroll"]',
26+
file: '#review-panel [data-component="file"][data-mode="diff"]',
27+
}
28+
const initialReviewNodes: Record<string, Element | null> = {}
1929
const sample = () => {
2030
if (!running || started === undefined) return
2131
setTimeout(() => {
2232
if (!running || started === undefined) return
2333
const observedAtMs = performance.now() - started
34+
const reviewPanel = document.querySelector<HTMLElement>("#review-panel")
35+
const reviewFile = reviewPanel?.querySelector('[data-component="file"][data-mode="diff"]')
36+
const initialReviewFile = initialReviewNodes.file
37+
const replacedLevels = Object.entries(reviewLevels).flatMap(([name, selector]) => {
38+
const initial = initialReviewNodes[name]
39+
if (!initial) return []
40+
const current = document.querySelector(selector)
41+
return current && current !== initial ? [name] : []
42+
})
43+
const review = reviewPanel
44+
? {
45+
fileHost: !!reviewFile,
46+
fileHostReplaced: !!initialReviewFile && !!reviewFile && reviewFile !== initialReviewFile,
47+
header:
48+
reviewPanel
49+
.querySelector<HTMLElement>('[data-slot="session-review-v2-file-header"]')
50+
?.textContent?.trim() ?? "",
51+
replacedLevels,
52+
}
53+
: undefined
2454
const root = [...document.querySelectorAll<HTMLElement>(".scroll-view__viewport")].find((element) =>
2555
element.querySelector("[data-timeline-row]"),
2656
)
@@ -44,9 +74,10 @@ async function installSessionSwitchProbe(
4474
hasVisibleRows,
4575
last: visible.includes(lastID),
4676
bottomErrorPx: spacer ? spacer.bottom - view.bottom : undefined,
77+
review,
4778
})
4879
} else {
49-
samples.push({ observedAtMs, destination: [], source: [], hasVisibleRows: false, last: false })
80+
samples.push({ observedAtMs, destination: [], source: [], hasVisibleRows: false, last: false, review })
5081
}
5182
requestAnimationFrame(sample)
5283
}, 0)
@@ -57,6 +88,9 @@ async function installSessionSwitchProbe(
5788
const link = event.target instanceof Element ? event.target.closest("a") : undefined
5889
if (link?.getAttribute("href") !== href) return
5990
started = performance.now()
91+
for (const [name, selector] of Object.entries(reviewLevels)) {
92+
initialReviewNodes[name] = document.querySelector(selector)
93+
}
6094
requestAnimationFrame(sample)
6195
},
6296
{ capture: true, once: true },

packages/app/e2e/performance/timeline/session-timeline-benchmark.fixture.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -93,36 +93,53 @@ const assistantMessage = {
9393
parts: [editPart],
9494
}
9595

96-
export async function setupTimelineBenchmark(page: Page, options: { historyTurns: number; eventBatch: number }) {
96+
export async function setupTimelineBenchmark(
97+
page: Page,
98+
options: {
99+
historyTurns: number
100+
eventBatch: number
101+
newLayoutDesigns?: boolean
102+
vcsDiff?: unknown[]
103+
turnDiffs?: unknown[]
104+
},
105+
) {
97106
const events: EventPayload[] = []
98107
let eventBatch = options.eventBatch
108+
const currentUserMessage = options.turnDiffs
109+
? { ...userMessage, info: { ...userMessage.info, summary: { diffs: options.turnDiffs } } }
110+
: userMessage
99111
await mockOpenCodeServer(page, {
100112
directory,
101113
project: project(),
102114
provider: provider(),
103115
sessions: [session()],
116+
vcsDiff: options.vcsDiff,
104117
pageMessages: () => ({
105118
items: [
106119
...Array.from({ length: options.historyTurns }, (_, index) => performanceTurn(index)).flat(),
107-
userMessage,
120+
currentUserMessage,
108121
assistantMessage,
109122
],
110123
}),
111124
events: () => events.splice(0, eventBatch),
112125
eventRetry: 16,
113126
})
114-
await page.addInitScript(() => {
115-
localStorage.setItem(
116-
"settings.v3",
117-
JSON.stringify({
118-
general: {
119-
editToolPartsExpanded: true,
120-
shellToolPartsExpanded: true,
121-
showReasoningSummaries: true,
122-
},
123-
}),
124-
)
125-
})
127+
await page.addInitScript(
128+
(input) => {
129+
localStorage.setItem(
130+
"settings.v3",
131+
JSON.stringify({
132+
general: {
133+
newLayoutDesigns: input.newLayoutDesigns,
134+
editToolPartsExpanded: true,
135+
shellToolPartsExpanded: true,
136+
showReasoningSummaries: true,
137+
},
138+
}),
139+
)
140+
},
141+
{ newLayoutDesigns: options.newLayoutDesigns ?? false },
142+
)
126143
await page.setViewportSize({ width: 1366, height: 768 })
127144
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
128145
const text = page.locator(`[data-timeline-part-id="${textPartID}"]`).first()

0 commit comments

Comments
 (0)