From ff4881060368b3cc17262844bc96a8e513af64b1 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Sat, 18 Apr 2026 17:44:45 +0800 Subject: [PATCH] fix(workspace-state): keep git-presence flag and branch poll in sync with runtime state Reconcile the cached is_git_repo flag against the filesystem on every watch flush so `git init` or deletion of .git is reflected immediately: sync the stored flag, drop stale git_snapshot data when the repo goes away, emit a meta delta when presence flips without any data change, and mark the event as requires_resync so the frontend re-fetches the snapshot to pick up the new flag. Replace the title-bar branch polling interval with a self-adjusting setTimeout chain that backs off to 60s when get_git_branch returns null or throws and drops back to 10s once a branch is detected, so branches created externally recover within one slow tick without hammering the backend on non-git folders. --- src-tauri/src/workspace_state/mod.rs | 27 +++++++++++++++++- src/components/layout/folder-title-bar.tsx | 33 +++++++++++++--------- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/src-tauri/src/workspace_state/mod.rs b/src-tauri/src/workspace_state/mod.rs index 019538d..cbb3bf7 100644 --- a/src-tauri/src/workspace_state/mod.rs +++ b/src-tauri/src/workspace_state/mod.rs @@ -678,6 +678,15 @@ async fn flush_watch_batch( Err(_) => return, }; + // Keep the cached git-presence flag in sync with the filesystem. + // When it flips, the snapshot response carries the new value, and the + // emitted event carries `requires_resync=true` so the frontend re-fetches + // to align its isGitRepo view. + let git_presence_changed = guard.is_git_repo != is_git; + if git_presence_changed { + guard.is_git_repo = is_git; + } + if let Some(tree) = refreshed_tree { if tree != guard.tree_snapshot { payload.push(WorkspaceDelta::TreeReplace { nodes: tree }); @@ -689,6 +698,22 @@ async fn flush_watch_batch( entries: git_snapshot, }); } + } else if !is_git && !guard.git_snapshot.is_empty() { + // .git vanished (or was never there) and we still hold stale git + // data — emit an empty GitReplace so the UI stops showing tracked + // files that no longer exist from git's perspective. + payload.push(WorkspaceDelta::GitReplace { + entries: Vec::new(), + }); + } + + // Presence flip with no data delta (e.g. `git init` in a clean folder) + // still needs to wake the frontend, otherwise the snapshot flag never + // propagates until an unrelated change happens. + if git_presence_changed && payload.is_empty() { + payload.push(WorkspaceDelta::Meta { + reason: format!("is_git_repo_changed:{is_git}"), + }); } if payload.is_empty() { @@ -709,7 +734,7 @@ async fn flush_watch_batch( "meta".to_string() }; - guard.append_event(kind, payload, false) + guard.append_event(kind, payload, git_presence_changed) }; emit_event(emitter, "folder://workspace-state-event", event); diff --git a/src/components/layout/folder-title-bar.tsx b/src/components/layout/folder-title-bar.tsx index 0cf5ee5..4d37206 100644 --- a/src/components/layout/folder-title-bar.tsx +++ b/src/components/layout/folder-title-bar.tsx @@ -82,7 +82,7 @@ export function FolderTitleBar() { const [branch, setBranch] = useState(null) const [searchOpen, setSearchOpen] = useState(false) const [browserOpen, setBrowserOpen] = useState(false) - const intervalRef = useRef | undefined>( + const pollTimerRef = useRef | undefined>( undefined ) @@ -116,36 +116,41 @@ export function FolderTitleBar() { if (!folderPath) return let cancelled = false + // 10s when we have a branch, 60s when we don't. The slow poll still + // discovers a branch created externally (e.g. `git init` in a terminal) + // without hammering the backend when there is nothing to find. + const POLL_FAST_MS = 10_000 + const POLL_SLOW_MS = 60_000 + const clearPoll = () => { - if (intervalRef.current !== undefined) { - clearInterval(intervalRef.current) - intervalRef.current = undefined + if (pollTimerRef.current !== undefined) { + clearTimeout(pollTimerRef.current) + pollTimerRef.current = undefined } } - const armPoll = () => { - if (intervalRef.current !== undefined) return - intervalRef.current = setInterval(() => { + const scheduleNext = (delayMs: number) => { + clearPoll() + pollTimerRef.current = setTimeout(() => { + pollTimerRef.current = undefined void doFetch() - }, 10_000) + }, delayMs) } async function doFetch() { if (document.visibilityState !== "visible") return + let nextDelayMs = POLL_FAST_MS try { const b = await getGitBranch(folderPath) if (cancelled) return setBranch(b) - if (b === null) { - clearPoll() - } else { - armPoll() - } + if (b === null) nextDelayMs = POLL_SLOW_MS } catch { if (!cancelled) setBranch(null) - clearPoll() + nextDelayMs = POLL_SLOW_MS } + if (!cancelled) scheduleNext(nextDelayMs) } function handleVisibilityChange() {