- Add git_resolve_conflict and git_commit_conflict_resolution Rust commands - Create useConflictResolver hook for per-file resolution state management - Create ConflictResolverModal with keyboard shortcuts (K/T/O/Enter/Esc) - Make StatusBar conflict chip clickable to open resolver modal - Add 'Resolve Conflicts' command to command palette (conditional) - Pause auto-pull while conflict resolver modal is open - Tests: 5 new Rust tests, 10 new hook tests, 1 new auto-sync test Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
97 lines
2.7 KiB
TypeScript
97 lines
2.7 KiB
TypeScript
import { useCallback, useState } from 'react'
|
|
import { invoke } from '@tauri-apps/api/core'
|
|
import { isTauri, mockInvoke } from '../mock-tauri'
|
|
|
|
function tauriCall<T>(cmd: string, args: Record<string, unknown>): Promise<T> {
|
|
return isTauri() ? invoke<T>(cmd, args) : mockInvoke<T>(cmd, args)
|
|
}
|
|
|
|
export type FileResolution = 'ours' | 'theirs' | 'manual' | null
|
|
|
|
export interface ConflictFileState {
|
|
file: string
|
|
resolution: FileResolution
|
|
resolving: boolean
|
|
}
|
|
|
|
interface UseConflictResolverConfig {
|
|
vaultPath: string
|
|
onResolved: () => void
|
|
onToast: (msg: string) => void
|
|
onOpenFile: (relativePath: string) => void
|
|
}
|
|
|
|
export function useConflictResolver({
|
|
vaultPath,
|
|
onResolved,
|
|
onToast,
|
|
onOpenFile,
|
|
}: UseConflictResolverConfig) {
|
|
const [fileStates, setFileStates] = useState<ConflictFileState[]>([])
|
|
const [committing, setCommitting] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
const initFiles = useCallback((files: string[]) => {
|
|
setFileStates(files.map(file => ({ file, resolution: null, resolving: false })))
|
|
setError(null)
|
|
setCommitting(false)
|
|
}, [])
|
|
|
|
const resolveFile = useCallback(async (file: string, strategy: 'ours' | 'theirs') => {
|
|
setFileStates(prev => prev.map(f =>
|
|
f.file === file ? { ...f, resolving: true } : f
|
|
))
|
|
setError(null)
|
|
|
|
try {
|
|
await tauriCall<void>('git_resolve_conflict', { vaultPath, file, strategy })
|
|
setFileStates(prev => prev.map(f =>
|
|
f.file === file ? { ...f, resolution: strategy, resolving: false } : f
|
|
))
|
|
} catch (err) {
|
|
setFileStates(prev => prev.map(f =>
|
|
f.file === file ? { ...f, resolving: false } : f
|
|
))
|
|
setError(`Failed to resolve ${file}: ${err}`)
|
|
}
|
|
}, [vaultPath])
|
|
|
|
const openInEditor = useCallback((file: string) => {
|
|
onOpenFile(file)
|
|
setFileStates(prev => prev.map(f =>
|
|
f.file === file ? { ...f, resolution: 'manual' } : f
|
|
))
|
|
}, [onOpenFile])
|
|
|
|
const allResolved = fileStates.length > 0 && fileStates.every(f => f.resolution !== null)
|
|
const anyResolving = fileStates.some(f => f.resolving)
|
|
|
|
const commitResolution = useCallback(async () => {
|
|
if (!allResolved || committing) return
|
|
setCommitting(true)
|
|
setError(null)
|
|
|
|
try {
|
|
await tauriCall<string>('git_commit_conflict_resolution', { vaultPath })
|
|
onResolved()
|
|
onToast('Conflicts resolved — sync resumed')
|
|
} catch (err) {
|
|
setError(`Commit failed: ${err}`)
|
|
} finally {
|
|
setCommitting(false)
|
|
}
|
|
}, [vaultPath, allResolved, committing, onResolved, onToast])
|
|
|
|
return {
|
|
fileStates,
|
|
committing,
|
|
error,
|
|
allResolved,
|
|
anyResolving,
|
|
initFiles,
|
|
resolveFile,
|
|
openInEditor,
|
|
commitResolution,
|
|
}
|
|
}
|