diff --git a/src/ClaudeDo.Ui/Services/IWorkerClient.cs b/src/ClaudeDo.Ui/Services/IWorkerClient.cs index a419f36..347e90d 100644 --- a/src/ClaudeDo.Ui/Services/IWorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/IWorkerClient.cs @@ -38,4 +38,5 @@ public interface IWorkerClient : INotifyPropertyChanged Task MergeAllPlanningAsync(string planningTaskId, string targetBranch); Task ContinuePlanningMergeAsync(string planningTaskId); Task AbortPlanningMergeAsync(string planningTaskId); + Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default); } diff --git a/src/ClaudeDo.Ui/Services/WorkerClient.cs b/src/ClaudeDo.Ui/Services/WorkerClient.cs index 859b1a7..e8c096c 100644 --- a/src/ClaudeDo.Ui/Services/WorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/WorkerClient.cs @@ -436,6 +436,11 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC await _hub.InvokeAsync("AbortPlanningMerge", planningTaskId); } + public async Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) + { + await _hub.InvokeAsync("QueuePlanningSubtasksAsync", parentTaskId, ct); + } + // IWorkerClient explicit implementations (drop typed return values) async Task IWorkerClient.StartPlanningSessionAsync(string taskId, CancellationToken ct) => await StartPlanningSessionAsync(taskId, ct); diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs index 5f5142f..9b7e576 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs @@ -25,6 +25,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase [ObservableProperty] private bool _dropHintBelow; [ObservableProperty] private string? _parentTaskId; [ObservableProperty] private bool _isExpanded = true; + [ObservableProperty] private bool _hasPlanningChildren; public DateTime CreatedAt { get; init; } public string CreatedAtFormatted => CreatedAt == default ? "—" : $"Created {CreatedAt:MMM d}"; @@ -34,7 +35,9 @@ public sealed partial class TaskRowViewModel : ViewModelBase public int StepsCompleted { get; init; } public bool IsChild => !string.IsNullOrEmpty(ParentTaskId); - public bool IsPlanningParent => Status == TaskStatus.Planning || Status == TaskStatus.Planned; + public bool IsPlanningParent => Status == TaskStatus.Planning + || Status == TaskStatus.Planned + || HasPlanningChildren; public bool IsDraft => Status == TaskStatus.Draft; public bool CanOpenPlanningSession => Status == TaskStatus.Manual && !IsChild; @@ -54,6 +57,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase public bool IsOverdue => ScheduledFor is { } d && d.Date < DateTime.Today && !Done; public bool IsRunning => Status == TaskStatus.Running; public bool IsQueued => Status == TaskStatus.Queued; + public bool IsWaiting => Status == TaskStatus.Waiting; public bool HasSchedule => ScheduledFor.HasValue; public bool HasLiveTail => IsRunning && !string.IsNullOrEmpty(LiveTail); @@ -67,6 +71,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase TaskStatus.Failed => "error", TaskStatus.Done => "review", TaskStatus.Queued => "queued", + TaskStatus.Waiting => "waiting", _ => "idle", }; @@ -75,6 +80,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase OnPropertyChanged(nameof(StatusChipClass)); OnPropertyChanged(nameof(IsRunning)); OnPropertyChanged(nameof(IsQueued)); + OnPropertyChanged(nameof(IsWaiting)); OnPropertyChanged(nameof(HasLiveTail)); OnPropertyChanged(nameof(IsPlanningParent)); OnPropertyChanged(nameof(PlanningBadge)); @@ -89,6 +95,9 @@ public sealed partial class TaskRowViewModel : ViewModelBase OnPropertyChanged(nameof(CanOpenPlanningSession)); } + partial void OnHasPlanningChildrenChanged(bool value) + => OnPropertyChanged(nameof(IsPlanningParent)); + partial void OnBranchChanged(string? value) => OnPropertyChanged(nameof(HasBranch)); partial void OnLiveTailChanged(string? value) => OnPropertyChanged(nameof(HasLiveTail)); partial void OnDoneChanged(bool value) => OnPropertyChanged(nameof(IsOverdue)); diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs index db74847..c2ddfcd 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs @@ -176,7 +176,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase ListKind.Smart when list.Id == "smart:planned" => all.Where(t => t.ScheduledFor != null), ListKind.Virtual when list.Id == "virtual:queued" => all.Where(t => (t.Status == TaskStatus.Queued && t.ParentTaskId == null) || - (IsPlanningStatus(t.Status) && all.Any(c => c.ParentTaskId == t.Id && c.Status == TaskStatus.Queued))), + (IsPlanningStatus(t.Status) && all.Any(c => c.ParentTaskId == t.Id && (c.Status == TaskStatus.Queued || c.Status == TaskStatus.Waiting)))), ListKind.Virtual when list.Id == "virtual:running" => all.Where(t => (t.Status == TaskStatus.Running && t.ParentTaskId == null) || (IsPlanningStatus(t.Status) && all.Any(c => c.ParentTaskId == t.Id && c.Status == TaskStatus.Running))), @@ -197,6 +197,16 @@ public sealed partial class TasksIslandViewModel : ViewModelBase foreach (var t in filteredList) Items.Add(TaskRowViewModel.FromEntity(t)); + // Mark any top-level row that has at least one child as a planning parent, + // so its subtasks remain expandable even after the parent is queued/running. + var parentsWithChildren = Items + .Where(r => r.IsChild && !string.IsNullOrEmpty(r.ParentTaskId)) + .Select(r => r.ParentTaskId!) + .ToHashSet(); + foreach (var r in Items) + if (parentsWithChildren.Contains(r.Id)) + r.HasPlanningChildren = true; + Regroup(); UpdateSubtitle(); } @@ -209,6 +219,23 @@ public sealed partial class TasksIslandViewModel : ViewModelBase OpenItems.Clear(); CompletedItems.Clear(); + // Auto-collapse planning parents whose every child is Done (unless the user + // has explicitly toggled the row — saved state wins). + var childrenByParent = Items + .Where(r => r.IsChild && !string.IsNullOrEmpty(r.ParentTaskId)) + .GroupBy(r => r.ParentTaskId!) + .ToDictionary(g => g.Key, g => g.ToList()); + foreach (var parent in Items.Where(r => r.IsPlanningParent && !r.IsChild)) + { + if (_expandedState.ContainsKey(parent.Id)) continue; + if (childrenByParent.TryGetValue(parent.Id, out var kids) + && kids.Count > 0 + && kids.All(c => c.Status == TaskStatus.Done)) + { + parent.IsExpanded = false; + } + } + // Restore IsExpanded from saved state foreach (var r in Items) { @@ -537,6 +564,14 @@ public sealed partial class TasksIslandViewModel : ViewModelBase catch { } } + [RelayCommand] + private async Task QueuePlanningSubtasksAsync(TaskRowViewModel? row) + { + if (row is null || _worker is null) return; + try { await _worker.QueuePlanningSubtasksAsync(row.Id); } + catch { } + } + [RelayCommand] private async Task FinalizePlanningSessionAsync(TaskRowViewModel? row) { diff --git a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml index d72d00d..0c701ef 100644 --- a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml +++ b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml @@ -47,6 +47,9 @@ +