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:
@@ -59,7 +59,6 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId);
|
||||
Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId);
|
||||
Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch);
|
||||
Task MergeAllPlanningAsync(string planningTaskId, string targetBranch);
|
||||
Task ContinuePlanningMergeAsync(string planningTaskId);
|
||||
Task AbortPlanningMergeAsync(string planningTaskId);
|
||||
Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default);
|
||||
|
||||
@@ -66,7 +66,7 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
|
||||
public event Action<PrimeFiredEvent>? PrimeFired;
|
||||
|
||||
public string? LastMergeAllTarget { get; private set; }
|
||||
public string? LastApproveTarget { get; private set; }
|
||||
|
||||
public WorkerClient(string signalRUrl)
|
||||
{
|
||||
@@ -412,7 +412,10 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
}
|
||||
|
||||
public Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch)
|
||||
=> TryInvokeAsync<MergeResultDto>("ApproveReview", taskId, targetBranch);
|
||||
{
|
||||
LastApproveTarget = targetBranch;
|
||||
return TryInvokeAsync<MergeResultDto>("ApproveReview", taskId, targetBranch);
|
||||
}
|
||||
|
||||
public Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch)
|
||||
=> TryInvokeAsync<MergePreviewDto>("PreviewMerge", taskId, targetBranch);
|
||||
@@ -486,12 +489,6 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
public Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch)
|
||||
=> TryInvokeAsync<CombinedDiffResultDto>("BuildPlanningIntegrationBranch", planningTaskId, targetBranch);
|
||||
|
||||
public async Task MergeAllPlanningAsync(string planningTaskId, string targetBranch)
|
||||
{
|
||||
LastMergeAllTarget = targetBranch;
|
||||
await _hub.InvokeAsync("MergeAllPlanning", planningTaskId, targetBranch);
|
||||
}
|
||||
|
||||
public async Task ContinuePlanningMergeAsync(string planningTaskId)
|
||||
{
|
||||
await _hub.InvokeAsync("ContinuePlanningMerge", planningTaskId);
|
||||
|
||||
@@ -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 */ }
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
|
||||
string subtaskTitle = subtaskId;
|
||||
string worktreePath = System.Environment.CurrentDirectory;
|
||||
string targetBranch = Worker?.LastMergeAllTarget ?? "main";
|
||||
string targetBranch = Worker?.LastApproveTarget ?? "main";
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -305,17 +305,7 @@
|
||||
</Button>
|
||||
<Button Classes="btn" Content="Review Combined Diff" Margin="0,0,8,8"
|
||||
Command="{Binding ReviewCombinedDiffCommand}" />
|
||||
<Button Classes="btn accent" Content="Merge All Subtasks" Margin="0,0,0,8"
|
||||
Command="{Binding MergeAllCommand}"
|
||||
IsEnabled="{Binding CanMergeAll}"
|
||||
ToolTip.Tip="{Binding MergeAllDisabledReason}" />
|
||||
</WrapPanel>
|
||||
|
||||
<TextBlock Text="{Binding MergeAllError}"
|
||||
Foreground="{DynamicResource BloodBrush}"
|
||||
TextWrapping="Wrap"
|
||||
IsVisible="{Binding MergeAllError,
|
||||
Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user