fix(ui): live-update child outcomes + enable Review combined diff for improvement parents

This commit is contained in:
mika kuns
2026-06-04 16:53:43 +02:00
parent 469e68bbc8
commit a3f407b0e5

View File

@@ -358,6 +358,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
_worker.TaskStartedEvent += (slot, taskId, startedAt) => _worker.TaskStartedEvent += (slot, taskId, startedAt) =>
{ {
if (Task?.Id == taskId) AgentState = "running"; if (Task?.Id == taskId) AgentState = "running";
_ = RefreshChildOutcomeAsync(taskId);
}; };
_worker.TaskFinishedEvent += (slot, taskId, status, finishedAt) => _worker.TaskFinishedEvent += (slot, taskId, status, finishedAt) =>
{ {
@@ -371,18 +372,21 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
AgentState = FinishedStatusToStateKey(status); AgentState = FinishedStatusToStateKey(status);
// Re-query to pick up worktree created during the run. // Re-query to pick up worktree created during the run.
_ = RefreshWorktreeAsync(taskId); _ = RefreshWorktreeAsync(taskId);
_ = RefreshChildOutcomeAsync(taskId);
}; };
_worker.WorktreeUpdatedEvent += taskId => _worker.WorktreeUpdatedEvent += taskId =>
{ {
if (Task?.Id == taskId) _ = RefreshWorktreeAsync(taskId); if (Task?.Id == taskId) _ = RefreshWorktreeAsync(taskId);
if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId); if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId);
_ = RefreshChildOutcomeAsync(taskId);
}; };
_worker.TaskUpdatedEvent += taskId => _worker.TaskUpdatedEvent += taskId =>
{ {
if (Task?.Id == taskId) _ = RefreshStatusAsync(taskId); if (Task?.Id == taskId) _ = RefreshStatusAsync(taskId);
if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId); if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId);
_ = RefreshChildOutcomeAsync(taskId);
}; };
Subtasks.CollectionChanged += (_, _) => Subtasks.CollectionChanged += (_, _) =>
@@ -391,6 +395,12 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
ReviewCombinedDiffCommand.NotifyCanExecuteChanged(); ReviewCombinedDiffCommand.NotifyCanExecuteChanged();
}; };
ChildOutcomes.CollectionChanged += (_, _) =>
{
RecomputeCanMergeAll();
ReviewCombinedDiffCommand.NotifyCanExecuteChanged();
};
PrepLog.CollectionChanged += (_, _) => OnPropertyChanged(nameof(ShowPrepEmptyState)); PrepLog.CollectionChanged += (_, _) => OnPropertyChanged(nameof(ShowPrepEmptyState));
} }
@@ -887,6 +897,30 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
catch { /* best-effort */ } 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() internal void RecomputeCanMergeAll()
{ {
// Improvement parent: merge is allowed once every child is terminal. The // Improvement parent: merge is allowed once every child is terminal. The
@@ -937,7 +971,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
await ShowPlanningDiffModal(vm); await ShowPlanningDiffModal(vm);
} }
private bool CanReviewDiff() => Task?.IsPlanningParent == true && Subtasks.Any(); private bool CanReviewDiff() => (Task?.IsPlanningParent == true && Subtasks.Any()) || HasChildOutcomes;
[RelayCommand(CanExecute = nameof(CanMergeAll))] [RelayCommand(CanExecute = nameof(CanMergeAll))]
private async System.Threading.Tasks.Task MergeAllAsync() 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; [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. // A suggested child's outcome on an improvement parent's review card. Observable so the
public sealed class ChildOutcomeRowViewModel // 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 Id { get; init; }
public required string Title { get; init; } public required string Title { get; init; }
public required ClaudeDo.Data.Models.TaskStatus Status { get; init; }
public int RoadblockCount { get; init; } [ObservableProperty]
public ClaudeDo.Data.Models.WorktreeState WorktreeState { get; init; } [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 public string StatusLabel => Status switch
{ {