feat(prime): add the action kind to the settings viewmodels

This commit is contained in:
mika kuns
2026-08-25 09:01:33 +02:00
parent 9ab17d827b
commit 57be4961a8
4 changed files with 176 additions and 6 deletions
+4 -1
View File
@@ -1,3 +1,5 @@
using ClaudeDo.Data.Models;
namespace ClaudeDo.Ui.Services; namespace ClaudeDo.Ui.Services;
public sealed record PrimeScheduleDto( public sealed record PrimeScheduleDto(
@@ -6,7 +8,8 @@ public sealed record PrimeScheduleDto(
TimeSpan TimeOfDay, TimeSpan TimeOfDay,
bool Enabled, bool Enabled,
DateTimeOffset? LastRunAt, DateTimeOffset? LastRunAt,
string? PromptOverride); string? PromptOverride,
PrimeActionKind Kind);
public sealed record PrimeFiredEvent( public sealed record PrimeFiredEvent(
Guid ScheduleId, Guid ScheduleId,
@@ -1,4 +1,7 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@@ -11,13 +14,36 @@ public sealed partial class PrimeClaudeTabViewModel : ViewModelBase
private readonly HashSet<Guid> _initialIds = new(); private readonly HashSet<Guid> _initialIds = new();
[ObservableProperty] private int _dailyPrepMaxTasks = 5; [ObservableProperty] private int _dailyPrepMaxTasks = 5;
[ObservableProperty] private bool _anyFillMyDay;
public ObservableCollection<PrimeScheduleRowViewModel> Rows { get; } = new(); public ObservableCollection<PrimeScheduleRowViewModel> Rows { get; } = new();
public PrimeClaudeTabViewModel(IPrimeScheduleApi api) => _api = api; public PrimeClaudeTabViewModel(IPrimeScheduleApi api)
{
_api = api;
Rows.CollectionChanged += OnRowsChanged;
}
private void OnRowsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.OldItems is not null)
foreach (PrimeScheduleRowViewModel r in e.OldItems) r.PropertyChanged -= OnRowChanged;
if (e.NewItems is not null)
foreach (PrimeScheduleRowViewModel r in e.NewItems) r.PropertyChanged += OnRowChanged;
UpdateAnyFillMyDay();
}
private void OnRowChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(PrimeScheduleRowViewModel.Kind)) UpdateAnyFillMyDay();
}
private void UpdateAnyFillMyDay() =>
AnyFillMyDay = Rows.Any(r => r.Kind == PrimeActionKind.FillMyDay);
public async Task LoadAsync() public async Task LoadAsync()
{ {
foreach (var r in Rows) r.PropertyChanged -= OnRowChanged;
Rows.Clear(); Rows.Clear();
_initialIds.Clear(); _initialIds.Clear();
var list = await _api.ListAsync(); var list = await _api.ListAsync();
@@ -36,6 +62,8 @@ public sealed partial class PrimeClaudeTabViewModel : ViewModelBase
return $"Schedule {r.TimeOfDay:hh\\:mm}: select at least one day."; return $"Schedule {r.TimeOfDay:hh\\:mm}: select at least one day.";
if (r.TimeOfDay < TimeSpan.Zero || r.TimeOfDay >= TimeSpan.FromDays(1)) if (r.TimeOfDay < TimeSpan.Zero || r.TimeOfDay >= TimeSpan.FromDays(1))
return "Time must be between 00:00 and 23:59."; return "Time must be between 00:00 and 23:59.";
if (r.Kind == PrimeActionKind.Custom && string.IsNullOrWhiteSpace(r.PromptOverride))
return $"Schedule {r.TimeOfDay:hh\\:mm}: a custom action needs a prompt.";
} }
return null; return null;
} }
@@ -60,7 +88,8 @@ public sealed partial class PrimeClaudeTabViewModel : ViewModelBase
TimeOfDay: new TimeSpan(7, 0, 0), TimeOfDay: new TimeSpan(7, 0, 0),
Enabled: true, Enabled: true,
LastRunAt: null, LastRunAt: null,
PromptOverride: null); PromptOverride: null,
Kind: PrimeActionKind.Ping);
Rows.Add(new PrimeScheduleRowViewModel(dto, isExisting: false)); Rows.Add(new PrimeScheduleRowViewModel(dto, isExisting: false));
} }
@@ -1,4 +1,6 @@
using System.Globalization; using System.Globalization;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
@@ -22,6 +24,7 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
[ObservableProperty] private TimeSpan _timeOfDay; [ObservableProperty] private TimeSpan _timeOfDay;
[ObservableProperty] private DateTimeOffset? _lastRunAt; [ObservableProperty] private DateTimeOffset? _lastRunAt;
[ObservableProperty] private string? _promptOverride; [ObservableProperty] private string? _promptOverride;
[ObservableProperty] private PrimeActionKind _kind;
public string LastRunLabel => LastRunAt is { } v ? v.LocalDateTime.ToString("g") : "—"; public string LastRunLabel => LastRunAt is { } v ? v.LocalDateTime.ToString("g") : "—";
@@ -41,6 +44,50 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
} }
} }
/// <summary>Per-row group name. A shared GroupName would make every schedule in the
/// ItemsControl share one selection.</summary>
public string RadioGroup => $"prime-kind-{Id}";
// Three booleans rather than an enum-to-bool converter: RadioButton.IsChecked binds
// TwoWay to a bool, and compiled bindings stay simple.
public bool IsPing
{
get => Kind == PrimeActionKind.Ping;
set { if (value) Kind = PrimeActionKind.Ping; }
}
public bool IsFillMyDay
{
get => Kind == PrimeActionKind.FillMyDay;
set { if (value) Kind = PrimeActionKind.FillMyDay; }
}
public bool IsCustom
{
get => Kind == PrimeActionKind.Custom;
set { if (value) Kind = PrimeActionKind.Custom; }
}
public bool ShowPrompt => Kind != PrimeActionKind.Ping;
public string PromptLabel => Kind == PrimeActionKind.Custom
? Loc.T("settings.prime.customPromptLabel")
: Loc.T("settings.prime.promptOverrideLabel");
public string PromptPlaceholder => Kind == PrimeActionKind.Custom
? Loc.T("settings.prime.customPromptPlaceholder")
: Loc.T("settings.prime.promptOverridePlaceholder");
partial void OnKindChanged(PrimeActionKind value)
{
OnPropertyChanged(nameof(IsPing));
OnPropertyChanged(nameof(IsFillMyDay));
OnPropertyChanged(nameof(IsCustom));
OnPropertyChanged(nameof(ShowPrompt));
OnPropertyChanged(nameof(PromptLabel));
OnPropertyChanged(nameof(PromptPlaceholder));
}
partial void OnLastRunAtChanged(DateTimeOffset? value) => OnPropertyChanged(nameof(LastRunLabel)); partial void OnLastRunAtChanged(DateTimeOffset? value) => OnPropertyChanged(nameof(LastRunLabel));
partial void OnTimeOfDayChanged(TimeSpan value) => OnPropertyChanged(nameof(TimeText)); partial void OnTimeOfDayChanged(TimeSpan value) => OnPropertyChanged(nameof(TimeText));
@@ -60,6 +107,7 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
TimeOfDay = dto.TimeOfDay; TimeOfDay = dto.TimeOfDay;
LastRunAt = dto.LastRunAt; LastRunAt = dto.LastRunAt;
PromptOverride = dto.PromptOverride; PromptOverride = dto.PromptOverride;
Kind = dto.Kind;
} }
public int DaysMask() public int DaysMask()
@@ -77,5 +125,5 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
public PrimeScheduleDto ToDto() => public PrimeScheduleDto ToDto() =>
new(Id, DaysMask(), TimeOfDay, Enabled, LastRunAt, new(Id, DaysMask(), TimeOfDay, Enabled, LastRunAt,
string.IsNullOrWhiteSpace(PromptOverride) ? null : PromptOverride); string.IsNullOrWhiteSpace(PromptOverride) ? null : PromptOverride, Kind);
} }
@@ -1,3 +1,4 @@
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Modals.Settings; using ClaudeDo.Ui.ViewModels.Modals.Settings;
@@ -27,8 +28,9 @@ public class PrimeClaudeTabViewModelTests
public Task DeleteAsync(Guid id) => Task.CompletedTask; public Task DeleteAsync(Guid id) => Task.CompletedTask;
} }
private static PrimeScheduleDto Dto(Guid id, int days, TimeSpan time) => private static PrimeScheduleDto Dto(Guid id, int days, TimeSpan time,
new(id, days, time, true, null, null); PrimeActionKind kind = PrimeActionKind.FillMyDay, string? prompt = null) =>
new(id, days, time, true, null, prompt, kind);
[Fact] [Fact]
public async Task Load_Populates_Rows() public async Task Load_Populates_Rows()
@@ -116,4 +118,92 @@ public class PrimeClaudeTabViewModelTests
var ex = await Assert.ThrowsAsync<Exception>(() => vm.SaveAsync()); var ex = await Assert.ThrowsAsync<Exception>(() => vm.SaveAsync());
Assert.Equal(api.ExceptionMessage, ex.Message); Assert.Equal(api.ExceptionMessage, ex.Message);
} }
[Fact]
public void AddSchedule_Defaults_To_Ping()
{
var vm = new PrimeClaudeTabViewModel(new FakeApi());
vm.AddScheduleCommand.Execute(null);
Assert.Equal(PrimeActionKind.Ping, vm.Rows[0].Kind);
Assert.True(vm.Rows[0].IsPing);
Assert.False(vm.Rows[0].IsFillMyDay);
Assert.False(vm.Rows[0].IsCustom);
}
[Fact]
public void Setting_IsCustom_Flips_Kind_And_Clears_The_Others()
{
var vm = new PrimeClaudeTabViewModel(new FakeApi());
vm.AddScheduleCommand.Execute(null);
var row = vm.Rows[0];
row.IsCustom = true;
Assert.Equal(PrimeActionKind.Custom, row.Kind);
Assert.False(row.IsPing);
Assert.False(row.IsFillMyDay);
}
[Fact]
public void Row_RadioGroup_Is_Unique_Per_Row()
{
var vm = new PrimeClaudeTabViewModel(new FakeApi());
vm.AddScheduleCommand.Execute(null);
vm.AddScheduleCommand.Execute(null);
Assert.NotEqual(vm.Rows[0].RadioGroup, vm.Rows[1].RadioGroup);
}
[Fact]
public void Validate_Rejects_Custom_Without_A_Prompt()
{
var vm = new PrimeClaudeTabViewModel(new FakeApi());
vm.AddScheduleCommand.Execute(null);
vm.Rows[0].IsCustom = true;
vm.Rows[0].PromptOverride = " ";
Assert.NotNull(vm.Validate());
}
[Fact]
public void Validate_Accepts_Custom_With_A_Prompt()
{
var vm = new PrimeClaudeTabViewModel(new FakeApi());
vm.AddScheduleCommand.Execute(null);
vm.Rows[0].IsCustom = true;
vm.Rows[0].PromptOverride = "queue the oldest task";
Assert.Null(vm.Validate());
}
[Fact]
public void AnyFillMyDay_Tracks_Kind_And_Row_Changes()
{
var vm = new PrimeClaudeTabViewModel(new FakeApi());
Assert.False(vm.AnyFillMyDay);
vm.AddScheduleCommand.Execute(null);
Assert.False(vm.AnyFillMyDay);
vm.Rows[0].IsFillMyDay = true;
Assert.True(vm.AnyFillMyDay);
vm.RemoveScheduleCommand.Execute(vm.Rows[0]);
Assert.False(vm.AnyFillMyDay);
}
[Fact]
public async Task Save_Round_Trips_The_Kind()
{
var api = new FakeApi();
var vm = new PrimeClaudeTabViewModel(api);
vm.AddScheduleCommand.Execute(null);
vm.Rows[0].IsCustom = true;
vm.Rows[0].PromptOverride = "do the thing";
await vm.SaveAsync();
Assert.Equal(PrimeActionKind.Custom, api.Upserts[0].Kind);
}
} }