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.Localization; 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; } } [RelayCommand] private void OpenInExplorer(ListNavItemViewModel? row) { var dir = row?.WorkingDir; if (string.IsNullOrWhiteSpace(dir) || !System.IO.Directory.Exists(dir)) return; try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = dir, UseShellExecute = true, }); } catch { /* best-effort */ } } [RelayCommand] private void OpenInTerminal(ListNavItemViewModel? row) { var dir = row?.WorkingDir; if (string.IsNullOrWhiteSpace(dir) || !System.IO.Directory.Exists(dir)) return; try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = "wt.exe", Arguments = $"-d \"{dir}\"", UseShellExecute = true, }); } catch { // Windows Terminal not installed — fall back to a plain console at the directory. try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = "cmd.exe", WorkingDirectory = dir, UseShellExecute = true, }); } catch { /* best-effort */ } } } 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 MachineNameLocal => Loc.T("vm.lists.localSuffix", 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(); } Loc.LanguageChanged += (_, _) => RefreshLocalizedLabels(); } private static string? SmartListNameKey(string id) => id switch { "smart:my-day" => "vm.lists.smartMyDay", "smart:important" => "vm.lists.smartImportant", "smart:planned" => "vm.lists.smartPlanned", "virtual:queued" => "vm.lists.virtualQueue", "virtual:running" => "vm.lists.virtualRunning", "virtual:review" => "vm.lists.virtualReview", _ => null, }; private void RefreshLocalizedLabels() { foreach (var item in SmartLists) if (SmartListNameKey(item.Id) is { } key) item.Name = Loc.T(key); OnPropertyChanged(nameof(MachineNameLocal)); } public async Task LoadAsync(CancellationToken ct = default) { Items.Clear(); SmartLists.Clear(); UserLists.Clear(); var smart = new[] { new ListNavItemViewModel { Id = "smart:my-day", Name = Loc.T("vm.lists.smartMyDay"), Kind = ListKind.Smart, IconKey = "Sun" }, new ListNavItemViewModel { Id = "smart:important", Name = Loc.T("vm.lists.smartImportant"), Kind = ListKind.Smart, IconKey = "Star" }, new ListNavItemViewModel { Id = "smart:planned", Name = Loc.T("vm.lists.smartPlanned"), Kind = ListKind.Smart, IconKey = "Calendar" }, new ListNavItemViewModel { Id = "virtual:queued", Name = Loc.T("vm.lists.virtualQueue"), Kind = ListKind.Virtual, IconKey = "Inbox" }, new ListNavItemViewModel { Id = "virtual:running", Name = Loc.T("vm.lists.virtualRunning"), Kind = ListKind.Virtual, IconKey = "Activity" }, new ListNavItemViewModel { Id = "virtual:review", Name = Loc.T("vm.lists.virtualReview"), 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 = Loc.T("vm.lists.newList"), 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); } } public void ClearDropHints() { foreach (var r in UserLists) { r.DropHintAbove = false; r.DropHintBelow = false; } } public void SetDropHint(ListNavItemViewModel target, bool placeBelow) { foreach (var r in UserLists) { var isTarget = ReferenceEquals(r, target); r.DropHintAbove = isTarget && !placeBelow; r.DropHintBelow = isTarget && placeBelow; } } public async Task ReorderAsync(ListNavItemViewModel source, ListNavItemViewModel target, bool placeBelow) { if (source.Kind != ListKind.User || target.Kind != ListKind.User) return; if (ReferenceEquals(source, target)) return; MoveWithinCollection(UserLists, source, target, placeBelow); var orderedIds = UserLists.Select(i => i.Id["user:".Length..]).ToList(); await using var ctx = await _dbFactory.CreateDbContextAsync(); var lists = new ListRepository(ctx); await lists.ReorderAsync(orderedIds); } private static void MoveWithinCollection( ObservableCollection coll, ListNavItemViewModel source, ListNavItemViewModel target, bool placeBelow) { var srcIdx = coll.IndexOf(source); var tgtIdx = coll.IndexOf(target); if (srcIdx < 0 || tgtIdx < 0 || srcIdx == tgtIdx) return; var finalIdx = placeBelow ? tgtIdx + 1 : tgtIdx; if (srcIdx < finalIdx) finalIdx--; if (finalIdx < 0) finalIdx = 0; if (finalIdx >= coll.Count) finalIdx = coll.Count - 1; if (finalIdx == srcIdx) return; coll.Move(srcIdx, finalIdx); } 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 */ } } }