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.
This commit is contained in:
@@ -678,6 +678,15 @@ async fn flush_watch_batch(
|
|||||||
Err(_) => return,
|
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 let Some(tree) = refreshed_tree {
|
||||||
if tree != guard.tree_snapshot {
|
if tree != guard.tree_snapshot {
|
||||||
payload.push(WorkspaceDelta::TreeReplace { nodes: tree });
|
payload.push(WorkspaceDelta::TreeReplace { nodes: tree });
|
||||||
@@ -689,6 +698,22 @@ async fn flush_watch_batch(
|
|||||||
entries: git_snapshot,
|
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() {
|
if payload.is_empty() {
|
||||||
@@ -709,7 +734,7 @@ async fn flush_watch_batch(
|
|||||||
"meta".to_string()
|
"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);
|
emit_event(emitter, "folder://workspace-state-event", event);
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export function FolderTitleBar() {
|
|||||||
const [branch, setBranch] = useState<string | null>(null)
|
const [branch, setBranch] = useState<string | null>(null)
|
||||||
const [searchOpen, setSearchOpen] = useState(false)
|
const [searchOpen, setSearchOpen] = useState(false)
|
||||||
const [browserOpen, setBrowserOpen] = useState(false)
|
const [browserOpen, setBrowserOpen] = useState(false)
|
||||||
const intervalRef = useRef<ReturnType<typeof setInterval> | undefined>(
|
const pollTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
||||||
undefined
|
undefined
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -116,36 +116,41 @@ export function FolderTitleBar() {
|
|||||||
if (!folderPath) return
|
if (!folderPath) return
|
||||||
let cancelled = false
|
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 = () => {
|
const clearPoll = () => {
|
||||||
if (intervalRef.current !== undefined) {
|
if (pollTimerRef.current !== undefined) {
|
||||||
clearInterval(intervalRef.current)
|
clearTimeout(pollTimerRef.current)
|
||||||
intervalRef.current = undefined
|
pollTimerRef.current = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const armPoll = () => {
|
const scheduleNext = (delayMs: number) => {
|
||||||
if (intervalRef.current !== undefined) return
|
clearPoll()
|
||||||
intervalRef.current = setInterval(() => {
|
pollTimerRef.current = setTimeout(() => {
|
||||||
|
pollTimerRef.current = undefined
|
||||||
void doFetch()
|
void doFetch()
|
||||||
}, 10_000)
|
}, delayMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doFetch() {
|
async function doFetch() {
|
||||||
if (document.visibilityState !== "visible") return
|
if (document.visibilityState !== "visible") return
|
||||||
|
|
||||||
|
let nextDelayMs = POLL_FAST_MS
|
||||||
try {
|
try {
|
||||||
const b = await getGitBranch(folderPath)
|
const b = await getGitBranch(folderPath)
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
setBranch(b)
|
setBranch(b)
|
||||||
if (b === null) {
|
if (b === null) nextDelayMs = POLL_SLOW_MS
|
||||||
clearPoll()
|
|
||||||
} else {
|
|
||||||
armPoll()
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) setBranch(null)
|
if (!cancelled) setBranch(null)
|
||||||
clearPoll()
|
nextDelayMs = POLL_SLOW_MS
|
||||||
}
|
}
|
||||||
|
if (!cancelled) scheduleNext(nextDelayMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleVisibilityChange() {
|
function handleVisibilityChange() {
|
||||||
|
|||||||
Reference in New Issue
Block a user