feat: update git commands and improve diff loading with timeout handling
This commit is contained in:
@@ -293,9 +293,12 @@ export const DiffView: React.FC = () => {
|
||||
const isGitRepo = useIsGitRepo(effectiveDirectory ?? null);
|
||||
const status = useGitStatus(effectiveDirectory ?? null);
|
||||
const isLoadingStatus = useGitStore((state) => state.isLoadingStatus);
|
||||
const { setActiveDirectory, fetchStatus } = useGitStore();
|
||||
|
||||
const { setActiveDirectory, fetchStatus, setDiff } = useGitStore();
|
||||
|
||||
const [selectedFile, setSelectedFile] = React.useState<string | null>(null);
|
||||
const [diffRetryNonce, setDiffRetryNonce] = React.useState(0);
|
||||
const [diffLoadError, setDiffLoadError] = React.useState<string | null>(null);
|
||||
const lastDiffRequestRef = React.useRef<string | null>(null);
|
||||
|
||||
const pendingDiffFile = useUIStore((state) => state.pendingDiffFile);
|
||||
const setPendingDiffFile = useUIStore((state) => state.setPendingDiffFile);
|
||||
@@ -399,6 +402,57 @@ export const DiffView: React.FC = () => {
|
||||
const hasCurrentDiff = !!selectedCachedDiff;
|
||||
const isCurrentFileLoading = !!selectedFile && !hasCurrentDiff;
|
||||
|
||||
React.useEffect(() => {
|
||||
setDiffLoadError(null);
|
||||
|
||||
if (!effectiveDirectory || !selectedFile) {
|
||||
lastDiffRequestRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedCachedDiff) {
|
||||
lastDiffRequestRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const requestKey = `${effectiveDirectory}::${selectedFile}::${diffRetryNonce}`;
|
||||
if (lastDiffRequestRef.current === requestKey) {
|
||||
return;
|
||||
}
|
||||
lastDiffRequestRef.current = requestKey;
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
const fetchPromise = git.getGitFileDiff(effectiveDirectory, { path: selectedFile });
|
||||
const timeoutMs = 15000;
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs);
|
||||
});
|
||||
|
||||
const response = await Promise.race([fetchPromise, timeoutPromise]);
|
||||
if (cancelled) return;
|
||||
|
||||
setDiff(effectiveDirectory, selectedFile, {
|
||||
original: response.original ?? '',
|
||||
modified: response.modified ?? '',
|
||||
});
|
||||
} catch (error) {
|
||||
if (cancelled) return;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setDiffLoadError(message);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (lastDiffRequestRef.current === requestKey) {
|
||||
// Allow a retry if this request was cancelled due to directory/path churn.
|
||||
lastDiffRequestRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [effectiveDirectory, selectedFile, selectedCachedDiff, git, setDiff, diffRetryNonce]);
|
||||
|
||||
// Render all diff viewers - they stay mounted
|
||||
const renderAllDiffViewers = () => {
|
||||
if (!effectiveDirectory || changedFiles.length === 0) return null;
|
||||
@@ -455,8 +509,31 @@ export const DiffView: React.FC = () => {
|
||||
{renderAllDiffViewers()}
|
||||
{isCurrentFileLoading && !hasCurrentDiff && (
|
||||
<div className="absolute inset-0 flex items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<RiLoader4Line size={16} className="animate-spin" />
|
||||
Loading diff…
|
||||
{diffLoadError ? (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="typography-ui-label font-semibold text-foreground">
|
||||
Failed to load diff
|
||||
</div>
|
||||
<div className="typography-meta text-muted-foreground max-w-[32rem] text-center">
|
||||
{diffLoadError}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="typography-ui-label text-primary hover:underline"
|
||||
onClick={() => {
|
||||
setDiffLoadError(null);
|
||||
setDiffRetryNonce((n) => n + 1);
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<RiLoader4Line size={16} className="animate-spin" />
|
||||
Loading diff…
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { useGitStore } from '@/stores/useGitStore';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import { useSessionStore } from '@/stores/useSessionStore';
|
||||
|
||||
/**
|
||||
* Background git polling hook - monitors git status regardless of which tab is open.
|
||||
@@ -9,23 +10,35 @@ import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
*/
|
||||
export function useGitPolling() {
|
||||
const { git } = useRuntimeAPIs();
|
||||
const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
|
||||
const fallbackDirectory = useDirectoryStore((state) => state.currentDirectory);
|
||||
const { currentSessionId, sessions, worktreeMetadata: worktreeMap } = useSessionStore();
|
||||
const { setActiveDirectory, startPolling, stopPolling, fetchAll } = useGitStore();
|
||||
|
||||
const effectiveDirectory = React.useMemo(() => {
|
||||
const worktreeMetadata = currentSessionId
|
||||
? worktreeMap.get(currentSessionId) ?? undefined
|
||||
: undefined;
|
||||
|
||||
const currentSession = sessions.find((session) => session.id === currentSessionId);
|
||||
const sessionDirectory = (currentSession as { directory?: string | null } | undefined)?.directory ?? null;
|
||||
|
||||
return worktreeMetadata?.path ?? sessionDirectory ?? fallbackDirectory ?? null;
|
||||
}, [currentSessionId, sessions, worktreeMap, fallbackDirectory]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentDirectory || !git) {
|
||||
if (!effectiveDirectory || !git) {
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveDirectory(currentDirectory);
|
||||
setActiveDirectory(effectiveDirectory);
|
||||
|
||||
fetchAll(currentDirectory, git);
|
||||
fetchAll(effectiveDirectory, git);
|
||||
|
||||
startPolling(git);
|
||||
|
||||
return () => {
|
||||
stopPolling();
|
||||
};
|
||||
}, [currentDirectory, git, setActiveDirectory, startPolling, stopPolling, fetchAll]);
|
||||
}, [effectiveDirectory, git, setActiveDirectory, startPolling, stopPolling, fetchAll]);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ const GIT_POLL_BASE_INTERVAL = 5000;
|
||||
const GIT_POLL_MAX_INTERVAL = 10000;
|
||||
const GIT_POLL_BACKOFF_STEP = 5000;
|
||||
const LOG_STALE_THRESHOLD = 10000;
|
||||
const DIFF_PREFETCH_MAX_FILES = 25;
|
||||
const DIFF_PREFETCH_CONCURRENCY = 4;
|
||||
const DIFF_PREFETCH_TIMEOUT_MS = 15000;
|
||||
|
||||
interface DirectoryGitState {
|
||||
isGitRepo: boolean | null;
|
||||
@@ -394,7 +397,7 @@ export const useGitStore = create<GitStore>()(
|
||||
await get().fetchIdentity(directory, git);
|
||||
|
||||
// Pre-fetch all diffs so they're ready when user opens Diff tab
|
||||
await get().fetchAllDiffs(directory, git);
|
||||
void get().fetchAllDiffs(directory, git);
|
||||
},
|
||||
|
||||
getDiff: (directory, filePath) => {
|
||||
@@ -429,18 +432,44 @@ export const useGitStore = create<GitStore>()(
|
||||
// Find files that need fetching (no cache)
|
||||
const filesToFetch = files.filter((file) => !dirState.diffCache.has(file.path));
|
||||
|
||||
if (filesToFetch.length === 0) return;
|
||||
const limitedFilesToFetch = filesToFetch.slice(0, DIFF_PREFETCH_MAX_FILES);
|
||||
if (limitedFilesToFetch.length === 0) return;
|
||||
|
||||
// Fetch all diffs in parallel
|
||||
const results = await Promise.allSettled(
|
||||
filesToFetch.map(async (file) => {
|
||||
const response = await git.getGitFileDiff(directory, { path: file.path });
|
||||
return {
|
||||
path: file.path,
|
||||
diff: { original: response.original ?? '', modified: response.modified ?? '' }
|
||||
};
|
||||
})
|
||||
);
|
||||
let nextIndex = 0;
|
||||
const results: Array<{ path: string; diff: { original: string; modified: string } }> = [];
|
||||
|
||||
const takeNext = () => {
|
||||
const current = nextIndex;
|
||||
nextIndex += 1;
|
||||
return current < limitedFilesToFetch.length ? limitedFilesToFetch[current] : null;
|
||||
};
|
||||
|
||||
const fetchWithTimeout = async (filePath: string) => {
|
||||
const fetchPromise = git.getGitFileDiff(directory, { path: filePath });
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error(`Timed out after ${DIFF_PREFETCH_TIMEOUT_MS}ms`)), DIFF_PREFETCH_TIMEOUT_MS);
|
||||
});
|
||||
const response = await Promise.race([fetchPromise, timeoutPromise]);
|
||||
return {
|
||||
path: filePath,
|
||||
diff: { original: response.original ?? '', modified: response.modified ?? '' },
|
||||
};
|
||||
};
|
||||
|
||||
const worker = async () => {
|
||||
for (;;) {
|
||||
const next = takeNext();
|
||||
if (!next) return;
|
||||
try {
|
||||
results.push(await fetchWithTimeout(next.path));
|
||||
} catch {
|
||||
// Ignore individual failures/timeouts during prefetch.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const workerCount = Math.min(DIFF_PREFETCH_CONCURRENCY, limitedFilesToFetch.length);
|
||||
await Promise.allSettled(Array.from({ length: workerCount }, () => worker()));
|
||||
|
||||
// Update diff cache with results
|
||||
const newDirectories = new Map(get().directories);
|
||||
@@ -451,12 +480,10 @@ export const useGitStore = create<GitStore>()(
|
||||
const now = Date.now();
|
||||
|
||||
results.forEach((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
newDiffCache.set(result.value.path, {
|
||||
...result.value.diff,
|
||||
fetchedAt: now
|
||||
});
|
||||
}
|
||||
newDiffCache.set(result.path, {
|
||||
...result.diff,
|
||||
fetchedAt: now
|
||||
});
|
||||
});
|
||||
|
||||
newDirectories.set(directory, { ...currentDirState, diffCache: newDiffCache });
|
||||
@@ -493,7 +520,7 @@ export const useGitStore = create<GitStore>()(
|
||||
if (statusChanged) {
|
||||
await get().fetchLog(activeDirectory, git);
|
||||
// Pre-fetch all diffs so they're ready when user opens Diff tab
|
||||
await get().fetchAllDiffs(activeDirectory, git);
|
||||
void get().fetchAllDiffs(activeDirectory, git);
|
||||
// Reset to base interval on changes
|
||||
set({ currentPollInterval: GIT_POLL_BASE_INTERVAL });
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user