Files
ClaudeDo/docs/superpowers/plans/2026-04-23-planning-sessions-plan-c-ui.md
mika kuns 43d517dcfc docs(plans): add planning sessions implementation plans A, B, C
- Plan A (Foundation): schema, enum, repos, auto-status hook
- Plan B (Worker MCP + Launcher): MCP server, SignalR endpoints, wt.exe launcher
- Plan C (UI): context menu, hierarchy rendering, dialog, client methods

Plans B and C depend on Plan A merging first (marker: migration file
AddPlanningSupport). B and C can run in parallel after A.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 17:36:02 +02:00

33 KiB

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<T> 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:

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:

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:

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):

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]:

[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:

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
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:

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:

public Task<PlanningSessionStartInfo> StartPlanningSessionAsync(string taskId, CancellationToken ct = default)
    => _connection.InvokeAsync<PlanningSessionStartInfo>("StartPlanningSessionAsync", taskId, ct);

public Task<PlanningSessionResumeInfo> ResumePlanningSessionAsync(string taskId, CancellationToken ct = default)
    => _connection.InvokeAsync<PlanningSessionResumeInfo>("ResumePlanningSessionAsync", taskId, ct);

public Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default)
    => _connection.InvokeAsync("DiscardPlanningSessionAsync", taskId, ct);

public Task<int> FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default)
    => _connection.InvokeAsync<int>("FinalizePlanningSessionAsync", taskId, queueAgentTasks, ct);

public Task<int> GetPendingDraftCountAsync(string taskId, CancellationToken ct = default)
    => _connection.InvokeAsync<int>("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
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:

private readonly Dictionary<string, bool> _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:

// Pseudocode — fit to the existing code shape.
var topLevel = allRows.Where(r => !r.IsChild).OrderBy(r => r.SortOrder);
var flat = new List<TaskRowViewModel>();
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:

public interface IDialogService
{
    Task<bool> 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:

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:

dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TasksIslandViewModelPlanningTests"
  • Step 4: Commit
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:

<Grid ColumnDefinitions="Auto,*">
    <Border Grid.Column="0"
            Width="24"
            IsVisible="{Binding IsChild}">
        <Rectangle Width="1" Fill="{DynamicResource TextFaintBrush}" HorizontalAlignment="Right" Margin="0,4"/>
    </Border>

    <Grid Grid.Column="1" ColumnDefinitions="Auto,*,Auto">
        <!-- Chevron for planning parents -->
        <Button Grid.Column="0"
                Classes="icon-btn chevron"
                Width="18" Height="18"
                IsVisible="{Binding IsPlanningParent}"
                Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).ToggleExpandCommand}"
                CommandParameter="{Binding}">
            <PathIcon Width="10" Height="10">
                <PathIcon.Data>
                    <MultiBinding Converter="{StaticResource ChevronDataConverter}">
                        <Binding Path="IsExpanded"/>
                    </MultiBinding>
                </PathIcon.Data>
            </PathIcon>
        </Button>

        <!-- existing title/description area -->
        <StackPanel Grid.Column="1" ...>
            <!-- existing title binding with added italic when IsDraft -->
            <TextBlock Text="{Binding Title}"
                       FontStyle="{Binding IsDraft, Converter={StaticResource BoolToItalicConverter}}"
                       Opacity="{Binding IsDraft, Converter={StaticResource BoolToDraftOpacityConverter}}" />
        </StackPanel>

        <!-- Badges -->
        <StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="4">
            <Border Classes="badge draft" IsVisible="{Binding IsDraft}">
                <TextBlock Text="DRAFT"/>
            </Border>
            <Border Classes="badge planning" IsVisible="{Binding IsPlanningParent}">
                <TextBlock Text="{Binding PlanningBadge}"/>
            </Border>
        </StackPanel>
    </Grid>
</Grid>

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:

<!-- in UserControl.Resources of TaskRowView.axaml, or in App.axaml for global -->
<Style Selector="Border.badge">
    <Setter Property="CornerRadius" Value="3"/>
    <Setter Property="Padding" Value="4,1"/>
    <Setter Property="Background" Value="{DynamicResource BadgeBgBrush}"/>
</Style>
<Style Selector="Border.badge.draft">
    <Setter Property="Background" Value="{DynamicResource DraftBadgeBrush}"/>
</Style>
<Style Selector="Border.badge.planning">
    <Setter Property="Background" Value="{DynamicResource PlanningBadgeBrush}"/>
</Style>

If converters must be code-based, a minimal BoolToItalicConverter:

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
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):

<MenuItem Header="Open planning Session"
          Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).OpenPlanningSessionCommand}"
          CommandParameter="{Binding}"
          IsVisible="{Binding Status, Converter={StaticResource IsManualAndNotChildConverter}, ConverterParameter={Binding IsChild}}"/>

<MenuItem Header="Resume planning Session"
          Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).ResumePlanningSessionCommand}"
          CommandParameter="{Binding}"
          IsVisible="{Binding Status, Converter={StaticResource IsPlanningConverter}}"/>

<MenuItem Header="Discard planning session"
          Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).DiscardPlanningSessionCommand}"
          CommandParameter="{Binding}"
          IsVisible="{Binding Status, Converter={StaticResource IsPlanningConverter}}"/>

Simpler alternative without multi-condition converters: expose direct bool VM properties that combine the logic — CanOpenPlanningSession, CanResumePlanningSession, CanDiscardPlanningSession:

// 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:

<MenuItem Header="Open planning Session"
          Command="{Binding ...OpenPlanningSessionCommand}"
          CommandParameter="{Binding}"
          IsVisible="{Binding CanOpenPlanningSession}"/>

Use this simpler path — cleaner.

  • Step 3: Build

Run: dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj Expected: builds.

  • Step 4: Commit
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 <Styles.Resources> or wherever brushes are defined:

<SolidColorBrush x:Key="DraftBadgeBrush" Color="#4A5568"/>
<SolidColorBrush x:Key="PlanningBadgeBrush" Color="#D69E2E"/>
<SolidColorBrush x:Key="PlannedBadgeBrush" Color="#3182CE"/>

Add styles:

<Style Selector="Border.badge">
    <Setter Property="CornerRadius" Value="3"/>
    <Setter Property="Padding" Value="4,1"/>
    <Setter Property="VerticalAlignment" Value="Center"/>
</Style>

<Style Selector="Border.badge > TextBlock">
    <Setter Property="FontSize" Value="9"/>
    <Setter Property="FontWeight" Value="Bold"/>
    <Setter Property="Foreground" Value="White"/>
</Style>

<Style Selector="Border.badge.draft">
    <Setter Property="Background" Value="{DynamicResource DraftBadgeBrush}"/>
</Style>

<Style Selector="Border.badge.planning">
    <Setter Property="Background" Value="{DynamicResource PlanningBadgeBrush}"/>
</Style>

<Style Selector="Border.badge.planned">
    <Setter Property="Background" Value="{DynamicResource PlannedBadgeBrush}"/>
</Style>
  • 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
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:

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<UnfinishedPlanningDialogResult> 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:

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:ClaudeDo.Ui.ViewModels.Dialogs"
        x:Class="ClaudeDo.Ui.Views.Dialogs.UnfinishedPlanningDialog"
        x:DataType="vm:UnfinishedPlanningDialogViewModel"
        Width="440" Height="220"
        WindowStartupLocation="CenterOwner"
        CanResize="False"
        Title="{Binding Title}">
    <StackPanel Margin="20" Spacing="12">
        <TextBlock Text="{Binding Title}" FontWeight="Bold" FontSize="15"/>
        <TextBlock Text="{Binding TaskTitle}" Opacity="0.85"/>
        <TextBlock>
            <Run Text="{Binding DraftCount}"/>
            <Run Text=" draft tasks waiting to be finalized."/>
        </TextBlock>

        <StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right">
            <Button Content="Discard" Command="{Binding DiscardCommand}"/>
            <Button Content="Finalize now" Command="{Binding FinalizeNowCommand}"/>
            <Button Content="Resume" Classes="accent" Command="{Binding ResumeCommand}"/>
        </StackPanel>
    </StackPanel>
</Window>

UnfinishedPlanningDialog.axaml.cs:

using Avalonia.Controls;
using Avalonia.Markup.Xaml;

namespace ClaudeDo.Ui.Views.Dialogs;

public partial class UnfinishedPlanningDialog : Window
{
    public UnfinishedPlanningDialog()
    {
        InitializeComponent();
    }
}
  • Step 3: Wire into TasksIslandViewModel

When the user right-clicks a Planning row OR when the app starts and a Planning row is present, show the dialog. Add a helper in the VM:

private async Task<UnfinishedPlanningDialogResult> AskUnfinishedPlanningAsync(TaskRowViewModel row)
{
    var dialogVm = new UnfinishedPlanningDialogViewModel
    {
        TaskTitle = row.Title,
        DraftCount = await _workerClient.GetPendingDraftCountAsync(row.Id),
    };
    var dlg = new UnfinishedPlanningDialog { DataContext = dialogVm };
    _ = dlg.ShowDialog(_ownerWindow);
    return await dialogVm.Result.Task;
}

Replace the direct resume/discard/finalize commands (from Task 3) with calls that first pop this dialog and dispatch based on result. For example:

[RelayCommand]
private async Task ResumePlanningSessionAsync(TaskRowViewModel? row)
{
    if (row is null || !row.IsPlanningParent) return;
    var choice = await AskUnfinishedPlanningAsync(row);
    switch (choice)
    {
        case UnfinishedPlanningDialogResult.Resume:
            await _workerClient.ResumePlanningSessionAsync(row.Id);
            break;
        case UnfinishedPlanningDialogResult.FinalizeNow:
            await _workerClient.FinalizePlanningSessionAsync(row.Id);
            break;
        case UnfinishedPlanningDialogResult.Discard:
            await _workerClient.DiscardPlanningSessionAsync(row.Id);
            break;
    }
}
  • Step 4: Build + manual run

Run: dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj Expected: builds.

  • Step 5: Commit
git add src/ClaudeDo.Ui/Views/Dialogs/UnfinishedPlanningDialog.axaml src/ClaudeDo.Ui/Views/Dialogs/UnfinishedPlanningDialog.axaml.cs src/ClaudeDo.Ui/ViewModels/Dialogs/UnfinishedPlanningDialogViewModel.cs src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs
git commit -m "feat(ui): unfinished planning session dialog"

Task 8: TasksIslandView — wire new templates

Files:

  • Modify: src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml

  • Step 1: No structural change required

The hierarchy is already handled by TasksIslandViewModel.RebuildFlatStreams interleaving children into OpenItems/CompletedItems. The existing ItemsControl bindings in TasksIslandView automatically pick up the new rows. Indentation/chevron/badge rendering is entirely inside TaskRowView (Task 4).

Verify the view does not have any logic that filters out children based on ParentTaskId IS NOT NULL today. If it does, remove that filter — the VM is now authoritative about what's in the stream.

  • Step 2: Build + manual check

Launch the UI, create a manual task, and manually update its status to Planning in the DB (or wait for Plan B). Create one child in DB. Verify indentation and chevron render.

  • Step 3: Commit (if any change was made)
git add src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml
git commit -m "chore(ui): verify tasks view renders hierarchy via flat stream"

If no change — skip the commit.


Task 9: Delete-with-children handling

Files:

  • Modify: src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs (existing delete command)

  • Step 1: Catch DbUpdateException from delete

Find the existing delete command. Wrap the repository/hub call:

[RelayCommand]
private async Task RemoveAsync(TaskRowViewModel? row)
{
    if (row is null) return;
    try
    {
        await _workerClient.DeleteTaskAsync(row.Id);
    }
    catch (HubException ex) when (ex.Message.Contains("foreign key", StringComparison.OrdinalIgnoreCase)
                                || ex.Message.Contains("FOREIGN KEY", StringComparison.OrdinalIgnoreCase)
                                || ex.Message.Contains("Restrict", StringComparison.OrdinalIgnoreCase))
    {
        var childrenCount = 1; // or query via a new hub method if exact count matters
        var choice = await _dialogs.ConfirmAsync(
            "Cannot delete",
            $"This task has child tasks. Delete all including children?");
        if (!choice) return;
        // Recursive delete — iterate children first. For v1 MVP, instruct user to
        // discard the planning session first. Simpler, safer.
        await _dialogs.ShowErrorAsync(
            "Cannot delete",
            "Discard the planning session or delete child tasks manually first.");
    }
}

Simplification for v1: do not implement "Delete all including children" yet. Show an error instructing the user to discard the planning session or delete children first. This avoids an additional hub endpoint and keeps Plan C bounded.

  • Step 2: Build

Run: dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj Expected: builds.

  • Step 3: Commit
git add src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs
git commit -m "feat(ui): friendly error when deleting task with children"

Task 10: Manual smoke test + final verification

Files: none

  • Step 1: Full test run

Run: dotnet test tests/ClaudeDo.Worker.Tests Expected: all tests pass.

  • Step 2: Build the full app

Run:

dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj

Expected: all succeed.

  • Step 3: Manual smoke test (requires Plan B merged)
  1. Launch the app.
  2. Create a Manual task with a title and some TODO-style description.
  3. Right-click → "Open planning Session".
  4. Verify Windows Terminal opens with Claude CLI running.
  5. In the terminal, ask Claude to create two child tasks (mcp__claudedo__create_child_task).
  6. Watch the UI: drafts appear under the parent (italic, grey, badge DRAFT).
  7. Ask Claude to finalize.
  8. Verify drafts become Manual/Queued children, parent flips to PLANNED badge.
  9. Close terminal without finalize on a new planning task; right-click the Planning task: dialog appears with Resume/Finalize/Discard.
  • Step 4: Document any UI tweaks needed in docs/open.md

Add a checklist item under UI verification for planning session visuals.

  • Step 5: Final commit
git add docs/open.md
git commit -m "docs(open): add planning-session manual verification checklist"

Out of scope for Plan C

  • Recursive delete of parent-with-children via UI (error-only in v1).
  • Collapse-state persistence across app restarts (in-memory only).
  • Keyboard shortcut for "Open planning Session".
  • Visual differentiation for PLANNED parents beyond a badge (e.g., subtle background tint) — can be added later if visually needed.