Files
ClaudeDo/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs

177 lines
6.2 KiB
C#

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<string> Models { get; } = new[] { "opus", "sonnet", "haiku" };
public IReadOnlyList<string> PermissionModes { get; } = new[]
{ "bypassPermissions", "acceptEdits", "plan", "default" };
public IReadOnlyList<string> 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 */ }
}
}