feat(ui): queue planning subtasks sequentially and surface waiting status

Adds a "Queue subtasks sequentially" context-menu entry on rows with planning children, wires it to WorkerHub.QueuePlanningSubtasksAsync via IWorkerClient. TaskRowViewModel exposes IsWaiting/StatusChipClass for the new Waiting status, and HasPlanningChildren keeps parents expandable after they leave the planning state. TasksIslandViewModel auto-collapses parents whose every child is Done and includes Waiting children in the queued virtual list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-04-25 09:37:04 +02:00
parent 45320427e8
commit 8f94dddbc5
6 changed files with 61 additions and 2 deletions

View File

@@ -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)
{