using System.Collections.ObjectModel; using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace ClaudeDo.Ui.ViewModels.Modals.Settings; public sealed partial class SessionSkillsSettingsTabViewModel : ViewModelBase { private readonly IWorkerClient _worker; public OperationStatus InstallOp { get; } = new(); public OperationStatus UpdateOp { get; } = new(); [ObservableProperty] private string _installUrl = ""; [ObservableProperty] private string _statusMessage = ""; [ObservableProperty] private bool _isBusy; [ObservableProperty] private bool _isEmpty = true; public ObservableCollection Skills { get; } = new(); public SessionSkillsSettingsTabViewModel(IWorkerClient worker) { _worker = worker; InstallOp.PropertyChanged += (_, e) => { if (e.PropertyName == nameof(OperationStatus.IsRunning)) InstallCommand.NotifyCanExecuteChanged(); }; UpdateOp.PropertyChanged += (_, e) => { if (e.PropertyName == nameof(OperationStatus.IsRunning)) UpdateCommand.NotifyCanExecuteChanged(); }; } /// Loads the installed skills, marking each row selected per . /// When omitted, the current selection is preserved across the refresh (e.g. after install/update/remove). public async Task LoadAsync(IReadOnlyCollection? selectedNames = null) { IsBusy = true; try { var selected = selectedNames ?? Skills.Where(s => s.IsSelected).Select(s => s.Name).ToList(); var selectedSet = new HashSet(selected); var skills = await _worker.GetSessionSkillsAsync(); Skills.Clear(); foreach (var s in skills) Skills.Add(new SessionSkillRowViewModel(s, selectedSet.Contains(s.Name))); IsEmpty = Skills.Count == 0; } finally { IsBusy = false; } } /// The globally-selected skill names, or null when none are selected (matches the /// AppSettingsDto.SessionSkills convention of "null means no global selection"). public List? SelectedSkillNames() { var names = Skills.Where(s => s.IsSelected).Select(s => s.Name).ToList(); return names.Count == 0 ? null : names; } private bool CanInstall() => !InstallOp.IsRunning; [RelayCommand(CanExecute = nameof(CanInstall))] private async Task InstallAsync() { if (string.IsNullOrWhiteSpace(InstallUrl)) return; StatusMessage = ""; using var op = InstallOp.Begin(Loc.T("ops.skills.installing")); try { var installed = await _worker.InstallSessionSkillAsync(InstallUrl.Trim()); StatusMessage = Loc.T("vm.sessionSkillsTab.installed", string.Join(", ", installed)); InstallUrl = ""; await LoadAsync(); } catch (Exception ex) { StatusMessage = Loc.T("vm.sessionSkillsTab.installFailed", ex.Message); } } private bool CanUpdate(string? sourceUrl) => !UpdateOp.IsRunning; [RelayCommand(CanExecute = nameof(CanUpdate))] private async Task UpdateAsync(string? sourceUrl) { if (string.IsNullOrWhiteSpace(sourceUrl)) return; StatusMessage = ""; using var op = UpdateOp.Begin(Loc.T("ops.skills.updating")); try { await _worker.UpdateSessionSkillAsync(sourceUrl); StatusMessage = Loc.T("vm.sessionSkillsTab.updated"); await LoadAsync(); } catch (Exception ex) { StatusMessage = Loc.T("vm.sessionSkillsTab.updateFailed", ex.Message); } } [RelayCommand] private async Task RemoveAsync(string? sourceUrl) { if (string.IsNullOrWhiteSpace(sourceUrl)) return; IsBusy = true; StatusMessage = ""; try { await _worker.RemoveSessionSkillAsync(sourceUrl); StatusMessage = Loc.T("vm.sessionSkillsTab.removed"); await LoadAsync(); } catch (Exception ex) { StatusMessage = Loc.T("vm.sessionSkillsTab.removeFailed", ex.Message); } finally { IsBusy = false; } } } /// One installed skill row: identity/management fields from /// plus the global "active for every task" checkbox state that used to live in a separate /// General-tab list. public sealed partial class SessionSkillRowViewModel : ViewModelBase { public string Name { get; } public string Description { get; } public string SourceUrl { get; } public string PinnedRef { get; } public DateTimeOffset AddedAt { get; } [ObservableProperty] private bool _isSelected; public SessionSkillRowViewModel(SessionSkillDto dto, bool isSelected) { Name = dto.Name; Description = dto.Description; SourceUrl = dto.SourceUrl; PinnedRef = dto.PinnedRef; AddedAt = dto.AddedAt; _isSelected = isSelected; } }