fix(claude-do): merge fix(ui): Settings-Politur — PowerUse-Zeitfenster validieren,
ClaudeDo-Task: f3f0a355-936c-4933-8236-d41c21fb73dc
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
"categoryGeneral": "Allgemein",
|
||||
"categoryExecution": "Ausführung",
|
||||
"categoryPrime": "Prime Claude",
|
||||
"categoryPrimeHint": "Geplante Claude-Läufe: Ping, My Day füllen oder ein eigener Prompt, zu festen Zeiten.",
|
||||
"categoryWorktrees": "Worktrees",
|
||||
"categoryFiles": "Dateien & Prompts",
|
||||
"categorySkills": "Session Skills",
|
||||
@@ -27,6 +28,7 @@
|
||||
"model": "Modell",
|
||||
"maxTurns": "Max. Durchläufe",
|
||||
"effort": "Effort",
|
||||
"effortHint": "Denkaufwand, den das Modell anwendet. Höher kostet mehr und denkt tiefer.",
|
||||
"modelPresets": "Vorgaben pro Modell",
|
||||
"modelPresetsHint": "Effort und Durchlauf-Budget, mit denen eine Sitzung unter diesem Modell läuft. Overrides auf Listen- und Aufgabenebene gewinnen weiterhin.",
|
||||
"maxTurnsCeiling": "Max. Durchläufe – Obergrenze",
|
||||
@@ -50,6 +52,7 @@
|
||||
"powerUseWindowPlaceholder": "HH:mm",
|
||||
"powerUseHandoffPct": "Übergabe bei (% von 5h)",
|
||||
"powerUseWindowHint": "Beide Zeiten leer = rund um die Uhr. Das Fenster wird bei jedem Start einer Handler-Phase geprüft — eine Kette, die über das Ende hinausläuft, macht ohne PowerUse weiter.",
|
||||
"powerUseWindowInvalid": "PowerUse-Zeiten müssen im Format HH:mm sein oder leer bleiben.",
|
||||
"throttleStagesHeading": "Throttle-Stufen",
|
||||
"throttleStagesHint": "Die Parallelität wird vor dem harten Gate begrenzt: soft auf 2 Slots, hard auf 1. Eingestellt wird das durch Ziehen der Marker an den Gauges im Usage-Monitor. Der Monitor schließt diese Einstellungen — vorher speichern, wenn du etwas geändert hast.",
|
||||
"throttleStagesValues": "5h: soft {0}% / hard {1}% · 7d: soft {2}% / hard {3}%",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"categoryGeneral": "General",
|
||||
"categoryExecution": "Execution",
|
||||
"categoryPrime": "Prime Claude",
|
||||
"categoryPrimeHint": "Scheduled Claude runs: Ping, Fill My Day, or a custom prompt, at fixed times.",
|
||||
"categoryWorktrees": "Worktrees",
|
||||
"categoryFiles": "Files & Prompts",
|
||||
"categorySkills": "Session Skills",
|
||||
@@ -27,6 +28,7 @@
|
||||
"model": "Model",
|
||||
"maxTurns": "Max turns",
|
||||
"effort": "Effort",
|
||||
"effortHint": "Reasoning effort the model applies. Higher costs more and reasons deeper.",
|
||||
"modelPresets": "Per-model defaults",
|
||||
"modelPresetsHint": "Reasoning effort and turn budget used when a session runs under that model. List- and task-level max-turn overrides still win.",
|
||||
"maxTurnsCeiling": "Max turns ceiling",
|
||||
@@ -50,6 +52,7 @@
|
||||
"powerUseWindowPlaceholder": "HH:mm",
|
||||
"powerUseHandoffPct": "Hand off at (% of 5h)",
|
||||
"powerUseWindowHint": "Leave both times empty for round the clock. A window is checked whenever a handler phase starts, so a chain that runs past the end time continues without PowerUse.",
|
||||
"powerUseWindowInvalid": "PowerUse window times must be HH:mm, or left blank.",
|
||||
"throttleStagesHeading": "Throttle stages",
|
||||
"throttleStagesHint": "Parallelism is capped before the hard gate: soft caps at 2 slots, hard at 1. Edited by dragging the markers on the usage-monitor gauges. Opening the monitor closes these settings — save first if you changed anything.",
|
||||
"throttleStagesValues": "5h: soft {0}% / hard {1}% · 7d: soft {2}% / hard {3}%",
|
||||
|
||||
@@ -119,8 +119,13 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
||||
foreach (var row in ModelPresets)
|
||||
if (row.MaxTurns is < 1 or > 200)
|
||||
return $"Max turns for {row.Model} must be between 1 and 200.";
|
||||
if (!IsValidWindowBound(PowerUseWindowStart) || !IsValidWindowBound(PowerUseWindowEnd))
|
||||
return Loc.T("settings.general.powerUseWindowInvalid");
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsValidWindowBound(string? value)
|
||||
=> string.IsNullOrWhiteSpace(value) || PowerUseWindow.Parse(value) is not null;
|
||||
}
|
||||
|
||||
/// <summary>One row of the per-model preset table. Single consumer, so it lives here.</summary>
|
||||
|
||||
@@ -190,7 +190,8 @@
|
||||
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
||||
<Grid ColumnDefinitions="90,12,*,12,110" Margin="0,4,0,0">
|
||||
<TextBlock Grid.Column="0" Classes="eyebrow" Text="{loc:Tr settings.general.model}"/>
|
||||
<TextBlock Grid.Column="2" Classes="eyebrow" Text="{loc:Tr settings.general.effort}"/>
|
||||
<TextBlock Grid.Column="2" Classes="eyebrow" Text="{loc:Tr settings.general.effort}"
|
||||
ToolTip.Tip="{loc:Tr settings.general.effortHint}"/>
|
||||
<TextBlock Grid.Column="4" Classes="eyebrow" Text="{loc:Tr settings.general.maxTurns}"/>
|
||||
</Grid>
|
||||
<ItemsControl ItemsSource="{Binding General.ModelPresets}">
|
||||
@@ -307,7 +308,8 @@
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="{loc:Tr settings.sidebar.categoryPrime}">
|
||||
<TabItem Header="{loc:Tr settings.sidebar.categoryPrime}"
|
||||
ToolTip.Tip="{loc:Tr settings.sidebar.categoryPrimeHint}">
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="12" Margin="0,8,0,0">
|
||||
<Border IsVisible="{Binding !HasLinkedRepo}"
|
||||
|
||||
@@ -30,4 +30,31 @@ public class GeneralSettingsTabViewModelTests
|
||||
|
||||
Assert.Equal(expectedValid, vm.Validate() is null);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, true)]
|
||||
[InlineData("", true)]
|
||||
[InlineData("09:00", true)]
|
||||
[InlineData("23:59", true)]
|
||||
[InlineData("9:0", false)]
|
||||
[InlineData("abc", false)]
|
||||
[InlineData("25:00", false)]
|
||||
public void Validate_PowerUseWindowStart_rejectsUnparseable(string? value, bool expectedValid)
|
||||
{
|
||||
var vm = new GeneralSettingsTabViewModel { PowerUseWindowStart = value };
|
||||
|
||||
Assert.Equal(expectedValid, vm.Validate() is null);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, true)]
|
||||
[InlineData("09:00", true)]
|
||||
[InlineData("9:0", false)]
|
||||
[InlineData("abc", false)]
|
||||
public void Validate_PowerUseWindowEnd_rejectsUnparseable(string? value, bool expectedValid)
|
||||
{
|
||||
var vm = new GeneralSettingsTabViewModel { PowerUseWindowEnd = value };
|
||||
|
||||
Assert.Equal(expectedValid, vm.Validate() is null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,10 +153,17 @@ public class SettingsModalViewModelTests : IDisposable
|
||||
Assert.Equal("18:00", vm.General.PowerUseWindowStart);
|
||||
Assert.Equal(88, vm.General.PowerUseHandoffPct);
|
||||
|
||||
// A typo in the window is normalized to null on save, not stored as a fake window.
|
||||
// A typo in the window blocks Save instead of silently becoming "round the clock".
|
||||
vm.General.PowerUseWindowEnd = "not a time";
|
||||
await vm.SaveCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.Null(worker.Saved);
|
||||
Assert.NotEqual("", vm.ValidationError);
|
||||
|
||||
// Clearing it back out is still a valid "round the clock" window and saves.
|
||||
vm.General.PowerUseWindowEnd = "";
|
||||
await vm.SaveCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.True(worker.Saved!.PowerUseEnabled);
|
||||
Assert.Equal("18:00", worker.Saved.PowerUseWindowStart);
|
||||
Assert.Null(worker.Saved.PowerUseWindowEnd);
|
||||
|
||||
Reference in New Issue
Block a user