feat(ui): split SettingsModalViewModel into per-tab VMs + add PrimeClaudeTabViewModel
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
@@ -12,41 +10,24 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
||||
{
|
||||
private readonly WorkerClient _worker;
|
||||
|
||||
[ObservableProperty] private string _defaultClaudeInstructions = "";
|
||||
[ObservableProperty] private string _defaultModel = "sonnet";
|
||||
[ObservableProperty] private int _defaultMaxTurns = 100;
|
||||
[ObservableProperty] private string _defaultPermissionMode = "auto";
|
||||
[ObservableProperty] private string _worktreeStrategy = "sibling";
|
||||
[ObservableProperty] private string? _centralWorktreeRoot;
|
||||
[ObservableProperty] private bool _worktreeAutoCleanupEnabled;
|
||||
[ObservableProperty] private int _worktreeAutoCleanupDays = 7;
|
||||
public GeneralSettingsTabViewModel General { get; }
|
||||
public WorktreesSettingsTabViewModel Worktrees { get; }
|
||||
public FilesSettingsTabViewModel Files { get; }
|
||||
public PrimeClaudeTabViewModel Prime { get; }
|
||||
|
||||
[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[]
|
||||
{ "auto", "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 string SystemPromptPath { get; } = PromptFiles.PathFor(PromptKind.System);
|
||||
public string PlanningPromptPath { get; } = PromptFiles.PathFor(PromptKind.Planning);
|
||||
public string AgentPromptPath { get; } = PromptFiles.PathFor(PromptKind.Agent);
|
||||
[ObservableProperty] private bool _isBusy;
|
||||
[ObservableProperty] private string _statusMessage = "";
|
||||
|
||||
public Action? CloseAction { get; set; }
|
||||
|
||||
public SettingsModalViewModel(WorkerClient worker)
|
||||
public SettingsModalViewModel(WorkerClient worker, PrimeClaudeTabViewModel prime)
|
||||
{
|
||||
_worker = worker;
|
||||
General = new GeneralSettingsTabViewModel();
|
||||
Worktrees = new WorktreesSettingsTabViewModel(worker);
|
||||
Files = new FilesSettingsTabViewModel(worker);
|
||||
Prime = prime;
|
||||
}
|
||||
|
||||
public async Task LoadAsync()
|
||||
@@ -57,166 +38,48 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
||||
var dto = await _worker.GetAppSettingsAsync();
|
||||
if (dto is not null)
|
||||
{
|
||||
DefaultClaudeInstructions = dto.DefaultClaudeInstructions ?? "";
|
||||
DefaultModel = dto.DefaultModel ?? "sonnet";
|
||||
DefaultMaxTurns = dto.DefaultMaxTurns;
|
||||
DefaultPermissionMode = dto.DefaultPermissionMode ?? "auto";
|
||||
WorktreeStrategy = dto.WorktreeStrategy ?? "sibling";
|
||||
CentralWorktreeRoot = dto.CentralWorktreeRoot;
|
||||
WorktreeAutoCleanupEnabled = dto.WorktreeAutoCleanupEnabled;
|
||||
WorktreeAutoCleanupDays = dto.WorktreeAutoCleanupDays;
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusMessage = "Worker offline — settings read-only.";
|
||||
General.DefaultClaudeInstructions = dto.DefaultClaudeInstructions ?? "";
|
||||
General.DefaultModel = dto.DefaultModel ?? "sonnet";
|
||||
General.DefaultMaxTurns = dto.DefaultMaxTurns;
|
||||
General.DefaultPermissionMode = dto.DefaultPermissionMode ?? "auto";
|
||||
Worktrees.WorktreeStrategy = dto.WorktreeStrategy ?? "sibling";
|
||||
Worktrees.CentralWorktreeRoot = dto.CentralWorktreeRoot;
|
||||
Worktrees.WorktreeAutoCleanupEnabled = dto.WorktreeAutoCleanupEnabled;
|
||||
Worktrees.WorktreeAutoCleanupDays = dto.WorktreeAutoCleanupDays;
|
||||
}
|
||||
else StatusMessage = "Worker offline — settings read-only.";
|
||||
|
||||
await Prime.LoadAsync();
|
||||
}
|
||||
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;
|
||||
var err = General.Validate() ?? Worktrees.Validate() ?? Prime.Validate();
|
||||
if (err is not null) { ValidationError = err; return; }
|
||||
ValidationError = "";
|
||||
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
var dto = new AppSettingsDto(
|
||||
DefaultClaudeInstructions ?? "",
|
||||
DefaultModel ?? "sonnet",
|
||||
DefaultMaxTurns,
|
||||
DefaultPermissionMode ?? "auto",
|
||||
WorktreeStrategy ?? "sibling",
|
||||
string.IsNullOrWhiteSpace(CentralWorktreeRoot) ? null : CentralWorktreeRoot,
|
||||
WorktreeAutoCleanupEnabled,
|
||||
WorktreeAutoCleanupDays);
|
||||
General.DefaultClaudeInstructions ?? "",
|
||||
General.DefaultModel ?? "sonnet",
|
||||
General.DefaultMaxTurns,
|
||||
General.DefaultPermissionMode ?? "auto",
|
||||
Worktrees.WorktreeStrategy ?? "sibling",
|
||||
string.IsNullOrWhiteSpace(Worktrees.CentralWorktreeRoot) ? null : Worktrees.CentralWorktreeRoot,
|
||||
Worktrees.WorktreeAutoCleanupEnabled,
|
||||
Worktrees.WorktreeAutoCleanupDays);
|
||||
await _worker.UpdateAppSettingsAsync(dto);
|
||||
await Prime.SaveAsync();
|
||||
CloseAction?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Save failed: {ex.Message}";
|
||||
}
|
||||
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 async Task RestoreDefaultAgents()
|
||||
{
|
||||
IsBusy = true;
|
||||
StatusMessage = "";
|
||||
try
|
||||
{
|
||||
var result = await _worker.RestoreDefaultAgentsAsync();
|
||||
if (result is null)
|
||||
StatusMessage = "Worker offline.";
|
||||
else if (result.Copied == 0 && result.Skipped == 0)
|
||||
StatusMessage = "No default agents bundled.";
|
||||
else if (result.Copied == 0)
|
||||
StatusMessage = "All default agents already present.";
|
||||
else
|
||||
StatusMessage = $"Restored {result.Copied} default agent(s).";
|
||||
|
||||
await _worker.RefreshAgentsAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Restore failed: {ex.Message}";
|
||||
}
|
||||
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 */ }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenPrompt(string? kindName)
|
||||
{
|
||||
if (!Enum.TryParse<PromptKind>(kindName, ignoreCase: true, out var kind)) return;
|
||||
try
|
||||
{
|
||||
PromptFiles.EnsureExists(kind);
|
||||
var path = PromptFiles.PathFor(kind);
|
||||
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Open failed: {ex.Message}";
|
||||
}
|
||||
}
|
||||
[RelayCommand] private void Cancel() => CloseAction?.Invoke();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user