using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using ClaudeDo.Data; using ClaudeDo.Data.Filtering; using ClaudeDo.Data.Models; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; using ClaudeDo.Data.Repositories; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Modals; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace ClaudeDo.Ui.ViewModels.Islands; public enum ListKind { Smart, Virtual, User } public sealed partial class ListsIslandViewModel : ViewModelBase { private readonly IDbContextFactory _dbFactory; private readonly IServiceProvider? _services; private readonly WorkerClient? _worker; private static readonly TaskListFilterRegistry _filters = new(); public event EventHandler? SelectionChanged; public event EventHandler? FocusSearchRequested; public void RequestFocusSearch() => FocusSearchRequested?.Invoke(this, EventArgs.Empty); public Func? ShowSettingsModal { get; set; } public Func? ShowListSettingsModal { get; set; } public Func? ShowWorktreesOverviewModal { get; set; } public Func? ShowRepoImportModal { get; set; } [RelayCommand] private async Task OpenSettings() { if (ShowSettingsModal is null || _services is null) return; var settingsVm = _services.GetRequiredService(); await settingsVm.LoadAsync(); await ShowSettingsModal(settingsVm); } [RelayCommand] private async System.Threading.Tasks.Task OpenListSettingsAsync(ListNavItemViewModel? row) { if (row is null || ShowListSettingsModal is null || _services is null) return; var rawId = row.Id.StartsWith("user:", StringComparison.Ordinal) ? row.Id["user:".Length..] : row.Id; var vm = _services.GetRequiredService(); await vm.LoadAsync(rawId, row.Name, row.WorkingDir, row.DefaultCommitType); await ShowListSettingsModal(vm); if (vm.Deleted) await LoadAsync(); else await RefreshRowAsync(row.Id); } [RelayCommand] private async System.Threading.Tasks.Task OpenRepoImportAsync() { if (ShowRepoImportModal is null || _services is null) return; var vm = _services.GetRequiredService(); await vm.LoadAsync(); await ShowRepoImportModal(vm); await LoadAsync(); } private bool _worktreesOverviewOpen; [RelayCommand] private async Task OpenWorktreesOverviewAsync(ListNavItemViewModel? row) { if (row is null || ShowWorktreesOverviewModal is null || _services is null) return; if (row.Kind != ListKind.User) return; if (_worktreesOverviewOpen) return; _worktreesOverviewOpen = true; try { var rawId = row.Id.StartsWith("user:", StringComparison.Ordinal) ? row.Id["user:".Length..] : row.Id; var vm = _services.GetRequiredService(); vm.Configure(rawId, row.Name); await vm.LoadAsync(); await ShowWorktreesOverviewModal(vm); } finally { _worktreesOverviewOpen = false; } } public ObservableCollection Items { get; } = new(); public ObservableCollection SmartLists { get; } = new(); public ObservableCollection UserLists { get; } = new(); [ObservableProperty] private string _searchText = ""; [ObservableProperty] private ListNavItemViewModel? _selectedList; public string UserName { get; } = Environment.UserName; public string MachineName { get; } = Environment.MachineName; public string UserInitials { get; } public ListsIslandViewModel(IDbContextFactory dbFactory, IServiceProvider? services = null, WorkerClient? worker = null) { _dbFactory = dbFactory; _services = services; _worker = worker; var parts = Environment.UserName.Split('.', '_', '-', ' '); UserInitials = parts.Length >= 2 ? $"{parts[0][0]}{parts[1][0]}".ToUpperInvariant() : Environment.UserName.Length >= 2 ? Environment.UserName[..2].ToUpperInvariant() : Environment.UserName.ToUpperInvariant(); if (_worker is not null) { _worker.ListUpdatedEvent += id => _ = RefreshRowAsync(id); _worker.TaskStartedEvent += (_slot, _id, _at) => _ = RefreshCountsAsync(); _worker.TaskFinishedEvent += (_slot, _id, _status, _at) => _ = RefreshCountsAsync(); _worker.TaskUpdatedEvent += _id => _ = RefreshCountsAsync(); _worker.WorktreeUpdatedEvent += _id => _ = RefreshCountsAsync(); _worker.ConnectionRestoredEvent += () => _ = RefreshCountsAsync(); } } public async Task LoadAsync(CancellationToken ct = default) { Items.Clear(); SmartLists.Clear(); UserLists.Clear(); var smart = new[] { new ListNavItemViewModel { Id = "smart:my-day", Name = "My Day", Kind = ListKind.Smart, IconKey = "Sun" }, new ListNavItemViewModel { Id = "smart:important", Name = "Important", Kind = ListKind.Smart, IconKey = "Star" }, new ListNavItemViewModel { Id = "smart:planned", Name = "Planned", Kind = ListKind.Smart, IconKey = "Calendar" }, new ListNavItemViewModel { Id = "virtual:queued", Name = "Queue", Kind = ListKind.Virtual, IconKey = "Inbox" }, new ListNavItemViewModel { Id = "virtual:running", Name = "Running", Kind = ListKind.Virtual, IconKey = "Activity" }, new ListNavItemViewModel { Id = "virtual:review", Name = "Review", Kind = ListKind.Virtual, IconKey = "Eye" }, }; foreach (var s in smart) { Items.Add(s); SmartLists.Add(s); } await using var ctx = await _dbFactory.CreateDbContextAsync(ct); var lists = new ListRepository(ctx); var seedNames = new HashSet(new[] { "My Day", "Important", "Planned" }); var dotColors = new[] { "Moss", "Peat", "Sage" }; int idx = 0; foreach (var l in await lists.GetAllAsync(ct)) { if (seedNames.Contains(l.Name)) continue; var item = new ListNavItemViewModel { Id = $"user:{l.Id}", Name = l.Name, Kind = ListKind.User, IconKey = "Folder", DotColorKey = dotColors[idx % dotColors.Length], WorkingDir = l.WorkingDir, DefaultCommitType = l.DefaultCommitType, }; Items.Add(item); UserLists.Add(item); idx++; } await RefreshCountsAsync(ct); SelectedList = Items.FirstOrDefault(); } public async Task RefreshCountsAsync(CancellationToken ct = default) { try { await using var ctx = await _dbFactory.CreateDbContextAsync(ct); // Single snapshot; counters and the list loader share the same filter strategies. var all = await ctx.Tasks.AsNoTracking() .Include(t => t.Worktree) .ToListAsync(ct); foreach (var item in SmartLists) { var filter = _filters.Resolve(item.Id); item.Count = filter is null ? 0 : all.Count(filter.ShouldCount); } foreach (var item in UserLists) { var filter = _filters.Resolve(item.Id); item.Count = filter is null ? 0 : all.Count(filter.ShouldCount); } } catch (OperationCanceledException) { throw; } catch { /* best-effort refresh */ } } [RelayCommand] private void Select(ListNavItemViewModel item) => SelectedList = item; [RelayCommand] private async Task CreateListAsync() { var entity = new ListEntity { Id = Guid.NewGuid().ToString("N"), Name = "New list", DefaultCommitType = CommitTypeRegistry.DefaultType, CreatedAt = DateTime.UtcNow, }; await using (var ctx = await _dbFactory.CreateDbContextAsync()) { var lists = new ListRepository(ctx); await lists.AddAsync(entity); } var item = new ListNavItemViewModel { Id = $"user:{entity.Id}", Name = entity.Name, Kind = ListKind.User, IconKey = "Folder", DotColorKey = "Moss", WorkingDir = entity.WorkingDir, DefaultCommitType = entity.DefaultCommitType, }; Items.Add(item); UserLists.Add(item); SelectedList = item; if (ShowListSettingsModal is not null && _services is not null) { var vm = _services.GetRequiredService(); await vm.LoadAsync(entity.Id, entity.Name, entity.WorkingDir, entity.DefaultCommitType); await ShowListSettingsModal(vm); if (vm.Deleted) await LoadAsync(); else await RefreshRowAsync(item.Id); } } partial void OnSelectedListChanged(ListNavItemViewModel? value) { foreach (var i in Items) i.IsActive = ReferenceEquals(i, value); SelectionChanged?.Invoke(this, EventArgs.Empty); } private async System.Threading.Tasks.Task RefreshRowAsync(string rowId) { try { var rawId = rowId.StartsWith("user:") ? rowId["user:".Length..] : rowId; var row = UserLists.FirstOrDefault(r => r.Id == rowId); if (row is null) return; await using var ctx = await _dbFactory.CreateDbContextAsync(); var lists = new ListRepository(ctx); var entity = await lists.GetByIdAsync(rawId); if (entity is null) return; row.Name = entity.Name; row.WorkingDir = entity.WorkingDir; row.DefaultCommitType = entity.DefaultCommitType; } catch { /* best-effort refresh */ } } }