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:
@@ -11,7 +11,8 @@ public sealed record ClaudeRunConfig(
|
||||
string? PermissionMode = null,
|
||||
string? McpConfigPath = null,
|
||||
string? AllowedTools = null,
|
||||
IReadOnlyList<string>? SkillNames = null
|
||||
IReadOnlyList<string>? SkillNames = null,
|
||||
string? Effort = null
|
||||
)
|
||||
{
|
||||
public IReadOnlyList<string> SkillNames { get; init; } = SkillNames ?? Array.Empty<string>();
|
||||
@@ -53,6 +54,12 @@ public sealed class ClaudeArgsBuilder
|
||||
args.Add(config.Model);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(config.Effort))
|
||||
{
|
||||
args.Add("--effort");
|
||||
args.Add(config.Effort);
|
||||
}
|
||||
|
||||
if (config.MaxTurns is int turns && turns > 0)
|
||||
{
|
||||
args.Add("--max-turns");
|
||||
|
||||
@@ -96,6 +96,11 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
||||
? WindowsTerminalLauncher.BuildResumeArgs(sessionId)
|
||||
: BuildFreshPromptArgs(task);
|
||||
|
||||
// Start the hand-driven session at the effort configured for the model this task would run
|
||||
// under, instead of inheriting whatever the user's global Claude Code config happens to be.
|
||||
// The model itself is deliberately NOT forced here — the user can still switch it in the TUI.
|
||||
args = WithEffort(args, EffortFor(globalSettings, task.Model ?? listConfig?.Model));
|
||||
|
||||
// Same run environment variable ClaudeProcess sets for every headless run: the
|
||||
// AskUser MCP tool call caps at 60s unless raised, and lifting it is harmless for
|
||||
// every other tool.
|
||||
@@ -123,7 +128,10 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
||||
};
|
||||
|
||||
return new LaunchSpec(
|
||||
ctx.WorkingDir, resolvedClaude, WindowsTerminalLauncher.BuildPlanningStartArgs(ctx), env);
|
||||
ctx.WorkingDir, resolvedClaude,
|
||||
WithEffort(WindowsTerminalLauncher.BuildPlanningStartArgs(ctx),
|
||||
EffortFor(ReadSettings(), ModelRegistry.PlanningAlias)),
|
||||
env);
|
||||
}
|
||||
|
||||
public LaunchSpec BuildPlanningResume(PlanningSessionResumeContext ctx)
|
||||
@@ -138,10 +146,13 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
||||
};
|
||||
|
||||
return new LaunchSpec(
|
||||
ctx.WorkingDir, resolvedClaude, WindowsTerminalLauncher.BuildPlanningResumeArgs(ctx.ClaudeSessionId), env);
|
||||
ctx.WorkingDir, resolvedClaude,
|
||||
WithEffort(WindowsTerminalLauncher.BuildPlanningResumeArgs(ctx.ClaudeSessionId),
|
||||
EffortFor(ReadSettings(), ModelRegistry.PlanningAlias)),
|
||||
env);
|
||||
}
|
||||
|
||||
public Task<LaunchSpec> BuildForDirectoryAsync(string directory, CancellationToken ct)
|
||||
public async Task<LaunchSpec> BuildForDirectoryAsync(string directory, CancellationToken ct)
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
throw new InvalidOperationException($"Directory does not exist: {directory}");
|
||||
@@ -149,12 +160,18 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
||||
var resolvedClaude = WindowsTerminalLauncher.Resolve(_claudePath)
|
||||
?? throw new InvalidOperationException($"claude executable not found: {_claudePath}");
|
||||
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
var settings = await new AppSettingsRepository(ctx).GetAsync(ct);
|
||||
|
||||
var env = new Dictionary<string, string>
|
||||
{
|
||||
["MCP_TOOL_TIMEOUT"] = "200000",
|
||||
};
|
||||
|
||||
return Task.FromResult(new LaunchSpec(directory, resolvedClaude, Array.Empty<string>(), env));
|
||||
// No task and no list here — the global default model's preset decides the effort.
|
||||
return new LaunchSpec(
|
||||
directory, resolvedClaude,
|
||||
WithEffort(Array.Empty<string>(), EffortFor(settings, settings.DefaultModel)), env);
|
||||
}
|
||||
|
||||
// Tools the merge helper may use without prompting: the claudedo MCP surface (run, poll,
|
||||
@@ -209,8 +226,12 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
||||
// (--allowedTools, --add-dir) first, then a single-value flag, then the single-line
|
||||
// positional kickoff LAST — a multi-line positional prompt truncates at the first
|
||||
// newline, so the full multi-line brief travels via the file exposed through --add-dir.
|
||||
var listConfig = await listRepo.GetConfigAsync(listId, ct);
|
||||
var settings = await new AppSettingsRepository(ctx).GetAsync(ct);
|
||||
|
||||
var args = new List<string>
|
||||
{
|
||||
"--effort", EffortFor(settings, listConfig?.Model),
|
||||
"--permission-mode", "default",
|
||||
"--allowedTools", MergeHelperAllowedTools,
|
||||
"--add-dir", sessionDir, repoDir,
|
||||
@@ -227,6 +248,26 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
||||
return new LaunchSpec(repoDir, resolvedClaude, args, env);
|
||||
}
|
||||
|
||||
// The reasoning effort configured for a model in Settings → General. Falls back to the shipped
|
||||
// preset for that model, so a missing/malformed settings row can never block a launch.
|
||||
private static string EffortFor(AppSettingsEntity settings, string? model)
|
||||
=> ModelPresets.For(settings.ModelPresets, model ?? settings.DefaultModel).Effort;
|
||||
|
||||
// Prepends `--effort <level>`. It has to lead: a positional kickoff prompt must stay last, and
|
||||
// it may only follow a single-value flag — a variadic flag would swallow it.
|
||||
private static IReadOnlyList<string> WithEffort(IReadOnlyList<string> args, string effort)
|
||||
{
|
||||
var result = new List<string>(args.Count + 2) { "--effort", effort };
|
||||
result.AddRange(args);
|
||||
return result;
|
||||
}
|
||||
|
||||
private AppSettingsEntity ReadSettings()
|
||||
{
|
||||
using var ctx = _dbFactory.CreateDbContext();
|
||||
return new AppSettingsRepository(ctx).GetAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
// The positional prompt claude opens the interactive session on. Empty (no positional arg)
|
||||
// if the task has neither a title nor a description.
|
||||
private static IReadOnlyList<string> BuildFreshPromptArgs(TaskEntity task)
|
||||
|
||||
@@ -516,14 +516,20 @@ public sealed class TaskRunner
|
||||
var requestedSkills = UnionSkillNames(task.SessionSkills, listConfig?.SessionSkills, global.SessionSkills);
|
||||
var skillNames = await FilterToInstalledSkillsAsync(task.Id, requestedSkills, ct);
|
||||
|
||||
// The model decides the global effort/turn defaults: one preset row per model alias
|
||||
// (Settings → General). List- and task-level max-turns overrides still win.
|
||||
var model = task.Model ?? listConfig?.Model ?? global.DefaultModel;
|
||||
var preset = Data.Models.ModelPresets.For(global.ModelPresets, model);
|
||||
|
||||
return new ClaudeRunConfig(
|
||||
Model: task.Model ?? listConfig?.Model ?? global.DefaultModel,
|
||||
Model: model,
|
||||
SystemPrompt: string.IsNullOrWhiteSpace(instructions) ? null : instructions,
|
||||
AgentPath: task.AgentPath ?? listConfig?.AgentPath,
|
||||
ResumeSessionId: resumeSessionId,
|
||||
MaxTurns: ResolveMaxTurns(task.MaxTurns, listConfig?.MaxTurns, global.DefaultMaxTurns),
|
||||
MaxTurns: ResolveMaxTurns(task.MaxTurns, listConfig?.MaxTurns, preset.MaxTurns),
|
||||
PermissionMode: global.DefaultPermissionMode,
|
||||
SkillNames: skillNames);
|
||||
SkillNames: skillNames,
|
||||
Effort: preset.Effort);
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<string>> FilterToInstalledSkillsAsync(
|
||||
|
||||
Reference in New Issue
Block a user