From a548d41d185e913f409f41c3726cd3374deb87b8 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Tue, 14 Apr 2026 10:20:22 +0200 Subject: [PATCH 01/19] feat(ui): add CheckboxBorderConverter for task status circles Co-Authored-By: Claude Sonnet 4.6 --- .../Converters/CheckboxBorderConverter.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/ClaudeDo.Ui/Converters/CheckboxBorderConverter.cs diff --git a/src/ClaudeDo.Ui/Converters/CheckboxBorderConverter.cs b/src/ClaudeDo.Ui/Converters/CheckboxBorderConverter.cs new file mode 100644 index 0000000..9e9e20f --- /dev/null +++ b/src/ClaudeDo.Ui/Converters/CheckboxBorderConverter.cs @@ -0,0 +1,30 @@ +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(); +} From d8c628c15ae5ba9594fa0d6253b25d925bb8167c Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Tue, 14 Apr 2026 10:20:58 +0200 Subject: [PATCH 02/19] style: add Forest Teal accent resources and force dark theme Co-Authored-By: Claude Opus 4.6 (1M context) --- src/ClaudeDo.App/App.axaml | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/ClaudeDo.App/App.axaml b/src/ClaudeDo.App/App.axaml index c2f87da..480c689 100644 --- a/src/ClaudeDo.App/App.axaml +++ b/src/ClaudeDo.App/App.axaml @@ -2,13 +2,37 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ClaudeDo.App.App" xmlns:local="using:ClaudeDo.App" - RequestedThemeVariant="Default"> - + RequestedThemeVariant="Dark"> + + + + + + + + + + + + + + + + + + + + + + + + + - + From 3c52e9c67f368855b11576eb9a4e476cb1563e60 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Tue, 14 Apr 2026 10:20:59 +0200 Subject: [PATCH 03/19] feat(ui): add colored dot brush to ListItemViewModel Co-Authored-By: Claude Opus 4.6 (1M context) --- src/ClaudeDo.Ui/ViewModels/ListItemViewModel.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/ClaudeDo.Ui/ViewModels/ListItemViewModel.cs b/src/ClaudeDo.Ui/ViewModels/ListItemViewModel.cs index 889a94c..8be03d5 100644 --- a/src/ClaudeDo.Ui/ViewModels/ListItemViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/ListItemViewModel.cs @@ -1,3 +1,5 @@ +using System; +using Avalonia.Media; using ClaudeDo.Data.Models; using CommunityToolkit.Mvvm.ComponentModel; @@ -9,6 +11,17 @@ public partial class ListItemViewModel : ViewModelBase [ObservableProperty] private string? _workingDir; [ObservableProperty] private string _defaultCommitType; + 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]; + public string Id { get; } public ListItemViewModel(ListEntity entity) From 0796b3c2d577370a55b38d4f6e3610bfd1272eb1 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Tue, 14 Apr 2026 10:21:47 +0200 Subject: [PATCH 04/19] feat(ui): add ToggleDone command and checkbox state to TaskItemViewModel --- .../ViewModels/TaskItemViewModel.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs b/src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs index bf4c3d7..af7ad09 100644 --- a/src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs @@ -20,9 +20,10 @@ public partial class TaskItemViewModel : ViewModelBase private readonly Func? _runNow; private readonly Func _canRunNow; + private readonly Func? _toggleDone; public TaskItemViewModel(TaskEntity entity, IReadOnlyList tags, - Func? runNow, Func canRunNow) + Func? runNow, Func canRunNow, Func? toggleDone = null) { Entity = entity; Id = entity.Id; @@ -35,8 +36,13 @@ public partial class TaskItemViewModel : ViewModelBase _description = entity.Description; _runNow = runNow; _canRunNow = canRunNow; + _toggleDone = toggleDone; } + public bool IsDone => Status == TaskStatus.Done; + public bool IsRunning => Status == TaskStatus.Running; + public bool CanToggleDone => Status != TaskStatus.Running && Status != TaskStatus.Failed; + public void Refresh(TaskEntity entity, IReadOnlyList tags) { Entity = entity; @@ -47,6 +53,10 @@ public partial class TaskItemViewModel : ViewModelBase CommitType = entity.CommitType; Description = entity.Description; RunNowCommand.NotifyCanExecuteChanged(); + OnPropertyChanged(nameof(IsDone)); + OnPropertyChanged(nameof(IsRunning)); + OnPropertyChanged(nameof(CanToggleDone)); + ToggleDoneCommand.NotifyCanExecuteChanged(); } [RelayCommand(CanExecute = nameof(CanRunNow))] @@ -58,4 +68,11 @@ public partial class TaskItemViewModel : ViewModelBase private bool CanRunNow() => _canRunNow() && Status == TaskStatus.Queued; + + [RelayCommand(CanExecute = nameof(CanToggleDone))] + private async Task ToggleDone() + { + if (_toggleDone is not null) + await _toggleDone(Id); + } } From a4da2e23a1f0e2d0a610f4a1ce65635643dedf9a Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Tue, 14 Apr 2026 10:23:58 +0200 Subject: [PATCH 05/19] feat(ui): add inline task creation, toggle-done, and list name to TaskListViewModel Co-Authored-By: Claude Sonnet 4.6 --- .../ViewModels/TaskListViewModel.cs | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs b/src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs index c81b37a..baf25f7 100644 --- a/src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs @@ -25,6 +25,8 @@ public partial class TaskListViewModel : ViewModelBase [ObservableProperty] private TaskItemViewModel? _selectedTask; [ObservableProperty, NotifyCanExecuteChangedFor(nameof(AddTaskCommand))] private string? _currentListId; + [ObservableProperty] private string _listName = "Tasks"; + [ObservableProperty] private string _inlineAddTitle = ""; public event Action? SelectedTaskChanged; @@ -61,6 +63,16 @@ public partial class TaskListViewModel : ViewModelBase Tasks.Clear(); SelectedTask = null; + if (listId is not null) + { + var list = await _listRepo.GetByIdAsync(listId); + ListName = list?.Name ?? "Tasks"; + } + else + { + ListName = "Tasks"; + } + if (listId is null) return; try @@ -69,7 +81,7 @@ public partial class TaskListViewModel : ViewModelBase foreach (var e in entities) { var tags = await _taskRepo.GetEffectiveTagsAsync(e.Id); - Tasks.Add(new TaskItemViewModel(e, tags, RunNowAsync, () => _worker.IsConnected)); + Tasks.Add(new TaskItemViewModel(e, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync)); } } catch (Exception ex) @@ -80,6 +92,40 @@ public partial class TaskListViewModel : ViewModelBase private bool CanAddTask() => CurrentListId is not null; + [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}"); + } + } + [RelayCommand(CanExecute = nameof(CanAddTask))] private async Task AddTask() { @@ -108,7 +154,7 @@ public partial class TaskListViewModel : ViewModelBase } var tags = await _taskRepo.GetEffectiveTagsAsync(saved.Id); - Tasks.Add(new TaskItemViewModel(saved, tags, RunNowAsync, () => _worker.IsConnected)); + Tasks.Add(new TaskItemViewModel(saved, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync)); // Auto wake-queue if agent+queued if (saved.Status == TaskStatus.Queued && @@ -206,6 +252,19 @@ public partial class TaskListViewModel : ViewModelBase } } + 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); + } + private async void OnTaskUpdated(string taskId) { if (CurrentListId is null) return; From 28a0d9b11f63d022711c8cb32d81c1425b13c8c1 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Tue, 14 Apr 2026 10:24:10 +0200 Subject: [PATCH 06/19] feat(ui): make TaskDetailViewModel editable with auto-save and tag CRUD Co-Authored-By: Claude Sonnet 4.6 --- .../ViewModels/TaskDetailViewModel.cs | 91 +++++++++++++++++-- 1 file changed, 84 insertions(+), 7 deletions(-) diff --git a/src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs b/src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs index ce09b6d..c6959f6 100644 --- a/src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs @@ -16,12 +16,18 @@ public partial class TaskDetailViewModel : ViewModelBase private readonly ListRepository _listRepo; private readonly GitService _git; private readonly WorkerClient _worker; + private readonly TagRepository _tagRepo; [ObservableProperty] private string _title = ""; [ObservableProperty] private string? _description; [ObservableProperty] private string? _result; [ObservableProperty] private string? _logPath; [ObservableProperty] private string _statusText = ""; + [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"]; // Worktree [ObservableProperty] private bool _hasWorktree; @@ -32,19 +38,25 @@ public partial class TaskDetailViewModel : ViewModelBase // Live stream public ObservableCollection LiveLines { get; } = new(); + public ObservableCollection Tags { get; } = new(); + [ObservableProperty] private string _newTagInput = ""; private string? _taskId; private string? _listId; + private bool _isLoading; private const int MaxLiveLines = 500; + public event Action? TaskChanged; + public TaskDetailViewModel(TaskRepository taskRepo, WorktreeRepository worktreeRepo, - ListRepository listRepo, GitService git, WorkerClient worker) + ListRepository listRepo, GitService git, WorkerClient worker, TagRepository tagRepo) { _taskRepo = taskRepo; _worktreeRepo = worktreeRepo; _listRepo = listRepo; _git = git; _worker = worker; + _tagRepo = tagRepo; worker.TaskMessageEvent += OnTaskMessage; worker.WorktreeUpdatedEvent += OnWorktreeUpdated; @@ -59,16 +71,77 @@ public partial class TaskDetailViewModel : ViewModelBase var task = await _taskRepo.GetByIdAsync(taskId); if (task is null) return; - _listId = task.ListId; - Title = task.Title; - Description = task.Description; - Result = task.Result; - LogPath = task.LogPath; - StatusText = task.Status.ToString().ToLowerInvariant(); + _isLoading = true; + try + { + _listId = task.ListId; + Title = task.Title; + Description = task.Description; + Result = task.Result; + LogPath = task.LogPath; + StatusText = task.Status.ToString().ToLowerInvariant(); + 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; + } await LoadWorktreeAsync(taskId); } + 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(StatusChoice, true, out var status)) + entity.Status = status; + + await _taskRepo.UpdateAsync(entity); + StatusText = entity.Status.ToString().ToLowerInvariant(); + TaskChanged?.Invoke(_taskId); + } + + [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); + + 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); + } + public void Clear() { _taskId = null; @@ -80,6 +153,10 @@ public partial class TaskDetailViewModel : ViewModelBase StatusText = ""; HasWorktree = false; LiveLines.Clear(); + Tags.Clear(); + NewTagInput = ""; + StatusChoice = "Manual"; + CommitType = "chore"; } private async Task LoadWorktreeAsync(string taskId) From f51278e1aaf379c9f22fbd7cfaaf7a2978935e90 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Tue, 14 Apr 2026 10:25:16 +0200 Subject: [PATCH 07/19] feat(ui): wire TaskDetail changes back to task list refresh Co-Authored-By: Claude Sonnet 4.6 --- src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs b/src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs index dc853dd..4b3d573 100644 --- a/src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs @@ -41,6 +41,7 @@ public partial class MainWindowViewModel : ViewModelBase StatusBar = statusBar; TaskList.SelectedTaskChanged += OnSelectedTaskChanged; + TaskDetail.TaskChanged += taskId => _ = TaskList.RefreshSingleAsync(taskId); } public async Task InitializeAsync() From 5b6c095a895de625587592dc0fea79e2374b2901 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Tue, 14 Apr 2026 10:25:22 +0200 Subject: [PATCH 08/19] fix(di): register TagRepository in TaskDetailViewModel constructor Co-Authored-By: Claude Sonnet 4.6 --- src/ClaudeDo.App/Program.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ClaudeDo.App/Program.cs b/src/ClaudeDo.App/Program.cs index e5842db..7a5ce37 100644 --- a/src/ClaudeDo.App/Program.cs +++ b/src/ClaudeDo.App/Program.cs @@ -60,7 +60,13 @@ sealed class Program sc.AddTransient(); sc.AddTransient(); sc.AddSingleton(); - sc.AddSingleton(); + sc.AddSingleton(sp => new TaskDetailViewModel( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService())); sc.AddSingleton(sp => { var taskRepo = sp.GetRequiredService(); From 2d6b5bbaffc522a3d0c13db5b95f0dfe80916592 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Tue, 14 Apr 2026 10:27:49 +0200 Subject: [PATCH 09/19] style(ui): redesign MainWindow with reactive layout, sidebar polish, and list header Co-Authored-By: Claude Sonnet 4.6 --- src/ClaudeDo.Ui/Views/MainWindow.axaml | 88 +++++++++++++++-------- src/ClaudeDo.Ui/Views/MainWindow.axaml.cs | 2 + 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/src/ClaudeDo.Ui/Views/MainWindow.axaml b/src/ClaudeDo.Ui/Views/MainWindow.axaml index 0fe8f56..0fd43b8 100644 --- a/src/ClaudeDo.Ui/Views/MainWindow.axaml +++ b/src/ClaudeDo.Ui/Views/MainWindow.axaml @@ -7,32 +7,49 @@ mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="700" x:Class="ClaudeDo.Ui.Views.MainWindow" x:DataType="vm:MainWindowViewModel" - Title="ClaudeDo" Width="1200" Height="700" - MinWidth="800" MinHeight="500"> + Title="ClaudeDo" + MinWidth="800" MinHeight="500" + KeyDown="OnGlobalKeyDown"> - - - - - - - - - -