From 74e8d522b987837596a2cec15d4446b4cf041fa0 Mon Sep 17 00:00:00 2001 From: Iuliia Ivashko Date: Fri, 4 Sep 2026 07:10:23 +0300 Subject: [PATCH] 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. --- .../work-status/WorkStatusPrimaryGroup.tsx | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/packages/ui/src/components/chat/work-status/WorkStatusPrimaryGroup.tsx b/packages/ui/src/components/chat/work-status/WorkStatusPrimaryGroup.tsx index 1b0a4763..9289c5f4 100644 --- a/packages/ui/src/components/chat/work-status/WorkStatusPrimaryGroup.tsx +++ b/packages/ui/src/components/chat/work-status/WorkStatusPrimaryGroup.tsx @@ -70,32 +70,38 @@ export const WorkStatusPrimaryGroup: React.FC = ({ 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(null); + const [postBootstrapRefreshDirectory, setPostBootstrapRefreshDirectory] = React.useState(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 = ({ 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 = ({ 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)