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:
mika kuns
2026-08-21 15:04:15 +02:00
parent a80a31eeac
commit 56c3719e98
10 changed files with 548 additions and 209 deletions
@@ -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,