Skip to content

Commit 99e1f10

Browse files
authored
fix: 🤖 Trigger refresh after Track All to update UI state (#321)
Follow-up to PR #318 (untracked files feature). Includes two improvements: ## 1. Trigger refresh after Track All After staging untracked files via Track All button, the UI should automatically refresh to reflect the changes (updated hunks, tree, and untracked count). **Implementation:** - Pass `onRefresh` callback from ReviewPanel through ReviewControls to UntrackedStatus - Call `onRefresh()` after successful `git add` operation in Track All handler - This refresh only happens from user action (clicking Track All), preventing infinite loops ## 2. Rename 'Dirty' to 'Uncommitted' for clarity The checkbox was labeled "Dirty" but actually includes ALL uncommitted changes (staged + unstaged) via `git diff HEAD`. "Uncommitted" is more accurate and less ambiguous. **Breaking change:** localStorage keys changed from `review-include-dirty` to `review-include-uncommitted`. Users will lose this preference per workspace (acceptable for early development phase). ## Testing 1. Open Code Review panel 2. Create an untracked file 3. Click the untracked badge to open tooltip 4. Click "Track All" button 5. ✅ UI automatically refreshes showing the files are now staged 6. ✅ Untracked count updates to 0 7. ✅ Hunks and tree reflect the new staged changes 8. ✅ Checkbox now labeled "Uncommitted" instead of "Dirty" _Generated with `cmux`_
1 parent 979de51 commit 99e1f10

File tree

4 files changed

+38
-22
lines changed

4 files changed

+38
-22
lines changed

src/components/RightSidebar/CodeReview/ReviewControls.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,8 @@ export const ReviewControls: React.FC<ReviewControlsProps> = ({
162162
}
163163
};
164164

165-
const handleDirtyToggle = (e: React.ChangeEvent<HTMLInputElement>) => {
166-
onFiltersChange({ ...filters, includeDirty: e.target.checked });
165+
const handleUncommittedToggle = (e: React.ChangeEvent<HTMLInputElement>) => {
166+
onFiltersChange({ ...filters, includeUncommitted: e.target.checked });
167167
};
168168

169169
const handleSetDefault = () => {
@@ -202,14 +202,19 @@ export const ReviewControls: React.FC<ReviewControlsProps> = ({
202202
)}
203203

204204
<CheckboxLabel>
205-
<input type="checkbox" checked={filters.includeDirty} onChange={handleDirtyToggle} />
206-
Dirty
205+
<input
206+
type="checkbox"
207+
checked={filters.includeUncommitted}
208+
onChange={handleUncommittedToggle}
209+
/>
210+
Uncommitted
207211
</CheckboxLabel>
208212

209213
<UntrackedStatus
210214
workspaceId={workspaceId}
211215
workspacePath={workspacePath}
212216
refreshTrigger={refreshTrigger}
217+
onRefresh={onRefresh}
213218
/>
214219

215220
<Separator />

src/components/RightSidebar/CodeReview/ReviewPanel.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,12 @@ interface DiagnosticInfo {
236236
}
237237

238238
/**
239-
* Build git diff command based on diffBase and includeDirty flag
239+
* Build git diff command based on diffBase and includeUncommitted flag
240240
* Shared logic between numstat (file tree) and diff (hunks) commands
241241
*/
242242
function buildGitDiffCommand(
243243
diffBase: string,
244-
includeDirty: boolean,
244+
includeUncommitted: boolean,
245245
pathFilter: string,
246246
command: "diff" | "numstat"
247247
): string {
@@ -258,8 +258,8 @@ function buildGitDiffCommand(
258258
cmd = `git diff ${diffBase}...HEAD${flag}${pathFilter}`;
259259
}
260260

261-
// Append dirty changes if requested
262-
if (includeDirty) {
261+
// Append uncommitted changes if requested
262+
if (includeUncommitted) {
263263
cmd += ` && git diff HEAD${flag}${pathFilter}`;
264264
}
265265

@@ -295,17 +295,17 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
295295
// Persist diff base per workspace (falls back to global default)
296296
const [diffBase, setDiffBase] = usePersistedState(`review-diff-base:${workspaceId}`, defaultBase);
297297

298-
// Persist includeDirty flag per workspace
299-
const [includeDirty, setIncludeDirty] = usePersistedState(
300-
`review-include-dirty:${workspaceId}`,
298+
// Persist includeUncommitted flag per workspace
299+
const [includeUncommitted, setIncludeUncommitted] = usePersistedState(
300+
`review-include-uncommitted:${workspaceId}`,
301301
false
302302
);
303303

304304
const [filters, setFilters] = useState<ReviewFiltersType>({
305305
showReviewed: true,
306306
statusFilter: "all",
307307
diffBase: diffBase,
308-
includeDirty: includeDirty,
308+
includeUncommitted: includeUncommitted,
309309
});
310310

311311
// Load file tree - when workspace, diffBase, or refreshTrigger changes
@@ -317,7 +317,7 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
317317
try {
318318
const numstatCommand = buildGitDiffCommand(
319319
filters.diffBase,
320-
filters.includeDirty,
320+
filters.includeUncommitted,
321321
"", // No path filter for file tree
322322
"numstat"
323323
);
@@ -352,7 +352,7 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
352352
return () => {
353353
cancelled = true;
354354
};
355-
}, [workspaceId, workspacePath, filters.diffBase, filters.includeDirty, refreshTrigger]);
355+
}, [workspaceId, workspacePath, filters.diffBase, filters.includeUncommitted, refreshTrigger]);
356356

357357
// Load diff hunks - when workspace, diffBase, selected path, or refreshTrigger changes
358358
useEffect(() => {
@@ -369,7 +369,7 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
369369

370370
const diffCommand = buildGitDiffCommand(
371371
filters.diffBase,
372-
filters.includeDirty,
372+
filters.includeUncommitted,
373373
pathFilter,
374374
"diff"
375375
);
@@ -435,7 +435,7 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
435435
workspaceId,
436436
workspacePath,
437437
filters.diffBase,
438-
filters.includeDirty,
438+
filters.includeUncommitted,
439439
selectedFilePath,
440440
refreshTrigger,
441441
]);
@@ -445,10 +445,10 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
445445
setDiffBase(filters.diffBase);
446446
}, [filters.diffBase, setDiffBase]);
447447

448-
// Persist includeDirty when it changes
448+
// Persist includeUncommitted when it changes
449449
useEffect(() => {
450-
setIncludeDirty(filters.includeDirty);
451-
}, [filters.includeDirty, setIncludeDirty]);
450+
setIncludeUncommitted(filters.includeUncommitted);
451+
}, [filters.includeUncommitted, setIncludeUncommitted]);
452452

453453
// For MVP: No review state tracking, just show all hunks
454454
const filteredHunks = hunks;

src/components/RightSidebar/CodeReview/UntrackedStatus.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface UntrackedStatusProps {
1010
workspaceId: string;
1111
workspacePath: string;
1212
refreshTrigger?: number;
13+
onRefresh?: () => void;
1314
}
1415

1516
const Container = styled.div`
@@ -128,19 +129,25 @@ export const UntrackedStatus: React.FC<UntrackedStatusProps> = ({
128129
workspaceId,
129130
workspacePath,
130131
refreshTrigger,
132+
onRefresh,
131133
}) => {
132134
const [untrackedFiles, setUntrackedFiles] = useState<string[]>([]);
133135
const [isLoading, setIsLoading] = useState(false);
134136
const [showTooltip, setShowTooltip] = useState(false);
135137
const [isTracking, setIsTracking] = useState(false);
136138
const containerRef = useRef<HTMLDivElement>(null);
137139
const hasLoadedOnce = useRef(false);
140+
const loadingRef = useRef(false); // Prevent concurrent loads
138141

139142
// Load untracked files
140143
useEffect(() => {
141144
let cancelled = false;
142145

143146
const loadUntracked = async () => {
147+
// Prevent concurrent loads
148+
if (loadingRef.current) return;
149+
loadingRef.current = true;
150+
144151
// Only show loading on first load ever, not on subsequent refreshes
145152
if (!hasLoadedOnce.current) {
146153
setIsLoading(true);
@@ -167,6 +174,7 @@ export const UntrackedStatus: React.FC<UntrackedStatusProps> = ({
167174
} catch (err) {
168175
console.error("Failed to load untracked files:", err);
169176
} finally {
177+
loadingRef.current = false;
170178
setIsLoading(false);
171179
}
172180
};
@@ -208,8 +216,11 @@ export const UntrackedStatus: React.FC<UntrackedStatusProps> = ({
208216
);
209217

210218
if (result.success) {
211-
setUntrackedFiles([]);
219+
// Close tooltip first
212220
setShowTooltip(false);
221+
// Trigger refresh - this will reload untracked files from git
222+
// Don't clear untrackedFiles optimistically to avoid flicker
223+
onRefresh?.();
213224
} else {
214225
console.error("Failed to track files:", result.error);
215226
}

src/types/review.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ export interface ReviewFilters {
8282
filePathFilter?: string;
8383
/** Base reference to diff against (e.g., "HEAD", "main", "origin/main") */
8484
diffBase: string;
85-
/** Whether to include uncommitted dirty changes in the diff */
86-
includeDirty: boolean;
85+
/** Whether to include uncommitted changes (staged + unstaged) in the diff */
86+
includeUncommitted: boolean;
8787
}
8888

8989
/**

0 commit comments

Comments
 (0)