"use client" import { useCallback, useEffect, useRef, useState } from "react" async function emitEvent(event: string, payload?: unknown) { try { const { emit } = await import("@tauri-apps/api/event") await emit(event, payload) } catch { /* not in Tauri */ } } import { Check, FileWarning, Loader2, X, CheckCheck } from "lucide-react" import { useTranslations } from "next-intl" import { toast } from "sonner" import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable" import { ScrollArea } from "@/components/ui/scroll-area" import { Button } from "@/components/ui/button" import { gitListConflicts, gitConflictFileVersions, gitResolveConflict, gitAbortOperation, gitContinueOperation, gitStartPullMerge, } from "@/lib/api" import { languageFromPath } from "@/lib/language-detect" import { toErrorMessage } from "@/lib/app-error" import type { GitConflictFileVersions } from "@/lib/types" import { ThreePaneMergeEditor } from "./three-pane-merge-editor" interface MergeWorkspaceProps { folderId: number folderPath: string operation: string upstreamCommit?: string onCompleted: () => void onAborted: () => void } export function MergeWorkspace({ folderId, folderPath, operation, upstreamCommit, onCompleted, onAborted, }: MergeWorkspaceProps) { const t = useTranslations("MergePage") const [files, setFiles] = useState([]) const [resolvedFiles, setResolvedFiles] = useState>(new Set()) const [selectedFile, setSelectedFile] = useState(null) const [versions, setVersions] = useState(null) const [loadingVersions, setLoadingVersions] = useState(false) const [resolving, setResolving] = useState(false) const [aborting, setAborting] = useState(false) const [completing, setCompleting] = useState(false) const currentContentRef = useRef("") const [hasUnresolvedConflicts, setHasUnresolvedConflicts] = useState(true) const [preparing, setPreparing] = useState(false) // Load conflict files on mount useEffect(() => { loadConflicts() }, [folderPath]) // eslint-disable-line react-hooks/exhaustive-deps async function loadConflicts() { try { // For pull operations, the merge was aborted during detection to keep // working tree clean. Re-start the merge to create conflict state. if (operation === "pull") { setPreparing(true) try { await gitStartPullMerge(folderPath, upstreamCommit) } finally { setPreparing(false) } } const conflictFiles = await gitListConflicts(folderPath) setFiles(conflictFiles) if (conflictFiles.length > 0 && !selectedFile) { selectFile(conflictFiles[0]) } } catch (err) { toast.error(toErrorMessage(err)) } } async function selectFile(file: string) { setSelectedFile(file) setLoadingVersions(true) try { const v = await gitConflictFileVersions(folderPath, file) setVersions(v) currentContentRef.current = v.base setHasUnresolvedConflicts(true) } catch (err) { toast.error(toErrorMessage(err)) setVersions(null) } finally { setLoadingVersions(false) } } const handleContentChange = useCallback((content: string) => { currentContentRef.current = content }, []) const handleConflictStatusChange = useCallback((hasUnresolved: boolean) => { setHasUnresolvedConflicts(hasUnresolved) }, []) async function handleResolve() { if (!selectedFile) return const content = currentContentRef.current if (hasUnresolvedConflicts) { toast.warning(t("unresolvedConflicts")) return } setResolving(true) try { await gitResolveConflict(folderPath, selectedFile, content) setResolvedFiles((prev) => new Set([...prev, selectedFile])) // Notify parent window await emitEvent("folder://merge-conflict-resolved", { folder_id: folderId, file: selectedFile, }) // Auto-select next unresolved file const nextUnresolved = files.find( (f) => f !== selectedFile && !resolvedFiles.has(f) ) if (nextUnresolved) { selectFile(nextUnresolved) } } catch (err) { toast.error(toErrorMessage(err)) } finally { setResolving(false) } } async function handleAbort() { setAborting(true) try { await gitAbortOperation(folderPath, operation) toast.success(t("abortSuccess")) await emitEvent("folder://merge-aborted", { folder_id: folderId }) onAborted() } catch (err) { toast.error(toErrorMessage(err)) } finally { setAborting(false) } } async function handleComplete() { setCompleting(true) try { await gitContinueOperation(folderPath, operation) toast.success(t("allResolved")) await emitEvent("folder://merge-completed", { folder_id: folderId }) onCompleted() } catch (err) { toast.error(toErrorMessage(err)) } finally { setCompleting(false) } } const allResolved = files.length > 0 && files.every((f) => resolvedFiles.has(f)) const language = selectedFile ? languageFromPath(selectedFile) : "plaintext" return (
{/* Left sidebar: conflict file list */}
{t("conflictFiles")} ({files.length})
{files.map((file) => { const isResolved = resolvedFiles.has(file) const isSelected = file === selectedFile return ( ) })} {files.length === 0 && (
{t("noConflicts")}
)}
{/* Main area: three-pane merge editor */} {preparing ? (
{t("preparingMerge")}
) : loadingVersions ? (
{t("loadingFile")}
) : versions && selectedFile ? ( ) : (
{t("selectFile")}
)}
{/* Bottom toolbar */}
{allResolved && ( )}
) }