using System.Diagnostics; using System.IO; using System.Reflection; using ClaudeDo.Data; using ClaudeDo.Ui.Services; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace ClaudeDo.Ui.ViewModels.Modals; public sealed partial class SettingsModalViewModel : ViewModelBase { private readonly WorkerClient _worker; [ObservableProperty] private string _defaultClaudeInstructions = ""; [ObservableProperty] private string _defaultModel = "sonnet"; [ObservableProperty] private int _defaultMaxTurns = 30; [ObservableProperty] private string _defaultPermissionMode = "bypassPermissions"; [ObservableProperty] private string _worktreeStrategy = "sibling"; [ObservableProperty] private string? _centralWorktreeRoot; [ObservableProperty] private bool _worktreeAutoCleanupEnabled; [ObservableProperty] private int _worktreeAutoCleanupDays = 7; [ObservableProperty] private string _statusMessage = ""; [ObservableProperty] private bool _isBusy; [ObservableProperty] private bool _showResetConfirm; [ObservableProperty] private string _validationError = ""; public IReadOnlyList Models { get; } = new[] { "opus", "sonnet", "haiku" }; public IReadOnlyList PermissionModes { get; } = new[] { "bypassPermissions", "acceptEdits", "plan", "default" }; public IReadOnlyList WorktreeStrategies { get; } = new[] { "sibling", "central" }; public string AppVersion { get; } = Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "0.0.0"; public string DataFolderPath { get; } = Paths.AppDataRoot(); public string LogsFolderPath { get; } = Path.Combine(Paths.AppDataRoot(), "logs"); public string WorkerConfigPath { get; } = Path.Combine(Paths.AppDataRoot(), "worker.config.json"); public Action? CloseAction { get; set; } public SettingsModalViewModel(WorkerClient worker) { _worker = worker; } public async Task LoadAsync() { IsBusy = true; try { var dto = await _worker.GetAppSettingsAsync(); if (dto is not null) { DefaultClaudeInstructions = dto.DefaultClaudeInstructions ?? ""; DefaultModel = dto.DefaultModel ?? "sonnet"; DefaultMaxTurns = dto.DefaultMaxTurns; DefaultPermissionMode = dto.DefaultPermissionMode ?? "bypassPermissions"; WorktreeStrategy = dto.WorktreeStrategy ?? "sibling"; CentralWorktreeRoot = dto.CentralWorktreeRoot; WorktreeAutoCleanupEnabled = dto.WorktreeAutoCleanupEnabled; WorktreeAutoCleanupDays = dto.WorktreeAutoCleanupDays; } else { StatusMessage = "Worker offline — settings read-only."; } } finally { IsBusy = false; } } private bool Validate() { if (DefaultMaxTurns < 1 || DefaultMaxTurns > 200) { ValidationError = "Max turns must be between 1 and 200."; return false; } if (WorktreeAutoCleanupEnabled && (WorktreeAutoCleanupDays < 1 || WorktreeAutoCleanupDays > 365)) { ValidationError = "Cleanup days must be between 1 and 365."; return false; } if (WorktreeStrategy == "central") { if (string.IsNullOrWhiteSpace(CentralWorktreeRoot)) { ValidationError = "Central worktree root is required for Central strategy."; return false; } if (!Directory.Exists(CentralWorktreeRoot)) { ValidationError = $"Directory not found: {CentralWorktreeRoot}"; return false; } } ValidationError = ""; return true; } [RelayCommand] private async Task Save() { if (!Validate()) return; IsBusy = true; try { var dto = new AppSettingsDto( DefaultClaudeInstructions ?? "", DefaultModel ?? "sonnet", DefaultMaxTurns, DefaultPermissionMode ?? "bypassPermissions", WorktreeStrategy ?? "sibling", string.IsNullOrWhiteSpace(CentralWorktreeRoot) ? null : CentralWorktreeRoot, WorktreeAutoCleanupEnabled, WorktreeAutoCleanupDays); await _worker.UpdateAppSettingsAsync(dto); CloseAction?.Invoke(); } catch (Exception ex) { StatusMessage = $"Save failed: {ex.Message}"; } finally { IsBusy = false; } } [RelayCommand] private void Cancel() => CloseAction?.Invoke(); [RelayCommand] private async Task CleanupWorktrees() { IsBusy = true; StatusMessage = ""; try { var result = await _worker.CleanupFinishedWorktreesAsync(); StatusMessage = result is null ? "Worker offline." : $"Removed {result.Removed} worktree(s)."; } finally { IsBusy = false; } } [RelayCommand] private void RequestResetConfirm() => ShowResetConfirm = true; [RelayCommand] private void CancelResetConfirm() => ShowResetConfirm = false; [RelayCommand] private async Task ConfirmResetAll() { ShowResetConfirm = false; IsBusy = true; StatusMessage = ""; try { var result = await _worker.ResetAllWorktreesAsync(); if (result is null) StatusMessage = "Worker offline."; else if (result.Blocked) StatusMessage = $"Cannot force-remove: {result.RunningTasks} task(s) still running. Cancel them first."; else StatusMessage = $"Removed {result.Removed} worktree(s) from {result.TasksAffected} task(s)."; } finally { IsBusy = false; } } [RelayCommand] private void OpenPath(string? path) { if (string.IsNullOrWhiteSpace(path)) return; try { var target = File.Exists(path) ? path : (Directory.Exists(path) ? path : null); if (target is null) return; Process.Start(new ProcessStartInfo("explorer.exe", $"\"{target}\"") { UseShellExecute = true }); } catch { /* ignore */ } } }