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>
This commit is contained in:
927
docs/superpowers/plans/2026-04-23-planning-sessions-plan-c-ui.md
Normal file
927
docs/superpowers/plans/2026-04-23-planning-sessions-plan-c-ui.md
Normal file
@@ -0,0 +1,927 @@
|
||||
# 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:
|
||||
|
||||
```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<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**
|
||||
|
||||
```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<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:
|
||||
|
||||
```csharp
|
||||
// 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:
|
||||
|
||||
```csharp
|
||||
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`:
|
||||
|
||||
```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
|
||||
<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:
|
||||
|
||||
```xml
|
||||
<!-- 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`:
|
||||
|
||||
```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
|
||||
<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`:
|
||||
|
||||
```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
|
||||
<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**
|
||||
|
||||
```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 `<Styles.Resources>` or wherever brushes are defined:
|
||||
|
||||
```xml
|
||||
<SolidColorBrush x:Key="DraftBadgeBrush" Color="#4A5568"/>
|
||||
<SolidColorBrush x:Key="PlanningBadgeBrush" Color="#D69E2E"/>
|
||||
<SolidColorBrush x:Key="PlannedBadgeBrush" Color="#3182CE"/>
|
||||
```
|
||||
|
||||
Add styles:
|
||||
|
||||
```xml
|
||||
<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**
|
||||
|
||||
```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<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`:
|
||||
|
||||
```xml
|
||||
<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`:
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
[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**
|
||||
|
||||
```bash
|
||||
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)**
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```csharp
|
||||
[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**
|
||||
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
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**
|
||||
|
||||
```bash
|
||||
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.
|
||||
Reference in New Issue
Block a user