using System.Collections.ObjectModel; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using ClaudeDo.Data; 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 Microsoft.EntityFrameworkCore; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Ui.ViewModels; public partial class TaskListViewModel : ViewModelBase { private readonly IDbContextFactory _dbFactory; 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(IDbContextFactory dbFactory, WorkerClient worker, Func editorFactory, Action showMessage) { _dbFactory = dbFactory; _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) { using var context = _dbFactory.CreateDbContext(); var listRepo = new ListRepository(context); var list = await listRepo.GetByIdAsync(listId); ListName = list?.Name ?? "Tasks"; } else { ListName = "Tasks"; } if (listId is null) return; try { using var context = _dbFactory.CreateDbContext(); var taskRepo = new TaskRepository(context); var entities = await taskRepo.GetByListIdAsync(listId); var taskIds = entities.Select(e => e.Id).ToList(); var subtaskCounts = await context.Subtasks .Where(s => taskIds.Contains(s.TaskId)) .GroupBy(s => s.TaskId) .ToDictionaryAsync(g => g.Key, g => g.Count()); foreach (var e in entities) { var tags = await taskRepo.GetEffectiveTagsAsync(e.Id); subtaskCounts.TryGetValue(e.Id, out var count); Tasks.Add(new TaskItemViewModel(e, tags, RunNowAsync, () => _worker.IsConnected, _dbFactory, count, 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; string defaultCommitType; using (var context = _dbFactory.CreateDbContext()) { var listRepo = new ListRepository(context); var list = await listRepo.GetByIdAsync(CurrentListId); 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 { using var context = _dbFactory.CreateDbContext(); var taskRepo = new TaskRepository(context); await taskRepo.AddAsync(entity); var tags = await taskRepo.GetEffectiveTagsAsync(entity.Id); var vm = new TaskItemViewModel(entity, tags, RunNowAsync, () => _worker.IsConnected, _dbFactory, 0, ToggleDoneAsync); Tasks.Add(vm); SelectedTask = vm; InlineAddTitle = ""; } catch (Exception ex) { _showMessage($"Error creating task: {ex.Message}"); } } [RelayCommand(CanExecute = nameof(CanAddTask))] private async Task AddTask() { string defaultCommitType; using (var context = _dbFactory.CreateDbContext()) { var listRepo = new ListRepository(context); var list = await listRepo.GetByIdAsync(CurrentListId); 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 { using var context = _dbFactory.CreateDbContext(); var taskRepo = new TaskRepository(context); var tagRepo = new TagRepository(context); 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, _dbFactory, 0, 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; TaskEntity? entity; List taskTags; using (var context = _dbFactory.CreateDbContext()) { var taskRepo = new TaskRepository(context); entity = await taskRepo.GetByIdAsync(SelectedTask.Id); if (entity is null) return; taskTags = await taskRepo.GetTagsAsync(entity.Id); } var editor = _editorFactory(); await editor.LoadAgentsAsync(_worker); await editor.InitForEditAsync(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 { using var context = _dbFactory.CreateDbContext(); var taskRepo = new TaskRepository(context); var tagRepo = new TagRepository(context); 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 { using var context = _dbFactory.CreateDbContext(); var taskRepo = new TaskRepository(context); 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) { using var context = _dbFactory.CreateDbContext(); var taskRepo = new TaskRepository(context); 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); var subtaskCount = await context.Subtasks.CountAsync(s => s.TaskId == taskId); await existing.RefreshSubtasksAsync(subtaskCount); } } 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) { using var context = _dbFactory.CreateDbContext(); var taskRepo = new TaskRepository(context); 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(); } } }