using System.Collections.ObjectModel; using ClaudeDo.Data.Models; using ClaudeDo.Localization; 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 GeneralSettingsTabViewModel : ViewModelBase { private readonly ILocalizer? _localizer; private readonly Action? _persist; [ObservableProperty] private string _defaultClaudeInstructions = ""; [ObservableProperty] private string _defaultModel = ModelRegistry.DefaultAlias; [ObservableProperty] private int _defaultMaxTurns = 40; // Hard ceiling every resolved max-turns value is clamped to before a run starts. Drives the // clamp hints on the preset rows below and on AgentConfigEditorViewModel. [ObservableProperty] private int _maxTurnsCeiling = 80; [ObservableProperty] private string _defaultPermissionMode = PermissionModeRegistry.DefaultMode; [ObservableProperty] private int _maxParallelExecutions = 1; // Percentage of the 5h/7d Claude usage window at which the autonomous queue pauses. 0 = gate off. [ObservableProperty] private int _usageGateFiveHourPct = 80; [ObservableProperty] private int _usageGateSevenDayPct = 90; // Off by default: auto-continues a usage-limit-failed task once the 5h window resets, and // wakes a gate-blocked queue exactly at that reset time instead of only the 30s backstop. [ObservableProperty] private bool _autoContinueOnUsageLimit; // Cap on a list-handler run's wait<->merge handoff chain. 4 = two merge rounds (the long-standing // default); raise it for runs meant to keep going for hours. Bounds a runaway chain, so never 0. [ObservableProperty] private int _listHandlerMaxHandoffs = 4; // PowerUse master switch: a handler run spends the rest of the window unattended. On its own it // changes nothing — the run's LIST must also opt in (list settings → PowerUse). [ObservableProperty] private bool _powerUseEnabled; // Optional local-time window (HH:mm) narrowing when PowerUse applies; both blank = round the // clock. Unparseable input is normalized to null on save, same as the worker does. [ObservableProperty] private string? _powerUseWindowStart; [ObservableProperty] private string? _powerUseWindowEnd; // 5h-usage percentage at which a PowerUse handler session hands off. Clamped 50..99. [ObservableProperty] private int _powerUseHandoffPct = 95; // Newline-separated path prefixes excluded from the weekly report. [ObservableProperty] private string _reportExcludedPaths = @"C:\Private"; // 0=Sunday..6=Saturday (System.DayOfWeek); default Wednesday. [ObservableProperty] private int _standupWeekday = (int)DayOfWeek.Wednesday; public IReadOnlyList Models { get; } = ModelRegistry.Aliases; public IReadOnlyList PermissionModes { get; } = PermissionModeRegistry.Modes; public ObservableCollection AccentPresetSwatches { get; } = new(); private Action? _persistAccent; public void InitAccentPresets(string saved, Action persist) { _persistAccent = persist; var current = AccentPresets.Find(saved); AccentPresetSwatches.Clear(); foreach (var p in AccentPresets.All) AccentPresetSwatches.Add(new AccentPresetSwatchViewModel(p, p.Name == current.Name)); } [RelayCommand] private void SelectAccentPreset(AccentPreset preset) { foreach (var s in AccentPresetSwatches) s.IsSelected = s.Preset.Name == preset.Name; AccentPresetService.Apply(preset); _persistAccent?.Invoke(preset.Name); } /// One editable row per model alias: the effort and turn budget a run gets under that /// model. Supplies the global defaults; list- and task-level max-turns overrides still win. public ObservableCollection ModelPresets { get; } = new(); public void LoadModelPresets(IReadOnlyCollection? presets, int ceiling) { ModelPresets.Clear(); var source = presets is { Count: > 0 } ? presets.Select(p => new ModelPreset(p.Model, p.Effort, p.MaxTurns)).ToList() : Data.Models.ModelPresets.Defaults.ToList(); foreach (var p in Data.Models.ModelPresets.Parse(Data.Models.ModelPresets.Serialize(source))) ModelPresets.Add(new ModelPresetRowViewModel(p, ceiling)); } public List ModelPresetDtos() => ModelPresets.Select(r => new ModelPresetDto(r.Model, r.Effort, (int)r.MaxTurns)).ToList(); public GeneralSettingsTabViewModel() { } public GeneralSettingsTabViewModel(ILocalizer localizer, Action persist) { _localizer = localizer; _persist = persist; Languages = localizer.AvailableLanguages; _selectedLanguage = Languages.FirstOrDefault(l => l.Code == localizer.CurrentCode); } public IReadOnlyList Languages { get; } = Array.Empty(); [ObservableProperty] private LanguageOption? _selectedLanguage; partial void OnSelectedLanguageChanged(LanguageOption? value) { if (value is null || _localizer is null) return; _localizer.SetLanguage(value.Value.Code); _persist?.Invoke(value.Value.Code); } public string? Validate() { if (MaxTurnsCeiling < 1) return Loc.T("settings.general.maxTurnsCeilingInvalid"); if (MaxParallelExecutions < 1 || MaxParallelExecutions > 20) return "Max parallel executions must be between 1 and 20."; if (UsageGateFiveHourPct < 0 || UsageGateFiveHourPct > 100) return "Usage gate (5h) must be between 0 and 100."; if (UsageGateSevenDayPct < 0 || UsageGateSevenDayPct > 100) return "Usage gate (7d) must be between 0 and 100."; foreach (var row in ModelPresets) if (row.MaxTurns is < 1 or > 200) return $"Max turns for {row.Model} must be between 1 and 200."; if (!IsValidWindowBound(PowerUseWindowStart) || !IsValidWindowBound(PowerUseWindowEnd)) return Loc.T("settings.general.powerUseWindowInvalid"); return null; } private static bool IsValidWindowBound(string? value) => string.IsNullOrWhiteSpace(value) || PowerUseWindow.Parse(value) is not null; } /// One row of the per-model preset table. Single consumer, so it lives here. public sealed partial class ModelPresetRowViewModel : ViewModelBase { public string Model { get; } public IReadOnlyList EffortLevels { get; } = EffortRegistry.Levels; [ObservableProperty] private string _effort; // decimal so it binds straight to a NumericUpDown, like the other numeric settings. [ObservableProperty] private decimal _maxTurns; [ObservableProperty] private int _ceiling; public ModelPresetRowViewModel(ModelPreset preset, int ceiling) { Model = preset.Model; _effort = preset.Effort; _maxTurns = preset.MaxTurns; _ceiling = ceiling; } public string ClampHint => MaxTurns > Ceiling ? Loc.T("settings.turnsCeilingHint", Ceiling) : ""; partial void OnMaxTurnsChanged(decimal value) => OnPropertyChanged(nameof(ClampHint)); partial void OnCeilingChanged(int value) => OnPropertyChanged(nameof(ClampHint)); }