# Planning Sessions — Plan C: UI 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:** Wire the planning-session feature into the UI: context-menu entries, hierarchical display of parent + children, draft styling, unfinished-session dialog, and the `WorkerClient` methods that call the hub endpoints built in Plan B. **Architecture:** Extend `TaskRowViewModel` with hierarchy-aware flags (`IsChild`, `IsPlanningParent`, `IsExpanded`). `TasksIslandViewModel` builds a flat stream that interleaves parents and their children based on expanded state. Context-menu entries on `TaskRowView` gate on task status. Draft styling lives in the existing island styles. A modal dialog reuses the project's `TaskCompletionSource` pattern. **Tech Stack:** .NET 8, Avalonia 12, CommunityToolkit.Mvvm (`[ObservableProperty]`, `[RelayCommand]`), compiled bindings, SignalR client. **Spec reference:** `docs/superpowers/specs/2026-04-23-planning-sessions-design.md` section 6. --- ## Prerequisite Gate This plan depends on Plan A being merged to `main`. Plan B's interface contract (hub method names, return types) is locked in the spec §6.6 and Plan B task 13 — this plan can proceed in parallel with Plan B. Before starting: ```bash git fetch origin main git checkout main git pull --ff-only ls src/ClaudeDo.Data/Migrations/*AddPlanningSupport*.cs ``` If the file is missing, wait for Plan A: ```bash while ! ls src/ClaudeDo.Data/Migrations/*AddPlanningSupport*.cs >/dev/null 2>&1; do echo "Waiting for Plan A to merge..." sleep 60 git fetch origin main && git pull --ff-only done ``` Then branch: ```bash git checkout -b feat/planning-sessions-ui ``` **Parallel-with-Plan-B note:** Plan B may not yet be merged when this plan runs. The `WorkerClient` methods in Task 9 will compile against Plan B's SignalR hub method names (they're string-based SignalR invocations), so they don't have a build-time dependency. Runtime end-to-end testing requires Plan B merged; until then, mock-test what's possible and smoke-test manually once both plans land. --- ## File Structure **Modified:** - `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs` — add `ParentTaskId`, `IsChild`, `IsPlanningParent`, `IsExpanded`, `PlanningBadge` properties. - `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` — add planning commands, expanded-state map, flat-stream rebuild logic. - `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml` — chevron, indentation, badges, draft styling hooks. - `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs` — context-menu event handlers (if code-behind is used; else inline). - `src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml` — use the extended TaskRowView template. - `src/ClaudeDo.Ui/Services/WorkerClient.cs` — five new hub method wrappers matching Plan B. - `src/ClaudeDo.Ui/Design/IslandStyles.axaml` — `.draft`, `.planning-parent`, `.planned-parent`, badge styles. **Created:** - `src/ClaudeDo.Ui/Views/Dialogs/UnfinishedPlanningDialog.axaml` + `.axaml.cs` — modal Resume/Finalize/Discard dialog. - `src/ClaudeDo.Ui/ViewModels/Dialogs/UnfinishedPlanningDialogViewModel.cs` — dialog VM. - `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs` — VM-level tests. - `tests/ClaudeDo.Worker.Tests/UiVm/TaskRowViewModelPlanningTests.cs` — VM-level tests. --- ## Task 1: Extend `TaskRowViewModel` **Files:** - Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs` - Create: `tests/ClaudeDo.Worker.Tests/UiVm/TaskRowViewModelPlanningTests.cs` - [ ] **Step 1: Write failing test for planning flags** Create the test file. Adapt the existing `TaskRowViewModelTests` pattern (look at `tests/ClaudeDo.Worker.Tests/UiVm/TaskRowViewModelTests.cs` for how VMs are constructed in tests): ```csharp using ClaudeDo.Ui.ViewModels.Islands; namespace ClaudeDo.Worker.Tests.UiVm; public sealed class TaskRowViewModelPlanningTests { [Fact] public void Draft_Status_SetsIsChildFlag_WhenParentIdIsNotNull() { // Adapt the constructor call to your actual TaskRowViewModel signature (see TaskRowViewModelTests). var vm = TestHelpers.MakeRow( status: "draft", parentTaskId: "parent-id"); Assert.True(vm.IsChild); Assert.False(vm.IsPlanningParent); } [Fact] public void Planning_Status_SetsIsPlanningParent() { var vm = TestHelpers.MakeRow(status: "planning", parentTaskId: null); Assert.True(vm.IsPlanningParent); Assert.False(vm.IsChild); Assert.Equal("PLANNING", vm.PlanningBadge); } [Fact] public void Planned_Status_ShowsPlannedBadge() { var vm = TestHelpers.MakeRow(status: "planned", parentTaskId: null); Assert.True(vm.IsPlanningParent); Assert.Equal("PLANNED", vm.PlanningBadge); } [Fact] public void NonPlanningStatus_NoBadge() { var vm = TestHelpers.MakeRow(status: "manual", parentTaskId: null); Assert.False(vm.IsPlanningParent); Assert.Null(vm.PlanningBadge); } } internal static class TestHelpers { public static TaskRowViewModel MakeRow(string status, string? parentTaskId) { // Implement based on actual TaskRowViewModel constructor. // The TaskRowViewModelTests.cs file in the same folder shows the existing pattern. throw new NotImplementedException("Adapt to your TaskRowViewModel constructor"); } } ``` Open `tests/ClaudeDo.Worker.Tests/UiVm/TaskRowViewModelTests.cs` first to see how the VM is constructed in tests, then fill in `TestHelpers.MakeRow` accordingly. - [ ] **Step 2: Run; verify fail** Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TaskRowViewModelPlanningTests"` Expected: FAIL (properties not yet on VM). - [ ] **Step 3: Extend the VM** In `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs` add the new properties using `[ObservableProperty]`: ```csharp [ObservableProperty] private string? parentTaskId; [ObservableProperty] private bool isExpanded = true; public bool IsChild => !string.IsNullOrEmpty(ParentTaskId); public bool IsPlanningParent => string.Equals(Status, "planning", StringComparison.OrdinalIgnoreCase) || string.Equals(Status, "planned", StringComparison.OrdinalIgnoreCase); public string? PlanningBadge => Status switch { string s when string.Equals(s, "planning", StringComparison.OrdinalIgnoreCase) => "PLANNING", string s when string.Equals(s, "planned", StringComparison.OrdinalIgnoreCase) => "PLANNED", _ => null, }; public bool IsDraft => string.Equals(Status, "draft", StringComparison.OrdinalIgnoreCase); ``` Since `IsChild`, `IsPlanningParent`, `PlanningBadge`, and `IsDraft` are computed from other observables, you must raise property-changed notifications when `Status` or `ParentTaskId` changes. Use `[ObservableProperty]` partial methods: ```csharp partial void OnStatusChanged(string value) { OnPropertyChanged(nameof(IsPlanningParent)); OnPropertyChanged(nameof(PlanningBadge)); OnPropertyChanged(nameof(IsDraft)); } partial void OnParentTaskIdChanged(string? value) { OnPropertyChanged(nameof(IsChild)); } ``` If the existing VM already has `OnStatusChanged` (check for generator outputs), merge into it rather than duplicating. - [ ] **Step 4: Run; verify pass** Expected: PASS. - [ ] **Step 5: Commit** ```bash git add src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs tests/ClaudeDo.Worker.Tests/UiVm/TaskRowViewModelPlanningTests.cs git commit -m "feat(ui): TaskRowViewModel gains planning hierarchy flags" ``` --- ## Task 2: `WorkerClient` planning methods **Files:** - Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs` - Create: DTOs matching Plan B return types (either inline in the client file or new file `src/ClaudeDo.Ui/Services/PlanningDtos.cs`). - [ ] **Step 1: Add DTOs** Create `src/ClaudeDo.Ui/Services/PlanningDtos.cs`: ```csharp namespace ClaudeDo.Ui.Services; public sealed record PlanningSessionFilesDto( string SessionDirectory, string McpConfigPath, string SystemPromptPath, string InitialPromptPath); public sealed record PlanningSessionStartInfo( string ParentTaskId, string WorkingDir, PlanningSessionFilesDto Files); public sealed record PlanningSessionResumeInfo( string ParentTaskId, string WorkingDir, string ClaudeSessionId, string McpConfigPath); ``` These field names must match Plan B's `PlanningSessionStartContext` / `PlanningSessionResumeContext` exactly (case-sensitive JSON deserialization through SignalR). - [ ] **Step 2: Add `WorkerClient` methods** In `src/ClaudeDo.Ui/Services/WorkerClient.cs`, add: ```csharp public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) => _connection.InvokeAsync("StartPlanningSessionAsync", taskId, ct); public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) => _connection.InvokeAsync("ResumePlanningSessionAsync", taskId, ct); public Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default) => _connection.InvokeAsync("DiscardPlanningSessionAsync", taskId, ct); public Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default) => _connection.InvokeAsync("FinalizePlanningSessionAsync", taskId, queueAgentTasks, ct); public Task GetPendingDraftCountAsync(string taskId, CancellationToken ct = default) => _connection.InvokeAsync("GetPendingDraftCountAsync", taskId, ct); ``` Replace `_connection` with whatever name the existing `WorkerClient` uses for its `HubConnection` field. - [ ] **Step 3: Build** Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj` Expected: builds. - [ ] **Step 4: Commit** ```bash git add src/ClaudeDo.Ui/Services/PlanningDtos.cs src/ClaudeDo.Ui/Services/WorkerClient.cs git commit -m "feat(ui): WorkerClient planning-session methods" ``` --- ## Task 3: `TasksIslandViewModel` — planning commands + expanded state **Files:** - Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` - Create: `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs` - [ ] **Step 1: Add commands to the VM** In `TasksIslandViewModel.cs`, add: ```csharp private readonly Dictionary _expandedState = new(); [RelayCommand] private async Task OpenPlanningSessionAsync(TaskRowViewModel? row) { if (row is null || !string.Equals(row.Status, "manual", StringComparison.OrdinalIgnoreCase)) return; try { await _workerClient.StartPlanningSessionAsync(row.Id); } catch (Exception ex) { await _dialogs.ShowErrorAsync("Could not start planning session", ex.Message); } } [RelayCommand] private async Task ResumePlanningSessionAsync(TaskRowViewModel? row) { if (row is null || !row.IsPlanningParent) return; try { await _workerClient.ResumePlanningSessionAsync(row.Id); } catch (Exception ex) { await _dialogs.ShowErrorAsync("Could not resume planning session", ex.Message); } } [RelayCommand] private async Task DiscardPlanningSessionAsync(TaskRowViewModel? row) { if (row is null) return; var confirm = await _dialogs.ConfirmAsync( "Discard planning session?", "This will delete all draft tasks and reset the parent to Manual."); if (!confirm) return; await _workerClient.DiscardPlanningSessionAsync(row.Id); } [RelayCommand] private async Task FinalizePlanningSessionAsync(TaskRowViewModel? row) { if (row is null) return; await _workerClient.FinalizePlanningSessionAsync(row.Id, queueAgentTasks: true); } [RelayCommand] private void ToggleExpand(TaskRowViewModel? row) { if (row is null) return; var next = !(_expandedState.TryGetValue(row.Id, out var current) ? current : true); _expandedState[row.Id] = next; row.IsExpanded = next; RebuildFlatStreams(); } private void RebuildFlatStreams() { // Existing code builds OpenItems/CompletedItems from the task list. // Modify it so that: after emitting a parent, if IsPlanningParent && IsExpanded, // its Draft / Manual / Queued / Running / Done children are emitted next. // Children already know they are children (ParentTaskId != null) and are styled as such. } ``` The existing `RebuildFlatStreams` (or equivalent) probably just groups tasks by status. You need to intersperse the hierarchy: ```csharp // Pseudocode — fit to the existing code shape. var topLevel = allRows.Where(r => !r.IsChild).OrderBy(r => r.SortOrder); var flat = new List(); foreach (var parent in topLevel) { flat.Add(parent); if (parent.IsPlanningParent && parent.IsExpanded) { var children = allRows .Where(r => r.ParentTaskId == parent.Id) .OrderBy(r => r.SortOrder) .ToList(); flat.AddRange(children); } } // Then bucket `flat` into OpenItems/CompletedItems like today, preserving order. ``` Pass dependencies: the VM already has a `WorkerClient` or equivalent — reuse it. Add a dialog service if not already injected: ```csharp public interface IDialogService { Task ConfirmAsync(string title, string message); Task ShowErrorAsync(string title, string message); } ``` If an analog already exists (check existing editor dialogs), use it. - [ ] **Step 2: Write failing VM tests** `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs`: ```csharp using ClaudeDo.Ui.ViewModels.Islands; namespace ClaudeDo.Worker.Tests.UiVm; public sealed class TasksIslandViewModelPlanningTests { [Fact] public void ToggleExpand_CollapsesChildrenOfPlanningParent() { // Arrange: create VM with one Planning parent and two Draft children. // Act: call ToggleExpandCommand with the parent. // Assert: flat stream no longer contains the children. // Adapt to how the existing TasksIslandViewModel is instantiated. } [Fact] public void OpenPlanningSessionCommand_ManualTaskOnly_CanExecuteTrue() { // Arrange VM with a Manual row. // Assert CanExecute for OpenPlanningSession command is true for Manual rows, // false for Queued/Running/Done/Failed rows. } } ``` These are skeleton tests — implement with the same construction pattern used by the existing `TasksIslandViewModelTests` if one exists, or build a minimal VM fake with a stub `WorkerClient`. - [ ] **Step 3: Build + test** Run: ```bash dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TasksIslandViewModelPlanningTests" ``` - [ ] **Step 4: Commit** ```bash git add src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs git commit -m "feat(ui): planning commands and expand/collapse in TasksIslandViewModel" ``` --- ## Task 4: `TaskRowView` — indent, chevron, badges, draft styling **Files:** - Modify: `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml` - [ ] **Step 1: Wrap the row content with a Grid that has an indent column** Open `TaskRowView.axaml`. The existing root is likely a `Grid` or `Border`. Replace/refactor the top-level layout to: ```xml ``` This is a structural edit — preserve all existing bindings for status color, completion toggle, star, scheduled-for, etc. The new indentation column and badges are additive. - [ ] **Step 2: Add the converters** If `ChevronDataConverter`, `BoolToItalicConverter`, `BoolToDraftOpacityConverter` do not exist, add them to `src/ClaudeDo.Ui/Converters/` (or inline as compiled converters). Example inline: ```xml ``` If converters must be code-based, a minimal `BoolToItalicConverter`: ```csharp using Avalonia.Data.Converters; using Avalonia.Media; namespace ClaudeDo.Ui.Converters; public sealed class BoolToItalicConverter : IValueConverter { public object Convert(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture) => value is true ? FontStyle.Italic : FontStyle.Normal; public object ConvertBack(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture) => throw new NotSupportedException(); } ``` Register in `App.axaml` resources. - [ ] **Step 3: Build** Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj` Expected: builds cleanly (XAML compiles). - [ ] **Step 4: Commit** ```bash git add src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml src/ClaudeDo.Ui/Converters/ git commit -m "feat(ui): TaskRowView hierarchy indentation, chevron, badges, draft italic" ``` --- ## Task 5: `TaskRowView` — planning context-menu entries **Files:** - Modify: `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml` - [ ] **Step 1: Locate the existing context menu** Open `TaskRowView.axaml`. The ContextMenu lives somewhere on the root element or as a `ContextMenu.Items`/`ContextFlyout`. Find the block that defines entries like "Edit", "Run now", etc. - [ ] **Step 2: Insert planning entries conditionally** Add within the existing menu (order: after "Run now" and a separator): ```xml ``` Simpler alternative without multi-condition converters: expose direct bool VM properties that combine the logic — `CanOpenPlanningSession`, `CanResumePlanningSession`, `CanDiscardPlanningSession`: ```csharp // In TaskRowViewModel public bool CanOpenPlanningSession => string.Equals(Status, "manual", StringComparison.OrdinalIgnoreCase) && !IsChild; public bool CanResumeOrDiscardPlanning => string.Equals(Status, "planning", StringComparison.OrdinalIgnoreCase); ``` Add `OnPropertyChanged(nameof(CanOpenPlanningSession))` and friends in the status/parent-id partial methods from Task 1. Then the XAML simplifies to: ```xml ``` Use this simpler path — cleaner. - [ ] **Step 3: Build** Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj` Expected: builds. - [ ] **Step 4: Commit** ```bash git add src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs git commit -m "feat(ui): planning entries in task context menu" ``` --- ## Task 6: Island styles — draft, badges **Files:** - Modify: `src/ClaudeDo.Ui/Design/IslandStyles.axaml` - [ ] **Step 1: Add brushes + styles** Append within `` or wherever brushes are defined: ```xml ``` Add styles: ```xml ``` - [ ] **Step 2: Build and manually verify** Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj` Launch app, create a task, right-click, use Open planning Session (if Plan B merged) or simulate via DB. Verify badge + italic draft rendering visually. - [ ] **Step 3: Commit** ```bash git add src/ClaudeDo.Ui/Design/IslandStyles.axaml git commit -m "feat(ui): draft and planning badge styles" ``` --- ## Task 7: Unfinished-planning-session dialog **Files:** - Create: `src/ClaudeDo.Ui/Views/Dialogs/UnfinishedPlanningDialog.axaml` + `.axaml.cs` - Create: `src/ClaudeDo.Ui/ViewModels/Dialogs/UnfinishedPlanningDialogViewModel.cs` - [ ] **Step 1: Create the VM** `src/ClaudeDo.Ui/ViewModels/Dialogs/UnfinishedPlanningDialogViewModel.cs`: ```csharp using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace ClaudeDo.Ui.ViewModels.Dialogs; public enum UnfinishedPlanningDialogResult { Cancel, Resume, FinalizeNow, Discard, } public sealed partial class UnfinishedPlanningDialogViewModel : ObservableObject { [ObservableProperty] private string title = "Unfinished planning session"; [ObservableProperty] private string taskTitle = ""; [ObservableProperty] private int draftCount; public TaskCompletionSource Result { get; } = new(); [RelayCommand] private void Resume() => Result.TrySetResult(UnfinishedPlanningDialogResult.Resume); [RelayCommand] private void FinalizeNow() => Result.TrySetResult(UnfinishedPlanningDialogResult.FinalizeNow); [RelayCommand] private void Discard() => Result.TrySetResult(UnfinishedPlanningDialogResult.Discard); [RelayCommand] private void Cancel() => Result.TrySetResult(UnfinishedPlanningDialogResult.Cancel); } ``` - [ ] **Step 2: Create the view** `src/ClaudeDo.Ui/Views/Dialogs/UnfinishedPlanningDialog.axaml`: ```xml