import { useCallback, useEffect, useRef, useState } from 'react'; import { api } from '@/api/client'; import type { GitDiffMode, GitDiffResult, GitDiscardFileInfo } from '@/api/types'; import { sessionEvents } from './sessionEvents'; export function useGitDiff(projectId: string | null | undefined, hideWhitespace = false) { const [mode, setMode] = useState('uncommitted'); const [pinned, setPinned] = useState(false); const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // FIX 4: non-null when user has pinned a mode that differs from the server's auto-selected mode. const [modeSuggestion, setModeSuggestion] = useState(null); // Coalescence guard: absorb concurrent refresh triggers into the running request. const inFlightRef = useRef(false); const refresh = useCallback(() => { if (!projectId || inFlightRef.current) return; inFlightRef.current = true; setLoading(true); setError(null); // FIX 1: when not pinned, omit mode param so the server auto-selects based on // dirty state (dirty → uncommitted, clean → committed). api.projects .gitDiff(projectId, pinned ? mode : null, hideWhitespace) .then((r) => { if (!pinned) { setMode(r.mode); } // FIX 4: if pinned and the server's auto-selected mode differs, surface a suggestion. if (pinned && r.auto_mode && r.auto_mode !== mode) { setModeSuggestion(r.auto_mode); } else { setModeSuggestion(null); } setResult(r); }) .catch((err: unknown) => { setError(err instanceof Error ? err.message : 'Failed to load diff'); }) .finally(() => { inFlightRef.current = false; setLoading(false); }); }, [projectId, mode, pinned, hideWhitespace]); // Re-run refresh when mode changes (user pinned a new mode). useEffect(() => { if (!projectId) { setResult(null); return; } refresh(); }, [projectId, mode, hideWhitespace]); // eslint-disable-line react-hooks/exhaustive-deps // Subscribe to git_diff_refresh events (tab open, message_complete, manual). useEffect(() => { return sessionEvents.subscribe((event) => { if (event.type === 'git_diff_refresh') refresh(); }); }, [refresh]); const selectMode = useCallback((m: GitDiffMode) => { setPinned(true); setMode(m); setModeSuggestion(null); // FIX 4: clear suggestion on explicit mode pick }, []); const [mutating, setMutating] = useState(false); const [mutateError, setMutateError] = useState(null); const runMutation = useCallback( async (fn: () => Promise): Promise => { if (!projectId) return false; setMutating(true); setMutateError(null); try { await fn(); sessionEvents.emit({ type: 'git_diff_refresh' }); return true; } catch (err) { setMutateError(err instanceof Error ? err.message : 'Operation failed'); return false; } finally { setMutating(false); } }, [projectId], ); const stage = useCallback( (files: string[]) => runMutation(() => api.projects.gitStage(projectId!, files)), [projectId, runMutation], ); const unstage = useCallback( (files: string[]) => runMutation(() => api.projects.gitUnstage(projectId!, files)), [projectId, runMutation], ); const commit = useCallback( (message: string, files?: string[]) => runMutation(() => api.projects.gitCommit(projectId!, { message, files })), [projectId, runMutation], ); const discard = useCallback( (files: GitDiscardFileInfo[]) => runMutation(() => api.projects.gitDiscard(projectId!, files)), [projectId, runMutation], ); return { result, loading, error, mode, selectMode, refresh, mutating, mutateError, stage, unstage, commit, discard, modeSuggestion }; }