Skip to content

Commit 215dee5

Browse files
committed
Refresh diff overlay on staging timeline events
1 parent 426280e commit 215dee5

File tree

2 files changed

+62
-0
lines changed

2 files changed

+62
-0
lines changed

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ describe('DiffOverlay', () => {
2121
(window as any).electronAPI = {
2222
events: {
2323
onGitStatusUpdated: vi.fn(),
24+
onTimelineEvent: vi.fn(),
2425
},
2526
};
2627
});
@@ -81,4 +82,38 @@ describe('DiffOverlay', () => {
8182
expect((API.sessions.getDiff as any).mock.calls.length).toBeGreaterThan(callsBefore);
8283
});
8384
});
85+
86+
it('refreshes when git staging timeline events arrive while open', async () => {
87+
const onTimelineEvent = (window as any).electronAPI.events.onTimelineEvent as any;
88+
let cb: ((data: any) => void) | null = null;
89+
onTimelineEvent.mockImplementation((fn: any) => {
90+
cb = fn;
91+
return () => {};
92+
});
93+
94+
render(
95+
<DiffOverlay
96+
isOpen={true}
97+
sessionId="s1"
98+
filePath="a.txt"
99+
target={{ kind: 'working', scope: 'all' } as any}
100+
onClose={vi.fn()}
101+
files={[]}
102+
/>
103+
);
104+
105+
await waitFor(() => {
106+
expect(onTimelineEvent).toHaveBeenCalled();
107+
});
108+
109+
const callsBefore = (API.sessions.getDiff as any).mock.calls.length;
110+
await act(async () => {
111+
cb?.({ sessionId: 's1', event: { kind: 'git.command', status: 'finished', meta: { source: 'gitStaging' } } });
112+
await new Promise((r) => setTimeout(r, 120));
113+
});
114+
115+
await waitFor(() => {
116+
expect((API.sessions.getDiff as any).mock.calls.length).toBeGreaterThan(callsBefore);
117+
});
118+
});
84119
});

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,33 @@ export const DiffOverlay: React.FC<DiffOverlayProps> = React.memo(({
250250
};
251251
}, [isOpen, sessionId, target, filePath, handleRefresh]);
252252

253+
// Fallback: staging operations always record a timeline event, while status updates can be throttled/skipped.
254+
// This keeps the overlay in sync when users stage/unstage via the RightPanel checkboxes.
255+
useEffect(() => {
256+
if (!isOpen || !sessionId || !target) return;
257+
if (target.kind !== 'working' || !filePath) return;
258+
const unsub = window.electronAPI?.events?.onTimelineEvent?.((data) => {
259+
if (!data || data.sessionId !== sessionId) return;
260+
const e = data.event as { kind?: unknown; status?: unknown; meta?: unknown } | undefined;
261+
if (!e || e.kind !== 'git.command') return;
262+
if (e.status !== 'finished' && e.status !== 'failed') return;
263+
const meta = (e.meta || {}) as Record<string, unknown>;
264+
const source = typeof meta.source === 'string' ? meta.source : '';
265+
if (source !== 'gitStaging') return;
266+
267+
if (overlayRefreshTimerRef.current) {
268+
window.clearTimeout(overlayRefreshTimerRef.current);
269+
}
270+
overlayRefreshTimerRef.current = window.setTimeout(() => {
271+
overlayRefreshTimerRef.current = null;
272+
void handleRefresh();
273+
}, 80);
274+
});
275+
return () => {
276+
if (unsub) unsub();
277+
};
278+
}, [isOpen, sessionId, target, filePath, handleRefresh]);
279+
253280
if (!isOpen) return null;
254281

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

0 commit comments

Comments
 (0)