@@ -10,7 +10,7 @@ import { useDiffComponent } from "../context/diff"
1010import { useI18n } from "../context/i18n"
1111import { getDirectory , getFilename } from "@opencode-ai/util/path"
1212import { 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"
1414import { createStore } from "solid-js/store"
1515import { type FileContent , type FileDiff } from "@opencode-ai/sdk/v2"
1616import { 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+
173183export 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" >
0 commit comments