From 5d34f95fe0d86ee8cb67dbe891cdff220e353425 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 4 Jun 2026 16:32:37 +0200 Subject: [PATCH] feat(ui): show improvement-child outcomes on the parent review card + enable tree-merge --- src/ClaudeDo.Localization/locales/de.json | 1 + src/ClaudeDo.Localization/locales/en.json | 1 + .../Islands/DetailsIslandViewModel.cs | 105 ++++++++++++++++++ .../Views/Islands/DetailsIslandView.axaml | 22 ++++ 4 files changed, 129 insertions(+) diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index 044baac..46a30ad 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -143,6 +143,7 @@ "mergeTargetLabel": "Merge-Ziel", "reviewCombinedDiff": "Kombiniertes Diff prüfen", "mergeAllSubtasks": "Alle Teilaufgaben mergen", + "childOutcomesLabel": "VERBESSERUNGEN", "stepsLabel": "SCHRITTE", "addStepPlaceholder": "Schritt hinzufügen...", "detailsLabel": "DETAILS", diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index b37a8a4..83f29fe 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -143,6 +143,7 @@ "mergeTargetLabel": "Merge target", "reviewCombinedDiff": "Review combined diff", "mergeAllSubtasks": "Merge all subtasks", + "childOutcomesLabel": "IMPROVEMENTS", "stepsLabel": "STEPS", "addStepPlaceholder": "Add a step...", "detailsLabel": "DETAILS", diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs index 1ee723b..da00ea0 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs @@ -237,6 +237,11 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase public ObservableCollection Log { get; } = new(); public ObservableCollection 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 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"; +} diff --git a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml index eda909f..28bbed6 100644 --- a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml +++ b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml @@ -223,6 +223,28 @@ + + + + + + + + + + + + + + + + + +