diff --git a/src/components/features/git/GitDiffPanel.tsx b/src/components/features/git/GitDiffPanel.tsx index 7a4aa1cd..00593223 100644 --- a/src/components/features/git/GitDiffPanel.tsx +++ b/src/components/features/git/GitDiffPanel.tsx @@ -6,6 +6,7 @@ import { gitUnstageFiles, type GitStatusResponse, } from '@/services/tauri'; +import { isGitRepo } from '@/services/tauri/git'; import { useGitWatch } from '@/hooks/useGitWatch'; import { useWorkspaceStore } from '@/stores'; import { useLayoutStore } from '@/stores/settings'; @@ -41,6 +42,13 @@ export function GitDiffPanel({ cwd, isActive }: GitDiffPanelProps) { const refreshGitStatus = useCallback(async () => { if (!cwd) return; + // Non-git cwd: clear, don't error-flash. Same gate as useGitWatch. + if (!(await isGitRepo(cwd))) { + setGitData(null); + setGitError(null); + setGitLoading(false); + return; + } setGitLoading(true); setGitError(null); try { diff --git a/src/hooks/useGitWatch.ts b/src/hooks/useGitWatch.ts index f396e537..c1bfe988 100644 --- a/src/hooks/useGitWatch.ts +++ b/src/hooks/useGitWatch.ts @@ -1,6 +1,7 @@ import { useEffect, useCallback, useRef } from 'react'; import { listen, type UnlistenFn } from '@tauri-apps/api/event'; import { startWatchFile, stopWatchFile } from '@/services/tauri/filesystem'; +import { isGitRepo } from '@/services/tauri/git'; import { isDesktopTauri } from '@/hooks/runtime'; /** @@ -24,6 +25,7 @@ export function useGitWatch(cwd: string | null, onRefresh: () => void, enabled = useEffect(() => { if (!cwd || !enabled) return; + let cancelled = false; const gitIndexPath = `${cwd}/.git/index`; const normalizedGitIndexPath = gitIndexPath.replace(/\\/g, '/'); @@ -64,14 +66,22 @@ export function useGitWatch(cwd: string | null, onRefresh: () => void, enabled = } }; - // Keep a low-frequency fallback for platforms where file events may be missed. - pollingRef.current = setInterval(() => { - debouncedRefresh(); - }, 2500); + // Preflight: if cwd isn't a git repo, skip polling and watcher entirely. + // Otherwise consumers' refresh callbacks loop on errors every 2.5s. + const initialize = async () => { + if (!(await isGitRepo(cwd))) return; + if (cancelled) return; + // Keep a low-frequency fallback for platforms where file events may be missed. + pollingRef.current = setInterval(() => { + debouncedRefresh(); + }, 2500); + void setupWatcher(); + }; - setupWatcher(); + void initialize(); return () => { + cancelled = true; if (unlistenRef.current) { unlistenRef.current(); unlistenRef.current = null; diff --git a/src/services/tauri/git.ts b/src/services/tauri/git.ts index 2e6b3179..fa60c088 100644 --- a/src/services/tauri/git.ts +++ b/src/services/tauri/git.ts @@ -1,4 +1,4 @@ -import { invokeTauri, isDesktopTauri, postJson, postNoContent } from './shared'; +import { invokeTauri, isDesktopTauri, postJson, postJsonWithOptions, postNoContent } from './shared'; export type GitStatusEntry = { path: string; @@ -66,6 +66,30 @@ export async function gitBranchInfo(cwd: string) { return await postJson('/api/git/branch-info', { cwd }); } +// Returns true iff `cwd` is a git working tree. Used by useGitWatch and +// downstream consumers to gate polling — non-git cwds otherwise loop on +// errors. Hits the same backend endpoint as gitBranchInfo but passes +// suppressToast so the shared API helper doesn't render a "Request failed" +// toast on the (expected) non-git case. Result is not cached because cwd +// changes are rare and the call is cheap. +export async function isGitRepo(cwd: string): Promise { + if (!cwd) return false; + try { + if (isDesktopTauri()) { + await invokeTauri('git_branch_info', { cwd }); + } else { + await postJsonWithOptions( + '/api/git/branch-info', + { cwd }, + { suppressToast: true }, + ); + } + return true; + } catch { + return false; + } +} + export async function gitListBranches(cwd: string) { if (isDesktopTauri()) { return await invokeTauri('git_list_branches', { cwd }); diff --git a/src/stores/useGitStatsStore.ts b/src/stores/useGitStatsStore.ts index cd902443..3494d6b9 100644 --- a/src/stores/useGitStatsStore.ts +++ b/src/stores/useGitStatsStore.ts @@ -1,5 +1,6 @@ import { create } from 'zustand'; import { gitDiffStats, gitStatus } from '@/services/tauri'; +import { isGitRepo } from '@/services/tauri/git'; interface GitStats { stagedFiles: number; @@ -42,6 +43,13 @@ export const useGitStatsStore = create((set) => ({ return; } + // Non-git cwds (e.g. claude session that lived in $HOME) — render empty + // instead of error-flashing. Same gate as useGitWatch's polling. + if (!(await isGitRepo(cwd))) { + set({ stats: null }); + return; + } + set((state) => ({ stats: state.stats ? { ...state.stats, isLoading: true } : { ...initialStats, isLoading: true }, }));