Skip to content

Commit 969d035

Browse files
committed
diff: sync hunk status after stage all
1 parent db84ec2 commit 969d035

File tree

2 files changed

+66
-3
lines changed

2 files changed

+66
-3
lines changed

packages/ui/src/components/layout/DiffOverlay.test.tsx

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
2-
import { render, waitFor } from '@testing-library/react';
2+
import { render, waitFor, act } from '@testing-library/react';
33
import { DiffOverlay } from './DiffOverlay';
44
import { API } from '../../utils/api';
55

@@ -17,6 +17,12 @@ describe('DiffOverlay', () => {
1717
vi.clearAllMocks();
1818
(API.sessions.getDiff as any).mockResolvedValue({ success: true, data: { diff: '' } });
1919
(API.sessions.getFileContent as any).mockResolvedValue({ success: true, data: { content: 'a\nb\nc' } });
20+
21+
(window as any).electronAPI = {
22+
events: {
23+
onGitStatusUpdated: vi.fn(),
24+
},
25+
};
2026
});
2127

2228
it('reloads when filePath changes while open', async () => {
@@ -41,5 +47,38 @@ describe('DiffOverlay', () => {
4147
expect(API.sessions.getFileContent).toHaveBeenCalledWith('s1', expect.objectContaining({ filePath: 'b.txt' }));
4248
});
4349
});
44-
});
4550

51+
it('refreshes when git status updates while open', async () => {
52+
const onGitStatusUpdated = (window as any).electronAPI.events.onGitStatusUpdated as any;
53+
let cb: ((data: any) => void) | null = null;
54+
onGitStatusUpdated.mockImplementation((fn: any) => {
55+
cb = fn;
56+
return () => {};
57+
});
58+
59+
render(
60+
<DiffOverlay
61+
isOpen={true}
62+
sessionId="s1"
63+
filePath="a.txt"
64+
target={{ kind: 'working', scope: 'all' } as any}
65+
onClose={vi.fn()}
66+
files={[]}
67+
/>
68+
);
69+
70+
await waitFor(() => {
71+
expect(onGitStatusUpdated).toHaveBeenCalled();
72+
});
73+
74+
const callsBefore = (API.sessions.getDiff as any).mock.calls.length;
75+
await act(async () => {
76+
cb?.({ sessionId: 's1', gitStatus: { state: 'modified' } });
77+
await new Promise((r) => setTimeout(r, 120));
78+
});
79+
80+
await waitFor(() => {
81+
expect((API.sessions.getDiff as any).mock.calls.length).toBeGreaterThan(callsBefore);
82+
});
83+
});
84+
});

packages/ui/src/components/layout/DiffOverlay.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect, useCallback, useMemo } from 'react';
1+
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
22
import { X, ArrowLeft, RefreshCw, Copy, Check } from 'lucide-react';
33
import { ZedDiffViewer } from '../panels/diff/ZedDiffViewer';
44
import { API } from '../../utils/api';
@@ -47,6 +47,7 @@ export const DiffOverlay: React.FC<DiffOverlayProps> = React.memo(({
4747
const [loading, setLoading] = useState(false);
4848
const [error, setError] = useState<string | null>(null);
4949
const [copied, setCopied] = useState(false);
50+
const overlayRefreshTimerRef = useRef<number | null>(null);
5051

5152
const derivedFiles = useMemo(() => {
5253
if (!diff) return [];
@@ -226,6 +227,29 @@ export const DiffOverlay: React.FC<DiffOverlayProps> = React.memo(({
226227
}
227228
}, [filePath]);
228229

230+
// Keep overlay in sync with staging actions triggered outside the overlay (e.g. Stage/Unstage All in RightPanel).
231+
useEffect(() => {
232+
if (!isOpen || !sessionId || !target) return;
233+
if (target.kind !== 'working' || !filePath) return;
234+
const unsub = window.electronAPI?.events?.onGitStatusUpdated?.((data) => {
235+
if (!data || data.sessionId !== sessionId) return;
236+
if (overlayRefreshTimerRef.current) {
237+
window.clearTimeout(overlayRefreshTimerRef.current);
238+
}
239+
overlayRefreshTimerRef.current = window.setTimeout(() => {
240+
overlayRefreshTimerRef.current = null;
241+
void handleRefresh();
242+
}, 80);
243+
});
244+
return () => {
245+
if (overlayRefreshTimerRef.current) {
246+
window.clearTimeout(overlayRefreshTimerRef.current);
247+
overlayRefreshTimerRef.current = null;
248+
}
249+
if (unsub) unsub();
250+
};
251+
}, [isOpen, sessionId, target, filePath, handleRefresh]);
252+
229253
if (!isOpen) return null;
230254

231255
const workingScope = target?.kind === 'working' ? (target.scope || 'all') : null;

0 commit comments

Comments
 (0)