feat(ui): show improvement-child outcomes on the parent review card + enable tree-merge
This commit is contained in:
@@ -237,6 +237,11 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
||||
public ObservableCollection<LogLineViewModel> Log { get; } = new();
|
||||
public ObservableCollection<SubtaskRowViewModel> Subtasks { get; } = new();
|
||||
|
||||
// Agent-suggested improvement children of a non-planning parent, surfaced on its
|
||||
// review card with each child's outcome and rolled-up roadblock count.
|
||||
public ObservableCollection<ChildOutcomeRowViewModel> ChildOutcomes { get; } = new();
|
||||
public bool HasChildOutcomes => ChildOutcomes.Count > 0;
|
||||
|
||||
[ObservableProperty] private string _newSubtaskTitle = "";
|
||||
|
||||
// Planning merge controls
|
||||
@@ -611,6 +616,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(TaskIdBadge));
|
||||
Log.Clear();
|
||||
Subtasks.Clear();
|
||||
ChildOutcomes.Clear();
|
||||
OnPropertyChanged(nameof(HasChildOutcomes));
|
||||
MergeTargetBranches.Clear();
|
||||
SelectedMergeTarget = null;
|
||||
CanMergeAll = false;
|
||||
@@ -701,10 +708,64 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
||||
{
|
||||
await LoadPlanningChildrenAsync(row.Id, ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
await LoadChildOutcomesAsync(row.Id, ct);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
|
||||
// Improvement parents (non-planning) surface their children's outcomes + roadblocks
|
||||
// on the review card, and reuse the planning merge controls to fold the tree in.
|
||||
private async System.Threading.Tasks.Task LoadChildOutcomesAsync(string parentTaskId, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
var children = await ctx.Tasks
|
||||
.AsNoTracking()
|
||||
.Include(t => t.Worktree)
|
||||
.Where(t => t.ParentTaskId == parentTaskId)
|
||||
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
if (children.Count == 0) return;
|
||||
|
||||
ChildOutcomes.Clear();
|
||||
foreach (var c in children)
|
||||
ChildOutcomes.Add(new ChildOutcomeRowViewModel
|
||||
{
|
||||
Id = c.Id,
|
||||
Title = c.Title,
|
||||
Status = c.Status,
|
||||
RoadblockCount = c.RoadblockCount,
|
||||
WorktreeState = c.Worktree?.State ?? ClaudeDo.Data.Models.WorktreeState.Active,
|
||||
});
|
||||
OnPropertyChanged(nameof(HasChildOutcomes));
|
||||
|
||||
if (MergeTargetBranches.Count == 0)
|
||||
{
|
||||
var childWithWorktree = children.FirstOrDefault(c => c.Worktree != null);
|
||||
if (childWithWorktree != null)
|
||||
{
|
||||
var targets = await _worker.GetMergeTargetsAsync(childWithWorktree.Id);
|
||||
if (targets != null)
|
||||
{
|
||||
MergeTargetBranches.Clear();
|
||||
foreach (var b in targets.LocalBranches)
|
||||
MergeTargetBranches.Add(b);
|
||||
SelectedMergeTarget = targets.DefaultBranch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RecomputeCanMergeAll();
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch { /* best-effort */ }
|
||||
}
|
||||
|
||||
private async System.Threading.Tasks.Task ReplayLogFileAsync(string? logPath, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(logPath)) return;
|
||||
@@ -828,6 +889,25 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -1202,3 +1282,28 @@ public sealed partial class SubtaskRowViewModel : ViewModelBase
|
||||
[ObservableProperty] private ClaudeDo.Data.Models.TaskStatus _status;
|
||||
[ObservableProperty] private ClaudeDo.Data.Models.WorktreeState _worktreeState = ClaudeDo.Data.Models.WorktreeState.Active;
|
||||
}
|
||||
|
||||
// Read-only row on an improvement parent's review card: a suggested child's outcome.
|
||||
public sealed class ChildOutcomeRowViewModel
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Title { get; init; }
|
||||
public required ClaudeDo.Data.Models.TaskStatus Status { get; init; }
|
||||
public int RoadblockCount { get; init; }
|
||||
public ClaudeDo.Data.Models.WorktreeState WorktreeState { get; init; }
|
||||
|
||||
public string StatusLabel => Status switch
|
||||
{
|
||||
ClaudeDo.Data.Models.TaskStatus.Done => Loc.T("vm.taskStatus.done"),
|
||||
ClaudeDo.Data.Models.TaskStatus.Failed => Loc.T("vm.taskStatus.failed"),
|
||||
ClaudeDo.Data.Models.TaskStatus.Cancelled => Loc.T("vm.taskStatus.cancelled"),
|
||||
ClaudeDo.Data.Models.TaskStatus.Running => Loc.T("vm.taskStatus.running"),
|
||||
ClaudeDo.Data.Models.TaskStatus.Queued => Loc.T("vm.taskStatus.queued"),
|
||||
ClaudeDo.Data.Models.TaskStatus.WaitingForReview => Loc.T("vm.taskStatus.waitingForReview"),
|
||||
ClaudeDo.Data.Models.TaskStatus.WaitingForChildren => Loc.T("vm.taskStatus.waitingForChildren"),
|
||||
_ => Loc.T("vm.taskStatus.idle"),
|
||||
};
|
||||
|
||||
public bool HasRoadblock => RoadblockCount > 0;
|
||||
public string RoadblockText => RoadblockCount == 1 ? "1 roadblock" : $"{RoadblockCount} roadblocks";
|
||||
}
|
||||
|
||||
@@ -223,6 +223,28 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Improvement-children outcomes — visible when this task has agent-suggested children -->
|
||||
<Border Classes="section-divider"
|
||||
IsVisible="{Binding HasChildOutcomes}">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Classes="section-label" Text="{loc:Tr details.childOutcomesLabel}" Margin="0,0,0,2"/>
|
||||
<ItemsControl ItemsSource="{Binding ChildOutcomes}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ChildOutcomeRowViewModel">
|
||||
<Grid ColumnDefinitions="*,Auto,Auto" Margin="0,2">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Title}" TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding RoadblockText}"
|
||||
IsVisible="{Binding HasRoadblock}"
|
||||
Foreground="#E0A030" Margin="8,0" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="2" Text="{Binding StatusLabel}"
|
||||
Opacity="0.75" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Steps section -->
|
||||
<Border Classes="section-divider">
|
||||
<StackPanel Spacing="6">
|
||||
|
||||
Reference in New Issue
Block a user