Merge task branch for: UI: Gate-Schwellen im Settings-Modal (General)
This commit is contained in:
@@ -21,6 +21,10 @@
|
|||||||
"permission": "Berechtigung",
|
"permission": "Berechtigung",
|
||||||
"maxParallelExecutions": "Max. parallele Ausführungen",
|
"maxParallelExecutions": "Max. parallele Ausführungen",
|
||||||
"maxParallelExecutionsHint": "Wie viele Aufgaben aus der Warteschlange der Worker gleichzeitig ausführt.",
|
"maxParallelExecutionsHint": "Wie viele Aufgaben aus der Warteschlange der Worker gleichzeitig ausführt.",
|
||||||
|
"usageGateHeading": "Usage-Limit-Stopp",
|
||||||
|
"usageGateHint": "Ab dieser Auslastung holt die Queue keine neuen Tasks mehr; laufende Tasks laufen zu Ende. 0 schaltet den Stopp ab.",
|
||||||
|
"usageGateFiveHourPct": "5-Stunden-Fenster (%)",
|
||||||
|
"usageGateSevenDayPct": "7-Tage-Fenster (%)",
|
||||||
"reportExcludedPaths": "Bericht: ausgeschlossene Pfade (einer pro Zeile)",
|
"reportExcludedPaths": "Bericht: ausgeschlossene Pfade (einer pro Zeile)",
|
||||||
"standupWeekday": "Standup-Wochentag",
|
"standupWeekday": "Standup-Wochentag",
|
||||||
"weekdaySunday": "Sonntag",
|
"weekdaySunday": "Sonntag",
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
"permission": "Permission",
|
"permission": "Permission",
|
||||||
"maxParallelExecutions": "Max parallel executions",
|
"maxParallelExecutions": "Max parallel executions",
|
||||||
"maxParallelExecutionsHint": "How many queued tasks the worker runs at once.",
|
"maxParallelExecutionsHint": "How many queued tasks the worker runs at once.",
|
||||||
|
"usageGateHeading": "Usage limit stop",
|
||||||
|
"usageGateHint": "Once usage reaches this level, the queue stops picking up new tasks; running tasks finish normally. 0 turns the stop off.",
|
||||||
|
"usageGateFiveHourPct": "5-hour window (%)",
|
||||||
|
"usageGateSevenDayPct": "7-day window (%)",
|
||||||
"reportExcludedPaths": "Report: excluded paths (one per line)",
|
"reportExcludedPaths": "Report: excluded paths (one per line)",
|
||||||
"standupWeekday": "Standup weekday",
|
"standupWeekday": "Standup weekday",
|
||||||
"weekdaySunday": "Sunday",
|
"weekdaySunday": "Sunday",
|
||||||
|
|||||||
@@ -623,7 +623,9 @@ public sealed record AppSettingsDto(
|
|||||||
int StandupWeekday,
|
int StandupWeekday,
|
||||||
int DailyPrepMaxTasks,
|
int DailyPrepMaxTasks,
|
||||||
List<string>? SessionSkills = null,
|
List<string>? SessionSkills = null,
|
||||||
List<ModelPresetDto>? ModelPresets = null);
|
List<ModelPresetDto>? ModelPresets = null,
|
||||||
|
int UsageGateFiveHourPct = 80,
|
||||||
|
int UsageGateSevenDayPct = 90);
|
||||||
|
|
||||||
// Per-model run defaults (effort + turn budget) edited in Settings → General.
|
// Per-model run defaults (effort + turn budget) edited in Settings → General.
|
||||||
public sealed record ModelPresetDto(string Model, string Effort, int MaxTurns);
|
public sealed record ModelPresetDto(string Model, string Effort, int MaxTurns);
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private int _defaultMaxTurns = 100;
|
[ObservableProperty] private int _defaultMaxTurns = 100;
|
||||||
[ObservableProperty] private string _defaultPermissionMode = PermissionModeRegistry.DefaultMode;
|
[ObservableProperty] private string _defaultPermissionMode = PermissionModeRegistry.DefaultMode;
|
||||||
[ObservableProperty] private int _maxParallelExecutions = 1;
|
[ObservableProperty] private int _maxParallelExecutions = 1;
|
||||||
|
// Percentage of the 5h/7d Claude usage window at which the autonomous queue pauses. 0 = gate off.
|
||||||
|
[ObservableProperty] private int _usageGateFiveHourPct = 80;
|
||||||
|
[ObservableProperty] private int _usageGateSevenDayPct = 90;
|
||||||
// Newline-separated path prefixes excluded from the weekly report.
|
// Newline-separated path prefixes excluded from the weekly report.
|
||||||
[ObservableProperty] private string _reportExcludedPaths = @"C:\Private";
|
[ObservableProperty] private string _reportExcludedPaths = @"C:\Private";
|
||||||
// 0=Sunday..6=Saturday (System.DayOfWeek); default Wednesday.
|
// 0=Sunday..6=Saturday (System.DayOfWeek); default Wednesday.
|
||||||
@@ -91,6 +94,10 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
if (MaxParallelExecutions < 1 || MaxParallelExecutions > 20)
|
if (MaxParallelExecutions < 1 || MaxParallelExecutions > 20)
|
||||||
return "Max parallel executions must be between 1 and 20.";
|
return "Max parallel executions must be between 1 and 20.";
|
||||||
|
if (UsageGateFiveHourPct < 0 || UsageGateFiveHourPct > 100)
|
||||||
|
return "Usage gate (5h) must be between 0 and 100.";
|
||||||
|
if (UsageGateSevenDayPct < 0 || UsageGateSevenDayPct > 100)
|
||||||
|
return "Usage gate (7d) must be between 0 and 100.";
|
||||||
foreach (var row in ModelPresets)
|
foreach (var row in ModelPresets)
|
||||||
if (row.MaxTurns is < 1 or > 200)
|
if (row.MaxTurns is < 1 or > 200)
|
||||||
return $"Max turns for {row.Model} must be between 1 and 200.";
|
return $"Max turns for {row.Model} must be between 1 and 200.";
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
General.DefaultMaxTurns = dto.DefaultMaxTurns;
|
General.DefaultMaxTurns = dto.DefaultMaxTurns;
|
||||||
General.DefaultPermissionMode = dto.DefaultPermissionMode ?? "auto";
|
General.DefaultPermissionMode = dto.DefaultPermissionMode ?? "auto";
|
||||||
General.MaxParallelExecutions = dto.MaxParallelExecutions;
|
General.MaxParallelExecutions = dto.MaxParallelExecutions;
|
||||||
|
General.UsageGateFiveHourPct = dto.UsageGateFiveHourPct;
|
||||||
|
General.UsageGateSevenDayPct = dto.UsageGateSevenDayPct;
|
||||||
Worktrees.WorktreeStrategy = dto.WorktreeStrategy ?? "sibling";
|
Worktrees.WorktreeStrategy = dto.WorktreeStrategy ?? "sibling";
|
||||||
Worktrees.CentralWorktreeRoot = dto.CentralWorktreeRoot;
|
Worktrees.CentralWorktreeRoot = dto.CentralWorktreeRoot;
|
||||||
Worktrees.WorktreeAutoCleanupEnabled = dto.WorktreeAutoCleanupEnabled;
|
Worktrees.WorktreeAutoCleanupEnabled = dto.WorktreeAutoCleanupEnabled;
|
||||||
@@ -109,7 +111,9 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
General.StandupWeekday,
|
General.StandupWeekday,
|
||||||
Prime.DailyPrepMaxTasks,
|
Prime.DailyPrepMaxTasks,
|
||||||
General.SelectedSessionSkillNames(),
|
General.SelectedSessionSkillNames(),
|
||||||
General.ModelPresetDtos());
|
General.ModelPresetDtos(),
|
||||||
|
General.UsageGateFiveHourPct,
|
||||||
|
General.UsageGateSevenDayPct);
|
||||||
await _worker.UpdateAppSettingsAsync(dto);
|
await _worker.UpdateAppSettingsAsync(dto);
|
||||||
await Prime.SaveAsync();
|
await Prime.SaveAsync();
|
||||||
await OnlineInbox.SaveAsync();
|
await OnlineInbox.SaveAsync();
|
||||||
|
|||||||
@@ -149,6 +149,25 @@
|
|||||||
<TextBlock Text="{loc:Tr settings.general.maxParallelExecutionsHint}"
|
<TextBlock Text="{loc:Tr settings.general.maxParallelExecutionsHint}"
|
||||||
Opacity="0.6" FontSize="12"/>
|
Opacity="0.6" FontSize="12"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.usageGateHeading}"/>
|
||||||
|
<TextBlock Text="{loc:Tr settings.general.usageGateHint}"
|
||||||
|
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Classes="eyebrow" Text="{loc:Tr settings.general.usageGateFiveHourPct}"/>
|
||||||
|
<NumericUpDown Value="{Binding General.UsageGateFiveHourPct, Mode=TwoWay}"
|
||||||
|
Minimum="0" Maximum="100" Increment="5" FormatString="0"
|
||||||
|
HorizontalAlignment="Left" Width="140"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Classes="eyebrow" Text="{loc:Tr settings.general.usageGateSevenDayPct}"/>
|
||||||
|
<NumericUpDown Value="{Binding General.UsageGateSevenDayPct, Mode=TwoWay}"
|
||||||
|
Minimum="0" Maximum="100" Increment="5" FormatString="0"
|
||||||
|
HorizontalAlignment="Left" Width="140"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.reportExcludedPaths}"/>
|
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.reportExcludedPaths}"/>
|
||||||
<TextBox AcceptsReturn="True" MinHeight="60" Text="{Binding General.ReportExcludedPaths, Mode=TwoWay}"/>
|
<TextBox AcceptsReturn="True" MinHeight="60" Text="{Binding General.ReportExcludedPaths, Mode=TwoWay}"/>
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ public record AppSettingsDto(
|
|||||||
int StandupWeekday,
|
int StandupWeekday,
|
||||||
int DailyPrepMaxTasks,
|
int DailyPrepMaxTasks,
|
||||||
List<string>? SessionSkills = null,
|
List<string>? SessionSkills = null,
|
||||||
List<ModelPresetDto>? ModelPresets = null);
|
List<ModelPresetDto>? ModelPresets = null,
|
||||||
|
int UsageGateFiveHourPct = 80,
|
||||||
|
int UsageGateSevenDayPct = 90);
|
||||||
|
|
||||||
// Per-model run defaults (effort + turn budget) edited in Settings -> General.
|
// Per-model run defaults (effort + turn budget) edited in Settings -> General.
|
||||||
public record ModelPresetDto(string Model, string Effort, int MaxTurns);
|
public record ModelPresetDto(string Model, string Effort, int MaxTurns);
|
||||||
@@ -378,7 +380,9 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
row.DailyPrepMaxTasks,
|
row.DailyPrepMaxTasks,
|
||||||
SkillsFromJson(row.SessionSkills),
|
SkillsFromJson(row.SessionSkills),
|
||||||
Data.Models.ModelPresets.Parse(row.ModelPresets)
|
Data.Models.ModelPresets.Parse(row.ModelPresets)
|
||||||
.Select(p => new ModelPresetDto(p.Model, p.Effort, p.MaxTurns)).ToList());
|
.Select(p => new ModelPresetDto(p.Model, p.Effort, p.MaxTurns)).ToList(),
|
||||||
|
row.UsageGateFiveHourPct,
|
||||||
|
row.UsageGateSevenDayPct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateAppSettings(AppSettingsDto dto)
|
public async Task UpdateAppSettings(AppSettingsDto dto)
|
||||||
@@ -406,6 +410,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
? Data.Models.ModelPresets.Serialize(
|
? Data.Models.ModelPresets.Serialize(
|
||||||
dto.ModelPresets.Select(p => new ModelPreset(p.Model, p.Effort, p.MaxTurns)))
|
dto.ModelPresets.Select(p => new ModelPreset(p.Model, p.Effort, p.MaxTurns)))
|
||||||
: Data.Models.ModelPresets.SerializeDefaults(),
|
: Data.Models.ModelPresets.SerializeDefaults(),
|
||||||
|
UsageGateFiveHourPct = dto.UsageGateFiveHourPct,
|
||||||
|
UsageGateSevenDayPct = dto.UsageGateSevenDayPct,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,4 +45,30 @@ public class GeneralSettingsTabViewModelTests
|
|||||||
|
|
||||||
Assert.Equal(new List<string> { "a" }, vm.SelectedSessionSkillNames());
|
Assert.Equal(new List<string> { "a" }, vm.SelectedSessionSkillNames());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(-1, false)]
|
||||||
|
[InlineData(0, true)]
|
||||||
|
[InlineData(80, true)]
|
||||||
|
[InlineData(100, true)]
|
||||||
|
[InlineData(101, false)]
|
||||||
|
public void Validate_UsageGateFiveHourPct_bounds(int value, bool expectedValid)
|
||||||
|
{
|
||||||
|
var vm = new GeneralSettingsTabViewModel { UsageGateFiveHourPct = value };
|
||||||
|
|
||||||
|
Assert.Equal(expectedValid, vm.Validate() is null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(-1, false)]
|
||||||
|
[InlineData(0, true)]
|
||||||
|
[InlineData(80, true)]
|
||||||
|
[InlineData(100, true)]
|
||||||
|
[InlineData(101, false)]
|
||||||
|
public void Validate_UsageGateSevenDayPct_bounds(int value, bool expectedValid)
|
||||||
|
{
|
||||||
|
var vm = new GeneralSettingsTabViewModel { UsageGateSevenDayPct = value };
|
||||||
|
|
||||||
|
Assert.Equal(expectedValid, vm.Validate() is null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using ClaudeDo.Localization;
|
||||||
|
using ClaudeDo.Ui;
|
||||||
|
using ClaudeDo.Ui.Services;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||||
|
|
||||||
|
public class SettingsModalViewModelTests
|
||||||
|
{
|
||||||
|
private sealed class FakeWorker : StubWorkerClient
|
||||||
|
{
|
||||||
|
public AppSettingsDto? AppToReturn;
|
||||||
|
public AppSettingsDto? Saved;
|
||||||
|
public override Task<AppSettingsDto?> GetAppSettingsAsync() => Task.FromResult(AppToReturn);
|
||||||
|
public override Task UpdateAppSettingsAsync(AppSettingsDto dto) { Saved = dto; return Task.CompletedTask; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakePrimeApi : IPrimeScheduleApi
|
||||||
|
{
|
||||||
|
public Task<List<PrimeScheduleDto>> ListAsync() => Task.FromResult(new List<PrimeScheduleDto>());
|
||||||
|
public Task<PrimeScheduleDto?> UpsertAsync(PrimeScheduleDto dto) => Task.FromResult<PrimeScheduleDto?>(dto);
|
||||||
|
public Task DeleteAsync(Guid id) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Localizer MakeLocalizer()
|
||||||
|
{
|
||||||
|
var dir = Path.Combine(Path.GetTempPath(), "loc_" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
File.WriteAllText(Path.Combine(dir, "en.json"),
|
||||||
|
"""{ "metadata": { "code": "en", "name": "English" }, "settings": { "save": "Save" } }""");
|
||||||
|
File.WriteAllText(Path.Combine(dir, "de.json"),
|
||||||
|
"""{ "metadata": { "code": "de", "name": "Deutsch" }, "settings": { "save": "Speichern" } }""");
|
||||||
|
return new Localizer(LocaleStore.Load(dir), "en");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AppSettingsDto DtoWith(int fiveHourPct, int sevenDayPct) =>
|
||||||
|
new(DefaultClaudeInstructions: "", DefaultModel: "sonnet", DefaultMaxTurns: 30,
|
||||||
|
DefaultPermissionMode: "auto", MaxParallelExecutions: 1, WorktreeStrategy: "sibling",
|
||||||
|
CentralWorktreeRoot: null, WorktreeAutoCleanupEnabled: false, WorktreeAutoCleanupDays: 7,
|
||||||
|
ReportExcludedPaths: null, StandupWeekday: 3, DailyPrepMaxTasks: 5,
|
||||||
|
SessionSkills: null, ModelPresets: null,
|
||||||
|
UsageGateFiveHourPct: fiveHourPct, UsageGateSevenDayPct: sevenDayPct);
|
||||||
|
|
||||||
|
private static SettingsModalViewModel MakeVm(FakeWorker worker) =>
|
||||||
|
new(worker, new PrimeClaudeTabViewModel(new FakePrimeApi()), new OnlineLoginService(),
|
||||||
|
MakeLocalizer(), new AppSettings());
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task LoadAsync_fills_usage_gate_fields_from_dto()
|
||||||
|
{
|
||||||
|
var worker = new FakeWorker { AppToReturn = DtoWith(65, 95) };
|
||||||
|
var vm = MakeVm(worker);
|
||||||
|
|
||||||
|
await vm.LoadAsync();
|
||||||
|
|
||||||
|
Assert.Equal(65, vm.General.UsageGateFiveHourPct);
|
||||||
|
Assert.Equal(95, vm.General.UsageGateSevenDayPct);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Save_sends_usage_gate_fields_in_dto()
|
||||||
|
{
|
||||||
|
var worker = new FakeWorker();
|
||||||
|
var vm = MakeVm(worker);
|
||||||
|
vm.General.UsageGateFiveHourPct = 42;
|
||||||
|
vm.General.UsageGateSevenDayPct = 77;
|
||||||
|
|
||||||
|
await vm.SaveCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
Assert.NotNull(worker.Saved);
|
||||||
|
Assert.Equal(42, worker.Saved!.UsageGateFiveHourPct);
|
||||||
|
Assert.Equal(77, worker.Saved.UsageGateSevenDayPct);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user