Files
ClaudeDo/docs/superpowers/plans/2026-04-14-todo-ux-redesign.md
Mika Kuns 9f61cd1449 docs: add UX redesign implementation plan (16 tasks)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:07:13 +02:00

57 KiB

ClaudeDo UX Redesign — 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: Redesign ClaudeDo's Avalonia UI for Microsoft To Do-style usability — inline task creation, editable detail pane with auto-save, keyboard shortcuts, and visual polish with Forest Teal accent.

Architecture: All changes are in ClaudeDo.Ui (Views, ViewModels, Converters) and ClaudeDo.App (App.axaml resources, DI registration). No changes to Data, Worker, or database schema. The existing MVVM + CommunityToolkit.Mvvm pattern is preserved.

Tech Stack: .NET 8.0, Avalonia 12.0.0 (Fluent theme, dark variant), CommunityToolkit.Mvvm, SQLite via raw ADO.NET

Spec: docs/superpowers/specs/2026-04-14-todo-ux-redesign-design.md


File Structure

Modify

File Responsibility
src/ClaudeDo.App/App.axaml Accent color brush resources, force dark theme
src/ClaudeDo.App/Program.cs DI registration: add TagRepository to TaskDetailViewModel
src/ClaudeDo.Ui/ViewModels/ListItemViewModel.cs Add DotBrush property for sidebar colored dots
src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs Add ToggleDoneCommand, IsDone, checkbox state helpers
src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs Inline add logic, toggle-done callback, list name property
src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs Editable properties, auto-save, tag CRUD, TaskChanged event
src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs Wire TaskDetail.TaskChanged → TaskList.RefreshSingleAsync
src/ClaudeDo.Ui/Views/MainWindow.axaml Reactive layout, sidebar polish, list name header
src/ClaudeDo.Ui/Views/MainWindow.axaml.cs Global keyboard shortcuts
src/ClaudeDo.Ui/Views/TaskListView.axaml Checkbox task rows, inline add field, remove toolbar
src/ClaudeDo.Ui/Views/TaskListView.axaml.cs Inline add KeyDown, task-scoped shortcuts
src/ClaudeDo.Ui/Views/TaskDetailView.axaml Editable fields, tag chips, auto-save layout
src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs LostFocus auto-save handlers, tag input KeyDown
src/ClaudeDo.Ui/Converters/StatusColorConverter.cs Keep existing, used for status text in detail pane

Create

File Responsibility
src/ClaudeDo.Ui/Converters/CheckboxBorderConverter.cs Task status string → checkbox border color brush

Task 1: Accent Color Resources + Dark Theme

Files:

  • Modify: src/ClaudeDo.App/App.axaml

  • Step 1: Read current App.axaml

Read src/ClaudeDo.App/App.axaml to confirm current content.

  • Step 2: Add resource dictionary and force dark theme

Replace the full App.axaml content with:

<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="ClaudeDo.App.App"
             xmlns:local="using:ClaudeDo.App"
             RequestedThemeVariant="Dark">

    <Application.DataTemplates>
        <local:ViewLocator/>
    </Application.DataTemplates>

    <Application.Styles>
        <FluentTheme />
    </Application.Styles>

    <Application.Resources>
        <!-- Accent: Forest Teal -->
        <SolidColorBrush x:Key="AccentBrush" Color="#3d9474"/>
        <SolidColorBrush x:Key="AccentLightBrush" Color="#6bb89e"/>
        <SolidColorBrush x:Key="AccentSubtleBrush" Color="#1A3D9474"/>
        <SolidColorBrush x:Key="AccentSelectedBrush" Color="#263D9474"/>

        <!-- Text -->
        <SolidColorBrush x:Key="TextPrimaryBrush" Color="#e2e8f0"/>
        <SolidColorBrush x:Key="TextSecondaryBrush" Color="#94a3b8"/>
        <SolidColorBrush x:Key="TextMutedBrush" Color="#5a6578"/>
        <SolidColorBrush x:Key="TextDimBrush" Color="#475569"/>

        <!-- Borders & Backgrounds -->
        <SolidColorBrush x:Key="BorderSubtleBrush" Color="#3a4560"/>
        <SolidColorBrush x:Key="SidebarBgBrush" Color="#1a1a2e"/>
        <SolidColorBrush x:Key="ContentBgBrush" Color="#16162a"/>

        <!-- Status colors (for checkboxes) -->
        <SolidColorBrush x:Key="StatusGrayBrush" Color="#475569"/>
        <SolidColorBrush x:Key="StatusOrangeBrush" Color="#e67e22"/>
        <SolidColorBrush x:Key="StatusGreenBrush" Color="#3d9474"/>
        <SolidColorBrush x:Key="StatusRedBrush" Color="#ef4444"/>
    </Application.Resources>
</Application>

Key changes: RequestedThemeVariant="Dark", all brush resources added.

  • Step 3: Build to verify

Run: dotnet build ClaudeDo.slnx Expected: Build succeeds with no errors.

  • Step 4: Commit
git add src/ClaudeDo.App/App.axaml
git commit -m "style: add Forest Teal accent resources and force dark theme"

Task 2: CheckboxBorderConverter

Files:

  • Create: src/ClaudeDo.Ui/Converters/CheckboxBorderConverter.cs

  • Step 1: Create the converter

Create src/ClaudeDo.Ui/Converters/CheckboxBorderConverter.cs:

using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;

namespace ClaudeDo.Ui.Converters;

public sealed class CheckboxBorderConverter : IValueConverter
{
    public static readonly CheckboxBorderConverter Instance = new();

    private static readonly ISolidColorBrush Gray = new SolidColorBrush(Color.Parse("#475569"));
    private static readonly ISolidColorBrush Orange = new SolidColorBrush(Color.Parse("#e67e22"));
    private static readonly ISolidColorBrush Green = new SolidColorBrush(Color.Parse("#3d9474"));
    private static readonly ISolidColorBrush Red = new SolidColorBrush(Color.Parse("#ef4444"));

    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        return value?.ToString()?.ToLowerInvariant() switch
        {
            "running" => Orange,
            "done" => Green,
            "failed" => Red,
            _ => Gray, // manual, queued
        };
    }

    public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
        => throw new NotSupportedException();
}
  • Step 2: Build to verify

Run: dotnet build ClaudeDo.slnx Expected: Build succeeds.

  • Step 3: Commit
git add src/ClaudeDo.Ui/Converters/CheckboxBorderConverter.cs
git commit -m "feat(ui): add CheckboxBorderConverter for task status circles"

Task 3: ListItemViewModel — DotBrush

Files:

  • Modify: src/ClaudeDo.Ui/ViewModels/ListItemViewModel.cs

  • Step 1: Read current file

Read src/ClaudeDo.Ui/ViewModels/ListItemViewModel.cs.

  • Step 2: Add DotBrush property

Add the following to ListItemViewModel, after the existing properties:

using Avalonia.Media;

Add the static palette and computed property:

private static readonly IBrush[] DotPalette =
[
    new SolidColorBrush(Color.Parse("#3d9474")), // green
    new SolidColorBrush(Color.Parse("#5571a1")), // blue
    new SolidColorBrush(Color.Parse("#d4964a")), // amber
    new SolidColorBrush(Color.Parse("#7c6aad")), // purple
    new SolidColorBrush(Color.Parse("#c25d6a")), // rose
];

public IBrush DotBrush => DotPalette[Math.Abs(Id.GetHashCode()) % DotPalette.Length];
  • Step 3: Build to verify

Run: dotnet build ClaudeDo.slnx Expected: Build succeeds.

  • Step 4: Commit
git add src/ClaudeDo.Ui/ViewModels/ListItemViewModel.cs
git commit -m "feat(ui): add colored dot brush to ListItemViewModel"

Task 4: TaskItemViewModel — ToggleDone + Checkbox State

Files:

  • Modify: src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs

  • Step 1: Read current file

Read src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs.

  • Step 2: Add toggle-done callback and properties

Update the constructor to accept a toggleDone callback:

private readonly Func<string, Task>? _toggleDone;

public TaskItemViewModel(TaskEntity entity, IReadOnlyList<TagEntity> tags,
    Func<string, Task>? runNow, Func<bool> canRunNow,
    Func<string, Task>? toggleDone)

Store it: _toggleDone = toggleDone;

Add these computed properties:

public bool IsDone => Status == TaskStatus.Done;
public bool IsRunning => Status == TaskStatus.Running;
public bool CanToggleDone => Status != TaskStatus.Running && Status != TaskStatus.Failed;

Add the ToggleDone command:

[RelayCommand(CanExecute = nameof(CanToggleDone))]
private async Task ToggleDone()
{
    if (_toggleDone is not null)
        await _toggleDone(Id);
}

In the Refresh method, after updating properties, notify the new computed properties:

OnPropertyChanged(nameof(IsDone));
OnPropertyChanged(nameof(IsRunning));
OnPropertyChanged(nameof(CanToggleDone));
ToggleDoneCommand.NotifyCanExecuteChanged();
  • Step 3: Build to verify (expect error)

Run: dotnet build ClaudeDo.slnx Expected: Build error in TaskListViewModel.cs because constructor call is missing the new parameter. This is expected — we fix it in Task 6.

  • Step 4: Commit
git add src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs
git commit -m "feat(ui): add ToggleDone command and checkbox state to TaskItemViewModel"

Task 5: TaskListViewModel — Inline Add + ToggleDone + ListName

Files:

  • Modify: src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs

  • Step 1: Read current file

Read src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs.

  • Step 2: Add ListName property

Add a property to expose the current list name for the header:

[ObservableProperty] private string _listName = "Tasks";

In LoadAsync, after setting CurrentListId, fetch and set the name:

if (listId is not null)
{
    var list = await _listRepo.GetByIdAsync(listId);
    ListName = list?.Name ?? "Tasks";
}
else
{
    ListName = "Tasks";
}
  • Step 3: Add inline add properties and command

Add:

[ObservableProperty] private string _inlineAddTitle = "";

Add the command:

[RelayCommand(CanExecute = nameof(CanAddTask))]
private async Task InlineAdd()
{
    var title = InlineAddTitle.Trim();
    if (string.IsNullOrEmpty(title) || CurrentListId is null) return;

    var list = await _listRepo.GetByIdAsync(CurrentListId);
    var defaultCommitType = list?.DefaultCommitType ?? "chore";

    var entity = new TaskEntity
    {
        Id = Guid.NewGuid().ToString(),
        ListId = CurrentListId,
        Title = title,
        Status = TaskStatus.Manual,
        CommitType = defaultCommitType,
        CreatedAt = DateTime.UtcNow,
    };

    try
    {
        await _taskRepo.AddAsync(entity);
        var tags = await _taskRepo.GetEffectiveTagsAsync(entity.Id);
        var vm = new TaskItemViewModel(entity, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync);
        Tasks.Add(vm);
        SelectedTask = vm;
        InlineAddTitle = "";
    }
    catch (Exception ex)
    {
        _showMessage($"Error creating task: {ex.Message}");
    }
}
  • Step 4: Add ToggleDone method
private async Task ToggleDoneAsync(string taskId)
{
    var entity = await _taskRepo.GetByIdAsync(taskId);
    if (entity is null) return;

    entity.Status = entity.Status == TaskStatus.Done ? TaskStatus.Manual : TaskStatus.Done;
    if (entity.Status == TaskStatus.Done)
        entity.FinishedAt = DateTime.UtcNow;

    await _taskRepo.UpdateAsync(entity);
    await RefreshSingleAsync(taskId);
}
  • Step 5: Update all TaskItemViewModel constructor calls

In LoadAsync, update the constructor call:

Tasks.Add(new TaskItemViewModel(e, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync));

In AddTask (the existing modal add), same change:

Tasks.Add(new TaskItemViewModel(saved, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync));
  • Step 6: Build to verify

Run: dotnet build ClaudeDo.slnx Expected: Build succeeds.

  • Step 7: Commit
git add src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs
git commit -m "feat(ui): add inline task creation, toggle-done, and list name to TaskListViewModel"

Task 6: TaskDetailViewModel — Editable + Auto-Save + Tags

Files:

  • Modify: src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs

  • Step 1: Read current file

Read src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs.

  • Step 2: Add TagRepository dependency and tag collection

Add to constructor parameters:

private readonly TagRepository _tagRepo;

Update constructor signature:

public TaskDetailViewModel(TaskRepository taskRepo, WorktreeRepository worktreeRepo,
    ListRepository listRepo, GitService git, WorkerClient worker, TagRepository tagRepo)

Store it: _tagRepo = tagRepo;

Add tag collection and new-tag input:

public ObservableCollection<TagEntity> Tags { get; } = new();

[ObservableProperty] private string _newTagInput = "";
  • Step 3: Add editable status and commit type properties

Add observable properties and static choice arrays:

[ObservableProperty] private string _statusChoice = "Manual";
[ObservableProperty] private string _commitType = "chore";

public static string[] StatusChoices { get; } = ["Manual", "Queued", "Running", "Done", "Failed"];
public static string[] CommitTypes { get; } = ["feat", "fix", "refactor", "docs", "test", "chore", "ci", "perf", "style", "build"];
  • Step 4: Add loading guard and TaskChanged event
private bool _isLoading;
public event Action<string>? TaskChanged;
  • Step 5: Update LoadAsync to populate editable fields and tags

In LoadAsync, after setting the existing properties, add:

_isLoading = true;
try
{
    // ... existing property assignments ...
    StatusChoice = task.Status.ToString();
    CommitType = task.CommitType;

    Tags.Clear();
    var tags = await _taskRepo.GetTagsAsync(taskId);
    foreach (var tag in tags)
        Tags.Add(tag);
}
finally
{
    _isLoading = false;
}

Wrap the existing assignments inside the try block. The _isLoading = true goes before any property sets, _isLoading = false in finally.

  • Step 6: Add SaveAsync method
public async Task SaveAsync()
{
    if (_isLoading || _taskId is null) return;

    var entity = await _taskRepo.GetByIdAsync(_taskId);
    if (entity is null) return;

    entity.Title = Title;
    entity.Description = Description;
    entity.CommitType = CommitType;

    if (Enum.TryParse<Data.Models.TaskStatus>(StatusChoice, true, out var status))
        entity.Status = status;

    await _taskRepo.UpdateAsync(entity);
    StatusText = entity.Status.ToString().ToLowerInvariant();
    TaskChanged?.Invoke(_taskId);
}
  • Step 7: Add tag CRUD commands
[RelayCommand]
private async Task AddTag()
{
    var name = NewTagInput.Trim();
    if (string.IsNullOrEmpty(name) || _taskId is null) return;

    var tagId = await _tagRepo.GetOrCreateAsync(name);
    await _taskRepo.AddTagAsync(_taskId, tagId);

    // Reload tags to get the full entity
    Tags.Clear();
    var tags = await _taskRepo.GetTagsAsync(_taskId);
    foreach (var tag in tags)
        Tags.Add(tag);

    NewTagInput = "";
    TaskChanged?.Invoke(_taskId);
}

[RelayCommand]
private async Task RemoveTag(TagEntity tag)
{
    if (_taskId is null) return;

    await _taskRepo.RemoveTagAsync(_taskId, tag.Id);
    Tags.Remove(tag);
    TaskChanged?.Invoke(_taskId);
}
  • Step 8: Update Clear to reset new fields

In Clear(), add:

Tags.Clear();
NewTagInput = "";
StatusChoice = "Manual";
CommitType = "chore";
  • Step 9: Build to verify (expect error)

Run: dotnet build ClaudeDo.slnx Expected: Build error in Program.cs because the TaskDetailViewModel constructor now requires TagRepository. This is fixed in Task 9.

  • Step 10: Commit
git add src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs
git commit -m "feat(ui): make TaskDetailViewModel editable with auto-save and tag CRUD"

Task 7: MainWindowViewModel — Wire TaskChanged Event

Files:

  • Modify: src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs

  • Step 1: Read current file

Read src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs.

  • Step 2: Subscribe to TaskChanged in constructor

In the constructor, after TaskList.SelectedTaskChanged += OnSelectedTaskChanged;, add:

TaskDetail.TaskChanged += taskId => _ = TaskList.RefreshSingleAsync(taskId);
  • Step 3: Commit
git add src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs
git commit -m "feat(ui): wire TaskDetail changes back to task list refresh"

Task 8: DI Registration Update

Files:

  • Modify: src/ClaudeDo.App/Program.cs

  • Step 1: Read current file

Read src/ClaudeDo.App/Program.cs.

  • Step 2: Update TaskDetailViewModel registration

Change the TaskDetailViewModel registration from implicit constructor injection:

sc.AddSingleton<TaskDetailViewModel>();

to explicit factory registration:

sc.AddSingleton<TaskDetailViewModel>(sp => new TaskDetailViewModel(
    sp.GetRequiredService<TaskRepository>(),
    sp.GetRequiredService<WorktreeRepository>(),
    sp.GetRequiredService<ListRepository>(),
    sp.GetRequiredService<GitService>(),
    sp.GetRequiredService<WorkerClient>(),
    sp.GetRequiredService<TagRepository>()));
  • Step 3: Build to verify

Run: dotnet build ClaudeDo.slnx Expected: Build succeeds — the constructor mismatch from Task 6 is now resolved.

  • Step 4: Commit
git add src/ClaudeDo.App/Program.cs
git commit -m "fix(di): register TagRepository in TaskDetailViewModel constructor"

Task 9: MainWindow.axaml — Reactive Layout + Sidebar + Header

Files:

  • Modify: src/ClaudeDo.Ui/Views/MainWindow.axaml

  • Step 1: Read current file

Read src/ClaudeDo.Ui/Views/MainWindow.axaml.

  • Step 2: Replace full MainWindow content

Replace the entire file with:

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:ClaudeDo.Ui.ViewModels"
        xmlns:v="using:ClaudeDo.Ui.Views"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="700"
        x:Class="ClaudeDo.Ui.Views.MainWindow"
        x:DataType="vm:MainWindowViewModel"
        Title="ClaudeDo"
        MinWidth="800" MinHeight="500">

    <DockPanel>
        <!-- Status Bar at bottom -->
        <v:StatusBarView DockPanel.Dock="Bottom" DataContext="{Binding StatusBar}" />

        <!-- Main 3-column layout: proportional sizing -->
        <Grid ColumnDefinitions="1*,Auto,2*,Auto,1.5*">

            <!-- Lists sidebar -->
            <DockPanel Grid.Column="0" MinWidth="180" MaxWidth="320"
                       Background="{StaticResource SidebarBgBrush}">

                <!-- Sidebar header -->
                <TextBlock DockPanel.Dock="Top"
                           Text="Lists" FontWeight="SemiBold" FontSize="13"
                           Foreground="{StaticResource TextSecondaryBrush}"
                           Margin="16,14,16,10"/>

                <!-- + New List link at bottom -->
                <Border DockPanel.Dock="Bottom" Padding="8,8"
                        BorderThickness="0,1,0,0" BorderBrush="{StaticResource BorderSubtleBrush}">
                    <Button Content="+ New List"
                            Command="{Binding AddListCommand}"
                            Background="Transparent"
                            Foreground="{StaticResource AccentBrush}"
                            BorderThickness="0"
                            Padding="12,8"
                            HorizontalAlignment="Stretch"
                            HorizontalContentAlignment="Left"
                            FontSize="13"
                            Cursor="Hand"/>
                </Border>

                <!-- List items -->
                <ListBox ItemsSource="{Binding Lists}"
                         SelectedItem="{Binding SelectedList}"
                         Background="Transparent"
                         Margin="4,0">
                    <ListBox.ItemTemplate>
                        <DataTemplate x:DataType="vm:ListItemViewModel">
                            <Grid ColumnDefinitions="Auto,*" Margin="8,6"
                                  Background="Transparent"
                                  DoubleTapped="OnListItemDoubleTapped"
                                  PointerPressed="OnListItemPointerPressed">
                                <Grid.ContextFlyout>
                                    <MenuFlyout>
                                        <MenuItem Header="Edit"
                                                  Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).EditListCommand}"/>
                                        <MenuItem Header="Delete"
                                                  Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DeleteListCommand}"/>
                                        <Separator/>
                                        <MenuItem Header="New Task"
                                                  Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).TaskList.AddTaskCommand}"/>
                                    </MenuFlyout>
                                </Grid.ContextFlyout>

                                <!-- Colored dot -->
                                <Ellipse Grid.Column="0" Width="8" Height="8"
                                         Fill="{Binding DotBrush}"
                                         VerticalAlignment="Center" Margin="0,0,10,0"/>

                                <!-- List name + working dir -->
                                <StackPanel Grid.Column="1">
                                    <TextBlock Text="{Binding Name}" FontWeight="Medium"
                                               Foreground="{StaticResource TextSecondaryBrush}"/>
                                    <TextBlock Text="{Binding WorkingDir}" FontSize="10"
                                               Foreground="{StaticResource TextDimBrush}"
                                               IsVisible="{Binding WorkingDir, Converter={x:Static ObjectConverters.IsNotNull}}"/>
                                </StackPanel>
                            </Grid>
                        </DataTemplate>
                    </ListBox.ItemTemplate>
                </ListBox>
            </DockPanel>

            <GridSplitter Grid.Column="1" Width="4" ResizeDirection="Columns"/>

            <!-- Tasks pane -->
            <DockPanel Grid.Column="2" Background="{StaticResource ContentBgBrush}">
                <!-- List name as header -->
                <TextBlock DockPanel.Dock="Top"
                           Text="{Binding TaskList.ListName, FallbackValue='Tasks'}"
                           FontWeight="SemiBold" FontSize="16"
                           Foreground="{StaticResource TextPrimaryBrush}"
                           Margin="16,14,16,10"/>

                <v:TaskListView DataContext="{Binding TaskList}" />
            </DockPanel>

            <GridSplitter Grid.Column="3" Width="4" ResizeDirection="Columns"/>

            <!-- Detail pane -->
            <v:TaskDetailView Grid.Column="4" DataContext="{Binding TaskDetail}"
                              MinWidth="280" MaxWidth="500" />
        </Grid>
    </DockPanel>
</Window>

Key changes:

  • ColumnDefinitions="1*,Auto,2*,Auto,1.5*" — proportional, reactive

  • Sidebar: colored dots, + New List link, styled backgrounds

  • Tasks pane: list name header from TaskList.ListName

  • Min/max constraints on sidebar and detail pane

  • List toolbar buttons removed (Edit/Delete via context menu)

  • Step 3: Build to verify

Run: dotnet build ClaudeDo.slnx Expected: Build succeeds.

  • Step 4: Commit
git add src/ClaudeDo.Ui/Views/MainWindow.axaml
git commit -m "style(ui): redesign MainWindow with reactive layout, sidebar polish, and list header"

Task 10: TaskListView.axaml — Checkbox Rows + Inline Add + Remove Toolbar

Files:

  • Modify: src/ClaudeDo.Ui/Views/TaskListView.axaml

  • Step 1: Read current file

Read src/ClaudeDo.Ui/Views/TaskListView.axaml.

  • Step 2: Replace full TaskListView content

Replace the entire file with:

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:vm="using:ClaudeDo.Ui.ViewModels"
             xmlns:conv="using:ClaudeDo.Ui.Converters"
             x:Class="ClaudeDo.Ui.Views.TaskListView"
             x:DataType="vm:TaskListViewModel"
             x:Name="Root">
    <DockPanel>
        <!-- Inline add field at bottom -->
        <Border DockPanel.Dock="Bottom" Padding="8,8"
                BorderThickness="0,1,0,0" BorderBrush="{StaticResource BorderSubtleBrush}">
            <TextBox x:Name="InlineAddBox"
                     Text="{Binding InlineAddTitle, Mode=TwoWay}"
                     Watermark="+ Add a task..."
                     BorderThickness="1"
                     BorderBrush="{StaticResource BorderSubtleBrush}"
                     CornerRadius="8"
                     Padding="10,8"
                     FontSize="13"
                     KeyDown="OnInlineAddKeyDown"
                     GotFocus="OnInlineAddGotFocus"
                     LostFocus="OnInlineAddLostFocus"/>
        </Border>

        <!-- Task list -->
        <ListBox ItemsSource="{Binding Tasks}"
                 SelectedItem="{Binding SelectedTask}"
                 Background="Transparent"
                 Margin="4,0"
                 KeyDown="OnTaskListKeyDown">
            <ListBox.ItemTemplate>
                <DataTemplate x:DataType="vm:TaskItemViewModel">
                    <Grid ColumnDefinitions="Auto,*" Margin="4,4"
                          Background="Transparent"
                          Opacity="{Binding IsDone, Converter={x:Static BoolConverters.ToOpacityConverter}}"
                          DoubleTapped="OnTaskItemDoubleTapped"
                          PointerPressed="OnTaskItemPointerPressed">
                        <Grid.ContextFlyout>
                            <MenuFlyout>
                                <MenuItem Header="Edit"
                                          Command="{Binding #Root.((vm:TaskListViewModel)DataContext).EditTaskCommand}"/>
                                <MenuItem Header="Delete"
                                          Command="{Binding #Root.((vm:TaskListViewModel)DataContext).DeleteTaskCommand}"/>
                                <Separator/>
                                <MenuItem Header="Run Now"
                                          Command="{Binding RunNowCommand}"/>
                            </MenuFlyout>
                        </Grid.ContextFlyout>

                        <!-- Circular checkbox -->
                        <Border Grid.Column="0" Width="22" Height="22"
                                CornerRadius="11"
                                BorderThickness="2"
                                BorderBrush="{Binding StatusText, Converter={x:Static conv:CheckboxBorderConverter.Instance}}"
                                Background="Transparent"
                                VerticalAlignment="Center" Margin="0,0,10,0"
                                Cursor="Hand"
                                PointerPressed="OnCheckboxPressed">
                            <!-- Checkmark for done -->
                            <PathGeometry x:Name="Checkmark"/>
                            <Canvas Width="12" Height="12"
                                    IsVisible="{Binding IsDone}"
                                    HorizontalAlignment="Center" VerticalAlignment="Center">
                                <Path Stroke="{StaticResource StatusGreenBrush}" StrokeThickness="2"
                                      Data="M 1,6 L 4.5,9.5 L 11,3"/>
                            </Canvas>
                            <!-- Running dot -->
                            <Ellipse Width="8" Height="8"
                                     Fill="{StaticResource StatusOrangeBrush}"
                                     IsVisible="{Binding IsRunning}"
                                     HorizontalAlignment="Center" VerticalAlignment="Center"/>
                        </Border>

                        <!-- Task content -->
                        <StackPanel Grid.Column="1" VerticalAlignment="Center">
                            <TextBlock Text="{Binding Title}" FontWeight="Medium"
                                       Foreground="{StaticResource TextPrimaryBrush}"
                                       TextTrimming="CharacterEllipsis">
                                <TextBlock.Styles>
                                    <Style Selector="TextBlock">
                                        <Style.Setters/>
                                    </Style>
                                </TextBlock.Styles>
                            </TextBlock>
                            <TextBlock FontSize="11"
                                       Foreground="{StaticResource TextDimBrush}"
                                       IsVisible="{Binding TagsText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
                                <TextBlock.Text>
                                    <MultiBinding StringFormat="{}{0} · {1}">
                                        <Binding Path="TagsText"/>
                                        <Binding Path="StatusText"/>
                                    </MultiBinding>
                                </TextBlock.Text>
                            </TextBlock>
                            <TextBlock Text="{Binding StatusText}" FontSize="11"
                                       Foreground="{StaticResource TextDimBrush}"
                                       IsVisible="{Binding TagsText, Converter={x:Static StringConverters.IsNullOrEmpty}}"/>
                        </StackPanel>
                    </Grid>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </DockPanel>
</UserControl>

Key changes:

  • Bottom toolbar removed, replaced with inline add TextBox
  • Task rows: circular checkbox on left, status shown as subtitle text instead of badge
  • Running indicator: filled orange dot inside circle
  • Done indicator: green checkmark path inside circle
  • Completed task styling via opacity (IsDone)
  • Title text trimming for responsive layout
  • Tags + status shown as subtitle line

Note: The BoolConverters.ToOpacityConverter may not exist in Avalonia. If it doesn't, we need a simple converter or use a Style with a DataTrigger. Alternative approach using a Style:

Replace the Opacity binding on the Grid with a class-based approach, or add a simple inline style:

<Grid.Styles>
    <Style Selector="Grid.done">
        <Setter Property="Opacity" Value="0.6"/>
    </Style>
</Grid.Styles>

If BoolConverters.ToOpacityConverter doesn't compile, replace the Opacity binding with a fixed value approach: add a RowOpacity property to TaskItemViewModel that returns 0.6 when done, 1.0 otherwise. Bind: Opacity="{Binding RowOpacity}".

  • Step 3: Build and fix any converter issues

Run: dotnet build ClaudeDo.slnx

If BoolConverters.ToOpacityConverter errors, add to TaskItemViewModel.cs:

public double RowOpacity => IsDone ? 0.6 : 1.0;

And in Refresh, add: OnPropertyChanged(nameof(RowOpacity));

Then use Opacity="{Binding RowOpacity}" instead.

  • Step 4: Commit
git add src/ClaudeDo.Ui/Views/TaskListView.axaml
git commit -m "style(ui): redesign task rows with checkboxes, inline add field, remove toolbar"

Task 11: TaskListView.axaml.cs — Inline Add + Task Shortcuts

Files:

  • Modify: src/ClaudeDo.Ui/Views/TaskListView.axaml.cs

  • Step 1: Read current file

Read src/ClaudeDo.Ui/Views/TaskListView.axaml.cs.

  • Step 2: Replace full code-behind

Replace with:

using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using ClaudeDo.Ui.ViewModels;

namespace ClaudeDo.Ui.Views;

public partial class TaskListView : UserControl
{
    public TaskListView()
    {
        InitializeComponent();
    }

    // --- Inline add handlers ---

    private void OnInlineAddKeyDown(object? sender, KeyEventArgs e)
    {
        if (DataContext is not TaskListViewModel vm) return;

        if (e.Key == Key.Enter)
        {
            vm.InlineAddCommand.Execute(null);
            e.Handled = true;
        }
        else if (e.Key == Key.Escape)
        {
            vm.InlineAddTitle = "";
            // Return focus to task list
            this.FindControl<ListBox>("TaskListBox")?.Focus();
            e.Handled = true;
        }
    }

    private void OnInlineAddGotFocus(object? sender, GotFocusEventArgs e)
    {
        if (sender is TextBox tb)
            tb.BorderBrush = App.Current?.FindResource("AccentBrush") as Avalonia.Media.IBrush;
    }

    private void OnInlineAddLostFocus(object? sender, RoutedEventArgs e)
    {
        if (sender is TextBox tb)
            tb.BorderBrush = App.Current?.FindResource("BorderSubtleBrush") as Avalonia.Media.IBrush;
    }

    // --- Task list keyboard shortcuts ---

    private void OnTaskListKeyDown(object? sender, KeyEventArgs e)
    {
        if (DataContext is not TaskListViewModel vm || vm.SelectedTask is null) return;

        switch (e.Key)
        {
            case Key.Delete:
                vm.DeleteTaskCommand.Execute(null);
                e.Handled = true;
                break;
            case Key.Space:
                if (vm.SelectedTask.CanToggleDone)
                {
                    vm.SelectedTask.ToggleDoneCommand.Execute(null);
                    e.Handled = true;
                }
                break;
            case Key.Enter:
            case Key.F2:
                // Focus the detail pane title field
                var detailView = this.GetVisualAncestors().OfType<Window>().FirstOrDefault()
                    ?.GetVisualDescendants().OfType<TaskDetailView>().FirstOrDefault();
                detailView?.FocusTitle();
                e.Handled = true;
                break;
        }
    }

    // --- Checkbox click ---

    private void OnCheckboxPressed(object? sender, PointerPressedEventArgs e)
    {
        if (sender is not Border { DataContext: TaskItemViewModel task }) return;
        if (task.CanToggleDone)
        {
            task.ToggleDoneCommand.Execute(null);
            e.Handled = true;
        }
    }

    // --- Existing handlers (right-click, double-tap) ---

    private void OnTaskItemDoubleTapped(object? sender, TappedEventArgs e)
    {
        if (DataContext is TaskListViewModel vm)
            vm.EditTaskCommand.Execute(null);
    }

    private void OnTaskItemPointerPressed(object? sender, PointerPressedEventArgs e)
    {
        var props = e.GetCurrentPoint(this).Properties;
        if (!props.IsRightButtonPressed) return;

        if (sender is Grid { DataContext: TaskItemViewModel item }
            && DataContext is TaskListViewModel vm)
        {
            vm.SelectedTask = item;
        }
    }

    /// <summary>Focus the inline add TextBox (called from keyboard shortcut).</summary>
    public void FocusInlineAdd()
    {
        this.FindControl<TextBox>("InlineAddBox")?.Focus();
    }

}

Note: The ListBox needs an x:Name for focus return. Go back to TaskListView.axaml and add x:Name="TaskListBox" to the ListBox element.

  • Step 3: Update TaskListView.axaml — add ListBox name

In TaskListView.axaml, find the ListBox and add x:Name="TaskListBox":

<ListBox x:Name="TaskListBox"
         ItemsSource="{Binding Tasks}"
  • Step 4: Build to verify

Run: dotnet build ClaudeDo.slnx Expected: Build succeeds. The App.Current?.FindResource calls may need the correct namespace — if App is ambiguous, use Avalonia.Application.Current?.FindResource.

  • Step 5: Commit
git add src/ClaudeDo.Ui/Views/TaskListView.axaml src/ClaudeDo.Ui/Views/TaskListView.axaml.cs
git commit -m "feat(ui): add inline add handlers, checkbox click, and task keyboard shortcuts"

Task 12: TaskDetailView.axaml — Editable Fields + Tag Chips

Files:

  • Modify: src/ClaudeDo.Ui/Views/TaskDetailView.axaml

  • Step 1: Read current file

Read src/ClaudeDo.Ui/Views/TaskDetailView.axaml.

  • Step 2: Replace full TaskDetailView content

Replace with:

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:vm="using:ClaudeDo.Ui.ViewModels"
             xmlns:conv="using:ClaudeDo.Ui.Converters"
             xmlns:m="using:ClaudeDo.Data.Models"
             x:Class="ClaudeDo.Ui.Views.TaskDetailView"
             x:DataType="vm:TaskDetailViewModel">
    <ScrollViewer>
        <StackPanel Margin="12" Spacing="8"
                    IsVisible="{Binding Title, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">

            <!-- === EDITABLE ZONE === -->

            <!-- Title (large, editable) -->
            <TextBox x:Name="TitleBox"
                     Text="{Binding Title}"
                     FontWeight="Bold" FontSize="16"
                     Foreground="{StaticResource TextPrimaryBrush}"
                     BorderThickness="0" Background="Transparent"
                     Padding="0,4"
                     LostFocus="OnFieldLostFocus"/>

            <!-- Status + Commit Type row -->
            <Grid ColumnDefinitions="*,16,*" Margin="0,4,0,0">
                <StackPanel Grid.Column="0" Spacing="4">
                    <TextBlock Text="Status" FontSize="12" FontWeight="SemiBold"
                               Foreground="{StaticResource TextSecondaryBrush}"/>
                    <ComboBox ItemsSource="{Binding StatusChoices}"
                              SelectedItem="{Binding StatusChoice}"
                              MinWidth="100"
                              LostFocus="OnFieldLostFocus"/>
                </StackPanel>
                <StackPanel Grid.Column="2" Spacing="4">
                    <TextBlock Text="Commit Type" FontSize="12" FontWeight="SemiBold"
                               Foreground="{StaticResource TextSecondaryBrush}"/>
                    <ComboBox ItemsSource="{Binding CommitTypes}"
                              SelectedItem="{Binding CommitType}"
                              MinWidth="100"
                              LostFocus="OnFieldLostFocus"/>
                </StackPanel>
            </Grid>

            <!-- Tags -->
            <StackPanel Spacing="4" Margin="0,8,0,0">
                <TextBlock Text="Tags" FontSize="12" FontWeight="SemiBold"
                           Foreground="{StaticResource TextSecondaryBrush}"/>
                <WrapPanel Orientation="Horizontal">
                    <ItemsControl ItemsSource="{Binding Tags}">
                        <ItemsControl.ItemsPanel>
                            <ItemsPanelTemplate>
                                <WrapPanel Orientation="Horizontal"/>
                            </ItemsPanelTemplate>
                        </ItemsControl.ItemsPanel>
                        <ItemsControl.ItemTemplate>
                            <DataTemplate x:DataType="m:TagEntity">
                                <Border CornerRadius="10" Padding="8,3" Margin="0,0,4,4"
                                        Background="{StaticResource AccentSubtleBrush}">
                                    <StackPanel Orientation="Horizontal" Spacing="4">
                                        <TextBlock Text="{Binding Name}" FontSize="12"
                                                   Foreground="{StaticResource AccentLightBrush}"
                                                   VerticalAlignment="Center"/>
                                        <Button Content="x" FontSize="10" Padding="2,0"
                                                Background="Transparent" BorderThickness="0"
                                                Foreground="{StaticResource TextMutedBrush}"
                                                Cursor="Hand"
                                                Command="{Binding $parent[UserControl].((vm:TaskDetailViewModel)DataContext).RemoveTagCommand}"
                                                CommandParameter="{Binding}"/>
                                    </StackPanel>
                                </Border>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                    </ItemsControl>
                    <TextBox Text="{Binding NewTagInput}"
                             Watermark="Add tag..."
                             Width="100" FontSize="12"
                             BorderThickness="0" Background="Transparent"
                             Padding="4,3"
                             KeyDown="OnTagInputKeyDown"/>
                </WrapPanel>
            </StackPanel>

            <!-- Description (editable) -->
            <TextBlock Text="Description" FontSize="12" FontWeight="SemiBold"
                       Foreground="{StaticResource TextSecondaryBrush}" Margin="0,8,0,2"/>
            <TextBox Text="{Binding Description}"
                     AcceptsReturn="True" TextWrapping="Wrap" MinHeight="60"
                     Foreground="{StaticResource TextPrimaryBrush}"
                     Watermark="Add a description..."
                     LostFocus="OnFieldLostFocus"/>

            <!-- === READ-ONLY ZONE === -->

            <!-- Result -->
            <TextBlock Text="Result" FontWeight="SemiBold" FontSize="12"
                       Foreground="{StaticResource TextSecondaryBrush}" Margin="0,12,0,2"/>
            <TextBox Text="{Binding Result, Mode=OneWay}" IsReadOnly="True"
                     AcceptsReturn="True" TextWrapping="Wrap" MinHeight="60"
                     IsVisible="{Binding Result, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
            <TextBlock Text="(no result yet)" Foreground="{StaticResource TextMutedBrush}" FontStyle="Italic"
                       IsVisible="{Binding Result, Converter={x:Static StringConverters.IsNullOrEmpty}}"/>

            <!-- Log path -->
            <StackPanel Orientation="Horizontal" Spacing="4"
                        IsVisible="{Binding LogPath, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
                <TextBlock Text="Log:" FontWeight="SemiBold" FontSize="12"
                           Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center"/>
                <TextBlock Text="{Binding LogPath}" FontSize="11"
                           Foreground="{StaticResource TextDimBrush}" VerticalAlignment="Center"/>
            </StackPanel>

            <!-- Live stream -->
            <TextBlock Text="Live Output" FontWeight="SemiBold" FontSize="12"
                       Foreground="{StaticResource TextSecondaryBrush}" Margin="0,8,0,2"/>
            <Border BorderBrush="{StaticResource BorderSubtleBrush}" BorderThickness="1"
                    CornerRadius="6" Padding="6" MaxHeight="200">
                <ScrollViewer>
                    <ItemsControl ItemsSource="{Binding LiveLines}">
                        <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <TextBlock Text="{Binding}" FontFamily="Consolas,Courier New,monospace"
                                           FontSize="11" TextWrapping="NoWrap"
                                           Foreground="{StaticResource TextPrimaryBrush}"/>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                    </ItemsControl>
                </ScrollViewer>
            </Border>

            <!-- Worktree section -->
            <Border IsVisible="{Binding HasWorktree}" BorderBrush="{StaticResource AccentBrush}"
                    BorderThickness="1" CornerRadius="8" Padding="10" Margin="0,8,0,0">
                <StackPanel Spacing="6">
                    <TextBlock Text="Worktree" FontWeight="Bold" FontSize="14"
                               Foreground="{StaticResource TextPrimaryBrush}"/>
                    <StackPanel Orientation="Horizontal" Spacing="8">
                        <TextBlock Text="Branch:" FontWeight="SemiBold"
                                   Foreground="{StaticResource TextSecondaryBrush}"/>
                        <TextBlock Text="{Binding BranchName}" FontFamily="Consolas,Courier New,monospace"
                                   Foreground="{StaticResource TextPrimaryBrush}"/>
                    </StackPanel>
                    <StackPanel Orientation="Horizontal" Spacing="8">
                        <TextBlock Text="State:" FontWeight="SemiBold"
                                   Foreground="{StaticResource TextSecondaryBrush}"/>
                        <TextBlock Text="{Binding WorktreeState}"
                                   Foreground="{StaticResource TextPrimaryBrush}"/>
                    </StackPanel>
                    <TextBlock Text="Diff Stat:" FontWeight="SemiBold"
                               Foreground="{StaticResource TextSecondaryBrush}"
                               IsVisible="{Binding DiffStat, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
                    <TextBox Text="{Binding DiffStat, Mode=OneWay}" IsReadOnly="True"
                             AcceptsReturn="True" FontFamily="Consolas,Courier New,monospace" FontSize="11"
                             IsVisible="{Binding DiffStat, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
                    <WrapPanel Orientation="Horizontal" Margin="0,4,0,0">
                        <Button Content="Open Worktree" Command="{Binding OpenWorktreeCommand}" Margin="0,0,4,4"/>
                        <Button Content="Show Diff" Command="{Binding ShowDiffCommand}" Margin="0,0,4,4"/>
                        <Button Content="Merge into main" Command="{Binding MergeIntoMainCommand}"
                                IsEnabled="{Binding CanWorktreeAction}" Margin="0,0,4,4"/>
                        <Button Content="Keep as branch" Command="{Binding KeepAsBranchCommand}"
                                IsEnabled="{Binding CanWorktreeAction}" Margin="0,0,4,4"/>
                        <Button Content="Discard" Command="{Binding DiscardCommand}"
                                IsEnabled="{Binding CanWorktreeAction}" Margin="0,0,4,4"/>
                    </WrapPanel>
                </StackPanel>
            </Border>
        </StackPanel>
    </ScrollViewer>
</UserControl>

Key changes:

  • Title is an editable TextBox (borderless, transparent background)

  • Status and CommitType are ComboBox dropdowns

  • Tags shown as chips with x remove button + inline add input

  • All editable fields have LostFocus="OnFieldLostFocus" for auto-save

  • Styled with accent color resources throughout

  • Read-only zone (result, log, live output, worktree) unchanged but restyled

  • Step 3: Build to verify

Run: dotnet build ClaudeDo.slnx Expected: Build error — OnFieldLostFocus and OnTagInputKeyDown not yet in code-behind. Fixed in next task.

  • Step 4: Commit
git add src/ClaudeDo.Ui/Views/TaskDetailView.axaml
git commit -m "style(ui): redesign TaskDetailView with editable fields, tag chips, and accent styling"

Task 13: TaskDetailView.axaml.cs — Auto-Save Handlers

Files:

  • Modify: src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs

  • Step 1: Read current file

Read src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs.

  • Step 2: Replace code-behind

Replace with:

using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using ClaudeDo.Ui.ViewModels;

namespace ClaudeDo.Ui.Views;

public partial class TaskDetailView : UserControl
{
    public TaskDetailView()
    {
        InitializeComponent();
    }

    private async void OnFieldLostFocus(object? sender, RoutedEventArgs e)
    {
        if (DataContext is TaskDetailViewModel vm)
            await vm.SaveAsync();
    }

    private void OnTagInputKeyDown(object? sender, KeyEventArgs e)
    {
        if (e.Key == Key.Enter && DataContext is TaskDetailViewModel vm)
        {
            vm.AddTagCommand.Execute(null);
            e.Handled = true;
        }
    }

    /// <summary>Focus the title TextBox (called from Enter/F2 on task list).</summary>
    public void FocusTitle()
    {
        this.FindControl<TextBox>("TitleBox")?.Focus();
    }
}
  • Step 3: Build to verify

Run: dotnet build ClaudeDo.slnx Expected: Build succeeds.

  • Step 4: Commit
git add src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs
git commit -m "feat(ui): add auto-save LostFocus handlers and tag input KeyDown"

Task 14: MainWindow.axaml.cs — Global Keyboard Shortcuts

Files:

  • Modify: src/ClaudeDo.Ui/Views/MainWindow.axaml.cs

  • Step 1: Read current file

Read src/ClaudeDo.Ui/Views/MainWindow.axaml.cs (currently in context from earlier read).

  • Step 2: Add global KeyDown handler

Add the KeyDown event to the Window in MainWindow.axaml:

<Window ... KeyDown="OnGlobalKeyDown">

Then update the code-behind. Keep existing handlers and add:

using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using ClaudeDo.Ui.ViewModels;

namespace ClaudeDo.Ui.Views;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    protected override async void OnOpened(EventArgs e)
    {
        base.OnOpened(e);
        if (DataContext is MainWindowViewModel vm)
            await vm.InitializeAsync();
    }

    // --- Global keyboard shortcuts ---

    private void OnGlobalKeyDown(object? sender, KeyEventArgs e)
    {
        if (DataContext is not MainWindowViewModel vm) return;

        var ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control);
        var shift = e.KeyModifiers.HasFlag(KeyModifiers.Shift);

        if (ctrl && shift && e.Key == Key.N)
        {
            // Ctrl+Shift+N → Add new list
            vm.AddListCommand.Execute(null);
            e.Handled = true;
        }
        else if (ctrl && e.Key == Key.N)
        {
            // Ctrl+N → Focus inline add
            var taskListView = this.FindDescendantOfType<TaskListView>();
            taskListView?.FocusInlineAdd();
            e.Handled = true;
        }
        else if (ctrl && e.Key == Key.L)
        {
            // Ctrl+L → Focus lists pane
            var listBox = this.FindControl<ListBox>("ListsBox");
            listBox?.Focus();
            e.Handled = true;
        }
        else if (ctrl && e.Key == Key.R)
        {
            // Ctrl+R → Run now
            if (vm.TaskList.SelectedTask is { } task)
            {
                task.RunNowCommand.Execute(null);
                e.Handled = true;
            }
        }
    }

    // --- Existing handlers ---

    private void OnListItemDoubleTapped(object? sender, TappedEventArgs e)
    {
        if (DataContext is MainWindowViewModel vm)
            vm.EditListCommand.Execute(null);
    }

    private void OnListItemPointerPressed(object? sender, PointerPressedEventArgs e)
    {
        var props = e.GetCurrentPoint(this).Properties;
        if (!props.IsRightButtonPressed) return;

        if (sender is Grid { DataContext: ListItemViewModel item }
            && DataContext is MainWindowViewModel vm)
        {
            vm.SelectedList = item;
        }
    }
}

Note: The Lists ListBox needs x:Name="ListsBox" in MainWindow.axaml for the Ctrl+L shortcut. Add it to the ListBox element:

<ListBox x:Name="ListsBox"
         ItemsSource="{Binding Lists}"
  • Step 3: Add x:Name and KeyDown to MainWindow.axaml

In MainWindow.axaml, add KeyDown="OnGlobalKeyDown" to the <Window> tag and x:Name="ListsBox" to the lists ListBox.

  • Step 4: Build to verify

Run: dotnet build ClaudeDo.slnx

If FindDescendantOfType<TaskListView>() doesn't compile, add using Avalonia.VisualTree; — this extension method is in that namespace. If it still doesn't resolve, use this.GetVisualDescendants().OfType<TaskListView>().FirstOrDefault().

Expected: Build succeeds.

  • Step 5: Commit
git add src/ClaudeDo.Ui/Views/MainWindow.axaml src/ClaudeDo.Ui/Views/MainWindow.axaml.cs
git commit -m "feat(ui): add global keyboard shortcuts (Ctrl+N, Ctrl+L, Ctrl+R, Ctrl+Shift+N)"

Task 15: Completed Task Strikethrough Styling

The opacity approach handles dimming. For strikethrough on done task titles, we need a style or converter.

Files:

  • Modify: src/ClaudeDo.Ui/Views/TaskListView.axaml

  • Modify: src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs

  • Step 1: Add strikethrough properties to TaskItemViewModel

In TaskItemViewModel.cs, add:

public TextDecorationCollection? TitleDecorations => IsDone
    ? TextDecorations.Strikethrough
    : null;

public IBrush TitleForeground => IsDone
    ? new SolidColorBrush(Color.Parse("#5a6578"))
    : new SolidColorBrush(Color.Parse("#e2e8f0"));

Add required using:

using Avalonia.Media;

In Refresh, add:

OnPropertyChanged(nameof(TitleDecorations));
OnPropertyChanged(nameof(TitleForeground));
  • Step 2: Update TaskListView.axaml title TextBlock

In TaskListView.axaml, update the title TextBlock in the task row template:

Find:

<TextBlock Text="{Binding Title}" FontWeight="Medium"
           Foreground="{StaticResource TextPrimaryBrush}"
           TextTrimming="CharacterEllipsis">
    <TextBlock.Styles>
        <Style Selector="TextBlock">
            <Style.Setters/>
        </Style>
    </TextBlock.Styles>
</TextBlock>

Replace with:

<TextBlock Text="{Binding Title}" FontWeight="Medium"
           Foreground="{Binding TitleForeground}"
           TextDecorations="{Binding TitleDecorations}"
           TextTrimming="CharacterEllipsis"/>
  • Step 3: Add RowOpacity to TaskItemViewModel (if not done in Task 10)

Ensure this exists in TaskItemViewModel.cs:

public double RowOpacity => IsDone ? 0.6 : 1.0;

And in Refresh: OnPropertyChanged(nameof(RowOpacity));

Update the Grid binding in TaskListView.axaml:

<Grid ... Opacity="{Binding RowOpacity}" ...>

(Remove the BoolConverters.ToOpacityConverter binding if it was used.)

  • Step 4: Build to verify

Run: dotnet build ClaudeDo.slnx Expected: Build succeeds.

  • Step 5: Commit
git add src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs src/ClaudeDo.Ui/Views/TaskListView.axaml
git commit -m "style(ui): add strikethrough and dimming for completed tasks"

Task 16: Final Build + Smoke Test

  • Step 1: Full clean build

Run: dotnet build ClaudeDo.slnx --no-incremental Expected: Build succeeds with 0 errors.

  • Step 2: Run existing tests

Run: dotnet test tests/ClaudeDo.Worker.Tests Expected: All tests pass (no Worker/Data changes were made).

  • Step 3: Launch app and verify

Run the app: dotnet run --project src/ClaudeDo.App

Verify:

  1. Dark theme with Forest Teal accent colors
  2. Lists sidebar shows colored dots and + New List link
  3. Selecting a list shows its name as the tasks header
  4. Task rows show circular checkboxes (gray border for Manual/Queued)
  5. Inline add field at bottom of task list — type and press Enter to create
  6. New task auto-selects and detail pane shows editable fields
  7. Edit title/description in detail pane, click away → auto-saves
  8. Add/remove tags in detail pane
  9. Click checkbox → toggles done (strikethrough + dimmed)
  10. Keyboard: Ctrl+N focuses inline add, Delete removes task, Space toggles done
  11. Ctrl+L focuses lists, Ctrl+Shift+N opens list dialog
  12. Ctrl+R runs selected task (when worker connected)
  13. Resizing window: columns resize proportionally
  14. Context menus still work on lists and tasks
  15. Double-click list/task still opens editor dialogs
  • Step 4: Commit any final fixes
git add -A
git commit -m "fix(ui): address smoke test issues from UX redesign"

(Only if fixes were needed.)


Summary

Task Description Files
1 Accent color resources + dark theme App.axaml
2 CheckboxBorderConverter new Converter
3 ListItemViewModel DotBrush ListItemViewModel.cs
4 TaskItemViewModel ToggleDone TaskItemViewModel.cs
5 TaskListViewModel inline add + toggle done TaskListViewModel.cs
6 TaskDetailViewModel editable + auto-save TaskDetailViewModel.cs
7 MainWindowViewModel wire TaskChanged MainWindowViewModel.cs
8 DI registration update Program.cs
9 MainWindow.axaml redesign MainWindow.axaml
10 TaskListView.axaml redesign TaskListView.axaml
11 TaskListView.axaml.cs handlers TaskListView.axaml.cs
12 TaskDetailView.axaml redesign TaskDetailView.axaml
13 TaskDetailView.axaml.cs handlers TaskDetailView.axaml.cs
14 MainWindow.axaml.cs keyboard shortcuts MainWindow.axaml.cs
15 Completed task strikethrough TaskItemViewModel + TaskListView
16 Final build + smoke test all