feat(ui): show improvement-child outcomes on the parent review card + enable tree-merge

This commit is contained in:
mika kuns
2026-06-04 16:32:37 +02:00
parent 0e130177fc
commit 5d34f95fe0
4 changed files with 129 additions and 0 deletions

View File

@@ -143,6 +143,7 @@
"mergeTargetLabel": "Merge-Ziel", "mergeTargetLabel": "Merge-Ziel",
"reviewCombinedDiff": "Kombiniertes Diff prüfen", "reviewCombinedDiff": "Kombiniertes Diff prüfen",
"mergeAllSubtasks": "Alle Teilaufgaben mergen", "mergeAllSubtasks": "Alle Teilaufgaben mergen",
"childOutcomesLabel": "VERBESSERUNGEN",
"stepsLabel": "SCHRITTE", "stepsLabel": "SCHRITTE",
"addStepPlaceholder": "Schritt hinzufügen...", "addStepPlaceholder": "Schritt hinzufügen...",
"detailsLabel": "DETAILS", "detailsLabel": "DETAILS",

View File

@@ -143,6 +143,7 @@
"mergeTargetLabel": "Merge target", "mergeTargetLabel": "Merge target",
"reviewCombinedDiff": "Review combined diff", "reviewCombinedDiff": "Review combined diff",
"mergeAllSubtasks": "Merge all subtasks", "mergeAllSubtasks": "Merge all subtasks",
"childOutcomesLabel": "IMPROVEMENTS",
"stepsLabel": "STEPS", "stepsLabel": "STEPS",
"addStepPlaceholder": "Add a step...", "addStepPlaceholder": "Add a step...",
"detailsLabel": "DETAILS", "detailsLabel": "DETAILS",

View File

@@ -237,6 +237,11 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
public ObservableCollection<LogLineViewModel> Log { get; } = new(); public ObservableCollection<LogLineViewModel> Log { get; } = new();
public ObservableCollection<SubtaskRowViewModel> Subtasks { 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 = ""; [ObservableProperty] private string _newSubtaskTitle = "";
// Planning merge controls // Planning merge controls
@@ -611,6 +616,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
OnPropertyChanged(nameof(TaskIdBadge)); OnPropertyChanged(nameof(TaskIdBadge));
Log.Clear(); Log.Clear();
Subtasks.Clear(); Subtasks.Clear();
ChildOutcomes.Clear();
OnPropertyChanged(nameof(HasChildOutcomes));
MergeTargetBranches.Clear(); MergeTargetBranches.Clear();
SelectedMergeTarget = null; SelectedMergeTarget = null;
CanMergeAll = false; CanMergeAll = false;
@@ -701,10 +708,64 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
{ {
await LoadPlanningChildrenAsync(row.Id, ct); await LoadPlanningChildrenAsync(row.Id, ct);
} }
else
{
await LoadChildOutcomesAsync(row.Id, ct);
}
} }
catch (OperationCanceledException) { } 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) private async System.Threading.Tasks.Task ReplayLogFileAsync(string? logPath, CancellationToken ct)
{ {
if (string.IsNullOrWhiteSpace(logPath)) return; if (string.IsNullOrWhiteSpace(logPath)) return;
@@ -828,6 +889,25 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
internal void RecomputeCanMergeAll() 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); var notDone = Subtasks.Count(c => c.Status != ClaudeDo.Data.Models.TaskStatus.Done);
if (notDone > 0) 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.TaskStatus _status;
[ObservableProperty] private ClaudeDo.Data.Models.WorktreeState _worktreeState = ClaudeDo.Data.Models.WorktreeState.Active; [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";
}

View File

@@ -223,6 +223,28 @@
</StackPanel> </StackPanel>
</Border> </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 --> <!-- Steps section -->
<Border Classes="section-divider"> <Border Classes="section-divider">
<StackPanel Spacing="6"> <StackPanel Spacing="6">