|
1 | | -import { useCallback, useEffect, useState } from 'react'; |
| 1 | +import { useCallback, useEffect, useRef, useState } from 'react'; |
2 | 2 | import { useCodeViewerStore } from '@/stores/codeViewerStore'; |
3 | 3 | import { fetchGitStatus, fetchGitDiff } from '@/services/api'; |
4 | 4 | import styles from './GitChanges.module.css'; |
@@ -43,26 +43,72 @@ export default function GitChanges({ sessionId }: GitChangesProps) { |
43 | 43 | const { gitStatus, setGitStatus, setGitDiff, setDiffFile, diffFile } = useCodeViewerStore(); |
44 | 44 | const [loading, setLoading] = useState(false); |
45 | 45 | const [error, setError] = useState<string | null>(null); |
| 46 | + const inFlightRef = useRef(false); |
46 | 47 |
|
47 | | - const loadStatus = useCallback(async () => { |
48 | | - setLoading(true); |
49 | | - setError(null); |
50 | | - try { |
51 | | - const status = await fetchGitStatus(sessionId); |
52 | | - setGitStatus(status); |
53 | | - } catch (err) { |
54 | | - setError(err instanceof Error ? err.message : 'Failed to load git status'); |
55 | | - } finally { |
56 | | - setLoading(false); |
57 | | - } |
58 | | - }, [sessionId, setGitStatus]); |
| 48 | + const loadStatus = useCallback( |
| 49 | + async (showSpinner = true) => { |
| 50 | + if (inFlightRef.current) return; |
| 51 | + inFlightRef.current = true; |
| 52 | + if (showSpinner) setLoading(true); |
| 53 | + try { |
| 54 | + const status = await fetchGitStatus(sessionId); |
| 55 | + setGitStatus(status); |
| 56 | + setError(null); |
| 57 | + } catch (err) { |
| 58 | + setError(err instanceof Error ? err.message : 'Failed to load git status'); |
| 59 | + } finally { |
| 60 | + if (showSpinner) setLoading(false); |
| 61 | + inFlightRef.current = false; |
| 62 | + } |
| 63 | + }, |
| 64 | + [sessionId, setGitStatus], |
| 65 | + ); |
59 | 66 |
|
60 | 67 | useEffect(() => { |
61 | 68 | if (!gitStatus) { |
62 | 69 | loadStatus(); |
63 | 70 | } |
64 | 71 | }, [gitStatus, loadStatus]); |
65 | 72 |
|
| 73 | + useEffect(() => { |
| 74 | + const POLL_MS = 3000; |
| 75 | + let timer: ReturnType<typeof setInterval> | null = null; |
| 76 | + |
| 77 | + const start = () => { |
| 78 | + if (timer) return; |
| 79 | + timer = setInterval(() => { |
| 80 | + if (document.visibilityState === 'visible') { |
| 81 | + loadStatus(false); |
| 82 | + } |
| 83 | + }, POLL_MS); |
| 84 | + }; |
| 85 | + const stop = () => { |
| 86 | + if (timer) { |
| 87 | + clearInterval(timer); |
| 88 | + timer = null; |
| 89 | + } |
| 90 | + }; |
| 91 | + const onVisibility = () => { |
| 92 | + if (document.visibilityState === 'visible') { |
| 93 | + loadStatus(false); |
| 94 | + start(); |
| 95 | + } else { |
| 96 | + stop(); |
| 97 | + } |
| 98 | + }; |
| 99 | + const onFocus = () => loadStatus(false); |
| 100 | + |
| 101 | + if (document.visibilityState === 'visible') start(); |
| 102 | + document.addEventListener('visibilitychange', onVisibility); |
| 103 | + window.addEventListener('focus', onFocus); |
| 104 | + |
| 105 | + return () => { |
| 106 | + stop(); |
| 107 | + document.removeEventListener('visibilitychange', onVisibility); |
| 108 | + window.removeEventListener('focus', onFocus); |
| 109 | + }; |
| 110 | + }, [loadStatus]); |
| 111 | + |
66 | 112 | const handleFileClick = useCallback( |
67 | 113 | async (path: string, staged: boolean, untracked: boolean) => { |
68 | 114 | setDiffFile(path); |
@@ -181,7 +227,7 @@ export default function GitChanges({ sessionId }: GitChangesProps) { |
181 | 227 | </span> |
182 | 228 | <button |
183 | 229 | className={styles.refreshBtn} |
184 | | - onClick={loadStatus} |
| 230 | + onClick={() => loadStatus(true)} |
185 | 231 | disabled={loading} |
186 | 232 | title="Refresh git status" |
187 | 233 | aria-label="Refresh git status" |
|
0 commit comments