feat(ui): wire avalonia desktop ui to data and worker

App: build a ServiceProvider in Program.cs (AppSettings, SqliteConnectionFactory,
all repositories, GitService, WorkerClient, all view-models), apply schema, then
hand control to Avalonia. App.OnFrameworkInitializationCompleted resolves
MainWindowViewModel from the container.

Ui:
- AppSettings POCO loaded from ~/.todo-app/ui.config.json (db path, hub url).
- WorkerClient wraps HubConnection with auto-reconnect, exposes IsConnected and
  ActiveTasks plus C# events for TaskStarted/Finished/Message/Updated and
  WorktreeUpdated; all inbound events are marshalled to the UI thread.
- ViewModels: MainWindow (lists CRUD via ListEditor dialog), TaskList (load by
  list, add/edit/delete, auto WakeQueue on agent+queued create), TaskItem
  (RunNow gated on connection + status), TaskDetail (description, result, live
  ndjson rolling buffer of 500 lines, worktree branch/diff with merge/keep/
  discard via GitService), StatusBar, ListEditor, TaskEditor.
- Views: 3-pane MainWindow (lists | tasks | detail) with GridSplitters, status
  bar, dialog windows for the editors. Status badges via StatusColorConverter.
- Markdown rendering, folder picker, delete-confirmation, settings dialog and
  scroll-to-bottom on the live log are intentionally TODO -- functional
  scaffold only.

Tests: also debounce the FIFO queue test (poll instead of Task.Delay(200)) so
the assertion isn't racy when the suite runs alongside the slower git tests.
38 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mika Kuns
2026-04-13 14:01:03 +02:00
parent 01235d986f
commit 48e4aabeb1
28 changed files with 1527 additions and 26 deletions

View File

@@ -0,0 +1,61 @@
using ClaudeDo.Data.Models;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Ui.ViewModels;
public partial class TaskItemViewModel : ViewModelBase
{
[ObservableProperty] private string _title;
[ObservableProperty] private string _statusText;
[ObservableProperty] private string _tagsText;
[ObservableProperty] private string _commitType;
[ObservableProperty] private string? _description;
[ObservableProperty] private TaskStatus _status;
public string Id { get; }
public string ListId { get; }
public TaskEntity Entity { get; private set; }
private readonly Func<string, Task>? _runNow;
private readonly Func<bool> _canRunNow;
public TaskItemViewModel(TaskEntity entity, IReadOnlyList<TagEntity> tags,
Func<string, Task>? runNow, Func<bool> canRunNow)
{
Entity = entity;
Id = entity.Id;
ListId = entity.ListId;
_title = entity.Title;
_status = entity.Status;
_statusText = entity.Status.ToString().ToLowerInvariant();
_tagsText = string.Join(", ", tags.Select(t => t.Name));
_commitType = entity.CommitType;
_description = entity.Description;
_runNow = runNow;
_canRunNow = canRunNow;
}
public void Refresh(TaskEntity entity, IReadOnlyList<TagEntity> tags)
{
Entity = entity;
Title = entity.Title;
Status = entity.Status;
StatusText = entity.Status.ToString().ToLowerInvariant();
TagsText = string.Join(", ", tags.Select(t => t.Name));
CommitType = entity.CommitType;
Description = entity.Description;
RunNowCommand.NotifyCanExecuteChanged();
}
[RelayCommand(CanExecute = nameof(CanRunNow))]
private async Task RunNowAsync()
{
if (_runNow is not null)
await _runNow(Id);
}
private bool CanRunNow() =>
_canRunNow() && Status == TaskStatus.Queued;
}