diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index 18e975a0..458872dc 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -10,6 +10,21 @@ "tabFiles": "Dateien", "tabPrime": "Prime Claude", "tabSkills": "Skills", + "sidebar": { + "groupBasis": "BASIS", + "groupAdvanced": "ERWEITERT", + "categoryGeneral": "Allgemein", + "categoryExecution": "Ausführung", + "categoryPrime": "Prime Claude", + "categoryWorktrees": "Worktrees", + "categoryFiles": "Dateien & Prompts", + "categorySkills": "Session Skills", + "categoryReports": "Berichte" + }, + "repoHint": { + "text": "Wirkt erst mit verknüpftem Repo.", + "link": "Repo importieren" + }, "general": { "defaultInstructions": "Standard-Anweisungen", "defaultInstructionsPlaceholder": "Basis-Anweisungen, die auf jede Aufgabe angewendet werden", diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index 43735c28..7d2dd51e 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -10,6 +10,21 @@ "tabFiles": "Files", "tabPrime": "Prime Claude", "tabSkills": "Skills", + "sidebar": { + "groupBasis": "BASICS", + "groupAdvanced": "ADVANCED", + "categoryGeneral": "General", + "categoryExecution": "Execution", + "categoryPrime": "Prime Claude", + "categoryWorktrees": "Worktrees", + "categoryFiles": "Files & Prompts", + "categorySkills": "Session Skills", + "categoryReports": "Reports" + }, + "repoHint": { + "text": "Only takes effect once a repo is linked.", + "link": "Import repo" + }, "general": { "defaultInstructions": "Default instructions", "defaultInstructionsPlaceholder": "Baseline instructions applied to every task", diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/GeneralSettingsTabViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/GeneralSettingsTabViewModel.cs index 7eec2ecf..75a8fdfd 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/GeneralSettingsTabViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/GeneralSettingsTabViewModel.cs @@ -3,7 +3,6 @@ using ClaudeDo.Data.Models; using ClaudeDo.Localization; using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; -using ClaudeDo.Ui.ViewModels.Agent; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -33,8 +32,6 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase public IReadOnlyList Models { get; } = ModelRegistry.Aliases; public IReadOnlyList PermissionModes { get; } = PermissionModeRegistry.Modes; - public ObservableCollection SessionSkills { get; } = new(); - public ObservableCollection AccentPresetSwatches { get; } = new(); private Action? _persistAccent; @@ -109,22 +106,6 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase return $"Max turns for {row.Model} must be between 1 and 200."; 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; - } } /// One row of the per-model preset table. Single consumer, so it lives here. diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/SessionSkillsSettingsTabViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/SessionSkillsSettingsTabViewModel.cs index ba48180b..91509daf 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/SessionSkillsSettingsTabViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/SessionSkillsSettingsTabViewModel.cs @@ -18,7 +18,7 @@ public sealed partial class SessionSkillsSettingsTabViewModel : ViewModelBase [ObservableProperty] private bool _isBusy; [ObservableProperty] private bool _isEmpty = true; - public ObservableCollection Skills { get; } = new(); + public ObservableCollection Skills { get; } = new(); public SessionSkillsSettingsTabViewModel(IWorkerClient worker) { @@ -33,19 +33,31 @@ public sealed partial class SessionSkillsSettingsTabViewModel : ViewModelBase }; } - public async Task LoadAsync() + /// 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(s); + 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))] @@ -96,3 +108,27 @@ public sealed partial class SessionSkillsSettingsTabViewModel : ViewModelBase 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; + } +} diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs index 5d3f38f6..273592ce 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs @@ -6,12 +6,19 @@ using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Modals.Settings; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using Microsoft.EntityFrameworkCore; namespace ClaudeDo.Ui.ViewModels.Modals; +/// One sidebar entry. Group is an internal split key ("Basis"/"Erweitert"), not +/// user-facing text — the sidebar renders the two groups as separate lists with their own +/// (localized) heading. RequiresRepo marks the categories that show the repo-hint strip. +public sealed record SettingsCategory(string Key, LocalizedString Label, string Group, bool IsVisible, bool RequiresRepo); + public sealed partial class SettingsModalViewModel : ViewModelBase { private readonly IWorkerClient _worker; + private readonly IDbContextFactory _dbFactory; public GeneralSettingsTabViewModel General { get; } public WorktreesSettingsTabViewModel Worktrees { get; } @@ -19,10 +26,44 @@ public sealed partial class SettingsModalViewModel : ViewModelBase public PrimeClaudeTabViewModel Prime { get; } public OnlineInboxSettingsViewModel OnlineInbox { get; } public SessionSkillsSettingsTabViewModel SessionSkills { get; } - + // Online Inbox ist unfertig und vor Usern verborgen — hier auf true für Reaktivierung. public bool ShowOnlineInbox => false; + // Fixed order — mirrors the TabItem order in SettingsModalView.axaml 1:1, so + // _categories.IndexOf(category) is a valid TabControl.SelectedIndex. + private readonly List _categories; + public IReadOnlyList Categories => _categories; + public IReadOnlyList BasisCategories { get; } + public IReadOnlyList ErweitertCategories { get; } + + private SettingsCategory? _selectedCategory; + public SettingsCategory? SelectedCategory + { + get => _selectedCategory; + set + { + // The two sidebar ListBoxes share this property; selecting an item in one pushes null + // into the other's binding (item not found in its own ItemsSource) — ignore that noise + // instead of letting it clobber the real selection. + if (value is null) return; + if (SetProperty(ref _selectedCategory, value)) + SelectedIndex = _categories.IndexOf(value); + } + } + + [ObservableProperty] private int _selectedIndex; + + // True once at least one list has a linked repo (WorkingDir). Drives the repo-hint strip on + // Worktrees/Prime Claude/Session Skills/Berichte — nothing is ever hidden, just annotated. + [ObservableProperty] private bool _hasLinkedRepo; + + // Wired by WindowDialogService to close this modal and open the existing repo-import flow. + public Action? RequestRepoImport { get; set; } + + [RelayCommand] + private void OpenRepoImport() => RequestRepoImport?.Invoke(); + [ObservableProperty] private string _validationError = ""; [ObservableProperty] private bool _isBusy; [ObservableProperty] private string _statusMessage = ""; @@ -31,9 +72,11 @@ public sealed partial class SettingsModalViewModel : ViewModelBase public SettingsModalViewModel(IWorkerClient worker, PrimeClaudeTabViewModel prime, IOnlineLoginService onlineLoginService, - ILocalizer localizer, AppSettings appSettings) + ILocalizer localizer, AppSettings appSettings, + IDbContextFactory dbFactory) { _worker = worker; + _dbFactory = dbFactory; General = new GeneralSettingsTabViewModel(localizer, code => { appSettings.Language = code; @@ -49,6 +92,21 @@ public sealed partial class SettingsModalViewModel : ViewModelBase Prime = prime; OnlineInbox = new OnlineInboxSettingsViewModel(worker, onlineLoginService); SessionSkills = new SessionSkillsSettingsTabViewModel(worker); + + _categories = new List + { + new("General", new LocalizedString(localizer, "settings.sidebar.categoryGeneral"), "Basis", true, false), + new("Execution", new LocalizedString(localizer, "settings.sidebar.categoryExecution"), "Basis", true, false), + new("Prime", new LocalizedString(localizer, "settings.sidebar.categoryPrime"), "Basis", true, true), + new("Worktrees", new LocalizedString(localizer, "settings.sidebar.categoryWorktrees"), "Erweitert", true, true), + new("Files", new LocalizedString(localizer, "settings.sidebar.categoryFiles"), "Erweitert", true, false), + new("SessionSkills", new LocalizedString(localizer, "settings.sidebar.categorySkills"), "Erweitert", true, true), + new("Reports", new LocalizedString(localizer, "settings.sidebar.categoryReports"), "Erweitert", true, true), + new("OnlineInbox", new LocalizedString(localizer, "settings.onlineInbox.tabHeader"), "Erweitert", ShowOnlineInbox, false), + }; + BasisCategories = _categories.Where(c => c.Group == "Basis").ToList(); + ErweitertCategories = _categories.Where(c => c.Group == "Erweitert").ToList(); + _selectedCategory = _categories[0]; } // Throttle stages are edited by dragging the usage-monitor gauges, not on any Settings tab — @@ -89,9 +147,11 @@ public sealed partial class SettingsModalViewModel : ViewModelBase await Prime.LoadAsync(); await OnlineInbox.LoadAsync(); - await SessionSkills.LoadAsync(); - await General.LoadSessionSkillsAsync(_worker, dto?.SessionSkills); + await SessionSkills.LoadAsync(dto?.SessionSkills); General.LoadModelPresets(dto?.ModelPresets, General.MaxTurnsCeiling); + + await using var ctx = await _dbFactory.CreateDbContextAsync(); + HasLinkedRepo = await ctx.Lists.AnyAsync(l => l.WorkingDir != null && l.WorkingDir != ""); } finally { IsBusy = false; } } @@ -121,7 +181,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase .Split('\n').Select(l => l.Trim().TrimEnd('\r')).Where(l => l.Length > 0).ToList()), General.StandupWeekday, Prime.DailyPrepMaxTasks, - General.SelectedSessionSkillNames(), + SessionSkills.SelectedSkillNames(), General.ModelPresetDtos(), General.UsageGateFiveHourPct, General.UsageGateSevenDayPct, diff --git a/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml index a5574c3b..1313c205 100644 --- a/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml +++ b/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml @@ -11,7 +11,7 @@ x:Class="ClaudeDo.Ui.Views.Modals.SettingsModalView" x:DataType="vm:SettingsModalViewModel" Title="{loc:Tr settings.title}" - Width="580" Height="760" MinWidth="480" MinHeight="520" + Width="700" Height="760" MinWidth="600" MinHeight="520" CanResize="True" WindowDecorations="BorderOnly" ExtendClientAreaToDecorationsHint="True" @@ -35,7 +35,7 @@ - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -101,6 +150,14 @@ PlaceholderText="{loc:Tr settings.general.defaultInstructionsPlaceholder}" Text="{Binding General.DefaultClaudeInstructions, Mode=TwoWay}"/> + + + + + + + + @@ -181,42 +238,83 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + +