229 lines
11 KiB
C#
229 lines
11 KiB
C#
using System.Linq;
|
|
using ClaudeDo.Data;
|
|
using ClaudeDo.Localization;
|
|
using ClaudeDo.Ui.Localization;
|
|
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; }
|
|
public FilesSettingsTabViewModel Files { get; }
|
|
public PrimeClaudeTabViewModel Prime { get; }
|
|
public OnlineInboxSettingsViewModel OnlineInbox { get; }
|
|
public SessionSkillsSettingsTabViewModel SessionSkills { get; }
|
|
public TicketSettingsTabViewModel Tickets { 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.
|
|
// Scope intentionally broader than ListsIslandViewModel.HasNoLinkedRepo (all Lists rows in
|
|
// the DB, not just UserLists).
|
|
[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();
|
|
|
|
// Same deal for the usage monitor, which owns the throttle-stage editor. It must *close* this
|
|
// modal rather than stack on top: Save() writes the throttle values it read at open, so a drag
|
|
// made while Settings is still open would be overwritten on save.
|
|
public Action? RequestUsageMonitor { get; set; }
|
|
|
|
[RelayCommand]
|
|
private void OpenUsageMonitor() => RequestUsageMonitor?.Invoke();
|
|
|
|
/// Read-only echo of the four throttle percentages, which are edited by dragging the
|
|
/// usage-monitor gauges — without this the Execution tab silently carries them through.
|
|
[ObservableProperty] private string _throttleSummary = "";
|
|
|
|
[ObservableProperty] private string _validationError = "";
|
|
[ObservableProperty] private bool _isBusy;
|
|
[ObservableProperty] private string _statusMessage = "";
|
|
|
|
public Action? CloseAction { get; set; }
|
|
|
|
public SettingsModalViewModel(IWorkerClient worker, PrimeClaudeTabViewModel prime,
|
|
OnlineLoginService onlineLoginService,
|
|
ILocalizer localizer, AppSettings appSettings,
|
|
IDbContextFactory<ClaudeDoDbContext> dbFactory)
|
|
{
|
|
_worker = worker;
|
|
_dbFactory = dbFactory;
|
|
General = new GeneralSettingsTabViewModel(localizer, code =>
|
|
{
|
|
appSettings.Language = code;
|
|
appSettings.Save();
|
|
});
|
|
General.InitAccentPresets(appSettings.AccentPreset, preset =>
|
|
{
|
|
appSettings.AccentPreset = preset;
|
|
appSettings.Save();
|
|
});
|
|
Worktrees = new WorktreesSettingsTabViewModel(worker);
|
|
Files = new FilesSettingsTabViewModel(worker);
|
|
Prime = prime;
|
|
OnlineInbox = new OnlineInboxSettingsViewModel(worker, onlineLoginService);
|
|
SessionSkills = new SessionSkillsSettingsTabViewModel(worker);
|
|
Tickets = new TicketSettingsTabViewModel(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),
|
|
new("Tickets", new LocalizedString(localizer, "settings.sidebar.categoryTickets"), "Erweitert", true, 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 —
|
|
// carried through load→save verbatim so saving Settings can never reset a dragged value.
|
|
private (int FiveSoft, int FiveHard, int SevenSoft, int SevenHard) _throttleStages = (50, 65, 50, 65);
|
|
|
|
public async Task LoadAsync()
|
|
{
|
|
IsBusy = true;
|
|
try
|
|
{
|
|
var dto = await _worker.GetAppSettingsAsync();
|
|
if (dto is not null)
|
|
{
|
|
General.DefaultClaudeInstructions = dto.DefaultClaudeInstructions ?? "";
|
|
General.DefaultModel = dto.DefaultModel ?? "sonnet";
|
|
General.DefaultMaxTurns = dto.DefaultMaxTurns;
|
|
General.MaxTurnsCeiling = dto.MaxTurnsCeiling;
|
|
General.DefaultPermissionMode = dto.DefaultPermissionMode ?? "auto";
|
|
General.MaxParallelExecutions = dto.MaxParallelExecutions;
|
|
General.UsageGateFiveHourPct = dto.UsageGateFiveHourPct;
|
|
General.UsageGateSevenDayPct = dto.UsageGateSevenDayPct;
|
|
General.AutoContinueOnUsageLimit = dto.AutoContinueOnUsageLimit;
|
|
_throttleStages = (
|
|
dto.UsageThrottleFiveHourSoftPct, dto.UsageThrottleFiveHourHardPct,
|
|
dto.UsageThrottleSevenDaySoftPct, dto.UsageThrottleSevenDayHardPct);
|
|
ThrottleSummary = Loc.T("settings.general.throttleStagesValues",
|
|
_throttleStages.FiveSoft, _throttleStages.FiveHard,
|
|
_throttleStages.SevenSoft, _throttleStages.SevenHard);
|
|
Worktrees.WorktreeStrategy = dto.WorktreeStrategy ?? "sibling";
|
|
Worktrees.CentralWorktreeRoot = dto.CentralWorktreeRoot;
|
|
Worktrees.WorktreeAutoCleanupEnabled = dto.WorktreeAutoCleanupEnabled;
|
|
Worktrees.WorktreeAutoCleanupDays = dto.WorktreeAutoCleanupDays;
|
|
General.ReportExcludedPaths = string.IsNullOrWhiteSpace(dto.ReportExcludedPaths)
|
|
? @"C:\Private"
|
|
: string.Join(Environment.NewLine,
|
|
System.Text.Json.JsonSerializer.Deserialize<List<string>>(dto.ReportExcludedPaths) ?? new());
|
|
General.StandupWeekday = dto.StandupWeekday is >= 0 and <= 6 ? dto.StandupWeekday : (int)DayOfWeek.Wednesday;
|
|
Prime.DailyPrepMaxTasks = dto.DailyPrepMaxTasks < 1 ? 5 : dto.DailyPrepMaxTasks;
|
|
}
|
|
else StatusMessage = Loc.T("vm.settingsModal.workerOffline");
|
|
|
|
await Files.LoadClaudeBinAsync();
|
|
await Prime.LoadAsync();
|
|
await OnlineInbox.LoadAsync();
|
|
await SessionSkills.LoadAsync(dto?.SessionSkills);
|
|
await Tickets.LoadAsync();
|
|
General.LoadModelPresets(dto?.ModelPresets, General.MaxTurnsCeiling);
|
|
|
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
|
HasLinkedRepo = await ctx.Lists.AnyAsync(RepoLinkage.IsLinkedInDb);
|
|
}
|
|
finally { IsBusy = false; }
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task Save()
|
|
{
|
|
var err = General.Validate() ?? Worktrees.Validate() ?? Prime.Validate();
|
|
if (err is not null) { ValidationError = err; return; }
|
|
ValidationError = "";
|
|
|
|
IsBusy = true;
|
|
try
|
|
{
|
|
var dto = new AppSettingsDto(
|
|
General.DefaultClaudeInstructions ?? "",
|
|
General.DefaultModel ?? "sonnet",
|
|
General.DefaultMaxTurns,
|
|
General.DefaultPermissionMode ?? "auto",
|
|
General.MaxParallelExecutions,
|
|
Worktrees.WorktreeStrategy ?? "sibling",
|
|
string.IsNullOrWhiteSpace(Worktrees.CentralWorktreeRoot) ? null : Worktrees.CentralWorktreeRoot,
|
|
Worktrees.WorktreeAutoCleanupEnabled,
|
|
Worktrees.WorktreeAutoCleanupDays,
|
|
System.Text.Json.JsonSerializer.Serialize(
|
|
General.ReportExcludedPaths
|
|
.Split('\n').Select(l => l.Trim().TrimEnd('\r')).Where(l => l.Length > 0).ToList()),
|
|
General.StandupWeekday,
|
|
Prime.DailyPrepMaxTasks,
|
|
SessionSkills.SelectedSkillNames(),
|
|
General.ModelPresetDtos(),
|
|
General.UsageGateFiveHourPct,
|
|
General.UsageGateSevenDayPct,
|
|
General.MaxTurnsCeiling,
|
|
_throttleStages.FiveSoft,
|
|
_throttleStages.FiveHard,
|
|
_throttleStages.SevenSoft,
|
|
_throttleStages.SevenHard,
|
|
General.AutoContinueOnUsageLimit);
|
|
await _worker.UpdateAppSettingsAsync(dto);
|
|
await Prime.SaveAsync();
|
|
await OnlineInbox.SaveAsync();
|
|
await Tickets.SaveAsync();
|
|
CloseAction?.Invoke();
|
|
}
|
|
catch (Exception ex) { StatusMessage = Loc.T("vm.settingsModal.saveFailed", ex.Message); }
|
|
finally { IsBusy = false; }
|
|
}
|
|
|
|
[RelayCommand] private void Cancel() => CloseAction?.Invoke();
|
|
}
|