Skip to content

Commit ee550f7

Browse files
perf(ui): collapse large diffs in review panel to prevent UI freeze
Files with >500 changed lines are now: - Collapsed by default when opening the review panel - Skipped by 'Expand All' - Guarded by click-to-confirm before expanding (3s timeout) - Auto-collapsed when the review panel is closed Adds i18n keys for large diff warnings across all 16 locales. Fixes anomalyco#13297
1 parent 81ca2df commit ee550f7

18 files changed

Lines changed: 118 additions & 6 deletions

File tree

‎packages/ui/src/components/session-review.css‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,20 @@
170170
color: var(--icon-diff-modified-base);
171171
}
172172

173+
[data-slot="session-review-large-diff-badge"] {
174+
font-family: var(--font-family-sans);
175+
font-size: var(--font-size-small);
176+
font-weight: var(--font-weight-medium);
177+
color: var(--text-weak);
178+
}
179+
180+
[data-slot="session-review-large-diff-warning"] {
181+
font-family: var(--font-family-sans);
182+
font-size: var(--font-size-small);
183+
font-weight: var(--font-weight-medium);
184+
color: var(--text-warning, var(--icon-diff-modified-base));
185+
}
186+
173187
[data-slot="session-review-file-container"] {
174188
padding: 0;
175189
}

‎packages/ui/src/components/session-review.tsx‎

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { useDiffComponent } from "../context/diff"
1010
import { useI18n } from "../context/i18n"
1111
import { getDirectory, getFilename } from "@opencode-ai/util/path"
1212
import { checksum } from "@opencode-ai/util/encode"
13-
import { createEffect, createMemo, createSignal, For, Match, Show, Switch, type JSX } from "solid-js"
13+
import { createEffect, createMemo, createSignal, For, Match, onCleanup, Show, Switch, type JSX } from "solid-js"
1414
import { createStore } from "solid-js/store"
1515
import { type FileContent, type FileDiff } from "@opencode-ai/sdk/v2"
1616
import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
@@ -170,32 +170,84 @@ function markerTop(wrapper: HTMLElement, marker: HTMLElement) {
170170
return rect.top - wrapperRect.top + Math.max(0, (rect.height - 20) / 2)
171171
}
172172

173+
const LARGE_DIFF_THRESHOLD = 500
174+
175+
function diffLines(diff: FileDiff) {
176+
return (diff.additions ?? 0) + (diff.deletions ?? 0)
177+
}
178+
179+
function isLargeDiff(diff: FileDiff) {
180+
return diffLines(diff) > LARGE_DIFF_THRESHOLD
181+
}
182+
173183
export const SessionReview = (props: SessionReviewProps) => {
174184
let scroll: HTMLDivElement | undefined
175185
let focusToken = 0
176186
const i18n = useI18n()
177187
const diffComponent = useDiffComponent()
178188
const anchors = new Map<string, HTMLElement>()
179189
const [store, setStore] = createStore({
180-
open: props.diffs.length > 10 ? [] : props.diffs.map((d) => d.file),
190+
open: props.diffs.length > 10 ? [] : props.diffs.filter((d) => !isLargeDiff(d)).map((d) => d.file),
181191
})
182192

183193
const [selection, setSelection] = createSignal<SessionReviewSelection | null>(null)
184194
const [commenting, setCommenting] = createSignal<SessionReviewSelection | null>(null)
185195
const [opened, setOpened] = createSignal<SessionReviewFocus | null>(null)
196+
const [confirmed, setConfirmed] = createSignal<Set<string>>(new Set())
197+
let confirmTimer: ReturnType<typeof setTimeout> | undefined
198+
onCleanup(() => {
199+
clearTimeout(confirmTimer)
200+
const current = open()
201+
if (!current || current.length === 0) return
202+
const large = largeDiffs()
203+
if (large.size === 0) return
204+
const filtered = current.filter((f) => !large.has(f))
205+
if (filtered.length === current.length) return
206+
props.onOpenChange?.(filtered)
207+
})
186208

187209
const open = () => props.open ?? store.open
188210
const diffStyle = () => props.diffStyle ?? (props.split ? "split" : "unified")
189211
const hasDiffs = () => props.diffs.length > 0
190212

191-
const handleChange = (open: string[]) => {
192-
props.onOpenChange?.(open)
213+
const largeDiffs = createMemo(() => {
214+
const map = new Map<string, number>()
215+
for (const d of props.diffs) {
216+
if (isLargeDiff(d)) map.set(d.file, diffLines(d))
217+
}
218+
return map
219+
})
220+
221+
const handleChange = (next: string[]) => {
222+
const current = open() ?? []
223+
const added = next.filter((f) => !current.includes(f))
224+
const needsConfirm = added.filter((f) => largeDiffs().has(f) && !confirmed().has(f))
225+
226+
if (needsConfirm.length > 0) {
227+
clearTimeout(confirmTimer)
228+
setConfirmed((prev: Set<string>) => {
229+
const s = new Set(prev)
230+
for (const f of needsConfirm) s.add(f)
231+
return s
232+
})
233+
confirmTimer = setTimeout(() => setConfirmed(new Set()), 3000)
234+
const filtered = next.filter((f) => !needsConfirm.includes(f))
235+
if (filtered.length !== current.length || filtered.some((f, i) => f !== current[i])) {
236+
props.onOpenChange?.(filtered)
237+
if (props.open !== undefined) return
238+
setStore("open", filtered)
239+
}
240+
return
241+
}
242+
243+
clearTimeout(confirmTimer)
244+
props.onOpenChange?.(next)
193245
if (props.open !== undefined) return
194-
setStore("open", open)
246+
setStore("open", next)
195247
}
196248

197249
const handleExpandOrCollapseAll = () => {
198-
const next = open().length > 0 ? [] : props.diffs.map((d) => d.file)
250+
const next = open().length > 0 ? [] : props.diffs.filter((d) => !isLargeDiff(d)).map((d) => d.file)
199251
handleChange(next)
200252
}
201253

@@ -537,6 +589,20 @@ export const SessionReview = (props: SessionReviewProps) => {
537589
</div>
538590
</div>
539591
<div data-slot="session-review-trigger-actions">
592+
<Show when={isLargeDiff(diff) && confirmed().has(diff.file) && !open().includes(diff.file)}>
593+
<span data-slot="session-review-large-diff-warning">
594+
{i18n.t("ui.sessionReview.largeDiff.clickToExpand", {
595+
lines: String(diffLines(diff)),
596+
})}
597+
</span>
598+
</Show>
599+
<Show when={isLargeDiff(diff) && !confirmed().has(diff.file)}>
600+
<span data-slot="session-review-large-diff-badge">
601+
{i18n.t("ui.sessionReview.largeDiff.label", {
602+
lines: String(diffLines(diff)),
603+
})}
604+
</span>
605+
</Show>
540606
<Switch>
541607
<Match when={isAdded()}>
542608
<span data-slot="session-review-change" data-type="added">

‎packages/ui/src/i18n/ar.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export const dict = {
88
"ui.sessionReview.change.added": "مضاف",
99
"ui.sessionReview.change.removed": "محذوف",
1010
"ui.sessionReview.change.modified": "معدل",
11+
"ui.sessionReview.largeDiff.label": "{{lines}} سطر",
12+
"ui.sessionReview.largeDiff.clickToExpand": "{{lines}} سطر — انقر مرة أخرى للتوسيع",
1113

1214
"ui.lineComment.label.prefix": "تعليق على ",
1315
"ui.lineComment.label.suffix": "",

‎packages/ui/src/i18n/br.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export const dict = {
88
"ui.sessionReview.change.added": "Adicionado",
99
"ui.sessionReview.change.removed": "Removido",
1010
"ui.sessionReview.change.modified": "Modificado",
11+
"ui.sessionReview.largeDiff.label": "{{lines}} linhas",
12+
"ui.sessionReview.largeDiff.clickToExpand": "{{lines}} linhas — clique novamente para expandir",
1113

1214
"ui.lineComment.label.prefix": "Comentar em ",
1315
"ui.lineComment.label.suffix": "",

‎packages/ui/src/i18n/bs.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export const dict = {
1212
"ui.sessionReview.change.added": "Dodano",
1313
"ui.sessionReview.change.removed": "Uklonjeno",
1414
"ui.sessionReview.change.modified": "Izmijenjeno",
15+
"ui.sessionReview.largeDiff.label": "{{lines}} linija",
16+
"ui.sessionReview.largeDiff.clickToExpand": "{{lines}} linija — kliknite ponovo za proširenje",
1517

1618
"ui.lineComment.label.prefix": "Komentar na ",
1719
"ui.lineComment.label.suffix": "",

‎packages/ui/src/i18n/da.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export const dict = {
99
"ui.sessionReview.change.added": "Tilføjet",
1010
"ui.sessionReview.change.removed": "Fjernet",
1111
"ui.sessionReview.change.modified": "Ændret",
12+
"ui.sessionReview.largeDiff.label": "{{lines}} linjer",
13+
"ui.sessionReview.largeDiff.clickToExpand": "{{lines}} linjer — klik igen for at udvide",
1214
"ui.lineComment.label.prefix": "Kommenter på ",
1315
"ui.lineComment.label.suffix": "",
1416
"ui.lineComment.editorLabel.prefix": "Kommenterer på ",

‎packages/ui/src/i18n/de.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export const dict = {
1313
"ui.sessionReview.change.added": "Hinzugefügt",
1414
"ui.sessionReview.change.removed": "Entfernt",
1515
"ui.sessionReview.change.modified": "Geändert",
16+
"ui.sessionReview.largeDiff.label": "{{lines}} Zeilen",
17+
"ui.sessionReview.largeDiff.clickToExpand": "{{lines}} Zeilen — erneut klicken zum Aufklappen",
1618
"ui.lineComment.label.prefix": "Kommentar zu ",
1719
"ui.lineComment.label.suffix": "",
1820
"ui.lineComment.editorLabel.prefix": "Kommentiere ",

‎packages/ui/src/i18n/en.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export const dict = {
88
"ui.sessionReview.change.added": "Added",
99
"ui.sessionReview.change.removed": "Removed",
1010
"ui.sessionReview.change.modified": "Modified",
11+
"ui.sessionReview.largeDiff.label": "{{lines}} lines",
12+
"ui.sessionReview.largeDiff.clickToExpand": "{{lines}} lines — click again to expand",
1113

1214
"ui.lineComment.label.prefix": "Comment on ",
1315
"ui.lineComment.label.suffix": "",

‎packages/ui/src/i18n/es.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export const dict = {
88
"ui.sessionReview.change.added": "Añadido",
99
"ui.sessionReview.change.removed": "Eliminado",
1010
"ui.sessionReview.change.modified": "Modificado",
11+
"ui.sessionReview.largeDiff.label": "{{lines}} líneas",
12+
"ui.sessionReview.largeDiff.clickToExpand": "{{lines}} líneas — clic de nuevo para expandir",
1113

1214
"ui.lineComment.label.prefix": "Comentar en ",
1315
"ui.lineComment.label.suffix": "",

‎packages/ui/src/i18n/fr.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export const dict = {
88
"ui.sessionReview.change.added": "Ajouté",
99
"ui.sessionReview.change.removed": "Supprimé",
1010
"ui.sessionReview.change.modified": "Modifié",
11+
"ui.sessionReview.largeDiff.label": "{{lines}} lignes",
12+
"ui.sessionReview.largeDiff.clickToExpand": "{{lines}} lignes — cliquez à nouveau pour développer",
1113

1214
"ui.lineComment.label.prefix": "Commenter sur ",
1315
"ui.lineComment.label.suffix": "",

0 commit comments

Comments
 (0)