"use client" import { useCallback, useEffect, useState } from "react" import { subscribe } from "@/lib/platform" import { AlertTriangle, Check, FileWarning, Loader2 } from "lucide-react" import { useTranslations } from "next-intl" import { toast } from "sonner" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { ScrollArea } from "@/components/ui/scroll-area" import { gitListConflicts, gitAbortOperation, gitContinueOperation, openMergeWindow, } from "@/lib/api" import type { GitConflictInfo } from "@/lib/types" interface ConflictDialogProps { conflictInfo: GitConflictInfo | null folderId: number folderPath: string onClose: () => void onResolved: () => void } export function ConflictDialog({ conflictInfo, folderId, folderPath, onClose, onResolved, }: ConflictDialogProps) { const t = useTranslations("Folder.branchDropdown.conflict") const [conflictedFiles, setConflictedFiles] = useState([]) const [resolvedFiles, setResolvedFiles] = useState>(new Set()) const [aborting, setAborting] = useState(false) const [completing, setCompleting] = useState(false) const [done, setDone] = useState(false) const open = conflictInfo !== null const operation = conflictInfo?.operation ?? "merge" // Initialize conflict files from conflictInfo useEffect(() => { if (conflictInfo) { setConflictedFiles(conflictInfo.conflicted_files) setResolvedFiles(new Set()) setDone(false) } }, [conflictInfo]) // Refresh conflict list to detect resolved files const refreshConflicts = useCallback(async () => { if (!folderPath || !open) return try { const remaining = await gitListConflicts(folderPath) const nowResolved = new Set( conflictedFiles.filter((f) => !remaining.includes(f)) ) setResolvedFiles(nowResolved) } catch { // ignore refresh errors } }, [folderPath, open, conflictedFiles]) // Listen for merge events from the merge window useEffect(() => { if (!open) return let unlistenResolved: (() => void) | null = null let unlistenCompleted: (() => void) | null = null let unlistenAborted: (() => void) | null = null subscribe<{ folder_id: number; file: string }>( "folder://merge-conflict-resolved", (payload) => { if (payload.folder_id !== folderId) return setResolvedFiles((prev) => new Set([...prev, payload.file])) } ) .then((fn) => { unlistenResolved = fn }) .catch(() => {}) subscribe<{ folder_id: number }>("folder://merge-completed", (payload) => { if (payload.folder_id !== folderId) return setDone(true) onResolved() onClose() }) .then((fn) => { unlistenCompleted = fn }) .catch(() => {}) // Merge was aborted (user clicked abort in merge window, or window closed) // Reset resolved state since abort reverts all changes subscribe<{ folder_id: number }>("folder://merge-aborted", (payload) => { if (payload.folder_id !== folderId) return setDone(true) setResolvedFiles(new Set()) onClose() }) .then((fn) => { unlistenAborted = fn }) .catch(() => {}) return () => { unlistenResolved?.() unlistenCompleted?.() unlistenAborted?.() } }, [open, folderId, onResolved, onClose]) // Periodically refresh conflict status (skip for pull — merge is aborted // until the merge tool re-starts it, so git index has no conflicts yet) useEffect(() => { if (!open || operation === "pull") return const interval = setInterval(refreshConflicts, 3000) return () => clearInterval(interval) }, [open, operation, refreshConflicts]) const allResolved = conflictedFiles.length > 0 && conflictedFiles.every((f) => resolvedFiles.has(f)) async function handleOpenMergeTool() { try { await openMergeWindow(folderId, operation, conflictInfo?.upstream_commit) } catch (err) { toast.error(String(err)) } } async function handleAbort() { // For pull operations, the merge was already aborted during conflict // detection, so there's nothing to abort — just close the dialog. if (operation === "pull") { onClose() return } setAborting(true) try { await gitAbortOperation(folderPath, operation) toast.success(t("abortSuccess")) onClose() onResolved() } catch (err) { toast.error(String(err)) } finally { setAborting(false) } } async function handleComplete() { if (done) return setCompleting(true) try { await gitContinueOperation(folderPath, operation) toast.success(t("completeSuccess")) onResolved() onClose() } catch (err) { toast.error(String(err)) } finally { setCompleting(false) } } return ( !v && onClose()}> {t("title")} {t("description")}
{conflictedFiles.map((file) => { const isResolved = resolvedFiles.has(file) return (
{isResolved ? ( ) : ( )} {file}
) })}
{allResolved && ( )}
) }