# Planning UX Polish + Sequential Subtask Queue — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add sequential execution of planning subtasks (new `Waiting` status, context-menu trigger, worker-side chain advancement) plus three small UX changes (auto-collapse done planning parents in the task list, collapsible Description in the Details pane, narrower island GridSplitters). **Architecture:** Foundation first — add the new `Waiting` enum value and its surface in the UI (chip, virtual-queued filter, row plumbing). Then ship the three UI polish items independently. Finally build the worker-side chain coordinator behind TDD and wire up the SignalR method + context-menu entry. **Tech Stack:** .NET 8, Avalonia 12, CommunityToolkit.Mvvm, EF Core (Sqlite), SignalR, xUnit. **Spec:** `docs/superpowers/specs/2026-04-24-planning-ux-and-sequential-subtasks-design.md` --- ## Task 1: Add `Waiting` status to the enum **Files:** - Modify: `src/ClaudeDo.Data/Models/TaskEntity.cs` (TaskStatus enum) - Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs` (chip class switch) - Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` (virtual-queued match predicate) - [ ] **Step 1: Add `Waiting` to the enum** Append `Waiting` as the last value (keeps existing numeric slots stable for any int-serialized rows). `src/ClaudeDo.Data/Models/TaskEntity.cs`: ```csharp public enum TaskStatus { Manual, Queued, Running, Done, Failed, Planning, Planned, Draft, Waiting, } ``` - [ ] **Step 2: Extend `StatusChipClass` switch in TaskRowViewModel** `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs` — update the switch: ```csharp public string StatusChipClass => Status switch { TaskStatus.Running => "running", TaskStatus.Failed => "error", TaskStatus.Done => "review", TaskStatus.Queued => "queued", TaskStatus.Waiting => "waiting", _ => "idle", }; ``` - [ ] **Step 3: Add `IsWaiting` and include it in virtual-queued matching** In the same `TaskRowViewModel.cs`, add alongside `IsQueued`: ```csharp public bool IsWaiting => Status == TaskStatus.Waiting; ``` In `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs`, find the `TaskMatchesList` static method and update the `virtual:queued` branch so tasks in `Waiting` also match. Locate the existing match for `ListKind.Virtual when list.Id == "virtual:queued"` and change it to match `t.Status == TaskStatus.Queued || t.Status == TaskStatus.Waiting`. If the existing line reads `t.Status == TaskStatus.Queued` exactly, replace it with `t.Status == TaskStatus.Queued || t.Status == TaskStatus.Waiting`. - [ ] **Step 4: Build** Run: ```bash dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj ``` Expected: both build with 0 errors. Existing warnings OK. - [ ] **Step 5: Commit** ```bash git add src/ClaudeDo.Data/Models/TaskEntity.cs \ src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs \ src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs git commit -m "feat(data): add Waiting task status and include it in virtual:queued" ``` --- ## Task 2: Narrower island GridSplitters **Files:** - Modify: `src/ClaudeDo.Ui/Views/MainWindow.axaml` (lines 158 and 170) - [ ] **Step 1: Halve the splitter width** Both `GridSplitter` elements currently use `Width="5"`. Change both to `Width="3"`. Leave all other attributes untouched. - [ ] **Step 2: Build** ```bash dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj ``` Expected: 0 errors. - [ ] **Step 3: Commit** ```bash git add src/ClaudeDo.Ui/Views/MainWindow.axaml git commit -m "style(ui): narrow island GridSplitters from 5 to 3" ``` --- ## Task 3: Collapsible Description section in Details pane **Files:** - Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` - Modify: `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml` - [ ] **Step 1: Add observable flag + toggle command** In `DetailsIslandViewModel.cs`, add beside the existing editable fields: ```csharp [ObservableProperty] private bool _isDescriptionExpanded = true; [RelayCommand] private void ToggleDescriptionExpanded() => IsDescriptionExpanded = !IsDescriptionExpanded; ``` - [ ] **Step 2: Reset flag when a new task is loaded** Find the method that handles a new `Task` being bound (the existing `OnTaskChanged` / `Bind` path — it's the spot that already sets `EditableTitle`, `EditableDescription`, etc.). At the start of the load path where fields get reset, add: ```csharp IsDescriptionExpanded = true; ``` (If the reset is scattered, put it next to the `EditableDescription = ""` assignment.) - [ ] **Step 3: Wrap the description TextBox in a collapsible section** In `DetailsIslandView.axaml`, locate the description TextBox. Wrap it so it looks like: ```xml ``` If the existing `Icon.ChevronDown` / `Icon.ChevronRight` static resources don't exist, inspect `App.axaml` (or wherever `StaticResource Icon.*` icons live) and pick the closest existing chevron pair. If only one direction exists, use a simple `▾` / `▸` TextBlock substitute: ```xml ``` - [ ] **Step 4: Build** ```bash dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj ``` Expected: 0 errors. - [ ] **Step 5: Manual verify** Launch the app (`dotnet run --project src/ClaudeDo.App`), open a task with a description, click the chevron. Verify the body collapses/expands; verify opening a different task restores the expanded default. - [ ] **Step 6: Commit** ```bash git add src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs \ src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml git commit -m "feat(ui): collapsible description section in details pane" ``` --- ## Task 4: Auto-collapse done planning parents in task list **Files:** - Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs` - Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` - Modify: `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml` - [ ] **Step 1: Add expansion state + "all children done" flag to `TaskRowViewModel`** In `TaskRowViewModel.cs`, add below the existing observable properties: ```csharp [ObservableProperty] private bool _areChildrenExpanded = true; [ObservableProperty] private bool _allChildrenDone; partial void OnAllChildrenDoneChanged(bool value) { // Default children to collapsed once the planning parent is fully done. if (value) AreChildrenExpanded = false; } [RelayCommand] private void ToggleChildrenExpanded() => AreChildrenExpanded = !AreChildrenExpanded; ``` - [ ] **Step 2: Compute `AllChildrenDone` during Regroup in `TasksIslandViewModel`** In `TasksIslandViewModel.cs`, locate the `Regroup()` method (the one that clears and repopulates `OverdueItems`/`OpenItems`/`CompletedItems`). Before it distributes rows, build a lookup of children by parent id: ```csharp var childrenByParent = Items .Where(r => r.IsChild && r.ParentTaskId is not null) .GroupBy(r => r.ParentTaskId!) .ToDictionary(g => g.Key, g => g.ToList()); foreach (var parent in Items.Where(r => r.IsPlanningParent && !r.IsChild)) { if (childrenByParent.TryGetValue(parent.Id, out var kids) && kids.Count > 0) parent.AllChildrenDone = kids.All(c => c.Status == TaskStatus.Done); else parent.AllChildrenDone = false; } ``` Then inside the existing distribution loop, skip child rows whose parent row has `AreChildrenExpanded == false`: ```csharp foreach (var row in Items) { if (row.IsChild && row.ParentTaskId is not null) { var parentRow = Items.FirstOrDefault(p => p.Id == row.ParentTaskId); if (parentRow is not null && !parentRow.AreChildrenExpanded) continue; } // ... existing distribution into Overdue/Open/Completed ... } ``` If `Regroup()` currently uses LINQ expressions instead of a loop, split them out into explicit foreach so the skip is clear. Keep the overdue/completed logic intact — children of a collapsed parent are excluded from every bucket. - [ ] **Step 3: Re-run Regroup when a row's expansion flag toggles** In `TasksIslandViewModel.cs`, in the constructor (after `Items` is created), subscribe to changes so toggling one row triggers a regroup: ```csharp Items.CollectionChanged += (_, e) => { if (e.NewItems is not null) foreach (TaskRowViewModel r in e.NewItems) r.PropertyChanged += OnItemPropertyChanged; if (e.OldItems is not null) foreach (TaskRowViewModel r in e.OldItems) r.PropertyChanged -= OnItemPropertyChanged; }; ``` Add the handler: ```csharp private void OnItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { if (e.PropertyName == nameof(TaskRowViewModel.AreChildrenExpanded)) Regroup(); } ``` - [ ] **Step 4: Add chevron toggle button to the planning-parent row** In `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml`, inside the main task card where the title/eyebrow row lives (co-located with `PlanningBadge`), add a chevron button visible only when `IsPlanningParent && HasPlanningChildren`: ```xml ``` Place it immediately before the title TextBlock in the parent-row layout. Leave child rows untouched. - [ ] **Step 5: Build** ```bash dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj ``` Expected: 0 errors. - [ ] **Step 6: Manual verify** Create a planning parent with ≥2 children. Mark both children `Done` (manually via DB if needed, or via a full planning run). Reload the list — the children should be hidden by default. Click the chevron on the parent — children appear. Click again — collapse. - [ ] **Step 7: Commit** ```bash git add src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs \ src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs \ src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml git commit -m "feat(ui): auto-collapse done planning parents in task list" ``` --- ## Task 5: PlanningChainCoordinator — worker-side chain advancement (TDD) **Files:** - Create: `src/ClaudeDo.Worker/Planning/PlanningChainCoordinator.cs` - Create: `tests/ClaudeDo.Worker.Tests/Planning/PlanningChainCoordinatorTests.cs` - [ ] **Step 1: Write the first failing test — queueing sets first child Queued, rest Waiting** Create `tests/ClaudeDo.Worker.Tests/Planning/PlanningChainCoordinatorTests.cs`: ```csharp using System.Threading.Tasks; using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Planning; using Microsoft.EntityFrameworkCore; using Xunit; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tests.Planning; public class PlanningChainCoordinatorTests { private static DbContextOptions InMemoryOptions() => new DbContextOptionsBuilder() .UseSqlite("DataSource=:memory:;Cache=Shared") .Options; private static async Task<(ClaudeDoDbContext ctx, TaskRepository repo)> NewDbAsync() { var ctx = new ClaudeDoDbContext(InMemoryOptions()); await ctx.Database.OpenConnectionAsync(); await ctx.Database.EnsureCreatedAsync(); return (ctx, new TaskRepository(ctx)); } private static async Task SeedPlanningFamily(TaskRepository repo, string parentId, int childCount) { await repo.AddAsync(new TaskEntity { Id = parentId, ListId = "L1", Title = "Parent", CreatedAt = System.DateTime.UtcNow, Status = TaskStatus.Planned, }); for (int i = 0; i < childCount; i++) { await repo.AddAsync(new TaskEntity { Id = $"{parentId}-c{i}", ListId = "L1", Title = $"Child {i}", CreatedAt = System.DateTime.UtcNow, Status = TaskStatus.Manual, ParentTaskId = parentId, SortOrder = i, }); } } [Fact] public async Task QueueSubtasksSequentially_SetsFirstQueued_RestWaiting() { var (ctx, repo) = await NewDbAsync(); await using var _ = ctx; await SeedPlanningFamily(repo, "P", 3); var coord = new PlanningChainCoordinator(repo); await coord.QueueSubtasksSequentiallyAsync("P", default); var kids = await ctx.Tasks.Where(t => t.ParentTaskId == "P").OrderBy(t => t.SortOrder).ToListAsync(); Assert.Equal(TaskStatus.Queued, kids[0].Status); Assert.Equal(TaskStatus.Waiting, kids[1].Status); Assert.Equal(TaskStatus.Waiting, kids[2].Status); } } ``` - [ ] **Step 2: Run the test — expect failure (class doesn't exist)** ```bash dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~PlanningChainCoordinatorTests ``` Expected: compile error "PlanningChainCoordinator not found". - [ ] **Step 3: Create the coordinator with the minimum to pass** `src/ClaudeDo.Worker/Planning/PlanningChainCoordinator.cs`: ```csharp using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; namespace ClaudeDo.Worker.Planning; public sealed class PlanningChainCoordinator { private readonly TaskRepository _tasks; public PlanningChainCoordinator(TaskRepository tasks) => _tasks = tasks; public async Task QueueSubtasksSequentiallyAsync(string parentTaskId, CancellationToken ct) { var parent = await _tasks.GetByIdAsync(parentTaskId, ct) ?? throw new InvalidOperationException($"Task {parentTaskId} not found."); var children = (await _tasks.GetChildrenAsync(parentTaskId, ct)) .OrderBy(t => t.SortOrder) .ToList(); if (children.Count == 0) throw new InvalidOperationException("Parent has no subtasks."); var bad = children.FirstOrDefault(c => c.Status is not (TaskStatus.Manual or TaskStatus.Planned)); if (bad is not null) throw new InvalidOperationException($"Child {bad.Id} is in status {bad.Status}; expected Manual or Planned."); for (int i = 0; i < children.Count; i++) { children[i].Status = i == 0 ? TaskStatus.Queued : TaskStatus.Waiting; await _tasks.UpdateAsync(children[i], ct); } } } ``` If `TaskRepository.GetChildrenAsync` does not yet exist, add it: ```csharp // in src/ClaudeDo.Data/Repositories/TaskRepository.cs public Task> GetChildrenAsync(string parentTaskId, CancellationToken ct = default) => _ctx.Tasks.Where(t => t.ParentTaskId == parentTaskId).ToListAsync(ct); ``` (If the repo uses `AsNoTracking()` elsewhere for reads, match that pattern. For this method we want tracked entities so `UpdateAsync` works without extra attach.) - [ ] **Step 4: Run the test — expect pass** ```bash dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~PlanningChainCoordinatorTests ``` Expected: 1 passed. - [ ] **Step 5: Add failing test — on child Done, next Waiting sibling flips to Queued** Append to `PlanningChainCoordinatorTests.cs`: ```csharp [Fact] public async Task OnChildDone_FlipsNextWaitingToQueued() { var (ctx, repo) = await NewDbAsync(); await using var _ = ctx; await SeedPlanningFamily(repo, "P", 3); var coord = new PlanningChainCoordinator(repo); await coord.QueueSubtasksSequentiallyAsync("P", default); // Simulate first child finishing Done. var first = await ctx.Tasks.FirstAsync(t => t.Id == "P-c0"); first.Status = TaskStatus.Done; await ctx.SaveChangesAsync(); var advanced = await coord.OnChildFinishedAsync("P-c0", TaskStatus.Done, default); Assert.Equal("P-c1", advanced); var kids = await ctx.Tasks.Where(t => t.ParentTaskId == "P").OrderBy(t => t.SortOrder).ToListAsync(); Assert.Equal(TaskStatus.Done, kids[0].Status); Assert.Equal(TaskStatus.Queued, kids[1].Status); Assert.Equal(TaskStatus.Waiting, kids[2].Status); } ``` - [ ] **Step 6: Run — expect failure** Expected: compile error "OnChildFinishedAsync does not exist". - [ ] **Step 7: Implement `OnChildFinishedAsync`** In `PlanningChainCoordinator.cs`: ```csharp /// /// Call after a child task transitions to a terminal status. /// Returns the id of the newly-queued sibling (if any), else null. /// public async Task OnChildFinishedAsync(string childTaskId, TaskStatus finalStatus, CancellationToken ct) { if (finalStatus != TaskStatus.Done) return null; var child = await _tasks.GetByIdAsync(childTaskId, ct); if (child?.ParentTaskId is null) return null; var siblings = (await _tasks.GetChildrenAsync(child.ParentTaskId, ct)) .OrderBy(t => t.SortOrder) .ToList(); var next = siblings .Where(s => s.SortOrder > child.SortOrder && s.Status == TaskStatus.Waiting) .FirstOrDefault(); if (next is null) return null; next.Status = TaskStatus.Queued; await _tasks.UpdateAsync(next, ct); return next.Id; } ``` - [ ] **Step 8: Run — expect pass** ```bash dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~PlanningChainCoordinatorTests ``` Expected: 2 passed. - [ ] **Step 9: Add failing test — on Failed, chain stops** ```csharp [Fact] public async Task OnChildFailed_DoesNotAdvanceChain() { var (ctx, repo) = await NewDbAsync(); await using var _ = ctx; await SeedPlanningFamily(repo, "P", 3); var coord = new PlanningChainCoordinator(repo); await coord.QueueSubtasksSequentiallyAsync("P", default); var first = await ctx.Tasks.FirstAsync(t => t.Id == "P-c0"); first.Status = TaskStatus.Failed; await ctx.SaveChangesAsync(); var advanced = await coord.OnChildFinishedAsync("P-c0", TaskStatus.Failed, default); Assert.Null(advanced); var kids = await ctx.Tasks.Where(t => t.ParentTaskId == "P").OrderBy(t => t.SortOrder).ToListAsync(); Assert.Equal(TaskStatus.Failed, kids[0].Status); Assert.Equal(TaskStatus.Waiting, kids[1].Status); Assert.Equal(TaskStatus.Waiting, kids[2].Status); } ``` - [ ] **Step 10: Run — expect pass (existing guard handles it)** ```bash dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~PlanningChainCoordinatorTests ``` Expected: 3 passed. - [ ] **Step 11: Commit** ```bash git add src/ClaudeDo.Worker/Planning/PlanningChainCoordinator.cs \ src/ClaudeDo.Data/Repositories/TaskRepository.cs \ tests/ClaudeDo.Worker.Tests/Planning/PlanningChainCoordinatorTests.cs git commit -m "feat(worker): add PlanningChainCoordinator with sequential subtask advancement" ``` --- ## Task 6: Hook chain advancement into TaskRunner finish path **Files:** - Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs` - Modify: `src/ClaudeDo.Worker/Program.cs` (DI registration) - [ ] **Step 1: Register `PlanningChainCoordinator` in DI** Locate `src/ClaudeDo.Worker/Program.cs` where other services are registered (look for `services.AddSingleton` or similar). Add: ```csharp services.AddScoped(); ``` Use `AddScoped` if `TaskRepository` is scoped (check how it's registered — match its lifetime). If `TaskRepository` is constructed ad-hoc inside the worker, add a constructor overload on `PlanningChainCoordinator` that takes `IDbContextFactory` and builds its own `TaskRepository` per call, then register as Singleton. Mirror the pattern used by `PlanningSessionManager`. - [ ] **Step 2: Inject coordinator into `TaskRunner`** In `src/ClaudeDo.Worker/Runner/TaskRunner.cs`, add `PlanningChainCoordinator` to the constructor parameter list and store it in a readonly field (match the style used for `_broadcaster`). If `TaskRunner` is not a good fit for direct injection (e.g., it's used in contexts without DI), instead inject `IServiceProvider` / `IDbContextFactory` and new-up a coordinator inside the finish handler. Pick whichever matches existing `TaskRunner` patterns. - [ ] **Step 3: Call coordinator after Done/Failed emission** Immediately after each `await _broadcaster.TaskFinished(slot, task.Id, "done", finishedAt);` on line ~338 and the two failed emissions on lines ~355 and ~372, add: ```csharp if (task.ParentTaskId is not null) { var advancedId = await _chainCoordinator.OnChildFinishedAsync( task.Id, /* Done or Failed based on path */, CancellationToken.None); if (advancedId is not null) await _broadcaster.TaskUpdated(advancedId); } ``` Use `TaskStatus.Done` in the done-path call site and `TaskStatus.Failed` in the failed-path call sites. For the failed paths that use `justFailed` rather than `task`, read `justFailed?.ParentTaskId` and `justFailed?.Id` to stay consistent with the surrounding code. After this call the existing queue-pickup loop will see the newly-Queued sibling and dispatch it on its next tick. - [ ] **Step 4: Build** ```bash dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj ``` Expected: 0 errors. - [ ] **Step 5: Run full test suite** ```bash dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj ``` Expected: all pre-existing tests + 3 new ones pass. - [ ] **Step 6: Commit** ```bash git add src/ClaudeDo.Worker/Runner/TaskRunner.cs \ src/ClaudeDo.Worker/Program.cs git commit -m "feat(worker): advance planning subtask chain on child finish" ``` --- ## Task 7: Hub method + client + context menu entry **Files:** - Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs` - Modify: `src/ClaudeDo.Ui/Services/IWorkerClient.cs` - Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs` - Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs` - Modify: `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml` - Modify: `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs` - [ ] **Step 1: Add hub method** In `src/ClaudeDo.Worker/Hub/WorkerHub.cs`, add (match the style of other planning methods): ```csharp public async Task QueuePlanningSubtasks(string parentTaskId) { await using var ctx = await _dbFactory.CreateDbContextAsync(); var repo = new TaskRepository(ctx); var coord = new PlanningChainCoordinator(repo); await coord.QueueSubtasksSequentiallyAsync(parentTaskId, CancellationToken.None); // Broadcast updates for the parent and all its children so the UI refreshes. var children = await ctx.Tasks .Where(t => t.ParentTaskId == parentTaskId) .Select(t => t.Id) .ToListAsync(); await _broadcaster.TaskUpdated(parentTaskId); foreach (var id in children) await _broadcaster.TaskUpdated(id); // Make sure the queue picks up the now-Queued first child immediately. _queueSignal.Wake(); } ``` If the existing hub constructs `PlanningSessionManager` via DI directly, inject `PlanningChainCoordinator` the same way and call `_chainCoordinator.QueueSubtasksSequentiallyAsync(...)` instead of newing one up. If the hub exposes a queue-wakeup via a different name than `_queueSignal`, use that (search the file for `WakeQueue` or `.Wake()`). - [ ] **Step 2: Add method to `IWorkerClient`** In `src/ClaudeDo.Ui/Services/IWorkerClient.cs`, add next to the other planning methods: ```csharp Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default); ``` - [ ] **Step 3: Implement in `WorkerClient`** In `src/ClaudeDo.Ui/Services/WorkerClient.cs`, add (match the pattern of `StartPlanningSessionAsync` etc.): ```csharp public Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) => _connection.InvokeAsync("QueuePlanningSubtasks", parentTaskId, ct); ``` - [ ] **Step 4: Add `CanQueueSubtasksSequentially` + `HasPlanningChildren` observable to `TaskRowViewModel`** Confirm `HasPlanningChildren` exists (it's referenced in the spec). If not, add it as `[ObservableProperty] bool _hasPlanningChildren;` and ensure `TasksIslandViewModel.Regroup()` already sets it (there should be a parent-side "has children" pass similar to the `AllChildrenDone` one added in Task 4 — if not, set it there). Then add: ```csharp public bool CanQueueSubtasksSequentially => IsPlanningParent && HasPlanningChildren && !IsChild; ``` Add `OnPropertyChanged(nameof(CanQueueSubtasksSequentially))` inside `OnStatusChanged` and `OnHasPlanningChildrenChanged` so the flag refreshes when status or children change. - [ ] **Step 5: Add context-menu entry** In `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml`, inside the existing ``, directly after the "Discard planning session" item: ```xml ``` - [ ] **Step 6: Add click handler in code-behind** In `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs`, add (match the other `On*Click` handlers — they pull the `TaskRowViewModel` from `DataContext` and call the shell / worker): ```csharp private async void OnQueueSubtasksSequentiallyClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) { if (DataContext is not TaskRowViewModel row) return; var worker = App.Services.GetRequiredService(); try { await worker.QueuePlanningSubtasksAsync(row.Id); } catch (Exception ex) { // Match the toast/log pattern used by OnSendToQueueClick et al. System.Diagnostics.Debug.WriteLine($"QueuePlanningSubtasks failed: {ex}"); } } ``` Use the same `App.Services` / `IWorkerClient` lookup pattern as `OnSendToQueueClick` — do not introduce a new DI pattern. If the existing handlers use a shell/mediator indirection, use that instead. - [ ] **Step 7: Build** ```bash dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj ``` Expected: 0 errors. - [ ] **Step 8: Manual verify end-to-end** 1. Launch app: `dotnet run --project src/ClaudeDo.App`. 2. Open a planning task with ≥2 subtasks (all in `Manual`/`Planned`). 3. Right-click parent → **Queue subtasks sequentially**. 4. Confirm in the task list: first child shows `Queued` chip, others show `Waiting` chip. 5. Let the first run to completion (or, for a quick smoke test, edit the DB to mark it `Done` and emit `TaskUpdated` via a restart). 6. Confirm the next child's status flips `Waiting → Queued` without user interaction. 7. Force-fail a child (cancel it mid-run) — confirm remaining `Waiting` children stay `Waiting`. - [ ] **Step 9: Commit** ```bash git add src/ClaudeDo.Worker/Hub/WorkerHub.cs \ src/ClaudeDo.Ui/Services/IWorkerClient.cs \ src/ClaudeDo.Ui/Services/WorkerClient.cs \ src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs \ src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml \ src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs git commit -m "feat(ui+worker): context menu to queue planning subtasks sequentially" ``` --- ## Self-review checklist (for the plan author before handing off) - All four spec items mapped: auto-collapse (Task 4), collapsible description (Task 3), narrower splitters (Task 2), sequential subtask queue (Tasks 1, 5, 6, 7). - `Waiting` enum touches: enum, chip class, virtual:queued filter — covered in Task 1. - TDD applied where it pays off (the coordinator); UI tasks rely on manual verification (correct for this codebase). - No placeholders. Every code step shows the code to paste. - Type names consistent: `PlanningChainCoordinator`, `QueueSubtasksSequentiallyAsync`, `OnChildFinishedAsync`, `QueuePlanningSubtasksAsync`, `AreChildrenExpanded`, `AllChildrenDone`, `IsDescriptionExpanded` — used the same across tasks. - Commits are small and conventional.