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:
@@ -7,6 +7,7 @@ using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels;
|
||||
using ClaudeDo.Ui.ViewModels.Islands;
|
||||
using ClaudeDo.Ui.ViewModels.Modals;
|
||||
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
@@ -94,6 +95,8 @@ sealed class Program
|
||||
|
||||
// ViewModels
|
||||
sc.AddTransient<WorktreeModalViewModel>();
|
||||
sc.AddSingleton<IPrimeScheduleApi, WorkerPrimeScheduleApi>();
|
||||
sc.AddTransient<PrimeClaudeTabViewModel>();
|
||||
sc.AddTransient<SettingsModalViewModel>();
|
||||
sc.AddTransient<MergeModalViewModel>();
|
||||
sc.AddTransient<ListSettingsModalViewModel>();
|
||||
|
||||
17
src/ClaudeDo.Ui/Services/IPrimeScheduleApi.cs
Normal file
17
src/ClaudeDo.Ui/Services/IPrimeScheduleApi.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace ClaudeDo.Ui.Services;
|
||||
|
||||
public interface IPrimeScheduleApi
|
||||
{
|
||||
Task<List<PrimeScheduleDto>> ListAsync();
|
||||
Task<PrimeScheduleDto?> UpsertAsync(PrimeScheduleDto dto);
|
||||
Task DeleteAsync(Guid id);
|
||||
}
|
||||
|
||||
public sealed class WorkerPrimeScheduleApi : IPrimeScheduleApi
|
||||
{
|
||||
private readonly WorkerClient _client;
|
||||
public WorkerPrimeScheduleApi(WorkerClient client) => _client = client;
|
||||
public Task<List<PrimeScheduleDto>> ListAsync() => _client.GetPrimeSchedulesAsync();
|
||||
public Task<PrimeScheduleDto?> UpsertAsync(PrimeScheduleDto dto) => _client.UpsertPrimeScheduleAsync(dto);
|
||||
public Task DeleteAsync(Guid id) => _client.DeletePrimeScheduleAsync(id);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Diagnostics;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||
|
||||
public sealed partial class FilesSettingsTabViewModel : ViewModelBase
|
||||
{
|
||||
private readonly WorkerClient _worker;
|
||||
|
||||
[ObservableProperty] private string _statusMessage = "";
|
||||
[ObservableProperty] private bool _isBusy;
|
||||
|
||||
public string SystemPromptPath { get; } = PromptFiles.PathFor(PromptKind.System);
|
||||
public string PlanningPromptPath { get; } = PromptFiles.PathFor(PromptKind.Planning);
|
||||
public string AgentPromptPath { get; } = PromptFiles.PathFor(PromptKind.Agent);
|
||||
|
||||
public FilesSettingsTabViewModel(WorkerClient worker) => _worker = worker;
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RestoreDefaultAgents()
|
||||
{
|
||||
IsBusy = true; StatusMessage = "";
|
||||
try
|
||||
{
|
||||
var r = await _worker.RestoreDefaultAgentsAsync();
|
||||
if (r is null) StatusMessage = "Worker offline.";
|
||||
else if (r.Copied == 0 && r.Skipped == 0) StatusMessage = "No default agents bundled.";
|
||||
else if (r.Copied == 0) StatusMessage = "All default agents already present.";
|
||||
else StatusMessage = $"Restored {r.Copied} default agent(s).";
|
||||
await _worker.RefreshAgentsAsync();
|
||||
}
|
||||
catch (Exception ex) { StatusMessage = $"Restore failed: {ex.Message}"; }
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
|
||||
[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}"; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||
|
||||
public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
||||
{
|
||||
[ObservableProperty] private string _defaultClaudeInstructions = "";
|
||||
[ObservableProperty] private string _defaultModel = "sonnet";
|
||||
[ObservableProperty] private int _defaultMaxTurns = 100;
|
||||
[ObservableProperty] private string _defaultPermissionMode = "auto";
|
||||
|
||||
public IReadOnlyList<string> Models { get; } = new[] { "opus", "sonnet", "haiku" };
|
||||
public IReadOnlyList<string> PermissionModes { get; } = new[]
|
||||
{ "auto", "bypassPermissions", "acceptEdits", "plan", "default" };
|
||||
|
||||
public string? Validate()
|
||||
{
|
||||
if (DefaultMaxTurns < 1 || DefaultMaxTurns > 200)
|
||||
return "Max turns must be between 1 and 200.";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||
|
||||
public sealed partial class PrimeClaudeTabViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IPrimeScheduleApi _api;
|
||||
private readonly HashSet<Guid> _initialIds = new();
|
||||
|
||||
public ObservableCollection<PrimeScheduleRowViewModel> Rows { get; } = new();
|
||||
|
||||
public PrimeClaudeTabViewModel(IPrimeScheduleApi api) => _api = api;
|
||||
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
Rows.Clear();
|
||||
_initialIds.Clear();
|
||||
var list = await _api.ListAsync();
|
||||
foreach (var dto in list)
|
||||
{
|
||||
Rows.Add(new PrimeScheduleRowViewModel(dto, isExisting: true));
|
||||
_initialIds.Add(dto.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public string? Validate()
|
||||
{
|
||||
foreach (var r in Rows)
|
||||
{
|
||||
if (r.StartDate > r.EndDate)
|
||||
return $"Schedule {r.TimeOfDay:hh\\:mm}: start date is after end date.";
|
||||
if (r.TimeOfDay < TimeSpan.Zero || r.TimeOfDay >= TimeSpan.FromDays(1))
|
||||
return "Time must be between 00:00 and 23:59.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task SaveAsync()
|
||||
{
|
||||
var keepIds = Rows.Select(r => r.Id).ToHashSet();
|
||||
foreach (var removed in _initialIds.Where(id => !keepIds.Contains(id)).ToList())
|
||||
await _api.DeleteAsync(removed);
|
||||
foreach (var r in Rows)
|
||||
await _api.UpsertAsync(r.ToDto());
|
||||
_initialIds.Clear();
|
||||
foreach (var id in keepIds) _initialIds.Add(id);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AddSchedule()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var dto = new PrimeScheduleDto(
|
||||
Id: Guid.NewGuid(),
|
||||
StartDate: today,
|
||||
EndDate: today.AddDays(30),
|
||||
TimeOfDay: new TimeSpan(7, 0, 0),
|
||||
WorkdaysOnly: true,
|
||||
Enabled: true,
|
||||
LastRunAt: null,
|
||||
PromptOverride: null);
|
||||
Rows.Add(new PrimeScheduleRowViewModel(dto, isExisting: false));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RemoveSchedule(PrimeScheduleRowViewModel? row)
|
||||
{
|
||||
if (row is null) return;
|
||||
Rows.Remove(row);
|
||||
}
|
||||
|
||||
public void ApplyFiredEvent(PrimeFiredEvent evt)
|
||||
{
|
||||
var row = Rows.FirstOrDefault(r => r.Id == evt.ScheduleId);
|
||||
if (row is null) return;
|
||||
if (evt.Success) row.LastRunAt = evt.FiredAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using ClaudeDo.Ui.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||
|
||||
public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
|
||||
{
|
||||
public Guid Id { get; }
|
||||
public bool IsExisting { get; }
|
||||
|
||||
[ObservableProperty] private bool _enabled;
|
||||
[ObservableProperty] private DateOnly _startDate;
|
||||
[ObservableProperty] private DateOnly _endDate;
|
||||
[ObservableProperty] private TimeSpan _timeOfDay;
|
||||
[ObservableProperty] private bool _workdaysOnly;
|
||||
[ObservableProperty] private DateTimeOffset? _lastRunAt;
|
||||
|
||||
public string LastRunLabel => LastRunAt is { } v ? v.LocalDateTime.ToString("g") : "—";
|
||||
|
||||
partial void OnLastRunAtChanged(DateTimeOffset? value) => OnPropertyChanged(nameof(LastRunLabel));
|
||||
|
||||
public PrimeScheduleRowViewModel(PrimeScheduleDto dto, bool isExisting)
|
||||
{
|
||||
Id = dto.Id;
|
||||
IsExisting = isExisting;
|
||||
Enabled = dto.Enabled;
|
||||
StartDate = dto.StartDate;
|
||||
EndDate = dto.EndDate;
|
||||
TimeOfDay = dto.TimeOfDay;
|
||||
WorkdaysOnly = dto.WorkdaysOnly;
|
||||
LastRunAt = dto.LastRunAt;
|
||||
}
|
||||
|
||||
public PrimeScheduleDto ToDto() =>
|
||||
new(Id, StartDate, EndDate, TimeOfDay, WorkdaysOnly, Enabled, LastRunAt, null);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.IO;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||
|
||||
public sealed partial class WorktreesSettingsTabViewModel : ViewModelBase
|
||||
{
|
||||
private readonly WorkerClient _worker;
|
||||
|
||||
[ObservableProperty] private string _worktreeStrategy = "sibling";
|
||||
[ObservableProperty] private string? _centralWorktreeRoot;
|
||||
[ObservableProperty] private bool _worktreeAutoCleanupEnabled;
|
||||
[ObservableProperty] private int _worktreeAutoCleanupDays = 7;
|
||||
|
||||
[ObservableProperty] private bool _showResetConfirm;
|
||||
[ObservableProperty] private string _statusMessage = "";
|
||||
[ObservableProperty] private bool _isBusy;
|
||||
|
||||
public IReadOnlyList<string> WorktreeStrategies { get; } = new[] { "sibling", "central" };
|
||||
|
||||
public WorktreesSettingsTabViewModel(WorkerClient worker) => _worker = worker;
|
||||
|
||||
public string? Validate()
|
||||
{
|
||||
if (WorktreeAutoCleanupEnabled && (WorktreeAutoCleanupDays < 1 || WorktreeAutoCleanupDays > 365))
|
||||
return "Cleanup days must be between 1 and 365.";
|
||||
if (WorktreeStrategy == "central")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(CentralWorktreeRoot))
|
||||
return "Central worktree root is required for Central strategy.";
|
||||
if (!Directory.Exists(CentralWorktreeRoot))
|
||||
return $"Directory not found: {CentralWorktreeRoot}";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task CleanupWorktrees()
|
||||
{
|
||||
IsBusy = true; StatusMessage = "";
|
||||
try
|
||||
{
|
||||
var r = await _worker.CleanupFinishedWorktreesAsync();
|
||||
StatusMessage = r is null ? "Worker offline." : $"Removed {r.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 r = await _worker.ResetAllWorktreesAsync();
|
||||
if (r is null) StatusMessage = "Worker offline.";
|
||||
else if (r.Blocked) StatusMessage = $"Cannot force-remove: {r.RunningTasks} task(s) still running. Cancel them first.";
|
||||
else StatusMessage = $"Removed {r.Removed} worktree(s) from {r.TasksAffected} task(s).";
|
||||
}
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
|
||||
x:Class="ClaudeDo.Ui.Views.Modals.SettingsModalView"
|
||||
x:DataType="vm:SettingsModalViewModel"
|
||||
x:CompileBindings="False"
|
||||
Title="Settings"
|
||||
Width="580" Height="760"
|
||||
SystemDecorations="None"
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
public class PrimeClaudeTabViewModelTests
|
||||
{
|
||||
private sealed class FakeApi : IPrimeScheduleApi
|
||||
{
|
||||
public List<PrimeScheduleDto> Stored { get; } = new();
|
||||
public List<PrimeScheduleDto> Upserts { get; } = new();
|
||||
public List<Guid> Deletes { get; } = new();
|
||||
public Task<List<PrimeScheduleDto>> ListAsync() => Task.FromResult(Stored.ToList());
|
||||
public Task<PrimeScheduleDto?> UpsertAsync(PrimeScheduleDto dto)
|
||||
{
|
||||
Upserts.Add(dto);
|
||||
return Task.FromResult<PrimeScheduleDto?>(dto);
|
||||
}
|
||||
public Task DeleteAsync(Guid id) { Deletes.Add(id); return Task.CompletedTask; }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Load_Populates_Rows()
|
||||
{
|
||||
var api = new FakeApi();
|
||||
api.Stored.Add(new PrimeScheduleDto(
|
||||
Guid.NewGuid(), new DateOnly(2026,5,1), new DateOnly(2026,5,31),
|
||||
new TimeSpan(7,0,0), true, true, null, null));
|
||||
var vm = new PrimeClaudeTabViewModel(api);
|
||||
await vm.LoadAsync();
|
||||
Assert.Single(vm.Rows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddSchedule_Appends_Row_With_Defaults()
|
||||
{
|
||||
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
||||
vm.AddScheduleCommand.Execute(null);
|
||||
Assert.Single(vm.Rows);
|
||||
Assert.True(vm.Rows[0].Enabled);
|
||||
Assert.True(vm.Rows[0].WorkdaysOnly);
|
||||
Assert.Equal(new TimeSpan(7,0,0), vm.Rows[0].TimeOfDay);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Save_Diffs_New_And_Removed_Rows()
|
||||
{
|
||||
var api = new FakeApi();
|
||||
var keptId = Guid.NewGuid();
|
||||
var deletedId = Guid.NewGuid();
|
||||
api.Stored.Add(new PrimeScheduleDto(keptId, new(2026,5,1), new(2026,5,31), new(7,0,0), true, true, null, null));
|
||||
api.Stored.Add(new PrimeScheduleDto(deletedId, new(2026,5,1), new(2026,5,31), new(8,0,0), true, true, null, null));
|
||||
|
||||
var vm = new PrimeClaudeTabViewModel(api);
|
||||
await vm.LoadAsync();
|
||||
vm.RemoveScheduleCommand.Execute(vm.Rows.Single(r => r.Id == deletedId));
|
||||
vm.AddScheduleCommand.Execute(null);
|
||||
|
||||
await vm.SaveAsync();
|
||||
|
||||
Assert.Contains(deletedId, api.Deletes);
|
||||
Assert.Equal(2, api.Upserts.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Reports_StartAfterEnd()
|
||||
{
|
||||
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
||||
vm.AddScheduleCommand.Execute(null);
|
||||
vm.Rows[0].StartDate = new DateOnly(2026, 6, 1);
|
||||
vm.Rows[0].EndDate = new DateOnly(2026, 5, 1);
|
||||
Assert.NotNull(vm.Validate());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user