Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/components/features/git/GitDiffPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down
20 changes: 15 additions & 5 deletions src/hooks/useGitWatch.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand All @@ -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, '/');

Expand Down Expand Up @@ -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;
Expand Down
26 changes: 25 additions & 1 deletion src/services/tauri/git.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { invokeTauri, isDesktopTauri, postJson, postNoContent } from './shared';
import { invokeTauri, isDesktopTauri, postJson, postJsonWithOptions, postNoContent } from './shared';

export type GitStatusEntry = {
path: string;
Expand Down Expand Up @@ -66,6 +66,30 @@ export async function gitBranchInfo(cwd: string) {
return await postJson<GitBranchInfoResponse>('/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<boolean> {
if (!cwd) return false;
try {
if (isDesktopTauri()) {
await invokeTauri<GitBranchInfoResponse>('git_branch_info', { cwd });
} else {
await postJsonWithOptions<GitBranchInfoResponse>(
'/api/git/branch-info',
{ cwd },
{ suppressToast: true },
);
}
return true;
} catch {
return false;
}
}

export async function gitListBranches(cwd: string) {
if (isDesktopTauri()) {
return await invokeTauri<GitBranchListResponse>('git_list_branches', { cwd });
Expand Down
8 changes: 8 additions & 0 deletions src/stores/useGitStatsStore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { create } from 'zustand';
import { gitDiffStats, gitStatus } from '@/services/tauri';
import { isGitRepo } from '@/services/tauri/git';

interface GitStats {
stagedFiles: number;
Expand Down Expand Up @@ -42,6 +43,13 @@ export const useGitStatsStore = create<GitStatsStore>((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 },
}));
Expand Down
Loading