diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index 435f2b63..a4880a3e 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -9,6 +9,7 @@ "tabWorktrees": "Worktrees", "tabFiles": "Dateien", "tabPrime": "Prime Claude", + "tabSkills": "Skills", "general": { "defaultInstructions": "Standard-Anweisungen", "defaultInstructionsPlaceholder": "Basis-Anweisungen, die auf jede Aufgabe angewendet werden", @@ -25,7 +26,9 @@ "weekdayWednesday": "Mittwoch", "weekdayThursday": "Donnerstag", "weekdayFriday": "Freitag", - "weekdaySaturday": "Samstag" + "weekdaySaturday": "Samstag", + "sessionSkills": "Session-Skills", + "sessionSkillsHint": "Gilt für jede Aufgabe. Kombiniert sich mit Listen- und Aufgaben-Auswahl." }, "worktrees": { "strategy": "Strategie", @@ -95,7 +98,17 @@ "systemPrompt": "System-Prompt (angehängt)", "promptPrepended": "Wird automatisch vorangestellt:", "agentFile": "Agent-Datei", - "browse": "Durchsuchen..." + "browse": "Durchsuchen...", + "sessionSkills": "Session-Skills", + "sessionSkillsHint": "Kombiniert sich additiv mit globaler (und Listen-)Auswahl." + }, + "skills": { + "installSection": "INSTALLIEREN", + "installUrlPlaceholder": "Git-Repo-URL (owner/repo oder vollständige URL)", + "installButton": "Installieren", + "installedSection": "INSTALLIERT", + "updateButton": "Aktualisieren", + "removeButton": "Entfernen" } }, "tasks": { @@ -494,7 +507,7 @@ "taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "waitingForChildren": "Wartet auf Verbesserungen", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen", "parked": "Geparkt" }, "planningBadge": { "active": "PLANUNG", "finalized": "GEPLANT" }, "taskRow": { "createdPrefix": "Erstellt {0}", "stepsText": "{0}/{1} Schritte" }, - "tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}", "runInteractiveFailed": "Interaktiv ausführen fehlgeschlagen: {0}", "planningOpenFailed": "Planungssitzung konnte nicht geöffnet werden: {0}", "pickUpInTerminalFailed": "Im Terminal fortsetzen fehlgeschlagen: {0}" }, + "tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}", "runInteractiveFailed": "Interaktiv ausführen fehlgeschlagen: {0}", "planningOpenFailed": "Planungssitzung konnte nicht geöffnet werden: {0}", "pickUpInTerminalFailed": "Im Terminal fortsetzen fehlgeschlagen: {0}", "approveFailed": "Genehmigen & Mergen fehlgeschlagen: {0}" }, "diff": { "loadFailed": "Diff konnte nicht geladen werden: {0}", "noChanges": "Keine Änderungen anzuzeigen.", "unavailable": "Diff nicht mehr verfügbar — Commit-Bereich unvollständig." }, "planningDiff": { "hubError": "Kombinierte Vorschau konnte nicht erstellt werden (Hub-Fehler).", "conflict": "Kombinierte Vorschau nicht möglich: Teilaufgabe {0} steht im Konflikt mit einer früheren Teilaufgabe ({1} Dateien)." }, "merge": { "commitMessage": "Merge-Aufgabe: {0}", "workerOfflineBranches": "Worker offline — Branches können nicht aufgelistet werden.", "loadBranchesFailed": "Branches konnten nicht geladen werden: {0}", "merged": "Zusammengeführt.", "conflict": "Merge-Konflikt — Ziel-Branch wiederhergestellt. Manuell oder über Fortsetzen lösen, dann erneut versuchen.", "blocked": "Blockiert: {0}", "unknownStatus": "Unbekannter Status: {0}", "mergeFailed": "Merge fehlgeschlagen: {0}" }, @@ -503,6 +516,7 @@ "onlineInbox": { "workerOffline": "Worker offline — Konfiguration kann nicht geladen werden.", "saved": "Konfiguration gespeichert.", "saveFailed": "Speichern fehlgeschlagen: {0}", "signedIn": "Erfolgreich angemeldet.", "signedInNoRole": "Angemeldet, aber diesem Konto fehlt die Rolle 'user' in Zitadel — die Online-Synchronisierung wird abgelehnt, bis die Rolle im ClaudeDo-Projekt zugewiesen wird.", "signInFailed": "Anmeldung fehlgeschlagen: {0}", "signedOut": "Abgemeldet.", "signOutFailed": "Abmeldung fehlgeschlagen: {0}" }, "weeklyReport": { "invalidRange": "Ungültiger Datumsbereich.", "generating": "Bericht wird erstellt…", "error": "Fehler: {0}" }, "filesTab": { "workerOffline": "Worker offline.", "noneBundled": "Keine Standard-Agenten mitgeliefert.", "allPresent": "Alle Standard-Agenten bereits vorhanden.", "restored": "{0} Standard-Agent(en) wiederhergestellt.", "restoreFailed": "Wiederherstellung fehlgeschlagen: {0}", "openFailed": "Öffnen fehlgeschlagen: {0}" }, + "sessionSkillsTab": { "installed": "Installiert: {0}", "installFailed": "Installation fehlgeschlagen: {0}", "updated": "Aktualisiert.", "updateFailed": "Aktualisierung fehlgeschlagen: {0}", "removed": "Entfernt.", "removeFailed": "Entfernen fehlgeschlagen: {0}" }, "worktreesTab": { "workerOffline": "Worker offline.", "removed": "{0} Worktree(s) entfernt.", "blocked": "Zwangsentfernung nicht möglich: {0} Aufgabe(n) laufen noch. Brich sie zuerst ab.", "removedFrom": "{0} Worktree(s) von {1} Aufgabe(n) entfernt." }, "worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "Liste", "cleanupFailed": "Aufräumen fehlgeschlagen.", "removed": "{0} Worktree(s) entfernt.", "discardFailed": "Worktree konnte nicht verworfen werden.", "keepFailed": "Worktree konnte nicht behalten werden.", "cannotForceRunning": "Eine laufende Aufgabe kann nicht zwangsweise entfernt werden.", "forceRemoveFailed": "Zwangsentfernung fehlgeschlagen.", "batchProgress": "Merge {0}/{1}…", "batchDone": "{0} gemergt, {1} zu lösen." }, "listSettings": { "untitled": "Unbenannt" }, diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index 90b39cab..948bd5a7 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -9,6 +9,7 @@ "tabWorktrees": "Worktrees", "tabFiles": "Files", "tabPrime": "Prime Claude", + "tabSkills": "Skills", "general": { "defaultInstructions": "Default instructions", "defaultInstructionsPlaceholder": "Baseline instructions applied to every task", @@ -25,7 +26,9 @@ "weekdayWednesday": "Wednesday", "weekdayThursday": "Thursday", "weekdayFriday": "Friday", - "weekdaySaturday": "Saturday" + "weekdaySaturday": "Saturday", + "sessionSkills": "Session skills", + "sessionSkillsHint": "Applied to every task. Combines with list- and task-level selections." }, "worktrees": { "strategy": "Strategy", @@ -95,7 +98,17 @@ "systemPrompt": "System prompt (appended)", "promptPrepended": "Prepended automatically:", "agentFile": "Agent file", - "browse": "Browse..." + "browse": "Browse...", + "sessionSkills": "Session skills", + "sessionSkillsHint": "Combines additively with global (and list) selections." + }, + "skills": { + "installSection": "INSTALL", + "installUrlPlaceholder": "Git repo URL (owner/repo or full URL)", + "installButton": "Install", + "installedSection": "INSTALLED", + "updateButton": "Update", + "removeButton": "Remove" } }, "tasks": { @@ -494,7 +507,7 @@ "taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "waitingForChildren": "Waiting for Improvements", "done": "Done", "failed": "Failed", "cancelled": "Cancelled", "parked": "Parked" }, "planningBadge": { "active": "PLANNING", "finalized": "PLANNED" }, "taskRow": { "createdPrefix": "Created {0}", "stepsText": "{0}/{1} steps" }, - "tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}", "runInteractiveFailed": "Run interactively failed: {0}", "planningOpenFailed": "Couldn't open planning session: {0}", "pickUpInTerminalFailed": "Pick up in terminal failed: {0}" }, + "tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}", "runInteractiveFailed": "Run interactively failed: {0}", "planningOpenFailed": "Couldn't open planning session: {0}", "pickUpInTerminalFailed": "Pick up in terminal failed: {0}", "approveFailed": "Approve & merge failed: {0}" }, "diff": { "loadFailed": "Failed to load diff: {0}", "noChanges": "No changes to show.", "unavailable": "Diff no longer available — commit range incomplete." }, "planningDiff": { "hubError": "Could not build combined preview (hub error).", "conflict": "Cannot build combined preview: subtask {0} conflicts with an earlier subtask ({1} files)." }, "merge": { "commitMessage": "Merge task: {0}", "workerOfflineBranches": "Worker offline — cannot list branches.", "loadBranchesFailed": "Failed to load branches: {0}", "merged": "Merged.", "conflict": "Merge conflict — target branch restored. Resolve manually or via Continue, then retry.", "blocked": "Blocked: {0}", "unknownStatus": "Unknown status: {0}", "mergeFailed": "Merge failed: {0}" }, @@ -503,6 +516,7 @@ "onlineInbox": { "workerOffline": "Worker offline — cannot load config.", "saved": "Config saved.", "saveFailed": "Save failed: {0}", "signedIn": "Signed in successfully.", "signedInNoRole": "Signed in, but this account is missing the 'user' role in Zitadel — online sync will be rejected until the role is granted in the ClaudeDo project.", "signInFailed": "Sign-in failed: {0}", "signedOut": "Signed out.", "signOutFailed": "Sign-out failed: {0}" }, "weeklyReport": { "invalidRange": "Invalid date range.", "generating": "Generating report…", "error": "Error: {0}" }, "filesTab": { "workerOffline": "Worker offline.", "noneBundled": "No default agents bundled.", "allPresent": "All default agents already present.", "restored": "Restored {0} default agent(s).", "restoreFailed": "Restore failed: {0}", "openFailed": "Open failed: {0}" }, + "sessionSkillsTab": { "installed": "Installed: {0}", "installFailed": "Install failed: {0}", "updated": "Updated.", "updateFailed": "Update failed: {0}", "removed": "Removed.", "removeFailed": "Remove failed: {0}" }, "worktreesTab": { "workerOffline": "Worker offline.", "removed": "Removed {0} worktree(s).", "blocked": "Cannot force-remove: {0} task(s) still running. Cancel them first.", "removedFrom": "Removed {0} worktree(s) from {1} task(s)." }, "worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "list", "cleanupFailed": "Cleanup failed.", "removed": "Removed {0} worktree(s).", "discardFailed": "Failed to discard worktree.", "keepFailed": "Failed to keep worktree.", "cannotForceRunning": "Cannot force-remove a running task.", "forceRemoveFailed": "Force remove failed.", "batchProgress": "Merging {0}/{1}…", "batchDone": "Merged {0}, {1} need resolution." }, "listSettings": { "untitled": "Untitled" }, diff --git a/src/ClaudeDo.Ui/ViewModels/Agent/AgentConfigEditorViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Agent/AgentConfigEditorViewModel.cs index ff6dee40..b74edfe0 100644 --- a/src/ClaudeDo.Ui/ViewModels/Agent/AgentConfigEditorViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Agent/AgentConfigEditorViewModel.cs @@ -1,4 +1,7 @@ using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Text.Json; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using ClaudeDo.Data.Models; @@ -57,6 +60,7 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa public ObservableCollection ModelOptions { get; } = new(ModelRegistry.Aliases); public ObservableCollection Agents { get; } = new(); + public ObservableCollection SessionSkills { get; } = new(); public AgentConfigEditorViewModel(IWorkerClient worker, AgentConfigScope scope) { @@ -67,9 +71,31 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa // short-lived modal recreated with the current language on each open. if (scope == AgentConfigScope.Task) Loc.LanguageChanged += _langChangedHandler; + + SessionSkills.CollectionChanged += OnSessionSkillsCollectionChanged; } - public void Dispose() => Loc.LanguageChanged -= _langChangedHandler; + private void OnSessionSkillsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.NewItems is not null) + foreach (SelectableSkillViewModel item in e.NewItems) + item.PropertyChanged += OnSkillItemPropertyChanged; + if (e.OldItems is not null) + foreach (SelectableSkillViewModel item in e.OldItems) + item.PropertyChanged -= OnSkillItemPropertyChanged; + } + + private void OnSkillItemPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(SelectableSkillViewModel.IsSelected)) QueueSave(); + } + + public void Dispose() + { + Loc.LanguageChanged -= _langChangedHandler; + SessionSkills.CollectionChanged -= OnSessionSkillsCollectionChanged; + foreach (var s in SessionSkills) s.PropertyChanged -= OnSkillItemPropertyChanged; + } partial void OnModelChanged(string? value) { RecomputeModelBadge(); QueueSave(); } @@ -154,11 +180,18 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa var sp = string.IsNullOrWhiteSpace(SystemPrompt) ? null : SystemPrompt; var ap = SelectedAgent is null || string.IsNullOrWhiteSpace(SelectedAgent.Path) ? null : SelectedAgent.Path; var turns = MaxTurns is decimal d ? (int?)d : null; + var skills = SelectedSessionSkillNames(); if (_scope == AgentConfigScope.Task) - await _worker.UpdateTaskAgentSettingsAsync(new UpdateTaskAgentSettingsDto(TargetId, model, sp, ap, turns)); + await _worker.UpdateTaskAgentSettingsAsync(new UpdateTaskAgentSettingsDto(TargetId, model, sp, ap, turns, skills)); else - await _worker.UpdateListConfigAsync(new UpdateListConfigDto(TargetId, model, sp, ap, turns)); + await _worker.UpdateListConfigAsync(new UpdateListConfigDto(TargetId, model, sp, ap, turns, skills)); + } + + private List? SelectedSessionSkillNames() + { + var names = SessionSkills.Where(s => s.IsSelected).Select(s => s.Name).ToList(); + return names.Count == 0 ? null : names; } public async System.Threading.Tasks.Task LoadForListAsync(string listId, CancellationToken ct = default) @@ -172,6 +205,7 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa var cfg = await _worker.GetListConfigAsync(listId); ApplyConfig(cfg?.Model, cfg?.MaxTurns, cfg?.SystemPrompt, cfg?.AgentPath); + await ReloadSessionSkillsAsync(cfg?.SessionSkills); _listModel = null; _listMaxTurns = null; _listAgentName = null; EffectiveSystemPromptHint = ""; @@ -189,6 +223,7 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa TargetId = entity.Id; await ReloadAgentsAsync("(inherited)"); ApplyConfig(entity.Model, entity.MaxTurns, entity.SystemPrompt, entity.AgentPath); + await ReloadSessionSkillsAsync(ParseSessionSkills(entity.SessionSkills)); var listCfg = await _worker.GetListConfigAsync(entity.ListId); await LoadGlobalDefaultsAsync(); @@ -214,12 +249,30 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa MaxTurns = null; SystemPrompt = ""; SelectedAgent = null; + foreach (var s in SessionSkills) s.IsSelected = false; } finally { _suppressSave = false; } EffectiveSystemPromptHint = ""; TargetId = null; } + private static List? ParseSessionSkills(string? json) + { + if (string.IsNullOrWhiteSpace(json)) return null; + try { return JsonSerializer.Deserialize>(json); } + catch (JsonException) { return null; } + } + + private async System.Threading.Tasks.Task ReloadSessionSkillsAsync(IReadOnlyCollection? selected) + { + var installed = await _worker.GetSessionSkillsAsync(); + var selectedSet = selected is null ? new HashSet() : new HashSet(selected); + foreach (var s in SessionSkills) s.PropertyChanged -= OnSkillItemPropertyChanged; + SessionSkills.Clear(); + foreach (var s in installed) + SessionSkills.Add(new SelectableSkillViewModel(s.Name, s.Description, selectedSet.Contains(s.Name))); + } + private async System.Threading.Tasks.Task ReloadAgentsAsync(string placeholderName) { Agents.Clear(); @@ -255,5 +308,6 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa MaxTurns = null; SystemPrompt = ""; SelectedAgent = Agents.Count > 0 ? Agents[0] : null; + foreach (var s in SessionSkills) s.IsSelected = false; } } diff --git a/src/ClaudeDo.Ui/ViewModels/Agent/SelectableSkillViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Agent/SelectableSkillViewModel.cs new file mode 100644 index 00000000..1c9200e4 --- /dev/null +++ b/src/ClaudeDo.Ui/ViewModels/Agent/SelectableSkillViewModel.cs @@ -0,0 +1,23 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace ClaudeDo.Ui.ViewModels.Agent; + +/// +/// One installed session skill shown as a checkbox row. Reused by the global (General tab), +/// list, and task selectors — selection is additive-union across all three levels, so this +/// is deliberately just a name + checked flag with no inheritance/override state. +/// +public sealed partial class SelectableSkillViewModel : ViewModelBase +{ + public string Name { get; } + public string Description { get; } + + [ObservableProperty] private bool _isSelected; + + public SelectableSkillViewModel(string name, string description = "", bool isSelected = false) + { + Name = name; + Description = description; + _isSelected = isSelected; + } +} diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/GeneralSettingsTabViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/GeneralSettingsTabViewModel.cs index 8ad2d5c9..d0c0ae1e 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/GeneralSettingsTabViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/GeneralSettingsTabViewModel.cs @@ -1,5 +1,8 @@ +using System.Collections.ObjectModel; using ClaudeDo.Data.Models; using ClaudeDo.Localization; +using ClaudeDo.Ui.Services; +using ClaudeDo.Ui.ViewModels.Agent; using CommunityToolkit.Mvvm.ComponentModel; namespace ClaudeDo.Ui.ViewModels.Modals.Settings; @@ -22,6 +25,8 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase public IReadOnlyList Models { get; } = ModelRegistry.Aliases; public IReadOnlyList PermissionModes { get; } = PermissionModeRegistry.Modes; + public ObservableCollection SessionSkills { get; } = new(); + public GeneralSettingsTabViewModel() { } public GeneralSettingsTabViewModel(ILocalizer localizer, Action persist) @@ -51,4 +56,20 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase return "Max parallel executions must be between 1 and 20."; return null; } + + /// Loads the installed-skills checkbox list, reflecting the given selection. + public async Task LoadSessionSkillsAsync(IWorkerClient worker, IReadOnlyCollection? selected) + { + var installed = await worker.GetSessionSkillsAsync(); + var selectedSet = selected is null ? new HashSet() : new HashSet(selected); + SessionSkills.Clear(); + foreach (var s in installed) + SessionSkills.Add(new SelectableSkillViewModel(s.Name, s.Description, selectedSet.Contains(s.Name))); + } + + public List? SelectedSessionSkillNames() + { + var names = SessionSkills.Where(s => s.IsSelected).Select(s => s.Name).ToList(); + return names.Count == 0 ? null : names; + } } diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/SessionSkillsSettingsTabViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/SessionSkillsSettingsTabViewModel.cs new file mode 100644 index 00000000..0c9af50a --- /dev/null +++ b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/SessionSkillsSettingsTabViewModel.cs @@ -0,0 +1,78 @@ +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; + + [ObservableProperty] private string _installUrl = ""; + [ObservableProperty] private string _statusMessage = ""; + [ObservableProperty] private bool _isBusy; + + public ObservableCollection Skills { get; } = new(); + + public SessionSkillsSettingsTabViewModel(IWorkerClient worker) => _worker = worker; + + public async Task LoadAsync() + { + IsBusy = true; + try + { + var skills = await _worker.GetSessionSkillsAsync(); + Skills.Clear(); + foreach (var s in skills) Skills.Add(s); + } + finally { IsBusy = false; } + } + + [RelayCommand] + private async Task InstallAsync() + { + if (string.IsNullOrWhiteSpace(InstallUrl)) return; + IsBusy = true; StatusMessage = ""; + 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); } + finally { IsBusy = false; } + } + + [RelayCommand] + private async Task UpdateAsync(string? sourceUrl) + { + if (string.IsNullOrWhiteSpace(sourceUrl)) return; + IsBusy = true; StatusMessage = ""; + try + { + await _worker.UpdateSessionSkillAsync(sourceUrl); + StatusMessage = Loc.T("vm.sessionSkillsTab.updated"); + await LoadAsync(); + } + catch (Exception ex) { StatusMessage = Loc.T("vm.sessionSkillsTab.updateFailed", ex.Message); } + finally { IsBusy = false; } + } + + [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; } + } +} diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs index b0cf6017..0842eed6 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs @@ -18,6 +18,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase public FilesSettingsTabViewModel Files { get; } public PrimeClaudeTabViewModel Prime { get; } public OnlineInboxSettingsViewModel OnlineInbox { get; } + public SessionSkillsSettingsTabViewModel SessionSkills { get; } [ObservableProperty] private string _validationError = ""; [ObservableProperty] private bool _isBusy; @@ -39,6 +40,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase Files = new FilesSettingsTabViewModel(worker); Prime = prime; OnlineInbox = new OnlineInboxSettingsViewModel(worker, onlineLoginService); + SessionSkills = new SessionSkillsSettingsTabViewModel(worker); } public async Task LoadAsync() @@ -69,6 +71,8 @@ public sealed partial class SettingsModalViewModel : ViewModelBase await Prime.LoadAsync(); await OnlineInbox.LoadAsync(); + await SessionSkills.LoadAsync(); + await General.LoadSessionSkillsAsync(_worker, dto?.SessionSkills); } finally { IsBusy = false; } } @@ -97,7 +101,8 @@ public sealed partial class SettingsModalViewModel : ViewModelBase General.ReportExcludedPaths .Split('\n').Select(l => l.Trim().TrimEnd('\r')).Where(l => l.Length > 0).ToList()), General.StandupWeekday, - Prime.DailyPrepMaxTasks); + Prime.DailyPrepMaxTasks, + General.SelectedSessionSkillNames()); await _worker.UpdateAppSettingsAsync(dto); await Prime.SaveAsync(); await OnlineInbox.SaveAsync(); diff --git a/src/ClaudeDo.Ui/Views/Controls/AgentConfigEditor.axaml b/src/ClaudeDo.Ui/Views/Controls/AgentConfigEditor.axaml index 30dda3bb..576ddbc2 100644 --- a/src/ClaudeDo.Ui/Views/Controls/AgentConfigEditor.axaml +++ b/src/ClaudeDo.Ui/Views/Controls/AgentConfigEditor.axaml @@ -81,5 +81,20 @@ IsVisible="{Binding SelectedAgent.Path, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> + + + + + + + + + + + + + diff --git a/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml index 4c7c095c..8cba8eda 100644 --- a/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml +++ b/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml @@ -2,6 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals" xmlns:settings="using:ClaudeDo.Ui.ViewModels.Modals.Settings" + xmlns:agent="using:ClaudeDo.Ui.ViewModels.Agent" + xmlns:services="using:ClaudeDo.Ui.Services" xmlns:ctl="using:ClaudeDo.Ui.Views.Controls" xmlns:loc="using:ClaudeDo.Ui.Localization" xmlns:locm="using:ClaudeDo.Localization" @@ -108,6 +110,19 @@ + + + + + + + + + + + @@ -354,6 +369,58 @@ + + + + + + + +