Phase 1: Core diff extraction and display - ChangeExtractorService: JSONL streaming parser with snippet extraction - FileContentResolver: 3-level content resolution (file-history → snippets → disk) - ReviewApplierService: hunk-level accept/reject with conflict detection - CodeMirrorDiffView: unified merge view with syntax highlighting - ReviewFileTree: file browser with status indicators - changeReviewSlice: Zustand state for review workflow Phase 2: Interactive review with accept/reject - Per-hunk and per-file accept/reject decisions - Conflict checking before apply - ReviewToolbar with bulk actions - DiffErrorBoundary for graceful degradation Phase 3: Per-task change scoping - TaskBoundaryParser: detects task boundaries in JSONL (Tier 1-4 confidence) - TaskChangeSetV2 with scope + warnings - ConfidenceBadge and ScopeWarningBanner components Phase 4: Enhanced features - Keyboard navigation (j/k/n/p/a/x shortcuts via useDiffNavigation) - Viewed file tracking (localStorage + useViewedFiles hook) - File edit timeline (chronological events per file) - Git fallback (GitDiffFallback service for incomplete JSONL data) - Auto-viewed detection (IntersectionObserver sentinel)
74 lines
2 KiB
TypeScript
74 lines
2 KiB
TypeScript
import { useCallback, useMemo, useState } from 'react';
|
|
|
|
import * as storage from '@renderer/utils/diffViewedStorage';
|
|
|
|
interface UseViewedFilesResult {
|
|
viewedSet: Set<string>;
|
|
isViewed: (filePath: string) => boolean;
|
|
markViewed: (filePath: string) => void;
|
|
unmarkViewed: (filePath: string) => void;
|
|
markAllViewed: (filePaths: string[]) => void;
|
|
clearAll: () => void;
|
|
viewedCount: number;
|
|
totalCount: number;
|
|
/** Progress 0-100 */
|
|
progress: number;
|
|
}
|
|
|
|
export function useViewedFiles(
|
|
teamName: string,
|
|
scopeKey: string,
|
|
totalFiles: string[]
|
|
): UseViewedFilesResult {
|
|
// version bump pattern for re-reading localStorage
|
|
const [version, setVersion] = useState(0);
|
|
|
|
const viewedSet = useMemo(() => {
|
|
// version is used to trigger re-read
|
|
if (version < 0) return new Set<string>();
|
|
return storage.getViewedFiles(teamName, scopeKey);
|
|
}, [teamName, scopeKey, version]);
|
|
|
|
const markViewed = useCallback(
|
|
(filePath: string) => {
|
|
storage.markFileViewed(teamName, scopeKey, filePath);
|
|
setVersion((v) => v + 1);
|
|
},
|
|
[teamName, scopeKey]
|
|
);
|
|
|
|
const unmarkViewed = useCallback(
|
|
(filePath: string) => {
|
|
storage.unmarkFileViewed(teamName, scopeKey, filePath);
|
|
setVersion((v) => v + 1);
|
|
},
|
|
[teamName, scopeKey]
|
|
);
|
|
|
|
const markAllViewedFn = useCallback(
|
|
(filePaths: string[]) => {
|
|
storage.markAllViewed(teamName, scopeKey, filePaths);
|
|
setVersion((v) => v + 1);
|
|
},
|
|
[teamName, scopeKey]
|
|
);
|
|
|
|
const clearAll = useCallback(() => {
|
|
storage.clearViewed(teamName, scopeKey);
|
|
setVersion((v) => v + 1);
|
|
}, [teamName, scopeKey]);
|
|
|
|
const viewedCount = totalFiles.filter((f) => viewedSet.has(f)).length;
|
|
|
|
return {
|
|
viewedSet,
|
|
isViewed: (fp: string) => viewedSet.has(fp),
|
|
markViewed,
|
|
unmarkViewed,
|
|
markAllViewed: markAllViewedFn,
|
|
clearAll,
|
|
viewedCount,
|
|
totalCount: totalFiles.length,
|
|
progress: totalFiles.length > 0 ? Math.round((viewedCount / totalFiles.length) * 100) : 0,
|
|
};
|
|
}
|