using System.Collections.ObjectModel; using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace ClaudeDo.Ui.ViewModels.Modals; public sealed partial class MergeModalViewModel : ViewModelBase { private readonly IWorkerClient _worker; public string TaskId { get; set; } = ""; public string TaskTitle { get; set; } = ""; public ObservableCollection Branches { get; } = new(); [ObservableProperty][NotifyCanExecuteChangedFor(nameof(SubmitCommand))] private string? _selectedBranch; [ObservableProperty] private bool _removeWorktree = true; [ObservableProperty] private string _commitMessage = ""; [ObservableProperty][NotifyCanExecuteChangedFor(nameof(SubmitCommand))] private bool _isBusy; [ObservableProperty] private string? _errorMessage; [ObservableProperty] private string? _warningMessage; [ObservableProperty] private string? _successMessage; [ObservableProperty][NotifyCanExecuteChangedFor(nameof(SubmitCommand))] private bool _hasConflict; [ObservableProperty] private IReadOnlyList _conflictFiles = Array.Empty(); public Action? CloseAction { get; set; } /// Set by the caller to hand a conflicting merge off to the in-app 3-pane editor /// instead of dead-ending on the conflict message. public Func? RequestConflictResolution { get; set; } /// True once a merge has succeeded — lets the caller (e.g. the diff window) /// close itself after this modal closes. public bool Merged { get; private set; } /// True once a conflict has been handed off to the resolver — also a cue to close the diff window. public bool RoutedToResolver { get; private set; } public MergeModalViewModel(IWorkerClient worker) { _worker = worker; } public async Task InitializeAsync(string taskId, string taskTitle) { TaskId = taskId; TaskTitle = taskTitle; CommitMessage = Loc.T("vm.merge.commitMessage", taskTitle); IsBusy = true; try { var targets = await _worker.GetMergeTargetsAsync(taskId); Branches.Clear(); if (targets is null) { ErrorMessage = Loc.T("vm.merge.workerOfflineBranches"); return; } foreach (var b in targets.LocalBranches) Branches.Add(b); SelectedBranch = Branches.Contains(targets.DefaultBranch) ? targets.DefaultBranch : Branches.FirstOrDefault(); } catch (Exception ex) { ErrorMessage = Loc.T("vm.merge.loadBranchesFailed", ex.Message); } finally { IsBusy = false; } } private bool CanSubmit() => !IsBusy && !HasConflict && !string.IsNullOrWhiteSpace(SelectedBranch); [RelayCommand(CanExecute = nameof(CanSubmit))] private async Task SubmitAsync() { if (string.IsNullOrWhiteSpace(SelectedBranch)) return; IsBusy = true; ErrorMessage = null; WarningMessage = null; SuccessMessage = null; try { var result = await _worker.MergeTaskAsync( TaskId, SelectedBranch!, RemoveWorktree, CommitMessage); switch (result.Status) { case "merged": Merged = true; SuccessMessage = result.ErrorMessage is not null ? $"Merged with warning: {result.ErrorMessage}" : Loc.T("vm.merge.merged"); // Auto-close after a short delay. _ = Task.Run(async () => { await Task.Delay(1200); Avalonia.Threading.Dispatcher.UIThread.Post(() => CloseAction?.Invoke()); }); break; case "conflict": // Hand off to the in-app 3-pane merge editor when wired (MergeTask aborted // cleanly, so the resolver re-starts the merge leaving conflicts in the tree). if (RequestConflictResolution is not null) { var branch = SelectedBranch!; RoutedToResolver = true; CloseAction?.Invoke(); await RequestConflictResolution(TaskId, branch); } else { HasConflict = true; ConflictFiles = result.ConflictFiles; ErrorMessage = Loc.T("vm.merge.conflict"); } break; case "blocked": ErrorMessage = Loc.T("vm.merge.blocked", result.ErrorMessage ?? ""); break; default: ErrorMessage = Loc.T("vm.merge.unknownStatus", result.Status); break; } } catch (Exception ex) { ErrorMessage = Loc.T("vm.merge.mergeFailed", ex.Message); } finally { IsBusy = false; } } [RelayCommand] private void Cancel() => CloseAction?.Invoke(); }