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)
145 lines
5.1 KiB
TypeScript
145 lines
5.1 KiB
TypeScript
import { useCallback, useState } from 'react';
|
|
|
|
import { cn } from '@renderer/lib/utils';
|
|
import { AlertTriangle, X } from 'lucide-react';
|
|
|
|
interface ConflictDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
filePath: string;
|
|
conflictContent: string;
|
|
onResolveKeepCurrent: () => void;
|
|
onResolveUseOriginal: () => void;
|
|
onResolveManual: (content: string) => void;
|
|
}
|
|
|
|
export const ConflictDialog = ({
|
|
open,
|
|
onOpenChange,
|
|
filePath,
|
|
conflictContent,
|
|
onResolveKeepCurrent,
|
|
onResolveUseOriginal,
|
|
onResolveManual,
|
|
}: ConflictDialogProps) => {
|
|
const [editMode, setEditMode] = useState(false);
|
|
const [editContent, setEditContent] = useState(conflictContent);
|
|
|
|
const handleManualSave = useCallback(() => {
|
|
onResolveManual(editContent);
|
|
setEditMode(false);
|
|
}, [editContent, onResolveManual]);
|
|
|
|
if (!open) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60">
|
|
<div className="mx-4 flex max-h-[80vh] w-full max-w-2xl flex-col overflow-hidden rounded-lg border border-border bg-surface shadow-2xl">
|
|
{/* Header */}
|
|
<div className="flex items-center gap-3 border-b border-border px-4 py-3">
|
|
<AlertTriangle className="size-4 text-yellow-400" />
|
|
<div className="flex-1">
|
|
<h3 className="text-sm font-medium text-text">Conflict Detected</h3>
|
|
<p className="text-xs text-text-muted">
|
|
This file has been modified since the agent's changes
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => onOpenChange(false)}
|
|
className="rounded p-1 text-text-muted transition-colors hover:bg-surface-raised hover:text-text"
|
|
>
|
|
<X className="size-4" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* File path */}
|
|
<div className="border-b border-border bg-surface-raised px-4 py-2">
|
|
<span className="font-mono text-xs text-text-secondary">{filePath}</span>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 overflow-auto p-4">
|
|
{editMode ? (
|
|
<textarea
|
|
value={editContent}
|
|
onChange={(e) => setEditContent(e.target.value)}
|
|
className="h-64 w-full resize-y rounded border border-border bg-surface p-3 font-mono text-xs text-text focus:border-blue-500/50 focus:outline-none"
|
|
spellCheck={false}
|
|
/>
|
|
) : (
|
|
<div className="max-h-64 overflow-auto rounded border border-border bg-surface font-mono text-xs leading-5">
|
|
{conflictContent.split('\n').map((line, i) => {
|
|
const isMarker =
|
|
line.startsWith('<<<<<<<') ||
|
|
line.startsWith('=======') ||
|
|
line.startsWith('>>>>>>>');
|
|
const lineClass = isMarker
|
|
? 'px-3 bg-yellow-500/10 text-yellow-400 font-medium'
|
|
: 'px-3 text-text-secondary';
|
|
|
|
return (
|
|
<div key={i} className={lineClass}>
|
|
<span className="mr-3 inline-block w-8 text-right text-text-muted opacity-50">
|
|
{i + 1}
|
|
</span>
|
|
<span className="whitespace-pre">{line}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex items-center justify-end gap-2 border-t border-border px-4 py-3">
|
|
{editMode ? (
|
|
<>
|
|
<button
|
|
onClick={() => setEditMode(false)}
|
|
className="rounded px-3 py-1.5 text-xs text-text-muted transition-colors hover:bg-surface-raised hover:text-text"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleManualSave}
|
|
className="rounded bg-blue-500/20 px-3 py-1.5 text-xs text-blue-400 transition-colors hover:bg-blue-500/30"
|
|
>
|
|
Save Resolution
|
|
</button>
|
|
</>
|
|
) : (
|
|
<>
|
|
<button
|
|
onClick={() => {
|
|
setEditContent(conflictContent);
|
|
setEditMode(true);
|
|
}}
|
|
className="rounded px-3 py-1.5 text-xs text-text-muted transition-colors hover:bg-surface-raised hover:text-text"
|
|
>
|
|
Edit Manually
|
|
</button>
|
|
<button
|
|
onClick={onResolveUseOriginal}
|
|
className={cn(
|
|
'rounded px-3 py-1.5 text-xs transition-colors',
|
|
'bg-red-500/15 text-red-400 hover:bg-red-500/25'
|
|
)}
|
|
>
|
|
Use Original
|
|
</button>
|
|
<button
|
|
onClick={onResolveKeepCurrent}
|
|
className={cn(
|
|
'rounded px-3 py-1.5 text-xs transition-colors',
|
|
'bg-green-500/15 text-green-400 hover:bg-green-500/25'
|
|
)}
|
|
>
|
|
Keep Current
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|