using System.Collections.ObjectModel; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Views; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Ui.ViewModels; public partial class TaskListViewModel : ViewModelBase { private readonly TaskRepository _taskRepo; private readonly TagRepository _tagRepo; private readonly ListRepository _listRepo; private readonly WorkerClient _worker; private readonly Func _editorFactory; private readonly Action _showMessage; public ObservableCollection Tasks { get; } = new(); [ObservableProperty] private TaskItemViewModel? _selectedTask; [ObservableProperty, NotifyCanExecuteChangedFor(nameof(AddTaskCommand))] private string? _currentListId; [ObservableProperty] private string _listName = "Tasks"; [ObservableProperty] private string _inlineAddTitle = ""; public event Action? SelectedTaskChanged; partial void OnSelectedTaskChanged(TaskItemViewModel? value) => SelectedTaskChanged?.Invoke(value); public TaskListViewModel(TaskRepository taskRepo, TagRepository tagRepo, ListRepository listRepo, WorkerClient worker, Func editorFactory, Action showMessage) { _taskRepo = taskRepo; _tagRepo = tagRepo; _listRepo = listRepo; _worker = worker; _editorFactory = editorFactory; _showMessage = showMessage; worker.TaskUpdatedEvent += OnTaskUpdated; worker.TaskFinishedEvent += (_, taskId, _, _) => OnTaskUpdated(taskId); worker.PropertyChanged += (_, e) => { if (e.PropertyName == nameof(WorkerClient.IsConnected)) Avalonia.Threading.Dispatcher.UIThread.Post(() => { foreach (var t in Tasks) t.RunNowCommand.NotifyCanExecuteChanged(); }); }; worker.RunNowRequestedEvent += taskId => { var item = Tasks.FirstOrDefault(t => t.Id == taskId); item?.SetStarting(); }; worker.TaskStartedEvent += (_, taskId, _) => { var item = Tasks.FirstOrDefault(t => t.Id == taskId); item?.ClearStarting(); }; } public async Task LoadAsync(string? listId) { CurrentListId = listId; 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 { var entities = await _taskRepo.GetByListAsync(listId); foreach (var e in entities) { var tags = await _taskRepo.GetEffectiveTagsAsync(e.Id); Tasks.Add(new TaskItemViewModel(e, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync)); } } catch (Exception ex) { _showMessage($"Error loading tasks: {ex.Message}"); } } 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() { // Get list default commit type var list = await _listRepo.GetByIdAsync(CurrentListId); var defaultCommitType = list?.DefaultCommitType ?? "chore"; var editor = _editorFactory(); await editor.LoadAgentsAsync(_worker); editor.InitForCreate(CurrentListId, defaultCommitType); var window = new TaskEditorView { DataContext = editor }; editor.RequestClose += () => window.Close(); window.Closed += (_, _) => editor.OnWindowClosed(); _ = ShowDialogAsync(window); var saved = await editor.ShowAndWaitAsync(); if (saved is null) return; try { await _taskRepo.AddAsync(saved); foreach (var tagName in editor.SelectedTagNames) { var tagId = await _tagRepo.GetOrCreateAsync(tagName); await _taskRepo.AddTagAsync(saved.Id, tagId); } var tags = await _taskRepo.GetEffectiveTagsAsync(saved.Id); Tasks.Add(new TaskItemViewModel(saved, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync)); // Auto wake-queue if agent+queued if (saved.Status == TaskStatus.Queued && tags.Any(t => t.Name == "agent")) { try { await _worker.WakeQueueAsync(); } catch { /* worker offline is fine */ } } } catch (Exception ex) { _showMessage($"Error creating task: {ex.Message}"); } } [RelayCommand] private async Task EditTask() { if (SelectedTask is null || CurrentListId is null) return; var entity = await _taskRepo.GetByIdAsync(SelectedTask.Id); if (entity is null) return; var taskTags = await _taskRepo.GetTagsAsync(entity.Id); var editor = _editorFactory(); await editor.LoadAgentsAsync(_worker); editor.InitForEdit(entity, taskTags); var window = new TaskEditorView { DataContext = editor }; editor.RequestClose += () => window.Close(); window.Closed += (_, _) => editor.OnWindowClosed(); _ = ShowDialogAsync(window); var saved = await editor.ShowAndWaitAsync(); if (saved is null) return; try { await _taskRepo.UpdateAsync(saved); var existingTags = await _taskRepo.GetTagsAsync(saved.Id); foreach (var old in existingTags) await _taskRepo.RemoveTagAsync(saved.Id, old.Id); foreach (var tagName in editor.SelectedTagNames) { var tagId = await _tagRepo.GetOrCreateAsync(tagName); await _taskRepo.AddTagAsync(saved.Id, tagId); } var newTags = await _taskRepo.GetEffectiveTagsAsync(saved.Id); SelectedTask.Refresh(saved, newTags); } catch (Exception ex) { _showMessage($"Error updating task: {ex.Message}"); } } [RelayCommand] private async Task DeleteTask() { if (SelectedTask is null) return; try { await _taskRepo.DeleteAsync(SelectedTask.Id); Tasks.Remove(SelectedTask); SelectedTask = null; } catch (Exception ex) { _showMessage($"Error deleting task: {ex.Message}"); } } public async Task RefreshSingleAsync(string taskId) { var entity = await _taskRepo.GetByIdAsync(taskId); var existing = Tasks.FirstOrDefault(t => t.Id == taskId); if (entity is null) { if (existing is not null) Tasks.Remove(existing); return; } var tags = await _taskRepo.GetEffectiveTagsAsync(taskId); if (existing is not null) existing.Refresh(entity, tags); } private async Task RunNowAsync(string taskId) { try { await _worker.RunNowAsync(taskId); } catch (Exception ex) { _showMessage($"RunNow failed: {ex.Message}"); } } 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; await RefreshSingleAsync(taskId); } private static async Task ShowDialogAsync(Window dialog) { if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow is not null) { await dialog.ShowDialog(desktop.MainWindow); } else { dialog.Show(); } } }