Files
boocode/apps/web/src/hooks/useGitDiff.ts
indifferentketchup 31d8efe66a feat(web): enhanced file panel — side-by-side diff, hide whitespace, inline review
Adds DiffSplitView component for side-by-side diff mode, whitespace-only
change filtering, inline review comments with thread/gutter cell UI, diff
preferences persistence, and write-file API support for in-browser editing.

Backend: hideWhitespace param on git diff endpoint, write_file route.
2026-06-07 22:16:20 +00:00

115 lines
4.0 KiB
TypeScript

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<GitDiffMode>('uncommitted');
const [pinned, setPinned] = useState(false);
const [result, setResult] = useState<GitDiffResult | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// FIX 4: non-null when user has pinned a mode that differs from the server's auto-selected mode.
const [modeSuggestion, setModeSuggestion] = useState<GitDiffMode | null>(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<string | null>(null);
const runMutation = useCallback(
async (fn: () => Promise<unknown>): Promise<boolean> => {
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 };
}