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 @@
+