diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs index 404472b..5aa5e16 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs @@ -148,6 +148,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase // Set by the view so DeleteTaskCommand can prompt yes/no before deleting public Func>? ConfirmAsync { get; set; } + // Set by the view so ReviewCombinedDiffCommand can show the planning diff modal + public Func? ShowPlanningDiffModal { get; set; } + // Set by the view so DeleteTaskCommand can show an error message public Func? ShowErrorAsync { get; set; } @@ -570,8 +573,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase [RelayCommand(CanExecute = nameof(CanReviewDiff))] private async System.Threading.Tasks.Task ReviewCombinedDiffAsync() { - // TODO(Task 14): open PlanningDiffView once it exists - await System.Threading.Tasks.Task.CompletedTask; + if (Task is null || ShowPlanningDiffModal is null) return; + var vm = new ViewModels.Planning.PlanningDiffViewModel(_worker, Task.Id, SelectedMergeTarget ?? "main"); + await vm.InitializeAsync(); + await ShowPlanningDiffModal(vm); } private bool CanReviewDiff() => Task?.IsPlanningParent == true && Subtasks.Any(); diff --git a/src/ClaudeDo.Ui/ViewModels/Planning/PlanningDiffViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Planning/PlanningDiffViewModel.cs new file mode 100644 index 0000000..485ceed --- /dev/null +++ b/src/ClaudeDo.Ui/ViewModels/Planning/PlanningDiffViewModel.cs @@ -0,0 +1,91 @@ +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using ClaudeDo.Ui.Services; + +namespace ClaudeDo.Ui.ViewModels.Planning; + +public sealed partial class PlanningDiffViewModel : ObservableObject +{ + private readonly IWorkerClient _worker; + private readonly string _planningTaskId; + private readonly string _targetBranch; + + public ObservableCollection Subtasks { get; } = new(); + + [ObservableProperty] private SubtaskDiffRow? _selectedSubtask; + [ObservableProperty] private string _displayedDiff = ""; + [ObservableProperty] private bool _isCombinedMode; + [ObservableProperty] private string? _combinedWarning; + [ObservableProperty] private bool _isLoadingCombined; + + public Action? CloseAction { get; set; } + + public PlanningDiffViewModel(IWorkerClient worker, string planningTaskId, string targetBranch) + { + _worker = worker; + _planningTaskId = planningTaskId; + _targetBranch = targetBranch; + } + + [RelayCommand] + private void Close() => CloseAction?.Invoke(); + + public async Task InitializeAsync() + { + var items = await _worker.GetPlanningAggregateAsync(_planningTaskId); + Subtasks.Clear(); + foreach (var i in items) + Subtasks.Add(new SubtaskDiffRow(i.SubtaskId, i.Title, i.DiffStat, i.UnifiedDiff)); + SelectedSubtask = Subtasks.FirstOrDefault(); + } + + partial void OnSelectedSubtaskChanged(SubtaskDiffRow? value) + { + if (!IsCombinedMode) + DisplayedDiff = value?.UnifiedDiff ?? ""; + } + + [RelayCommand] + private async Task ToggleCombinedAsync() + { + if (IsCombinedMode) + { + IsLoadingCombined = true; + try + { + var result = await _worker.BuildPlanningIntegrationBranchAsync(_planningTaskId, _targetBranch); + + if (result is null) + { + DisplayedDiff = ""; + CombinedWarning = "Could not build combined preview (hub error)."; + } + else if (result.Success) + { + DisplayedDiff = result.UnifiedDiff ?? ""; + CombinedWarning = null; + } + else + { + var files = result.ConflictedFiles?.Count ?? 0; + CombinedWarning = $"Cannot build combined preview: subtask {result.FirstConflictSubtaskId} conflicts with an earlier subtask ({files} files)."; + DisplayedDiff = ""; + } + } + finally + { + IsLoadingCombined = false; + } + } + else + { + DisplayedDiff = SelectedSubtask?.UnifiedDiff ?? ""; + CombinedWarning = null; + } + } + + partial void OnIsCombinedModeChanged(bool value) => ToggleCombinedCommand.Execute(null); +} + +public sealed record SubtaskDiffRow(string Id, string Title, string? DiffStat, string UnifiedDiff); diff --git a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs index 62016b7..9b2816f 100644 --- a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs +++ b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs @@ -5,6 +5,7 @@ using Avalonia.Layout; using Avalonia.Media; using ClaudeDo.Ui.ViewModels.Islands; using ClaudeDo.Ui.Views.Modals; +using ClaudeDo.Ui.Views.Planning; namespace ClaudeDo.Ui.Views.Islands; @@ -44,6 +45,14 @@ public partial class DetailsIslandView : UserControl await modal.ShowDialog(owner); }; + vm.ShowPlanningDiffModal = async (planningDiffVm) => + { + var owner = TopLevel.GetTopLevel(this) as Window; + if (owner == null) return; + var modal = new PlanningDiffView { DataContext = planningDiffVm }; + await modal.ShowDialog(owner); + }; + vm.ConfirmAsync = ShowConfirmAsync; vm.ShowErrorAsync = ShowErrorDialogAsync; } diff --git a/src/ClaudeDo.Ui/Views/Planning/PlanningDiffView.axaml b/src/ClaudeDo.Ui/Views/Planning/PlanningDiffView.axaml new file mode 100644 index 0000000..1db85a5 --- /dev/null +++ b/src/ClaudeDo.Ui/Views/Planning/PlanningDiffView.axaml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + +