fix(ui): hold the changed-files gate until post-bootstrap status lands

Refresh hints fired while setup commands run can cache a mid-creation
dirty snapshot; lifting the gate the moment bootstrap settles flashed
that stale snapshot until the forced fetch resolved. The gate now stays
down until the post-bootstrap status fetch completes.
This commit is contained in:
Iuliia Ivashko
2026-09-04 07:10:23 +03:00
parent 7519167031
commit 74e8d522b9
@@ -70,32 +70,38 @@ export const WorkStatusPrimaryGroup: React.FC<Props> = ({ sessionId, directory,
// A worktree that is still being created transiently looks dirty until its // A worktree that is still being created transiently looks dirty until its
// setup commands and initial git reset finish. Those files are not changes // setup commands and initial git reset finish. Those files are not changes
// on the branch, so status is neither fetched nor displayed until the // on the branch, so the changed-files readout stays hidden while the
// bootstrap settles. // bootstrap runs — and stays hidden until one fresh status fetch completes
// afterwards, because the shared cache may still hold a snapshot captured
// mid-creation (refresh hints fire while setup commands touch files) and
// lifting the gate onto it would flash the transient state.
const worktreeCreationPending = useWorktreeBootstrapPending(gitDirectory); const worktreeCreationPending = useWorktreeBootstrapPending(gitDirectory);
const awaitingPostBootstrapStatusRef = React.useRef<string | null>(null); const [postBootstrapRefreshDirectory, setPostBootstrapRefreshDirectory] = React.useState<string | null>(null);
const awaitingPostBootstrapStatus = postBootstrapRefreshDirectory !== null
&& postBootstrapRefreshDirectory === gitDirectory;
// Warm the shared git cache through the background-network gate so the panel // Warm the shared git cache through the background-network gate so the panel
// never competes with the chat's own bootstrap traffic for sockets. // never competes with the chat's own bootstrap traffic for sockets.
React.useEffect(() => { React.useEffect(() => {
if (!showRepository || !gitDirectory || !git) return; if (!showRepository || !gitDirectory || !git) return;
if (worktreeCreationPending) { if (worktreeCreationPending) {
awaitingPostBootstrapStatusRef.current = gitDirectory; setPostBootstrapRefreshDirectory(gitDirectory);
return; return;
} }
// Right after bootstrap the cache may still hold a status captured if (awaitingPostBootstrapStatus) {
// mid-creation; force one fetch so the lifted gate reveals the real let cancelled = false;
// (reset) working tree instead of the transient one. void runBackgroundNetworkTask(() => fetchStatus(gitDirectory, git, { silent: true }))
const finishedBootstrap = awaitingPostBootstrapStatusRef.current === gitDirectory; .finally(() => {
awaitingPostBootstrapStatusRef.current = null; if (!cancelled) {
void runBackgroundNetworkTask(async () => { setPostBootstrapRefreshDirectory((current) => (current === gitDirectory ? null : current));
if (finishedBootstrap) { }
await fetchStatus(gitDirectory, git, { silent: true }); });
} else { return () => {
await ensureStatus(gitDirectory, git); cancelled = true;
} };
}); }
}, [gitDirectory, git, ensureStatus, fetchStatus, showRepository, worktreeCreationPending]); void runBackgroundNetworkTask(() => ensureStatus(gitDirectory, git));
}, [gitDirectory, git, ensureStatus, fetchStatus, showRepository, worktreeCreationPending, awaitingPostBootstrapStatus]);
// Own the live invalidation for the repository readout. The desktop // Own the live invalidation for the repository readout. The desktop
// composer's changed-files row no longer renders, so this panel must not // composer's changed-files row no longer renders, so this panel must not
@@ -198,7 +204,7 @@ export const WorkStatusPrimaryGroup: React.FC<Props> = ({ sessionId, directory,
// event is reset to an empty array too, and carries real content only on // event is reset to an empty array too, and carries real content only on
// revert. Git status is the one authoritative, already-cached answer. // revert. Git status is the one authoritative, already-cached answer.
const changed = React.useMemo(() => { const changed = React.useMemo(() => {
if (worktreeCreationPending) return null; if (worktreeCreationPending || awaitingPostBootstrapStatus) return null;
const files = gitStatus?.files ?? []; const files = gitStatus?.files ?? [];
if (files.length === 0) return null; if (files.length === 0) return null;
const stats = gitStatus?.diffStats; const stats = gitStatus?.diffStats;
@@ -211,7 +217,7 @@ export const WorkStatusPrimaryGroup: React.FC<Props> = ({ sessionId, directory,
} }
} }
return { files: files.length, additions, deletions, hasStats: Boolean(stats) }; return { files: files.length, additions, deletions, hasStats: Boolean(stats) };
}, [gitStatus?.files, gitStatus?.diffStats, worktreeCreationPending]); }, [gitStatus?.files, gitStatus?.diffStats, worktreeCreationPending, awaitingPostBootstrapStatus]);
const attentionReason = gitStatus?.attentionReason const attentionReason = gitStatus?.attentionReason
?? (gitStatus?.rebaseInProgress ? 'rebase' : null) ?? (gitStatus?.rebaseInProgress ? 'rebase' : null)