feat(ui): Settings-Modal auf Sidebar-Kategorien umbauen
TabControl bleibt Content-Host, TabStrip wird retempliert (nur PART_SelectedContentHost) und durch eine zweigruppige Sidebar (BASIS/ERWEITERT) ersetzt. General wird in Allgemein/Ausfuehrung/Berichte gesplittet, Session Skills (Checkbox-Liste aus General + Skills-Tab) zu einer Liste zusammengelegt, und ein Repo-Hinweisstreifen erscheint auf Worktrees/Prime Claude/Session Skills/Berichte solange keine Liste ein WorkingDir hat. Fensterbreite 580 -> 700.
This commit is contained in:
@@ -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<string> Models { get; } = ModelRegistry.Aliases;
|
||||
public IReadOnlyList<string> PermissionModes { get; } = PermissionModeRegistry.Modes;
|
||||
|
||||
public ObservableCollection<SelectableSkillViewModel> SessionSkills { get; } = new();
|
||||
|
||||
public ObservableCollection<AccentPresetSwatchViewModel> AccentPresetSwatches { get; } = new();
|
||||
private Action<string>? _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;
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>One row of the per-model preset table. Single consumer, so it lives here.</summary>
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed partial class SessionSkillsSettingsTabViewModel : ViewModelBase
|
||||
[ObservableProperty] private bool _isBusy;
|
||||
[ObservableProperty] private bool _isEmpty = true;
|
||||
|
||||
public ObservableCollection<SessionSkillDto> Skills { get; } = new();
|
||||
public ObservableCollection<SessionSkillRowViewModel> Skills { get; } = new();
|
||||
|
||||
public SessionSkillsSettingsTabViewModel(IWorkerClient worker)
|
||||
{
|
||||
@@ -33,19 +33,31 @@ public sealed partial class SessionSkillsSettingsTabViewModel : ViewModelBase
|
||||
};
|
||||
}
|
||||
|
||||
public async Task LoadAsync()
|
||||
/// <summary>Loads the installed skills, marking each row selected per <paramref name="selectedNames"/>.
|
||||
/// When omitted, the current selection is preserved across the refresh (e.g. after install/update/remove).</summary>
|
||||
public async Task LoadAsync(IReadOnlyCollection<string>? selectedNames = null)
|
||||
{
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
var selected = selectedNames ?? Skills.Where(s => s.IsSelected).Select(s => s.Name).ToList();
|
||||
var selectedSet = new HashSet<string>(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; }
|
||||
}
|
||||
|
||||
/// <summary>The globally-selected skill names, or null when none are selected (matches the
|
||||
/// AppSettingsDto.SessionSkills convention of "null means no global selection").</summary>
|
||||
public List<string>? 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; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>One installed skill row: identity/management fields from <see cref="SessionSkillDto"/>
|
||||
/// plus the global "active for every task" checkbox state that used to live in a separate
|
||||
/// General-tab list.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>One sidebar entry. <c>Group</c> 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. <c>RequiresRepo</c> marks the categories that show the repo-hint strip.</summary>
|
||||
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<ClaudeDoDbContext> _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<SettingsCategory> _categories;
|
||||
public IReadOnlyList<SettingsCategory> Categories => _categories;
|
||||
public IReadOnlyList<SettingsCategory> BasisCategories { get; }
|
||||
public IReadOnlyList<SettingsCategory> 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<ClaudeDoDbContext> 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<SettingsCategory>
|
||||
{
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user