@@ -44,6 +44,7 @@ export const DiffOverlay: React.FC<DiffOverlayProps> = React.memo(({
4444 const [ stagedDiff , setStagedDiff ] = useState < string | null > ( null ) ;
4545 const [ unstagedDiff , setUnstagedDiff ] = useState < string | null > ( null ) ;
4646 const [ fileSource , setFileSource ] = useState < string | null > ( null ) ;
47+ const [ fileSources , setFileSources ] = useState < Record < string , string > | null > ( null ) ;
4748 const [ loading , setLoading ] = useState ( false ) ;
4849 const [ error , setError ] = useState < string | null > ( null ) ;
4950 const [ copied , setCopied ] = useState ( false ) ;
@@ -85,6 +86,7 @@ export const DiffOverlay: React.FC<DiffOverlayProps> = React.memo(({
8586 setStagedDiff ( null ) ;
8687 setUnstagedDiff ( null ) ;
8788 setFileSource ( null ) ;
89+ setFileSources ( null ) ;
8890 return ;
8991 }
9092
@@ -93,6 +95,97 @@ export const DiffOverlay: React.FC<DiffOverlayProps> = React.memo(({
9395 setError ( null ) ;
9496
9597 try {
98+ const workingScope = target . kind === 'working' ? ( ( target as any ) . scope || 'all' ) : null ;
99+
100+ if ( target . kind === 'working' ) {
101+ // For working tree, always load all + staged + unstaged diffs so we can determine per-hunk status in one view (Zed-like).
102+ const [ allRes , stagedRes , unstagedRes ] = await Promise . all ( [
103+ withTimeout ( API . sessions . getDiff ( sessionId , { kind : 'working' , scope : 'all' } as any ) , 15_000 , 'Load diff' ) ,
104+ withTimeout ( API . sessions . getDiff ( sessionId , { kind : 'working' , scope : 'staged' } as any ) , 15_000 , 'Load staged diff' ) ,
105+ withTimeout ( API . sessions . getDiff ( sessionId , { kind : 'working' , scope : 'unstaged' } as any ) , 15_000 , 'Load unstaged diff' ) ,
106+ ] ) ;
107+
108+ if ( ! allRes . success ) throw new Error ( allRes . error || 'Failed to load diff' ) ;
109+ if ( ! stagedRes . success ) throw new Error ( stagedRes . error || 'Failed to load staged diff' ) ;
110+ if ( ! unstagedRes . success ) throw new Error ( unstagedRes . error || 'Failed to load unstaged diff' ) ;
111+
112+ setDiff ( allRes . data ?. diff ?? '' ) ;
113+ setStagedDiff ( stagedRes . data ?. diff ?? '' ) ;
114+ setUnstagedDiff ( unstagedRes . data ?. diff ?? '' ) ;
115+
116+ // Single-file view: expand to full file using a best-effort file source.
117+ if ( filePath ) {
118+ setFileSources ( null ) ;
119+ const preferredRef = workingScope === 'untracked' ? 'WORKTREE' : 'HEAD' ;
120+ let sourceRes = await withTimeout (
121+ API . sessions . getFileContent ( sessionId , { filePath, ref : preferredRef , maxBytes : 1024 * 1024 } ) ,
122+ 15_000 ,
123+ 'Load file content'
124+ ) ;
125+ if ( ! sourceRes . success && preferredRef !== 'WORKTREE' ) {
126+ sourceRes = await withTimeout (
127+ API . sessions . getFileContent ( sessionId , { filePath, ref : 'WORKTREE' , maxBytes : 1024 * 1024 } ) ,
128+ 15_000 ,
129+ 'Load file content'
130+ ) ;
131+ }
132+ setFileSource ( sourceRes . success ? ( sourceRes . data ?. content ?? '' ) : null ) ;
133+ } else {
134+ // Project diff view: expand each file to include unchanged lines between hunks (Zed-like).
135+ setFileSource ( null ) ;
136+
137+ const wt = ( allRes . data as { workingTree ?: unknown } | undefined ) ?. workingTree as
138+ | { staged : Array < { path : string ; isNew ?: boolean } > ; unstaged : Array < { path : string ; isNew ?: boolean } > ; untracked : Array < { path : string ; isNew ?: boolean } > }
139+ | undefined ;
140+
141+ const untracked = new Set < string > ( [
142+ ...( wt ?. untracked || [ ] ) . map ( ( f ) => f . path ) ,
143+ ...( wt ?. staged || [ ] ) . filter ( ( f ) => Boolean ( ( f as any ) . isNew ) ) . map ( ( f ) => f . path ) ,
144+ ] ) ;
145+
146+ const changed = Array . isArray ( ( allRes . data as { changedFiles ?: unknown } | undefined ) ?. changedFiles )
147+ ? ( ( ( allRes . data as { changedFiles ?: unknown } ) . changedFiles as unknown [ ] ) || [ ] ) . filter ( ( v ) : v is string => typeof v === 'string' && v . trim ( ) . length > 0 )
148+ : [ ] ;
149+
150+ const maxFiles = 80 ;
151+ const targets = changed . slice ( 0 , maxFiles ) ;
152+ const results : Record < string , string > = { } ;
153+
154+ // Small concurrency pool to avoid UI stalls.
155+ const concurrency = 6 ;
156+ let cursor = 0 ;
157+ const workers = Array . from ( { length : concurrency } ) . map ( async ( ) => {
158+ while ( cursor < targets . length ) {
159+ const idx = cursor ++ ;
160+ const p = targets [ idx ] ;
161+ const prefer = untracked . has ( p ) ? 'WORKTREE' : 'HEAD' ;
162+ try {
163+ let r = await withTimeout (
164+ API . sessions . getFileContent ( sessionId , { filePath : p , ref : prefer as any , maxBytes : 1024 * 1024 } ) ,
165+ 15_000 ,
166+ 'Load file content'
167+ ) ;
168+ if ( ! r . success && prefer !== 'WORKTREE' ) {
169+ r = await withTimeout (
170+ API . sessions . getFileContent ( sessionId , { filePath : p , ref : 'WORKTREE' , maxBytes : 1024 * 1024 } ) ,
171+ 15_000 ,
172+ 'Load file content'
173+ ) ;
174+ }
175+ if ( r . success ) {
176+ results [ p ] = r . data ?. content ?? '' ;
177+ }
178+ } catch {
179+ // best-effort
180+ }
181+ }
182+ } ) ;
183+ await Promise . all ( workers ) ;
184+ setFileSources ( Object . keys ( results ) . length > 0 ? results : null ) ;
185+ }
186+ return ;
187+ }
188+
96189 if ( target . kind === 'working' && filePath ) {
97190 const [ allRes , stagedRes , unstagedRes ] = await Promise . all ( [
98191 withTimeout ( API . sessions . getDiff ( sessionId , { kind : 'working' , scope : 'all' } as any ) , 15_000 , 'Load diff' ) ,
@@ -122,14 +215,15 @@ export const DiffOverlay: React.FC<DiffOverlayProps> = React.memo(({
122215 'Load file content'
123216 ) ;
124217 }
125- setFileSource ( sourceRes . success ? ( sourceRes . data ?. content ?? '' ) : '' ) ;
218+ setFileSource ( sourceRes . success ? ( sourceRes . data ?. content ?? '' ) : null ) ;
126219 } else {
127220 const response = await withTimeout ( API . sessions . getDiff ( sessionId , target ) , 15_000 , 'Load diff' ) ;
128221 if ( response . success && response . data ) {
129222 setDiff ( response . data . diff ?? '' ) ;
130223 setStagedDiff ( null ) ;
131224 setUnstagedDiff ( null ) ;
132225 setFileSource ( null ) ;
226+ setFileSources ( null ) ;
133227 } else {
134228 const message = response . error || 'Failed to load diff' ;
135229 const isStaleCommit =
@@ -159,7 +253,9 @@ export const DiffOverlay: React.FC<DiffOverlayProps> = React.memo(({
159253 setError ( null ) ;
160254
161255 try {
162- if ( target . kind === 'working' && filePath ) {
256+ const workingScope = target . kind === 'working' ? ( ( target as any ) . scope || 'all' ) : null ;
257+
258+ if ( target . kind === 'working' ) {
163259 const [ allRes , stagedRes , unstagedRes ] = await Promise . all ( [
164260 withTimeout ( API . sessions . getDiff ( sessionId , { kind : 'working' , scope : 'all' } as any ) , 15_000 , 'Load diff' ) ,
165261 withTimeout ( API . sessions . getDiff ( sessionId , { kind : 'working' , scope : 'staged' } as any ) , 15_000 , 'Load staged diff' ) ,
@@ -174,28 +270,81 @@ export const DiffOverlay: React.FC<DiffOverlayProps> = React.memo(({
174270 setStagedDiff ( stagedRes . data ?. diff ?? '' ) ;
175271 setUnstagedDiff ( unstagedRes . data ?. diff ?? '' ) ;
176272
177- // Keep full-file rendering stable during refresh as well.
178- const preferredRef = target . scope === 'untracked' ? 'WORKTREE' : 'HEAD' ;
179- let sourceRes = await withTimeout (
180- API . sessions . getFileContent ( sessionId , { filePath, ref : preferredRef , maxBytes : 1024 * 1024 } ) ,
181- 15_000 ,
182- 'Load file content'
183- ) ;
184- if ( ! sourceRes . success && preferredRef !== 'WORKTREE' ) {
185- sourceRes = await withTimeout (
186- API . sessions . getFileContent ( sessionId , { filePath, ref : 'WORKTREE' , maxBytes : 1024 * 1024 } ) ,
273+ if ( filePath ) {
274+ setFileSources ( null ) ;
275+ const preferredRef = workingScope === 'untracked' ? 'WORKTREE' : 'HEAD' ;
276+ let sourceRes = await withTimeout (
277+ API . sessions . getFileContent ( sessionId , { filePath, ref : preferredRef , maxBytes : 1024 * 1024 } ) ,
187278 15_000 ,
188279 'Load file content'
189280 ) ;
281+ if ( ! sourceRes . success && preferredRef !== 'WORKTREE' ) {
282+ sourceRes = await withTimeout (
283+ API . sessions . getFileContent ( sessionId , { filePath, ref : 'WORKTREE' , maxBytes : 1024 * 1024 } ) ,
284+ 15_000 ,
285+ 'Load file content'
286+ ) ;
287+ }
288+ setFileSource ( sourceRes . success ? ( sourceRes . data ?. content ?? '' ) : null ) ;
289+ } else {
290+ setFileSource ( null ) ;
291+
292+ const wt = ( allRes . data as { workingTree ?: unknown } | undefined ) ?. workingTree as
293+ | { staged : Array < { path : string ; isNew ?: boolean } > ; unstaged : Array < { path : string ; isNew ?: boolean } > ; untracked : Array < { path : string ; isNew ?: boolean } > }
294+ | undefined ;
295+
296+ const untracked = new Set < string > ( [
297+ ...( wt ?. untracked || [ ] ) . map ( ( f ) => f . path ) ,
298+ ...( wt ?. staged || [ ] ) . filter ( ( f ) => Boolean ( ( f as any ) . isNew ) ) . map ( ( f ) => f . path ) ,
299+ ] ) ;
300+
301+ const changed = Array . isArray ( ( allRes . data as { changedFiles ?: unknown } | undefined ) ?. changedFiles )
302+ ? ( ( ( allRes . data as { changedFiles ?: unknown } ) . changedFiles as unknown [ ] ) || [ ] ) . filter ( ( v ) : v is string => typeof v === 'string' && v . trim ( ) . length > 0 )
303+ : [ ] ;
304+
305+ const maxFiles = 80 ;
306+ const targets = changed . slice ( 0 , maxFiles ) ;
307+ const results : Record < string , string > = { } ;
308+ const concurrency = 6 ;
309+ let cursor = 0 ;
310+ const workers = Array . from ( { length : concurrency } ) . map ( async ( ) => {
311+ while ( cursor < targets . length ) {
312+ const idx = cursor ++ ;
313+ const p = targets [ idx ] ;
314+ const prefer = untracked . has ( p ) ? 'WORKTREE' : 'HEAD' ;
315+ try {
316+ let r = await withTimeout (
317+ API . sessions . getFileContent ( sessionId , { filePath : p , ref : prefer as any , maxBytes : 1024 * 1024 } ) ,
318+ 15_000 ,
319+ 'Load file content'
320+ ) ;
321+ if ( ! r . success && prefer !== 'WORKTREE' ) {
322+ r = await withTimeout (
323+ API . sessions . getFileContent ( sessionId , { filePath : p , ref : 'WORKTREE' , maxBytes : 1024 * 1024 } ) ,
324+ 15_000 ,
325+ 'Load file content'
326+ ) ;
327+ }
328+ if ( r . success ) {
329+ results [ p ] = r . data ?. content ?? '' ;
330+ }
331+ } catch {
332+ // best-effort
333+ }
334+ }
335+ } ) ;
336+ await Promise . all ( workers ) ;
337+ setFileSources ( Object . keys ( results ) . length > 0 ? results : null ) ;
190338 }
191- setFileSource ( sourceRes . success ? ( sourceRes . data ?. content ?? '' ) : '' ) ;
339+ return ;
192340 } else {
193341 const response = await withTimeout ( API . sessions . getDiff ( sessionId , target ) , 15_000 , 'Load diff' ) ;
194342 if ( response . success && response . data ) {
195343 setDiff ( response . data . diff ?? '' ) ;
196344 setStagedDiff ( null ) ;
197345 setUnstagedDiff ( null ) ;
198346 setFileSource ( null ) ;
347+ setFileSources ( null ) ;
199348 } else {
200349 const message = response . error || 'Failed to load diff' ;
201350 const isStaleCommit =
@@ -469,7 +618,7 @@ export const DiffOverlay: React.FC<DiffOverlayProps> = React.memo(({
469618 currentScope = { target ?. kind === 'working' ? ( target . scope as any ) : undefined }
470619 stagedDiff = { stagedDiff ?? undefined }
471620 unstagedDiff = { unstagedDiff ?? undefined }
472- fileSources = { filePath && fileSource != null ? { [ filePath ] : fileSource } : undefined }
621+ fileSources = { filePath && fileSource != null ? { [ filePath ] : fileSource } : ( fileSources ?? undefined ) }
473622 fileOrder = { viewerFiles . length > 0 ? viewerFiles . map ( ( f ) => f . path ) : undefined }
474623 onChanged = { handleRefresh }
475624 />
0 commit comments