import { useState, useEffect, useRef, useCallback } from 'react' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { AlertTriangle, FileText, Check, Loader2 } from 'lucide-react' import type { ConflictFileState } from '../hooks/useConflictResolver' interface ConflictResolverModalProps { open: boolean fileStates: ConflictFileState[] allResolved: boolean committing: boolean error: string | null onResolveFile: (file: string, strategy: 'ours' | 'theirs') => void onOpenInEditor: (file: string) => void onCommit: () => void onClose: () => void } function isBinaryFile(file: string): boolean { const binaryExts = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.pdf', '.zip', '.tar', '.gz', '.mp3', '.mp4', '.wav', '.ogg', '.woff', '.woff2', '.ttf', '.otf', '.eot'] return binaryExts.some(ext => file.toLowerCase().endsWith(ext)) } function fileName(path: string): string { return path.split('/').pop() ?? path } function ResolutionLabel({ resolution }: { resolution: ConflictFileState['resolution'] }) { if (!resolution) return null const labels = { ours: 'Keeping mine', theirs: 'Keeping theirs', manual: 'Edited manually' } return ( {labels[resolution]} ) } function ConflictFileRow({ state, focused, onResolve, onOpenInEditor, onFocus, }: { state: ConflictFileState focused: boolean onResolve: (strategy: 'ours' | 'theirs') => void onOpenInEditor: () => void onFocus: () => void }) { const rowRef = useRef(null) const binary = isBinaryFile(state.file) const resolved = state.resolution !== null useEffect(() => { if (focused) rowRef.current?.scrollIntoView({ block: 'nearest' }) }, [focused]) return (
{fileName(state.file)}
{state.resolving ? ( ) : ( <> {!binary && ( )} )}
) } export function ConflictResolverModal({ open, fileStates, allResolved, committing, error, onResolveFile, onOpenInEditor, onCommit, onClose, }: ConflictResolverModalProps) { const [focusIdx, setFocusIdx] = useState(0) const focusIdxRef = useRef(0) const listRef = useRef(null) useEffect(() => { if (open) { setFocusIdx(0) // eslint-disable-line react-hooks/set-state-in-effect -- reset on dialog open focusIdxRef.current = 0 } }, [open]) const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === 'Escape') { onClose() return } const idx = focusIdxRef.current const file = fileStates[idx] if (e.key === 'ArrowDown' || (e.key === 'Tab' && !e.shiftKey)) { e.preventDefault() const next = Math.min(idx + 1, fileStates.length - 1) setFocusIdx(next) focusIdxRef.current = next return } if (e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey)) { e.preventDefault() const prev = Math.max(idx - 1, 0) setFocusIdx(prev) focusIdxRef.current = prev return } if ((e.key === 'k' || e.key === 'K') && file && !file.resolving && !e.metaKey && !e.ctrlKey) { e.preventDefault() onResolveFile(file.file, 'ours') } else if ((e.key === 't' || e.key === 'T') && file && !file.resolving && !e.metaKey && !e.ctrlKey) { e.preventDefault() onResolveFile(file.file, 'theirs') } else if ((e.key === 'o' || e.key === 'O') && file && !isBinaryFile(file.file) && !e.metaKey && !e.ctrlKey) { e.preventDefault() onOpenInEditor(file.file) } else if (e.key === 'Enter' && allResolved && !committing) { e.preventDefault() onCommit() } }, [fileStates, allResolved, committing, onResolveFile, onOpenInEditor, onCommit, onClose]) return ( { if (!isOpen) onClose() }}>
Resolve Merge Conflicts
{fileStates.length} file{fileStates.length !== 1 ? 's have' : ' has'} merge conflicts. Choose how to resolve each file.
{fileStates.map((state, i) => ( onResolveFile(state.file, strategy)} onOpenInEditor={() => onOpenInEditor(state.file)} onFocus={() => { setFocusIdx(i) focusIdxRef.current = i }} /> ))}
{error && (

{error}

)} K = keep mine · T = keep theirs · O = open · Enter = commit
) }