177 lines
6.2 KiB
C#
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 */ }
|
|
}
|
|
}
|