import { useState, useEffect } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; import { GitMerge, AlertTriangle, Trash2, Wrench, Sparkles, XCircle } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; import { getElectronAPI } from '@/lib/electron'; import { toast } from 'sonner'; import { BranchAutocomplete } from '@/components/ui/branch-autocomplete'; import type { WorktreeInfo, BranchInfo, MergeConflictInfo } from '../worktree-panel/types'; export type { MergeConflictInfo } from '../worktree-panel/types'; interface MergeWorktreeDialogProps { open: boolean; onOpenChange: (open: boolean) => void; projectPath: string; worktree: WorktreeInfo | null; /** Called when integration is successful. integratedWorktree indicates the integrated worktree and deletedBranch indicates if the branch was also deleted. */ onIntegrated: (integratedWorktree: WorktreeInfo, deletedBranch: boolean) => void; onCreateConflictResolutionFeature?: (conflictInfo: MergeConflictInfo) => void; } export function MergeWorktreeDialog({ open, onOpenChange, projectPath, worktree, onIntegrated, onCreateConflictResolutionFeature, }: MergeWorktreeDialogProps) { const [isLoading, setIsLoading] = useState(false); const [targetBranch, setTargetBranch] = useState('main'); const [availableBranches, setAvailableBranches] = useState([]); const [loadingBranches, setLoadingBranches] = useState(false); const [deleteWorktreeAndBranch, setDeleteWorktreeAndBranch] = useState(false); const [mergeConflict, setMergeConflict] = useState(null); // Fetch available branches when dialog opens useEffect(() => { if (open && worktree && projectPath) { setLoadingBranches(true); const api = getElectronAPI(); if (api?.worktree?.listBranches) { api.worktree .listBranches(projectPath, false) .then((result) => { if (result.success && result.result?.branches) { // Filter out the source branch (can't merge into itself) and remote branches const branches = result.result.branches .filter((b: BranchInfo) => !b.isRemote && b.name !== worktree.branch) .map((b: BranchInfo) => b.name); setAvailableBranches(branches); } }) .catch((err) => { console.error('Failed to fetch branches:', err); }) .finally(() => { setLoadingBranches(false); }); } else { setLoadingBranches(false); } } }, [open, worktree, projectPath]); // Reset state when dialog opens useEffect(() => { if (open) { setIsLoading(false); setTargetBranch('main'); setDeleteWorktreeAndBranch(false); setMergeConflict(null); } }, [open]); const handleMerge = async () => { if (!worktree) return; setIsLoading(true); try { const api = getElectronAPI(); if (!api?.worktree?.mergeFeature) { toast.error('Worktree API not available'); return; } // Pass branchName, worktreePath, targetBranch, and options to the API const result = await api.worktree.mergeFeature( projectPath, worktree.branch, worktree.path, targetBranch, { deleteWorktreeAndBranch } ); if (result.success) { const description = deleteWorktreeAndBranch ? `Branch "${worktree.branch}" has been integrated into "${targetBranch}" and the worktree and branch were deleted` : `Branch "${worktree.branch}" has been integrated into "${targetBranch}"`; toast.success(`Branch integrated into ${targetBranch}`, { description }); onIntegrated(worktree, deleteWorktreeAndBranch); onOpenChange(false); } else { // Check if the error indicates merge conflicts const errorMessage = result.error || ''; const hasConflicts = errorMessage.toLowerCase().includes('conflict') || errorMessage.toLowerCase().includes('merge failed') || errorMessage.includes('CONFLICT') || result.hasConflicts; if (hasConflicts) { // Set merge conflict state to show the conflict resolution UI setMergeConflict({ sourceBranch: worktree.branch, targetBranch: targetBranch, targetWorktreePath: projectPath, // The merge happens in the target branch's worktree conflictFiles: result.conflictFiles || [], operationType: 'merge', }); toast.error('Integrate conflicts detected', { description: 'Choose how to resolve the conflicts below.', }); } else { toast.error('Failed to integrate branch', { description: result.error, }); } } } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Unknown error'; // Check if the error indicates merge conflicts const hasConflicts = errorMessage.toLowerCase().includes('conflict') || errorMessage.toLowerCase().includes('merge failed') || errorMessage.includes('CONFLICT'); if (hasConflicts) { setMergeConflict({ sourceBranch: worktree.branch, targetBranch: targetBranch, targetWorktreePath: projectPath, conflictFiles: [], operationType: 'merge', }); toast.error('Integrate conflicts detected', { description: 'Choose how to resolve the conflicts below.', }); } else { toast.error('Failed to integrate branch', { description: errorMessage, }); } } finally { setIsLoading(false); } }; const handleResolveWithAI = () => { if (mergeConflict && onCreateConflictResolutionFeature) { onCreateConflictResolutionFeature(mergeConflict); onOpenChange(false); } }; const handleResolveManually = () => { toast.info('Conflict markers left in place', { description: 'Edit the conflicting files to resolve conflicts manually.', duration: 6000, }); onOpenChange(false); }; if (!worktree) return null; // Show conflict resolution UI if there are merge conflicts if (mergeConflict) { return ( Integrate Conflicts Detected
There are conflicts when integrating{' '} {mergeConflict.sourceBranch} {' '} into{' '} {mergeConflict.targetBranch} . {mergeConflict.conflictFiles && mergeConflict.conflictFiles.length > 0 && (
Conflicting files ({mergeConflict.conflictFiles.length}):
{mergeConflict.conflictFiles.map((file) => (
{file}
))}
)}

Choose how to resolve:

  • Resolve with AI — Creates a task to analyze and resolve conflicts automatically
  • Resolve Manually — Leaves conflict markers in place for you to edit directly
{onCreateConflictResolutionFeature && ( )}
); } return ( Integrate Branch
Integrate {worktree.branch}{' '} into:
{loadingBranches ? (
Loading branches...
) : ( )}
{worktree.hasChanges && (
This worktree has {worktree.changedFilesCount} uncommitted change(s). Please commit or discard them before integrating.
)}
setDeleteWorktreeAndBranch(checked === true)} />
{deleteWorktreeAndBranch && (
The worktree and branch will be permanently deleted. Any features assigned to this branch will be unassigned.
)}
); }