feat(ui): session skills registry tab + per-level selectors

This commit is contained in:
Mika Kuns
2026-07-23 16:47:14 +02:00
committed by mika kuns
parent b4c58087d2
commit 7c3c061428
12 changed files with 562 additions and 10 deletions
+17 -3
View File
@@ -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" },
+17 -3
View File
@@ -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" },
@@ -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<string> ModelOptions { get; } = new(ModelRegistry.Aliases);
public ObservableCollection<AgentInfo> Agents { get; } = new();
public ObservableCollection<SelectableSkillViewModel> 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<string>? 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<string>? ParseSessionSkills(string? json)
{
if (string.IsNullOrWhiteSpace(json)) return null;
try { return JsonSerializer.Deserialize<List<string>>(json); }
catch (JsonException) { return null; }
}
private async System.Threading.Tasks.Task ReloadSessionSkillsAsync(IReadOnlyCollection<string>? selected)
{
var installed = await _worker.GetSessionSkillsAsync();
var selectedSet = selected is null ? new HashSet<string>() : new HashSet<string>(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;
}
}
@@ -0,0 +1,23 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace ClaudeDo.Ui.ViewModels.Agent;
/// <summary>
/// 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.
/// </summary>
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;
}
}
@@ -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<string> Models { get; } = ModelRegistry.Aliases;
public IReadOnlyList<string> PermissionModes { get; } = PermissionModeRegistry.Modes;
public ObservableCollection<SelectableSkillViewModel> SessionSkills { get; } = new();
public GeneralSettingsTabViewModel() { }
public GeneralSettingsTabViewModel(ILocalizer localizer, Action<string> persist)
@@ -51,4 +56,20 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
return "Max parallel executions must be between 1 and 20.";
return null;
}
/// <summary>Loads the installed-skills checkbox list, reflecting the given selection.</summary>
public async Task LoadSessionSkillsAsync(IWorkerClient worker, IReadOnlyCollection<string>? selected)
{
var installed = await worker.GetSessionSkillsAsync();
var selectedSet = selected is null ? new HashSet<string>() : new HashSet<string>(selected);
SessionSkills.Clear();
foreach (var s in installed)
SessionSkills.Add(new SelectableSkillViewModel(s.Name, s.Description, selectedSet.Contains(s.Name)));
}
public List<string>? SelectedSessionSkillNames()
{
var names = SessionSkills.Where(s => s.IsSelected).Select(s => s.Name).ToList();
return names.Count == 0 ? null : names;
}
}
@@ -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<SessionSkillDto> 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; }
}
}
@@ -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();
@@ -81,5 +81,20 @@
IsVisible="{Binding SelectedAgent.Path, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<!-- Session skills (additive union with list/global; no inheritance badge) -->
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="{loc:Tr settings.agentEditor.sessionSkills}"/>
<TextBlock Classes="meta" Opacity="0.6" TextWrapping="Wrap"
Text="{loc:Tr settings.agentEditor.sessionSkillsHint}"/>
<ItemsControl ItemsSource="{Binding SessionSkills}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:SelectableSkillViewModel">
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" Content="{Binding Name}"
ToolTip.Tip="{Binding Description}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</StackPanel>
</UserControl>
@@ -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 @@
<ComboBoxItem Content="{loc:Tr settings.general.weekdaySaturday}"/>
</ComboBox>
</StackPanel>
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.sessionSkills}"/>
<TextBlock Classes="meta" Opacity="0.6" TextWrapping="Wrap"
Text="{loc:Tr settings.general.sessionSkillsHint}"/>
<ItemsControl ItemsSource="{Binding General.SessionSkills}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="agent:SelectableSkillViewModel">
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" Content="{Binding Name}"
ToolTip.Tip="{Binding Description}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
@@ -354,6 +369,58 @@
</ScrollViewer>
</TabItem>
<TabItem Header="{loc:Tr settings.tabSkills}">
<ScrollViewer>
<StackPanel Spacing="12" Margin="0,8,0,0">
<StackPanel Spacing="6">
<TextBlock Classes="section-label" Text="{loc:Tr settings.skills.installSection}"/>
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="8">
<TextBox Grid.Column="0" Text="{Binding SessionSkills.InstallUrl, Mode=TwoWay}"
PlaceholderText="{loc:Tr settings.skills.installUrlPlaceholder}"/>
<Button Grid.Column="1" Classes="btn" Content="{loc:Tr settings.skills.installButton}"
Command="{Binding SessionSkills.InstallCommand}"
IsEnabled="{Binding !SessionSkills.IsBusy}"/>
</Grid>
</StackPanel>
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0" Margin="0,2,0,0"/>
<StackPanel Spacing="6">
<TextBlock Classes="section-label" Text="{loc:Tr settings.skills.installedSection}"/>
<ItemsControl ItemsSource="{Binding SessionSkills.Skills}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="services:SessionSkillDto">
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="1"
CornerRadius="6" Padding="10,8" Margin="0,0,0,8"
Background="{DynamicResource DeepBrush}">
<StackPanel Spacing="4">
<Grid ColumnDefinitions="*,Auto,Auto">
<TextBlock Grid.Column="0" Classes="title" Text="{Binding Name}"/>
<Button Grid.Column="1" Classes="btn" Content="{loc:Tr settings.skills.updateButton}"
Margin="0,0,6,0"
Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).SessionSkills.UpdateCommand}"
CommandParameter="{Binding SourceUrl}"/>
<Button Grid.Column="2" Classes="btn danger" Content="{loc:Tr settings.skills.removeButton}"
Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).SessionSkills.RemoveCommand}"
CommandParameter="{Binding SourceUrl}"/>
</Grid>
<TextBlock Classes="meta" Text="{Binding Description}" TextWrapping="Wrap"
IsVisible="{Binding Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<TextBlock Classes="meta" Opacity="0.6" Text="{Binding PinnedRef}"/>
<TextBlock Classes="path-mono" Text="{Binding SourceUrl}" TextTrimming="PrefixCharacterEllipsis"/>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<TextBlock Classes="meta" Text="{Binding SessionSkills.StatusMessage}"
IsVisible="{Binding SessionSkills.StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
</DockPanel>