@@ -34,75 +34,105 @@ export function DiffEditor(props: { cwd: string; prNumber?: number | null }) {
3434 const [ error , setError ] = createSignal < string | null > ( null ) ;
3535 const [ collapsedHunks , setCollapsedHunks ] = createSignal < Set < string > > ( new Set ( ) ) ;
3636 const [ gitPanelOpen , setGitPanelOpen ] = createSignal ( true ) ;
37+ const [ diffMode , setDiffMode ] = createSignal < "both" | "pr" | "worktree" > ( "both" ) ;
38+
39+ // Separate storage for PR diff and worktree diff
40+ const [ prFiles , setPrFiles ] = createSignal < ChangedFile [ ] > ( [ ] ) ;
41+ const [ prDiffs , setPrDiffs ] = createSignal < FileDiff [ ] > ( [ ] ) ;
42+ const [ wtFiles , setWtFiles ] = createSignal < ChangedFile [ ] > ( [ ] ) ;
43+ const [ wtDiffs , setWtDiffs ] = createSignal < FileDiff [ ] > ( [ ] ) ;
44+
45+ const hasPr = ( ) => ! ! props . prNumber ;
3746
3847 function close ( ) {
3948 const tab = appStore . store . activeTab ;
4049 if ( tab ) setStore ( "threadDiffOpen" , tab , false ) ;
4150 }
4251
43- // Cache diffs per cwd to avoid re-fetching on thread switch
44- const diffCache = new Map < string , { files : any [ ] ; diffs : any [ ] } > ( ) ;
45-
4652 async function loadAll ( ) {
4753 const cwd = props . cwd ;
4854 const prNum = props . prNumber ;
4955 if ( ! cwd || cwd === "." ) {
50- setFiles ( [ ] ) ;
51- setDiffs ( [ ] ) ;
52- setLoading ( false ) ;
53- return ;
54- }
55-
56- const cacheKey = prNum ? `pr:${ prNum } :${ cwd } ` : cwd ;
57-
58- // Check cache first
59- const cached = diffCache . get ( cacheKey ) ;
60- if ( cached ) {
61- setFiles ( cached . files ) ;
62- setDiffs ( cached . diffs ) ;
63- if ( cached . files . length > 0 ) setSelectedFile ( cached . files [ 0 ] . path ) ;
56+ setFiles ( [ ] ) ; setDiffs ( [ ] ) ;
6457 setLoading ( false ) ;
6558 return ;
6659 }
6760
6861 setLoading ( true ) ;
6962 setError ( null ) ;
70- setFiles ( [ ] ) ;
71- setDiffs ( [ ] ) ;
63+ setFiles ( [ ] ) ; setDiffs ( [ ] ) ;
64+ setPrFiles ( [ ] ) ; setPrDiffs ( [ ] ) ;
65+ setWtFiles ( [ ] ) ; setWtDiffs ( [ ] ) ;
7266 setSelectedFile ( null ) ;
7367
7468 try {
75- let changedFiles : any [ ] ;
76- let sessionDiffs : any [ ] ;
69+ // Always fetch worktree/local changes
70+ const [ localFiles , localDiffs ] = await Promise . all ( [
71+ ipc . getChangedFiles ( cwd ) . catch ( ( ) => [ ] ) ,
72+ ipc . getSessionDiff ( cwd ) . catch ( ( ) => [ ] ) ,
73+ ] ) ;
74+ if ( props . cwd === cwd ) {
75+ setWtFiles ( localFiles as any ) ;
76+ setWtDiffs ( localDiffs as any ) ;
77+ }
7778
79+ // If PR linked, also fetch PR diff
7880 if ( prNum ) {
79- // PR mode: fetch PR diff instead of working directory diff
80- const prDiffRaw = await ipc . getPrDiff ( cwd , prNum ) ;
81- // Parse the raw diff into our FileDiff format
82- const parsed = parsePrDiff ( prDiffRaw ) ;
83- changedFiles = parsed . files ;
84- sessionDiffs = parsed . diffs ;
85- } else {
86- [ changedFiles , sessionDiffs ] = await Promise . all ( [
87- ipc . getChangedFiles ( cwd ) ,
88- ipc . getSessionDiff ( cwd ) ,
89- ] ) ;
81+ try {
82+ const prDiffRaw = await ipc . getPrDiff ( cwd , prNum ) ;
83+ const parsed = parsePrDiff ( prDiffRaw ) ;
84+ if ( props . cwd === cwd ) {
85+ setPrFiles ( parsed . files ) ;
86+ setPrDiffs ( parsed . diffs ) ;
87+ }
88+ } catch { }
9089 }
9190
92- if ( props . cwd === cwd ) {
93- setFiles ( changedFiles ) ;
94- setDiffs ( sessionDiffs ) ;
95- if ( changedFiles . length > 0 ) setSelectedFile ( changedFiles [ 0 ] . path ) ;
96- diffCache . set ( cacheKey , { files : changedFiles , diffs : sessionDiffs } ) ;
97- setTimeout ( ( ) => diffCache . delete ( cacheKey ) , 30000 ) ;
98- }
91+ // Apply current mode
92+ if ( props . cwd === cwd ) applyMode ( ) ;
9993 } catch ( e ) {
10094 if ( props . cwd === cwd ) setError ( String ( e ) ) ;
10195 } finally {
10296 if ( props . cwd === cwd ) setLoading ( false ) ;
10397 }
10498 }
10599
100+ function applyMode ( ) {
101+ const mode = diffMode ( ) ;
102+ let mergedFiles : any [ ] = [ ] ;
103+ let mergedDiffs : any [ ] = [ ] ;
104+
105+ if ( mode === "pr" && hasPr ( ) ) {
106+ mergedFiles = prFiles ( ) ;
107+ mergedDiffs = prDiffs ( ) ;
108+ } else if ( mode === "worktree" ) {
109+ mergedFiles = wtFiles ( ) ;
110+ mergedDiffs = wtDiffs ( ) ;
111+ } else {
112+ // "both" — merge PR + worktree, deduplicating by path (worktree wins)
113+ const fileMap = new Map < string , any > ( ) ;
114+ const diffMap = new Map < string , any > ( ) ;
115+ for ( const f of prFiles ( ) ) { fileMap . set ( f . path , { ...f , source : "pr" } ) ; }
116+ for ( const d of prDiffs ( ) ) { diffMap . set ( d . path , d ) ; }
117+ for ( const f of wtFiles ( ) ) { fileMap . set ( f . path , { ...f , source : "worktree" } ) ; }
118+ for ( const d of wtDiffs ( ) ) { diffMap . set ( d . path , d ) ; }
119+ mergedFiles = Array . from ( fileMap . values ( ) ) ;
120+ mergedDiffs = Array . from ( diffMap . values ( ) ) ;
121+ }
122+
123+ setFiles ( mergedFiles ) ;
124+ setDiffs ( mergedDiffs ) ;
125+ if ( mergedFiles . length > 0 && ! selectedFile ( ) ) {
126+ setSelectedFile ( mergedFiles [ 0 ] . path ) ;
127+ }
128+ }
129+
130+ // Re-apply mode when diffMode changes (without re-fetching)
131+ createEffect ( ( ) => {
132+ const _ = diffMode ( ) ;
133+ if ( ! loading ( ) ) applyMode ( ) ;
134+ } ) ;
135+
106136 /** Parse raw unified diff text into ChangedFile[] and FileDiff[] */
107137 function parsePrDiff ( raw : string ) : { files : any [ ] ; diffs : any [ ] } {
108138 const files : any [ ] = [ ] ;
@@ -197,6 +227,25 @@ export function DiffEditor(props: { cwd: string; prNumber?: number | null }) {
197227 </ span >
198228 </ Show >
199229 </ div >
230+ < Show when = { hasPr ( ) } >
231+ < div class = "de-mode-toggle" >
232+ < button
233+ class = "de-mode-btn"
234+ classList = { { "de-mode-btn--active" : diffMode ( ) === "both" } }
235+ onClick = { ( ) => setDiffMode ( "both" ) }
236+ > Both</ button >
237+ < button
238+ class = "de-mode-btn"
239+ classList = { { "de-mode-btn--active" : diffMode ( ) === "pr" } }
240+ onClick = { ( ) => setDiffMode ( "pr" ) }
241+ > PR</ button >
242+ < button
243+ class = "de-mode-btn"
244+ classList = { { "de-mode-btn--active" : diffMode ( ) === "worktree" } }
245+ onClick = { ( ) => setDiffMode ( "worktree" ) }
246+ > Local</ button >
247+ </ div >
248+ </ Show >
200249 < div class = "de-header-actions" >
201250 < button class = "de-icon-btn" onClick = { loadAll } title = "Refresh" >
202251 < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
@@ -389,6 +438,28 @@ const DIFF_STYLES = `
389438 font-weight: 500;
390439 }
391440
441+ .de-mode-toggle {
442+ display: flex;
443+ gap: 1px;
444+ background: var(--bg-muted);
445+ border-radius: var(--radius-sm);
446+ padding: 2px;
447+ margin-left: auto;
448+ }
449+ .de-mode-btn {
450+ font-size: 10px;
451+ font-weight: 600;
452+ padding: 3px 8px;
453+ border-radius: 4px;
454+ color: var(--text-tertiary);
455+ transition: all 0.1s;
456+ }
457+ .de-mode-btn:hover { color: var(--text-secondary); }
458+ .de-mode-btn--active {
459+ background: var(--bg-accent);
460+ color: var(--text);
461+ box-shadow: 0 1px 3px rgba(0,0,0,0.15);
462+ }
392463 .de-header-actions { display: flex; align-items: center; gap: 4px; }
393464 .de-icon-btn {
394465 color: var(--text-tertiary);
0 commit comments