@@ -15,30 +15,18 @@ export namespace TreeUtils {
1515 const checkedNodes : FileChangeNode [ ] = [ ] ;
1616 const uncheckedNodes : FileChangeNode [ ] = [ ] ;
1717
18- // The first item is the one the user actually clicked.
19- // Only collect missing descendants if a directory was clicked directly.
20- const firstNode = checkboxUpdates . items [ 0 ] ?. [ 0 ] ;
21-
22- const eventNodes = new Set < TreeNode > ( checkboxUpdates . items . map ( ( [ node ] ) => node ) ) ;
23-
24- checkboxUpdates . items . forEach ( checkboxUpdate => {
25- const node = checkboxUpdate [ 0 ] ;
26- const newState = checkboxUpdate [ 1 ] ;
27-
18+ for ( const [ node , newState ] of checkboxUpdates . items ) {
2819 if ( node instanceof FileChangeNode ) {
2920 if ( newState === vscode . TreeItemCheckboxState . Checked ) {
3021 checkedNodes . push ( node ) ;
3122 } else {
3223 uncheckedNodes . push ( node ) ;
3324 }
34- } else if ( firstNode instanceof DirectoryTreeNode && node === firstNode ) {
35- // VS Code auto-propagates to rendered children, but unrendered children
36- // (due to virtual scrolling) won't be in the event. Collect those missing ones.
37- collectMissingDescendants ( firstNode , newState , checkedNodes , uncheckedNodes , eventNodes ) ;
25+ node . updateFromCheckboxChanged ( newState ) ;
26+ } else if ( node instanceof DirectoryTreeNode ) {
27+ collectAllDescendants ( node , newState , checkedNodes , uncheckedNodes ) ;
3828 }
39-
40- node . updateFromCheckboxChanged ( newState ) ;
41- } ) ;
29+ }
4230
4331 if ( selectionContainsUpdates ) {
4432 for ( const selected of selection ) {
@@ -48,41 +36,72 @@ export namespace TreeUtils {
4836 if ( ! checkedNodes . includes ( selected ) && ! uncheckedNodes . includes ( selected ) ) {
4937 // Only process files that have checkboxes (files without checkboxState, like those under commits, are skipped)
5038 if ( selected . checkboxState ?. state === vscode . TreeItemCheckboxState . Unchecked ) {
39+ selected . updateFromCheckboxChanged ( vscode . TreeItemCheckboxState . Checked ) ;
5140 checkedNodes . push ( selected ) ;
5241 } else if ( selected . checkboxState ?. state === vscode . TreeItemCheckboxState . Checked ) {
42+ selected . updateFromCheckboxChanged ( vscode . TreeItemCheckboxState . Unchecked ) ;
5343 uncheckedNodes . push ( selected ) ;
5444 }
5545 }
5646 }
5747 }
5848
49+ // Refresh the tree so checkbox visual state updates.
50+ // Refreshing the topmost affected directory will cascade to all descendants.
51+ const allAffected = [ ...checkedNodes , ...uncheckedNodes ] ;
52+ const refreshedDirs = new Set < DirectoryTreeNode > ( ) ;
53+ for ( const node of allAffected ) {
54+ let topDir : DirectoryTreeNode | undefined ;
55+ let parent = node . getParent ( ) ;
56+ while ( parent instanceof DirectoryTreeNode ) {
57+ topDir = parent ;
58+ parent = parent . getParent ( ) ;
59+ }
60+ if ( topDir && ! refreshedDirs . has ( topDir ) ) {
61+ refreshedDirs . add ( topDir ) ;
62+ topDir . refresh ( topDir ) ;
63+ }
64+ }
65+ // If a directory was clicked directly, also refresh it
66+ for ( const [ node ] of checkboxUpdates . items ) {
67+ if ( node instanceof DirectoryTreeNode && ! refreshedDirs . has ( node ) ) {
68+ refreshedDirs . add ( node ) ;
69+ node . refresh ( node ) ;
70+ }
71+ }
72+ // For flat layout (files have no directory parent), refresh file nodes directly
73+ for ( const node of allAffected ) {
74+ const parent = node . getParent ( ) ;
75+ if ( ! ( parent instanceof DirectoryTreeNode ) ) {
76+ node . refresh ( node ) ;
77+ }
78+ }
79+
80+ // Send API requests without firing state change events (UI is already updated optimistically).
81+ // This prevents race conditions where overlapping markFiles calls cause checkboxes to flicker.
5982 if ( checkedNodes . length > 0 ) {
6083 const prModel = checkedNodes [ 0 ] . pullRequest ;
6184 const filenames = checkedNodes . map ( n => n . fileName ) ;
62- prModel . markFiles ( filenames , true , 'viewed' ) ;
85+ prModel . markFiles ( filenames , false , 'viewed' ) . then ( ( ) => {
86+ checkedNodes [ 0 ] . refreshFileViewedContext ( ) ;
87+ } ) ;
6388 }
6489 if ( uncheckedNodes . length > 0 ) {
6590 const prModel = uncheckedNodes [ 0 ] . pullRequest ;
6691 const filenames = uncheckedNodes . map ( n => n . fileName ) ;
67- prModel . markFiles ( filenames , true , 'unviewed' ) ;
92+ prModel . markFiles ( filenames , false , 'unviewed' ) . then ( ( ) => {
93+ uncheckedNodes [ 0 ] . refreshFileViewedContext ( ) ;
94+ } ) ;
6895 }
6996 }
7097
71- /**
72- * Collect descendant FileChangeNodes that are NOT already in the event.
73- * These are children VS Code missed because they weren't rendered (virtual scrolling).
74- */
75- function collectMissingDescendants (
98+ function collectAllDescendants (
7699 dirNode : DirectoryTreeNode ,
77100 newState : vscode . TreeItemCheckboxState ,
78101 checkedNodes : FileChangeNode [ ] ,
79- uncheckedNodes : FileChangeNode [ ] ,
80- eventNodes : Set < TreeNode >
102+ uncheckedNodes : FileChangeNode [ ]
81103 ) : void {
82104 for ( const child of dirNode . _children ) {
83- if ( eventNodes . has ( child ) ) {
84- continue ;
85- }
86105 if ( child instanceof FileChangeNode ) {
87106 if ( newState === vscode . TreeItemCheckboxState . Checked ) {
88107 checkedNodes . push ( child ) ;
@@ -91,7 +110,7 @@ export namespace TreeUtils {
91110 }
92111 child . updateFromCheckboxChanged ( newState ) ;
93112 } else if ( child instanceof DirectoryTreeNode ) {
94- collectMissingDescendants ( child , newState , checkedNodes , uncheckedNodes , eventNodes ) ;
113+ collectAllDescendants ( child , newState , checkedNodes , uncheckedNodes ) ;
95114 }
96115 }
97116 }
0 commit comments