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:
Mika Kuns
2026-07-27 15:02:51 +02:00
parent c93a20f20c
commit fde9615b34
27 changed files with 1318 additions and 40 deletions
@@ -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;
}
}