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 Listlink, 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
xremove 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:
- Dark theme with Forest Teal accent colors
- Lists sidebar shows colored dots and
+ New Listlink - Selecting a list shows its name as the tasks header
- Task rows show circular checkboxes (gray border for Manual/Queued)
- Inline add field at bottom of task list — type and press Enter to create
- New task auto-selects and detail pane shows editable fields
- Edit title/description in detail pane, click away → auto-saves
- Add/remove tags in detail pane
- Click checkbox → toggles done (strikethrough + dimmed)
- Keyboard: Ctrl+N focuses inline add, Delete removes task, Space toggles done
- Ctrl+L focuses lists, Ctrl+Shift+N opens list dialog
- Ctrl+R runs selected task (when worker connected)
- Resizing window: columns resize proportionally
- Context menus still work on lists and tasks
- 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 |