feat(ui): single approve action merges the whole unit

Approve & Merge is now the only review+merge entry. For a parent with
children it drives the unit merge via the worker (conflicts still surface
through the existing PlanningMergeConflict dialog); the separate Merge All
Subtasks button, MergeAllCommand, CanMergeAll plumbing, and the dead
MergeAllPlanningAsync client method are removed. Combined-diff preview and
conflict continue/abort are kept.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-09 11:43:04 +02:00
parent 1abb429f12
commit a8b86e25e6
8 changed files with 9 additions and 159 deletions

View File

@@ -393,11 +393,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
// Planning merge controls
[ObservableProperty] private ObservableCollection<string> _mergeTargetBranches = new();
[ObservableProperty] private string? _selectedMergeTarget;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(MergeAllCommand))]
private bool _canMergeAll;
[ObservableProperty] private string? _mergeAllDisabledReason;
[ObservableProperty] private string? _mergeAllError;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowMergePreviewMuted))]
@@ -580,13 +575,11 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
Subtasks.CollectionChanged += (_, _) =>
{
RecomputeCanMergeAll();
ReviewCombinedDiffCommand.NotifyCanExecuteChanged();
};
ChildOutcomes.CollectionChanged += (_, _) =>
{
RecomputeCanMergeAll();
ReviewCombinedDiffCommand.NotifyCanExecuteChanged();
NotifySessionSections();
};
@@ -836,9 +829,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
OnPropertyChanged(nameof(HasChildOutcomes));
MergeTargetBranches.Clear();
SelectedMergeTarget = null;
CanMergeAll = false;
MergeAllDisabledReason = null;
MergeAllError = null;
SessionOutcome = null;
Roadblocks = null;
_claudeBuf.Clear();
@@ -993,7 +983,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
}
}
RecomputeCanMergeAll();
}
catch (OperationCanceledException) { }
catch { /* best-effort */ }
@@ -1090,7 +1079,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
}
}
RecomputeCanMergeAll();
}
catch (OperationCanceledException) { }
catch { /* best-effort */ }
@@ -1115,7 +1103,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
existing.WorktreeState = child.Worktree?.State ?? ClaudeDo.Data.Models.WorktreeState.Active;
}
RecomputeCanMergeAll();
}
catch { /* best-effort */ }
}
@@ -1137,54 +1124,11 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
row.Status = child.Status;
row.RoadblockCount = child.RoadblockCount;
row.WorktreeState = child.Worktree?.State ?? ClaudeDo.Data.Models.WorktreeState.Active;
RecomputeCanMergeAll();
MergeAllCommand.NotifyCanExecuteChanged();
ReviewCombinedDiffCommand.NotifyCanExecuteChanged();
}
catch { /* best-effort */ }
}
internal void RecomputeCanMergeAll()
{
// Improvement parent: merge is allowed once every child is terminal. The
// orchestrator folds the parent's own branch and skips failed/cancelled children.
if (ChildOutcomes.Count > 0)
{
var unfinished = ChildOutcomes.Count(c =>
c.Status != ClaudeDo.Data.Models.TaskStatus.Done
&& c.Status != ClaudeDo.Data.Models.TaskStatus.Failed
&& c.Status != ClaudeDo.Data.Models.TaskStatus.Cancelled);
if (unfinished > 0)
{
CanMergeAll = false;
MergeAllDisabledReason = $"{unfinished} improvement(s) not finished";
return;
}
CanMergeAll = true;
MergeAllDisabledReason = null;
return;
}
var notDone = Subtasks.Count(c => c.Status != ClaudeDo.Data.Models.TaskStatus.Done);
if (notDone > 0)
{
CanMergeAll = false;
MergeAllDisabledReason = $"{notDone} subtask(s) not done";
return;
}
var badWt = Subtasks.FirstOrDefault(c =>
c.WorktreeState == ClaudeDo.Data.Models.WorktreeState.Discarded ||
c.WorktreeState == ClaudeDo.Data.Models.WorktreeState.Kept);
if (badWt is not null)
{
CanMergeAll = false;
MergeAllDisabledReason = "at least one worktree was discarded/kept";
return;
}
CanMergeAll = true;
MergeAllDisabledReason = null;
}
[RelayCommand(CanExecute = nameof(CanReviewDiff))]
private async System.Threading.Tasks.Task ReviewCombinedDiffAsync()
{
@@ -1196,20 +1140,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
private bool CanReviewDiff() => (Task?.IsPlanningParent == true && Subtasks.Any()) || HasChildOutcomes;
[RelayCommand(CanExecute = nameof(CanMergeAll))]
private async System.Threading.Tasks.Task MergeAllAsync()
{
MergeAllError = null;
try
{
await _worker.MergeAllPlanningAsync(Task!.Id, SelectedMergeTarget ?? "main");
}
catch (Exception ex)
{
MergeAllError = ex.Message;
}
}
private async System.Threading.Tasks.Task RefreshWorktreeAsync(string taskId)
{
try
@@ -1526,8 +1456,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
if (Task is null || !_worker.IsConnected) return;
try
{
var hasChildren = Subtasks.Count > 0 || ChildOutcomes.Count > 0;
var result = await _worker.ApproveReviewAsync(Task.Id, SelectedMergeTarget ?? "");
if (result?.Status == "conflict")
if (!hasChildren && result?.Status == "conflict")
{
if (RequestConflictResolution is not null)
{
@@ -1540,6 +1471,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
MergePreviewText = text; MergeIsClean = false; MergeIsConflict = true;
}
}
// hasChildren: conflicts arrive via PlanningMergeConflictEvent → conflict dialog
}
catch { /* stale review action; broadcast reconciles */ }
}