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",
|
"categoryGeneral": "Allgemein",
|
||||||
"categoryExecution": "Ausführung",
|
"categoryExecution": "Ausführung",
|
||||||
"categoryPrime": "Prime Claude",
|
"categoryPrime": "Prime Claude",
|
||||||
|
"categoryPrimeHint": "Geplante Claude-Läufe: Ping, My Day füllen oder ein eigener Prompt, zu festen Zeiten.",
|
||||||
"categoryWorktrees": "Worktrees",
|
"categoryWorktrees": "Worktrees",
|
||||||
"categoryFiles": "Dateien & Prompts",
|
"categoryFiles": "Dateien & Prompts",
|
||||||
"categorySkills": "Session Skills",
|
"categorySkills": "Session Skills",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"model": "Modell",
|
"model": "Modell",
|
||||||
"maxTurns": "Max. Durchläufe",
|
"maxTurns": "Max. Durchläufe",
|
||||||
"effort": "Effort",
|
"effort": "Effort",
|
||||||
|
"effortHint": "Denkaufwand, den das Modell anwendet. Höher kostet mehr und denkt tiefer.",
|
||||||
"modelPresets": "Vorgaben pro Modell",
|
"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.",
|
"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",
|
"maxTurnsCeiling": "Max. Durchläufe – Obergrenze",
|
||||||
@@ -50,6 +52,7 @@
|
|||||||
"powerUseWindowPlaceholder": "HH:mm",
|
"powerUseWindowPlaceholder": "HH:mm",
|
||||||
"powerUseHandoffPct": "Übergabe bei (% von 5h)",
|
"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.",
|
"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",
|
"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.",
|
"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}%",
|
"throttleStagesValues": "5h: soft {0}% / hard {1}% · 7d: soft {2}% / hard {3}%",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"categoryGeneral": "General",
|
"categoryGeneral": "General",
|
||||||
"categoryExecution": "Execution",
|
"categoryExecution": "Execution",
|
||||||
"categoryPrime": "Prime Claude",
|
"categoryPrime": "Prime Claude",
|
||||||
|
"categoryPrimeHint": "Scheduled Claude runs: Ping, Fill My Day, or a custom prompt, at fixed times.",
|
||||||
"categoryWorktrees": "Worktrees",
|
"categoryWorktrees": "Worktrees",
|
||||||
"categoryFiles": "Files & Prompts",
|
"categoryFiles": "Files & Prompts",
|
||||||
"categorySkills": "Session Skills",
|
"categorySkills": "Session Skills",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"model": "Model",
|
"model": "Model",
|
||||||
"maxTurns": "Max turns",
|
"maxTurns": "Max turns",
|
||||||
"effort": "Effort",
|
"effort": "Effort",
|
||||||
|
"effortHint": "Reasoning effort the model applies. Higher costs more and reasons deeper.",
|
||||||
"modelPresets": "Per-model defaults",
|
"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.",
|
"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",
|
"maxTurnsCeiling": "Max turns ceiling",
|
||||||
@@ -50,6 +52,7 @@
|
|||||||
"powerUseWindowPlaceholder": "HH:mm",
|
"powerUseWindowPlaceholder": "HH:mm",
|
||||||
"powerUseHandoffPct": "Hand off at (% of 5h)",
|
"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.",
|
"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",
|
"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.",
|
"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}%",
|
"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)
|
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.";
|
||||||
|
if (!IsValidWindowBound(PowerUseWindowStart) || !IsValidWindowBound(PowerUseWindowEnd))
|
||||||
|
return Loc.T("settings.general.powerUseWindowInvalid");
|
||||||
return null;
|
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>
|
/// <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"/>
|
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
||||||
<Grid ColumnDefinitions="90,12,*,12,110" Margin="0,4,0,0">
|
<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="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}"/>
|
<TextBlock Grid.Column="4" Classes="eyebrow" Text="{loc:Tr settings.general.maxTurns}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<ItemsControl ItemsSource="{Binding General.ModelPresets}">
|
<ItemsControl ItemsSource="{Binding General.ModelPresets}">
|
||||||
@@ -307,7 +308,8 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<TabItem Header="{loc:Tr settings.sidebar.categoryPrime}">
|
<TabItem Header="{loc:Tr settings.sidebar.categoryPrime}"
|
||||||
|
ToolTip.Tip="{loc:Tr settings.sidebar.categoryPrimeHint}">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Spacing="12" Margin="0,8,0,0">
|
<StackPanel Spacing="12" Margin="0,8,0,0">
|
||||||
<Border IsVisible="{Binding !HasLinkedRepo}"
|
<Border IsVisible="{Binding !HasLinkedRepo}"
|
||||||
|
|||||||
@@ -30,4 +30,31 @@ public class GeneralSettingsTabViewModelTests
|
|||||||
|
|
||||||
Assert.Equal(expectedValid, vm.Validate() is null);
|
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("18:00", vm.General.PowerUseWindowStart);
|
||||||
Assert.Equal(88, vm.General.PowerUseHandoffPct);
|
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";
|
vm.General.PowerUseWindowEnd = "not a time";
|
||||||
await vm.SaveCommand.ExecuteAsync(null);
|
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.True(worker.Saved!.PowerUseEnabled);
|
||||||
Assert.Equal("18:00", worker.Saved.PowerUseWindowStart);
|
Assert.Equal("18:00", worker.Saved.PowerUseWindowStart);
|
||||||
Assert.Null(worker.Saved.PowerUseWindowEnd);
|
Assert.Null(worker.Saved.PowerUseWindowEnd);
|
||||||
|
|||||||
Reference in New Issue
Block a user