@@ -1016,7 +1016,7 @@ const ReviewApp: React.FC = () => {
10161016 // Shared helper: fetch a diff switch and update state.
10171017 // Returns true on success, false on failure — callers that optimistically
10181018 // updated UI state (e.g. the base picker) can use this to revert.
1019- const fetchDiffSwitch = useCallback ( async ( fullDiffType : string , baseOverride ?: string ) : Promise < boolean > => {
1019+ const fetchDiffSwitch = useCallback ( async ( fullDiffType : string , baseOverride ?: string , options ?: { preserveFile ?: boolean } ) : Promise < boolean > => {
10201020 setIsLoadingDiff ( true ) ;
10211021 try {
10221022 const res = await fetch ( '/api/diff/switch' , {
@@ -1027,6 +1027,7 @@ const ReviewApp: React.FC = () => {
10271027 // Server ignores base for modes that don't use it (uncommitted/staged/etc),
10281028 // so forwarding unconditionally is safe and keeps the request shape uniform.
10291029 ...( ( baseOverride ?? selectedBase ) && { base : baseOverride ?? selectedBase } ) ,
1030+ hideWhitespace : diffHideWhitespace ,
10301031 } ) ,
10311032 } ) ;
10321033
@@ -1042,46 +1043,59 @@ const ReviewApp: React.FC = () => {
10421043 } ;
10431044
10441045 const nextFiles = parseDiffToFiles ( data . rawPatch ) ;
1045- dockApi ?. getPanel ( REVIEW_DIFF_PANEL_ID ) ?. api . close ( ) ;
1046- needsInitialDiffPanel . current = true ;
1047- setDiffData ( prev => prev ? { ...prev , rawPatch : data . rawPatch , gitRef : data . gitRef , diffType : data . diffType } : prev ) ;
1048- setFiles ( nextFiles ) ;
1049- setDiffType ( data . diffType ) ;
1050- // Adopt the server's echoed base. The server trusts whatever we sent
1051- // verbatim — this sync just makes sure selectedBase and committedBase
1052- // match the server's view (important when the caller didn't send a
1053- // base and the server used the detected default instead).
1054- if ( data . base ) {
1055- setSelectedBase ( data . base ) ;
1056- setCommittedBase ( data . base ) ;
1057- }
1058- // Merge only the per-cwd fields so the sidebar reflects the worktree
1059- // we're now in. Keep the original `worktrees` list (already filtered to
1060- // exclude the server's startup cwd — replacing it with the new context's
1061- // list would duplicate the "Main repo" entry) and `availableBranches`
1062- // (shared across worktrees of the same repo).
1063- //
1064- // IMPORTANT: we deliberately do NOT overwrite `currentBranch`. The
1065- // WorktreePicker's top "launch" row uses it as a label, and that row
1066- // represents the cwd plannotator was launched in — not whichever
1067- // worktree is currently active. Freezing `currentBranch` at its
1068- // initial-load value keeps that label truthful. `defaultBranch` and
1069- // `diffOptions` update because they describe the active diff, which
1070- // other UI (empty-state text, diff-type picker) should see fresh.
1071- if ( data . gitContext ) {
1072- setGitContext ( ( prev ) => {
1073- if ( ! prev ) return data . gitContext ! ;
1074- return {
1075- ...prev ,
1076- defaultBranch : data . gitContext ! . defaultBranch ,
1077- diffOptions : data . gitContext ! . diffOptions ,
1078- } ;
1079- } ) ;
1046+
1047+ if ( options ?. preserveFile ) {
1048+ // Whitespace toggle: update patch in-place, keep the active file.
1049+ // If the current file was removed (whitespace-only), retarget the
1050+ // dock panel to the first remaining file.
1051+ setDiffData ( prev => prev ? { ...prev , rawPatch : data . rawPatch , gitRef : data . gitRef } : prev ) ;
1052+ setFiles ( nextFiles ) ;
1053+ const currentPath = files [ activeFileIndex ] ?. path ;
1054+ const nextIdx = currentPath ? nextFiles . findIndex ( f => f . path === currentPath ) : - 1 ;
1055+ if ( nextIdx !== - 1 ) {
1056+ setActiveFileIndex ( nextIdx ) ;
1057+ } else if ( nextFiles . length > 0 ) {
1058+ setActiveFileIndex ( 0 ) ;
1059+ openDiffFile ( nextFiles [ 0 ] . path ) ;
1060+ }
1061+ } else {
1062+ dockApi ?. getPanel ( REVIEW_DIFF_PANEL_ID ) ?. api . close ( ) ;
1063+ needsInitialDiffPanel . current = true ;
1064+ setDiffData ( prev => prev ? { ...prev , rawPatch : data . rawPatch , gitRef : data . gitRef , diffType : data . diffType } : prev ) ;
1065+ setFiles ( nextFiles ) ;
1066+ setDiffType ( data . diffType ) ;
1067+ if ( data . base ) {
1068+ setSelectedBase ( data . base ) ;
1069+ setCommittedBase ( data . base ) ;
1070+ }
1071+ // Merge only the per-cwd fields so the sidebar reflects the worktree
1072+ // we're now in. Keep the original `worktrees` list (already filtered to
1073+ // exclude the server's startup cwd — replacing it with the new context's
1074+ // list would duplicate the "Main repo" entry) and `availableBranches`
1075+ // (shared across worktrees of the same repo).
1076+ //
1077+ // IMPORTANT: we deliberately do NOT overwrite `currentBranch`. The
1078+ // WorktreePicker's top "launch" row uses it as a label, and that row
1079+ // represents the cwd plannotator was launched in — not whichever
1080+ // worktree is currently active. Freezing `currentBranch` at its
1081+ // initial-load value keeps that label truthful. `defaultBranch` and
1082+ // `diffOptions` update because they describe the active diff, which
1083+ // other UI (empty-state text, diff-type picker) should see fresh.
1084+ if ( data . gitContext ) {
1085+ setGitContext ( ( prev ) => {
1086+ if ( ! prev ) return data . gitContext ! ;
1087+ return {
1088+ ...prev ,
1089+ defaultBranch : data . gitContext ! . defaultBranch ,
1090+ diffOptions : data . gitContext ! . diffOptions ,
1091+ } ;
1092+ } ) ;
1093+ }
1094+ setActiveFileIndex ( 0 ) ;
1095+ setPendingSelection ( null ) ;
1096+ resetStagedFiles ( ) ;
10801097 }
1081- setActiveFileIndex ( 0 ) ;
1082- setPendingSelection ( null ) ;
10831098 setDiffError ( data . error || null ) ;
1084- resetStagedFiles ( ) ;
10851099 return true ;
10861100 } catch ( err ) {
10871101 console . error ( 'Failed to switch diff:' , err ) ;
@@ -1090,7 +1104,7 @@ const ReviewApp: React.FC = () => {
10901104 } finally {
10911105 setIsLoadingDiff ( false ) ;
10921106 }
1093- } , [ dockApi , resetStagedFiles , selectedBase ] ) ;
1107+ } , [ dockApi , resetStagedFiles , selectedBase , diffHideWhitespace , files , activeFileIndex , openDiffFile ] ) ;
10941108
10951109 // Switch the base branch the current diff compares against.
10961110 // Only triggers a refetch when the active mode actually uses a base.
@@ -1130,6 +1144,18 @@ const ReviewApp: React.FC = () => {
11301144 await fetchDiffSwitch ( fullDiffType ) ;
11311145 } , [ activeWorktreePath , activeDiffBase , fetchDiffSwitch ] ) ;
11321146
1147+ // Re-fetch diff when hideWhitespace toggles so the server applies git diff -w.
1148+ // Preserves the active file since only whitespace hunks change.
1149+ const hideWhitespaceInitialized = useRef ( false ) ;
1150+ useEffect ( ( ) => {
1151+ if ( ! origin || ! gitContext ) return ;
1152+ if ( ! hideWhitespaceInitialized . current ) {
1153+ hideWhitespaceInitialized . current = true ;
1154+ return ;
1155+ }
1156+ fetchDiffSwitch ( diffType , selectedBase , { preserveFile : true } ) ;
1157+ } , [ diffHideWhitespace , origin ] ) ; // eslint-disable-line react-hooks/exhaustive-deps
1158+
11331159 // Select annotation - switches file if needed and scrolls to it
11341160 const handleSelectAnnotation = useCallback ( ( id : string | null ) => {
11351161 if ( ! id ) {
@@ -1192,7 +1218,6 @@ const ReviewApp: React.FC = () => {
11921218 lineDiffType : diffLineDiffType ,
11931219 disableLineNumbers : ! diffShowLineNumbers ,
11941220 disableBackground : ! diffShowBackground ,
1195- hideWhitespace : diffHideWhitespace ,
11961221 fontFamily : diffFontFamily || undefined ,
11971222 fontSize : diffFontSize || undefined ,
11981223 // Only propagate base for modes where it affects old/new content. Avoids
@@ -1249,7 +1274,7 @@ const ReviewApp: React.FC = () => {
12491274 openTourPanel : handleOpenTour ,
12501275 } ) , [
12511276 files , activeFileIndex , diffStyle , diffOverflow , diffIndicators ,
1252- diffLineDiffType , diffShowLineNumbers , diffShowBackground , diffHideWhitespace ,
1277+ diffLineDiffType , diffShowLineNumbers , diffShowBackground ,
12531278 diffFontFamily , diffFontSize , activeDiffBase , committedBase , feedbackDiffContext , prReviewScopeLabel , prDiffScope ,
12541279 allAnnotations , externalAnnotations ,
12551280 selectedAnnotationId , pendingSelection , handleLineSelection ,
0 commit comments