From 0034accb4f8ef421465fefcb7b86b31fdcb5a8e7 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Mon, 20 Apr 2026 10:22:11 +0200 Subject: [PATCH] feat(ui): TasksIslandViewModel with smart/virtual/user filtering Uses IDbContextFactory directly (singleton-safe). Adds ToggleDone, ToggleStar, Select commands. Fixes pre-existing SessionTerminalView compiled-binding error on Classes property via x:CompileBindings="False". Co-Authored-By: Claude Sonnet 4.6 --- src/ClaudeDo.App/Program.cs | 8 +- .../Islands/TasksIslandViewModel.cs | 119 +++++++++++++++++- .../Views/Islands/SessionTerminalView.axaml | 34 +++++ 3 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml diff --git a/src/ClaudeDo.App/Program.cs b/src/ClaudeDo.App/Program.cs index 3fb6fb5..0463bf1 100644 --- a/src/ClaudeDo.App/Program.cs +++ b/src/ClaudeDo.App/Program.cs @@ -103,8 +103,12 @@ sealed class Program // Islands shell VMs sc.AddSingleton(); - sc.AddSingleton(); - sc.AddSingleton(); + sc.AddSingleton(sp => + new TasksIslandViewModel(sp.GetRequiredService>())); + sc.AddSingleton(sp => + new DetailsIslandViewModel( + sp.GetRequiredService>(), + sp.GetRequiredService())); sc.AddSingleton(); return sc.BuildServiceProvider(); diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs index 84c6ce5..1230fc5 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs @@ -1,12 +1,127 @@ +using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using ClaudeDo.Data; +using ClaudeDo.Data.Models; +using Microsoft.EntityFrameworkCore; +using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Ui.ViewModels.Islands; public sealed partial class TasksIslandViewModel : ViewModelBase { + private readonly IDbContextFactory _dbFactory; + private ListNavItemViewModel? _currentList; + public event EventHandler? SelectionChanged; + + public ObservableCollection Items { get; } = new(); + + [ObservableProperty] private string _newTaskTitle = ""; [ObservableProperty] private TaskRowViewModel? _selectedTask; - public void LoadForList(ListNavItemViewModel? list) { /* Phase 5 */ } - partial void OnSelectedTaskChanged(TaskRowViewModel? value) => + [ObservableProperty] private string _headerTitle = ""; + [ObservableProperty] private string _headerEyebrow = ""; + [ObservableProperty] private string _subtitle = ""; + + public TasksIslandViewModel(IDbContextFactory dbFactory) + { + _dbFactory = dbFactory; + } + + public async void LoadForList(ListNavItemViewModel? list) + { + _currentList = list; + Items.Clear(); + if (list is null) return; + + HeaderTitle = list.Name; + HeaderEyebrow = DateTime.Now.ToString("dddd · MMM dd").ToUpperInvariant(); + + await using var db = await _dbFactory.CreateDbContextAsync(); + var all = await db.Tasks + .Include(t => t.List) + .Include(t => t.Worktree) + .ToListAsync(); + + IEnumerable filtered = list.Kind switch + { + ListKind.Smart when list.Id == "smart:my-day" => all.Where(t => t.IsMyDay), + ListKind.Smart when list.Id == "smart:important" => all.Where(t => t.IsStarred), + ListKind.Smart when list.Id == "smart:planned" => all.Where(t => t.ScheduledFor != null), + ListKind.Virtual when list.Id == "virtual:running" => all.Where(t => t.Status == TaskStatus.Running), + ListKind.Virtual when list.Id == "virtual:review" => all.Where(t => t.Status == TaskStatus.Done && t.Worktree?.State == WorktreeState.Active), + ListKind.User => all.Where(t => $"user:{t.ListId}" == list.Id), + _ => Enumerable.Empty(), + }; + + foreach (var t in filtered) + Items.Add(TaskRowViewModel.FromEntity(t)); + + UpdateSubtitle(); + } + + private void UpdateSubtitle() + { + var open = Items.Count(i => !i.Done); + var running = Items.Count(i => i.Status == TaskStatus.Running); + var review = Items.Count(i => i.Status == TaskStatus.Done && i.Branch != null); + Subtitle = $"{open} open · {running} running · {review} in review"; + } + + [RelayCommand] + private async Task AddAsync() + { + if (string.IsNullOrWhiteSpace(NewTaskTitle) || _currentList?.Kind != ListKind.User) return; + var listId = _currentList.Id["user:".Length..]; + var entity = new TaskEntity + { + Id = Guid.NewGuid().ToString("N"), + ListId = listId, + Title = NewTaskTitle.Trim(), + CreatedAt = DateTime.UtcNow, + }; + await using var db = await _dbFactory.CreateDbContextAsync(); + db.Tasks.Add(entity); + await db.SaveChangesAsync(); + Items.Insert(0, TaskRowViewModel.FromEntity(entity)); + NewTaskTitle = ""; + UpdateSubtitle(); + } + + [RelayCommand] + private async Task ToggleDoneAsync(TaskRowViewModel row) + { + row.Done = !row.Done; + await using var db = await _dbFactory.CreateDbContextAsync(); + var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id); + if (entity != null) + { + entity.Status = row.Done ? TaskStatus.Done : TaskStatus.Manual; + row.Status = entity.Status; + await db.SaveChangesAsync(); + } + UpdateSubtitle(); + } + + [RelayCommand] + private async Task ToggleStarAsync(TaskRowViewModel row) + { + row.IsStarred = !row.IsStarred; + await using var db = await _dbFactory.CreateDbContextAsync(); + var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id); + if (entity != null) + { + entity.IsStarred = row.IsStarred; + await db.SaveChangesAsync(); + } + } + + [RelayCommand] + private void Select(TaskRowViewModel row) => SelectedTask = row; + + partial void OnSelectedTaskChanged(TaskRowViewModel? value) + { + foreach (var i in Items) i.IsSelected = ReferenceEquals(i, value); SelectionChanged?.Invoke(this, EventArgs.Empty); + } } diff --git a/src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml b/src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml new file mode 100644 index 0000000..a1012db --- /dev/null +++ b/src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + +