diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs index da00ea0..14da506 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs @@ -358,6 +358,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase _worker.TaskStartedEvent += (slot, taskId, startedAt) => { if (Task?.Id == taskId) AgentState = "running"; + _ = RefreshChildOutcomeAsync(taskId); }; _worker.TaskFinishedEvent += (slot, taskId, status, finishedAt) => { @@ -371,18 +372,21 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase AgentState = FinishedStatusToStateKey(status); // Re-query to pick up worktree created during the run. _ = RefreshWorktreeAsync(taskId); + _ = RefreshChildOutcomeAsync(taskId); }; _worker.WorktreeUpdatedEvent += taskId => { if (Task?.Id == taskId) _ = RefreshWorktreeAsync(taskId); if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId); + _ = RefreshChildOutcomeAsync(taskId); }; _worker.TaskUpdatedEvent += taskId => { if (Task?.Id == taskId) _ = RefreshStatusAsync(taskId); if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId); + _ = RefreshChildOutcomeAsync(taskId); }; Subtasks.CollectionChanged += (_, _) => @@ -391,6 +395,12 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase ReviewCombinedDiffCommand.NotifyCanExecuteChanged(); }; + ChildOutcomes.CollectionChanged += (_, _) => + { + RecomputeCanMergeAll(); + ReviewCombinedDiffCommand.NotifyCanExecuteChanged(); + }; + PrepLog.CollectionChanged += (_, _) => OnPropertyChanged(nameof(ShowPrepEmptyState)); } @@ -887,6 +897,30 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase catch { /* best-effort */ } } + // Live-update a single improvement child's outcome row from a task event. No-op if the + // updated task isn't one of this parent's children. + private async System.Threading.Tasks.Task RefreshChildOutcomeAsync(string childTaskId) + { + var row = ChildOutcomes.FirstOrDefault(c => c.Id == childTaskId); + if (row is null) return; + try + { + await using var ctx = await _dbFactory.CreateDbContextAsync(); + var child = await ctx.Tasks + .AsNoTracking() + .Include(t => t.Worktree) + .FirstOrDefaultAsync(t => t.Id == childTaskId); + if (child is null) return; + 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 @@ -937,7 +971,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase await ShowPlanningDiffModal(vm); } - private bool CanReviewDiff() => Task?.IsPlanningParent == true && Subtasks.Any(); + private bool CanReviewDiff() => (Task?.IsPlanningParent == true && Subtasks.Any()) || HasChildOutcomes; [RelayCommand(CanExecute = nameof(CanMergeAll))] private async System.Threading.Tasks.Task MergeAllAsync() @@ -1283,14 +1317,24 @@ public sealed partial class SubtaskRowViewModel : ViewModelBase [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 +// A suggested child's outcome on an improvement parent's review card. Observable so the +// row reflects the child's live status (Idle → Running → Done/Failed) as it executes. +public sealed partial class ChildOutcomeRowViewModel : ViewModelBase { 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; } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(StatusLabel))] + private ClaudeDo.Data.Models.TaskStatus _status; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasRoadblock))] + [NotifyPropertyChangedFor(nameof(RoadblockText))] + private int _roadblockCount; + + [ObservableProperty] + private ClaudeDo.Data.Models.WorktreeState _worktreeState = ClaudeDo.Data.Models.WorktreeState.Active; public string StatusLabel => Status switch {