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
// setup commands and initial git reset finish. Those files are not changes
// on the branch, so status is neither fetched nor displayed until the
// bootstrap settles.
// on the branch, so the changed-files readout stays hidden while the
// 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 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
// never competes with the chat's own bootstrap traffic for sockets.
React.useEffect(() => {
if (!showRepository || !gitDirectory || !git) return;
if (worktreeCreationPending) {
awaitingPostBootstrapStatusRef.current = gitDirectory;
setPostBootstrapRefreshDirectory(gitDirectory);
return;
}
// Right after bootstrap the cache may still hold a status captured
// mid-creation; force one fetch so the lifted gate reveals the real
// (reset) working tree instead of the transient one.
const finishedBootstrap = awaitingPostBootstrapStatusRef.current === gitDirectory;
awaitingPostBootstrapStatusRef.current = null;
void runBackgroundNetworkTask(async () => {
if (finishedBootstrap) {
await fetchStatus(gitDirectory, git, { silent: true });
} else {
await ensureStatus(gitDirectory, git);
}
});
}, [gitDirectory, git, ensureStatus, fetchStatus, showRepository, worktreeCreationPending]);
if (awaitingPostBootstrapStatus) {
let cancelled = false;
void runBackgroundNetworkTask(() => fetchStatus(gitDirectory, git, { silent: true }))
.finally(() => {
if (!cancelled) {
setPostBootstrapRefreshDirectory((current) => (current === gitDirectory ? null : current));
}
});
return () => {
cancelled = true;
};
}
void runBackgroundNetworkTask(() => ensureStatus(gitDirectory, git));
}, [gitDirectory, git, ensureStatus, fetchStatus, showRepository, worktreeCreationPending, awaitingPostBootstrapStatus]);
// Own the live invalidation for the repository readout. The desktop
// 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
// revert. Git status is the one authoritative, already-cached answer.
const changed = React.useMemo(() => {
if (worktreeCreationPending) return null;
if (worktreeCreationPending || awaitingPostBootstrapStatus) return null;
const files = gitStatus?.files ?? [];
if (files.length === 0) return null;
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) };
}, [gitStatus?.files, gitStatus?.diffStats, worktreeCreationPending]);
}, [gitStatus?.files, gitStatus?.diffStats, worktreeCreationPending, awaitingPostBootstrapStatus]);
const attentionReason = gitStatus?.attentionReason
?? (gitStatus?.rebaseInProgress ? 'rebase' : null)