29 KiB
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
Waitingto the enum
Append Waiting as the last value (keeps existing numeric slots stable for any int-serialized rows).
src/ClaudeDo.Data/Models/TaskEntity.cs:
public enum TaskStatus
{
Manual,
Queued,
Running,
Done,
Failed,
Planning,
Planned,
Draft,
Waiting,
}
- Step 2: Extend
StatusChipClassswitch in TaskRowViewModel
src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs — update the switch:
public string StatusChipClass => Status switch
{
TaskStatus.Running => "running",
TaskStatus.Failed => "error",
TaskStatus.Done => "review",
TaskStatus.Queued => "queued",
TaskStatus.Waiting => "waiting",
_ => "idle",
};
- Step 3: Add
IsWaitingand include it in virtual-queued matching
In the same TaskRowViewModel.cs, add alongside IsQueued:
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:
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
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
dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
Expected: 0 errors.
- Step 3: Commit
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:
[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:
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:
<StackPanel Spacing="4">
<Button Classes="flat"
Command="{Binding ToggleDescriptionExpandedCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="0">
<StackPanel Orientation="Horizontal" Spacing="6">
<PathIcon Width="10" Height="10"
Data="{StaticResource Icon.ChevronDown}"
IsVisible="{Binding IsDescriptionExpanded}"/>
<PathIcon Width="10" Height="10"
Data="{StaticResource Icon.ChevronRight}"
IsVisible="{Binding !IsDescriptionExpanded}"/>
<TextBlock Classes="eyebrow" Text="DESCRIPTION"/>
</StackPanel>
</Button>
<!-- existing description TextBox goes here unchanged, but add: -->
<TextBox ...existing attributes...
IsVisible="{Binding IsDescriptionExpanded}"/>
</StackPanel>
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:
<TextBlock Text="▾" IsVisible="{Binding IsDescriptionExpanded}"/>
<TextBlock Text="▸" IsVisible="{Binding !IsDescriptionExpanded}"/>
- Step 4: Build
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
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:
[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
AllChildrenDoneduring Regroup inTasksIslandViewModel
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:
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:
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:
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:
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:
<Button Classes="flat"
Command="{Binding ToggleChildrenExpandedCommand}"
IsVisible="{Binding HasPlanningChildren}"
Padding="0" Margin="0,0,6,0"
VerticalAlignment="Center">
<TextBlock FontSize="10"
Text="▾"
IsVisible="{Binding AreChildrenExpanded}"/>
<TextBlock FontSize="10"
Text="▸"
IsVisible="{Binding !AreChildrenExpanded}"/>
</Button>
Place it immediately before the title TextBlock in the parent-row layout. Leave child rows untouched.
- Step 5: Build
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
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:
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<ClaudeDoDbContext> InMemoryOptions() =>
new DbContextOptionsBuilder<ClaudeDoDbContext>()
.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)
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:
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:
// in src/ClaudeDo.Data/Repositories/TaskRepository.cs
public Task<List<TaskEntity>> 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
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:
[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:
/// <summary>
/// Call after a child task transitions to a terminal status.
/// Returns the id of the newly-queued sibling (if any), else null.
/// </summary>
public async Task<string?> 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
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
[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)
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~PlanningChainCoordinatorTests
Expected: 3 passed.
- Step 11: Commit
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
PlanningChainCoordinatorin DI
Locate src/ClaudeDo.Worker/Program.cs where other services are registered (look for services.AddSingleton<PlanningSessionManager> or similar). Add:
services.AddScoped<PlanningChainCoordinator>();
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<ClaudeDoDbContext> 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<ClaudeDoDbContext> 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:
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
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
Expected: 0 errors.
- Step 5: Run full test suite
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj
Expected: all pre-existing tests + 3 new ones pass.
- Step 6: Commit
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):
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:
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.):
public Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) =>
_connection.InvokeAsync("QueuePlanningSubtasks", parentTaskId, ct);
- Step 4: Add
CanQueueSubtasksSequentially+HasPlanningChildrenobservable toTaskRowViewModel
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:
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 <ContextMenu>, directly after the "Discard planning session" item:
<Separator IsVisible="{Binding CanQueueSubtasksSequentially}"/>
<MenuItem Header="Queue subtasks sequentially"
IsVisible="{Binding CanQueueSubtasksSequentially}"
Click="OnQueueSubtasksSequentiallyClick"/>
- 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):
private async void OnQueueSubtasksSequentiallyClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (DataContext is not TaskRowViewModel row) return;
var worker = App.Services.GetRequiredService<IWorkerClient>();
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
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
- Launch app:
dotnet run --project src/ClaudeDo.App. - Open a planning task with ≥2 subtasks (all in
Manual/Planned). - Right-click parent → Queue subtasks sequentially.
- Confirm in the task list: first child shows
Queuedchip, others showWaitingchip. - Let the first run to completion (or, for a quick smoke test, edit the DB to mark it
Doneand emitTaskUpdatedvia a restart). - Confirm the next child's status flips
Waiting → Queuedwithout user interaction. - Force-fail a child (cancel it mid-run) — confirm remaining
Waitingchildren stayWaiting.
- Step 9: Commit
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).
Waitingenum 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.