feat(settings): per-model effort and turn presets
ClaudeDo never passed --effort, so every session inherited whatever effortLevel the user's Claude Code config happened to carry. Settings -> General now holds one row per model alias (haiku medium/20, sonnet high/30, opus high/40, fable high/25) supplying the global effort and turn defaults; list- and task-level max-turns overrides still win, and the agent editor's inherited badge follows the model. --effort is applied to autonomous runs and to every ConPTY spec (task session, planning start/resume, ad-hoc, list handler). The model itself is deliberately not forced on interactive sessions. The single global 'Max turns' field is replaced by the table, and 'fable' joins ModelRegistry.Aliases. The migration also adds the is_manual columns used by the next commit.
This commit is contained in:
@@ -607,7 +607,11 @@ public sealed record AppSettingsDto(
|
||||
string? ReportExcludedPaths,
|
||||
int StandupWeekday,
|
||||
int DailyPrepMaxTasks,
|
||||
List<string>? SessionSkills = null);
|
||||
List<string>? SessionSkills = null,
|
||||
List<ModelPresetDto>? ModelPresets = null);
|
||||
|
||||
// Per-model run defaults (effort + turn budget) edited in Settings → General.
|
||||
public sealed record ModelPresetDto(string Model, string Effort, int MaxTurns);
|
||||
|
||||
public sealed record SessionSkillDto(
|
||||
string Name,
|
||||
|
||||
@@ -47,7 +47,11 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
||||
[ObservableProperty] private string _effectiveSystemPromptHint = "";
|
||||
|
||||
private string _globalModel = ModelRegistry.DefaultAlias;
|
||||
private int _globalMaxTurns = 100;
|
||||
// The global max-turns default is per-model (Settings -> General), so it moves with whichever
|
||||
// model actually ends up in effect here.
|
||||
private IReadOnlyList<ModelPreset> _presets = ModelPresets.Defaults;
|
||||
private string EffectiveModel => Model ?? _listModel ?? _globalModel;
|
||||
private int GlobalMaxTurns => ModelPresets.For(_presets, EffectiveModel).MaxTurns;
|
||||
private string? _listModel; // Task scope only
|
||||
private int? _listMaxTurns; // Task scope only
|
||||
private string? _listAgentName; // Task scope only
|
||||
@@ -56,7 +60,7 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
||||
private CancellationTokenSource? _saveCts;
|
||||
|
||||
public int EffectiveMaxTurns =>
|
||||
MaxTurns is decimal t ? (int)t : (_listMaxTurns ?? _globalMaxTurns);
|
||||
MaxTurns is decimal t ? (int)t : (_listMaxTurns ?? GlobalMaxTurns);
|
||||
|
||||
public ObservableCollection<string> ModelOptions { get; } = new(ModelRegistry.Aliases);
|
||||
public ObservableCollection<AgentInfo> Agents { get; } = new();
|
||||
@@ -97,7 +101,14 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
||||
foreach (var s in SessionSkills) s.PropertyChanged -= OnSkillItemPropertyChanged;
|
||||
}
|
||||
|
||||
partial void OnModelChanged(string? value) { RecomputeModelBadge(); QueueSave(); }
|
||||
partial void OnModelChanged(string? value)
|
||||
{
|
||||
RecomputeModelBadge();
|
||||
// A different model means a different global turn default.
|
||||
RecomputeTurnsBadge();
|
||||
OnPropertyChanged(nameof(EffectiveMaxTurns));
|
||||
QueueSave();
|
||||
}
|
||||
|
||||
partial void OnMaxTurnsChanged(decimal? value)
|
||||
{
|
||||
@@ -130,8 +141,8 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
||||
{
|
||||
var own = MaxTurns?.ToString();
|
||||
var (value, source) = _scope == AgentConfigScope.Task
|
||||
? InheritanceResolver.Resolve(own, _listMaxTurns?.ToString(), _globalMaxTurns.ToString())
|
||||
: InheritanceResolver.ResolveList(own, _globalMaxTurns.ToString());
|
||||
? InheritanceResolver.Resolve(own, _listMaxTurns?.ToString(), GlobalMaxTurns.ToString())
|
||||
: InheritanceResolver.ResolveList(own, GlobalMaxTurns.ToString());
|
||||
TurnsInheritedHint = value;
|
||||
TurnsBadge = BadgeFor(source, MaxTurns is not null);
|
||||
}
|
||||
@@ -284,7 +295,9 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
||||
{
|
||||
var app = await _worker.GetAppSettingsAsync();
|
||||
_globalModel = app?.DefaultModel ?? ModelRegistry.DefaultAlias;
|
||||
_globalMaxTurns = app?.DefaultMaxTurns ?? 100;
|
||||
_presets = app?.ModelPresets is { Count: > 0 } rows
|
||||
? rows.Select(r => new ModelPreset(r.Model, r.Effort, r.MaxTurns)).ToList()
|
||||
: ModelPresets.Defaults;
|
||||
}
|
||||
|
||||
private void ApplyConfig(string? model, int? maxTurns, string? systemPrompt, string? agentPath)
|
||||
|
||||
@@ -27,6 +27,23 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
||||
|
||||
public ObservableCollection<SelectableSkillViewModel> SessionSkills { get; } = new();
|
||||
|
||||
/// <summary>One editable row per model alias: the effort and turn budget a run gets under that
|
||||
/// model. Supplies the global defaults; list- and task-level max-turns overrides still win.</summary>
|
||||
public ObservableCollection<ModelPresetRowViewModel> ModelPresets { get; } = new();
|
||||
|
||||
public void LoadModelPresets(IReadOnlyCollection<ModelPresetDto>? presets)
|
||||
{
|
||||
ModelPresets.Clear();
|
||||
var source = presets is { Count: > 0 }
|
||||
? presets.Select(p => new ModelPreset(p.Model, p.Effort, p.MaxTurns)).ToList()
|
||||
: Data.Models.ModelPresets.Defaults.ToList();
|
||||
foreach (var p in Data.Models.ModelPresets.Parse(Data.Models.ModelPresets.Serialize(source)))
|
||||
ModelPresets.Add(new ModelPresetRowViewModel(p));
|
||||
}
|
||||
|
||||
public List<ModelPresetDto> ModelPresetDtos()
|
||||
=> ModelPresets.Select(r => new ModelPresetDto(r.Model, r.Effort, (int)r.MaxTurns)).ToList();
|
||||
|
||||
public GeneralSettingsTabViewModel() { }
|
||||
|
||||
public GeneralSettingsTabViewModel(ILocalizer localizer, Action<string> persist)
|
||||
@@ -50,10 +67,11 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
||||
|
||||
public string? Validate()
|
||||
{
|
||||
if (DefaultMaxTurns < 1 || DefaultMaxTurns > 200)
|
||||
return "Max turns must be between 1 and 200.";
|
||||
if (MaxParallelExecutions < 1 || MaxParallelExecutions > 20)
|
||||
return "Max parallel executions must be between 1 and 20.";
|
||||
foreach (var row in ModelPresets)
|
||||
if (row.MaxTurns is < 1 or > 200)
|
||||
return $"Max turns for {row.Model} must be between 1 and 200.";
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -73,3 +91,21 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
||||
return names.Count == 0 ? null : names;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>One row of the per-model preset table. Single consumer, so it lives here.</summary>
|
||||
public sealed partial class ModelPresetRowViewModel : ViewModelBase
|
||||
{
|
||||
public string Model { get; }
|
||||
public IReadOnlyList<string> EffortLevels { get; } = EffortRegistry.Levels;
|
||||
|
||||
[ObservableProperty] private string _effort;
|
||||
// decimal so it binds straight to a NumericUpDown, like the other numeric settings.
|
||||
[ObservableProperty] private decimal _maxTurns;
|
||||
|
||||
public ModelPresetRowViewModel(ModelPreset preset)
|
||||
{
|
||||
Model = preset.Model;
|
||||
_effort = preset.Effort;
|
||||
_maxTurns = preset.MaxTurns;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
||||
await OnlineInbox.LoadAsync();
|
||||
await SessionSkills.LoadAsync();
|
||||
await General.LoadSessionSkillsAsync(_worker, dto?.SessionSkills);
|
||||
General.LoadModelPresets(dto?.ModelPresets);
|
||||
}
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
@@ -102,7 +103,8 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
||||
.Split('\n').Select(l => l.Trim().TrimEnd('\r')).Where(l => l.Length > 0).ToList()),
|
||||
General.StandupWeekday,
|
||||
Prime.DailyPrepMaxTasks,
|
||||
General.SelectedSessionSkillNames());
|
||||
General.SelectedSessionSkillNames(),
|
||||
General.ModelPresetDtos());
|
||||
await _worker.UpdateAppSettingsAsync(dto);
|
||||
await Prime.SaveAsync();
|
||||
await OnlineInbox.SaveAsync();
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
PlaceholderText="{loc:Tr settings.general.defaultInstructionsPlaceholder}"
|
||||
Text="{Binding General.DefaultClaudeInstructions, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
<Grid ColumnDefinitions="*,12,*,12,*">
|
||||
<Grid ColumnDefinitions="*,12,*">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.model}"/>
|
||||
<ComboBox ItemsSource="{Binding General.Models}"
|
||||
@@ -75,17 +75,38 @@
|
||||
HorizontalAlignment="Stretch"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Spacing="4">
|
||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.maxTurns}"/>
|
||||
<NumericUpDown Value="{Binding General.DefaultMaxTurns, Mode=TwoWay}"
|
||||
Minimum="1" Maximum="200" Increment="1" FormatString="0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="4" Spacing="4">
|
||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.permission}"/>
|
||||
<ComboBox ItemsSource="{Binding General.PermissionModes}"
|
||||
SelectedItem="{Binding General.DefaultPermissionMode, Mode=TwoWay}"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Per-model run defaults: replaces the single global "max turns" field. -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.modelPresets}"/>
|
||||
<TextBlock Text="{loc:Tr settings.general.modelPresetsHint}"
|
||||
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="4" Classes="eyebrow" Text="{loc:Tr settings.general.maxTurns}"/>
|
||||
</Grid>
|
||||
<ItemsControl ItemsSource="{Binding General.ModelPresets}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="settings:ModelPresetRowViewModel">
|
||||
<Grid ColumnDefinitions="90,12,*,12,110" Margin="0,0,0,6">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Model}" VerticalAlignment="Center"/>
|
||||
<ComboBox Grid.Column="2" ItemsSource="{Binding EffortLevels}"
|
||||
SelectedItem="{Binding Effort, Mode=TwoWay}"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
<NumericUpDown Grid.Column="4" Value="{Binding MaxTurns, Mode=TwoWay}"
|
||||
Minimum="1" Maximum="200" Increment="1" FormatString="0"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.maxParallelExecutions}"/>
|
||||
<NumericUpDown Value="{Binding General.MaxParallelExecutions, Mode=TwoWay}"
|
||||
|
||||
Reference in New Issue
Block a user