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:
@@ -45,6 +45,7 @@ public class AppSettingsEntityConfiguration : IEntityTypeConfiguration<AppSettin
|
|||||||
.HasColumnName("daily_prep_max_tasks").IsRequired().HasDefaultValue(5);
|
.HasColumnName("daily_prep_max_tasks").IsRequired().HasDefaultValue(5);
|
||||||
|
|
||||||
builder.Property(s => s.SessionSkills).HasColumnName("session_skills");
|
builder.Property(s => s.SessionSkills).HasColumnName("session_skills");
|
||||||
|
builder.Property(s => s.ModelPresets).HasColumnName("model_presets");
|
||||||
|
|
||||||
builder.HasData(new AppSettingsEntity { Id = AppSettingsEntity.SingletonId });
|
builder.HasData(new AppSettingsEntity { Id = AppSettingsEntity.SingletonId });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public class ListEntityConfiguration : IEntityTypeConfiguration<ListEntity>
|
|||||||
builder.Property(l => l.WorkingDir).HasColumnName("working_dir");
|
builder.Property(l => l.WorkingDir).HasColumnName("working_dir");
|
||||||
builder.Property(l => l.DefaultCommitType).HasColumnName("default_commit_type").IsRequired().HasDefaultValue("chore");
|
builder.Property(l => l.DefaultCommitType).HasColumnName("default_commit_type").IsRequired().HasDefaultValue("chore");
|
||||||
builder.Property(l => l.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
|
builder.Property(l => l.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
|
||||||
|
builder.Property(l => l.IsManual).HasColumnName("is_manual").IsRequired().HasDefaultValue(false);
|
||||||
|
|
||||||
builder.HasIndex(l => l.SortOrder).HasDatabaseName("idx_lists_sort");
|
builder.HasIndex(l => l.SortOrder).HasDatabaseName("idx_lists_sort");
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
|||||||
builder.Property(t => t.MaxTurns).HasColumnName("max_turns");
|
builder.Property(t => t.MaxTurns).HasColumnName("max_turns");
|
||||||
builder.Property(t => t.IsStarred).HasColumnName("is_starred").HasDefaultValue(false);
|
builder.Property(t => t.IsStarred).HasColumnName("is_starred").HasDefaultValue(false);
|
||||||
builder.Property(t => t.IsMyDay).HasColumnName("is_my_day").HasDefaultValue(false);
|
builder.Property(t => t.IsMyDay).HasColumnName("is_my_day").HasDefaultValue(false);
|
||||||
|
builder.Property(t => t.IsManual).HasColumnName("is_manual").HasDefaultValue(false);
|
||||||
builder.Property(t => t.Notes).HasColumnName("notes");
|
builder.Property(t => t.Notes).HasColumnName("notes");
|
||||||
builder.Property(t => t.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
|
builder.Property(t => t.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
|
||||||
builder.Property(t => t.SessionSkills).HasColumnName("session_skills");
|
builder.Property(t => t.SessionSkills).HasColumnName("session_skills");
|
||||||
|
|||||||
+802
@@ -0,0 +1,802 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ClaudeDoDbContext))]
|
||||||
|
[Migration("20260727114206_AddModelPresetsAndManualFlag")]
|
||||||
|
partial class AddModelPresetsAndManualFlag
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("CentralWorktreeRoot")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("central_worktree_root");
|
||||||
|
|
||||||
|
b.Property<int>("DailyPrepMaxTasks")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(5)
|
||||||
|
.HasColumnName("daily_prep_max_tasks");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultClaudeInstructions")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("")
|
||||||
|
.HasColumnName("default_claude_instructions");
|
||||||
|
|
||||||
|
b.Property<int>("DefaultMaxTurns")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(30)
|
||||||
|
.HasColumnName("default_max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultModel")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sonnet")
|
||||||
|
.HasColumnName("default_model");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultPermissionMode")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("bypassPermissions")
|
||||||
|
.HasColumnName("default_permission_mode");
|
||||||
|
|
||||||
|
b.Property<int>("MaxParallelExecutions")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(1)
|
||||||
|
.HasColumnName("max_parallel_executions");
|
||||||
|
|
||||||
|
b.Property<string>("ModelPresets")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model_presets");
|
||||||
|
|
||||||
|
b.Property<string>("RepoImportFolders")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("repo_import_folders");
|
||||||
|
|
||||||
|
b.Property<string>("ReportExcludedPaths")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("report_excluded_paths");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<int>("StandupWeekday")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(3)
|
||||||
|
.HasColumnName("standup_weekday");
|
||||||
|
|
||||||
|
b.Property<int>("WorktreeAutoCleanupDays")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(7)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_days");
|
||||||
|
|
||||||
|
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||||
|
|
||||||
|
b.Property<string>("WorktreeStrategy")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sibling")
|
||||||
|
.HasColumnName("worktree_strategy");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("app_settings", (string)null);
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
DailyPrepMaxTasks = 5,
|
||||||
|
DefaultClaudeInstructions = "",
|
||||||
|
DefaultMaxTurns = 100,
|
||||||
|
DefaultModel = "sonnet",
|
||||||
|
DefaultPermissionMode = "auto",
|
||||||
|
MaxParallelExecutions = 1,
|
||||||
|
StandupWeekday = 3,
|
||||||
|
WorktreeAutoCleanupDays = 7,
|
||||||
|
WorktreeAutoCleanupEnabled = false,
|
||||||
|
WorktreeStrategy = "sibling"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("note_date");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Date");
|
||||||
|
|
||||||
|
b.ToTable("daily_notes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.HasKey("ListId");
|
||||||
|
|
||||||
|
b.ToTable("list_config", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultCommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("default_commit_type");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("WorkingDir")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("working_dir");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SortOrder")
|
||||||
|
.HasDatabaseName("idx_lists_sort");
|
||||||
|
|
||||||
|
b.ToTable("lists", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("Days")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(31)
|
||||||
|
.HasColumnName("days_of_week");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("enabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastRunAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last_run_at");
|
||||||
|
|
||||||
|
b.Property<string>("PromptOverride")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt_override");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("TimeOfDay")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("time_of_day");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("prime_schedules", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SessionSkillEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AddedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("added_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("PinnedRef")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("pinned_ref");
|
||||||
|
|
||||||
|
b.Property<string>("SourceUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("source_url");
|
||||||
|
|
||||||
|
b.Property<string>("Subpath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("subpath");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("session_skills", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<bool>("Completed")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("completed");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("OrderNum")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("order_num");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_subtasks_task_id");
|
||||||
|
|
||||||
|
b.ToTable("subtasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<long>("ByteSize")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("byte_size");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("file_name");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_attachments_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_attachments", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<string>("BlockedByTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("blocked_by_task_id");
|
||||||
|
|
||||||
|
b.Property<string>("CommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("commit_type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_by");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
b.Property<bool>("IsMyDay")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_my_day");
|
||||||
|
|
||||||
|
b.Property<bool>("IsStarred")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_starred");
|
||||||
|
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("notes");
|
||||||
|
|
||||||
|
b.Property<string>("ParentTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("parent_task_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_finalized_at");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningPhase")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("none")
|
||||||
|
.HasColumnName("planning_phase");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_id");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionToken")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_token");
|
||||||
|
|
||||||
|
b.Property<string>("Result")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result");
|
||||||
|
|
||||||
|
b.Property<string>("ReviewFeedback")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("review_feedback");
|
||||||
|
|
||||||
|
b.Property<int>("RoadblockCount")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("roadblock_count");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ScheduledFor")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("scheduled_for");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BlockedByTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_blocked_by");
|
||||||
|
|
||||||
|
b.HasIndex("ListId")
|
||||||
|
.HasDatabaseName("idx_tasks_list_id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||||
|
|
||||||
|
b.HasIndex("Status")
|
||||||
|
.HasDatabaseName("idx_tasks_status");
|
||||||
|
|
||||||
|
b.HasIndex("ListId", "SortOrder")
|
||||||
|
.HasDatabaseName("idx_tasks_list_sort");
|
||||||
|
|
||||||
|
b.ToTable("tasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("error_markdown");
|
||||||
|
|
||||||
|
b.Property<int?>("ExitCode")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("exit_code");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRetry")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_retry");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<string>("Prompt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt");
|
||||||
|
|
||||||
|
b.Property<string>("ResultMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result_markdown");
|
||||||
|
|
||||||
|
b.Property<int>("RunNumber")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("run_number");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("StructuredOutputJson")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("structured_output");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensIn")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_in");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensOut")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_out");
|
||||||
|
|
||||||
|
b.Property<int?>("TurnCount")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("turn_count");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_runs_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_runs", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WeekReportEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EndDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("end_date");
|
||||||
|
|
||||||
|
b.Property<DateTime>("GeneratedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("generated_at");
|
||||||
|
|
||||||
|
b.Property<string>("Markdown")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("markdown");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("StartDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("start_date");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StartDate", "EndDate")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("week_reports", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("BaseCommit")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("BranchName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("branch_name");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DiffStat")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("diff_stat");
|
||||||
|
|
||||||
|
b.Property<string>("HeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("head_commit");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("path");
|
||||||
|
|
||||||
|
b.Property<string>("State")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("active")
|
||||||
|
.HasColumnName("state");
|
||||||
|
|
||||||
|
b.HasKey("TaskId");
|
||||||
|
|
||||||
|
b.ToTable("worktrees", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithOne("Config")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Subtasks")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("BlockedByTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithMany("Tasks")
|
||||||
|
.HasForeignKey("ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||||
|
.WithMany("Children")
|
||||||
|
.HasForeignKey("ParentTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Runs")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithOne("Worktree")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Config");
|
||||||
|
|
||||||
|
b.Navigation("Tasks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Children");
|
||||||
|
|
||||||
|
b.Navigation("Runs");
|
||||||
|
|
||||||
|
b.Navigation("Subtasks");
|
||||||
|
|
||||||
|
b.Navigation("Worktree");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddModelPresetsAndManualFlag : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "is_manual",
|
||||||
|
table: "tasks",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "is_manual",
|
||||||
|
table: "lists",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "model_presets",
|
||||||
|
table: "app_settings",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "app_settings",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "model_presets",
|
||||||
|
value: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "is_manual",
|
||||||
|
table: "tasks");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "is_manual",
|
||||||
|
table: "lists");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "model_presets",
|
||||||
|
table: "app_settings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,10 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasDefaultValue(1)
|
.HasDefaultValue(1)
|
||||||
.HasColumnName("max_parallel_executions");
|
.HasColumnName("max_parallel_executions");
|
||||||
|
|
||||||
|
b.Property<string>("ModelPresets")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model_presets");
|
||||||
|
|
||||||
b.Property<string>("RepoImportFolders")
|
b.Property<string>("RepoImportFolders")
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("repo_import_folders");
|
.HasColumnName("repo_import_folders");
|
||||||
@@ -202,6 +206,12 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasDefaultValue("chore")
|
.HasDefaultValue("chore")
|
||||||
.HasColumnName("default_commit_type");
|
.HasColumnName("default_commit_type");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
@@ -406,6 +416,12 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("finished_at");
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
b.Property<bool>("IsMyDay")
|
b.Property<bool>("IsMyDay")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
|
|||||||
@@ -30,4 +30,8 @@ public sealed class AppSettingsEntity
|
|||||||
|
|
||||||
// JSON array of session skill names applied by default to new tasks.
|
// JSON array of session skill names applied by default to new tasks.
|
||||||
public string? SessionSkills { get; set; }
|
public string? SessionSkills { get; set; }
|
||||||
|
|
||||||
|
// JSON array of ModelPreset rows (model → effort + max turns). Supplies the global effort and
|
||||||
|
// turn defaults per model; list-/task-level max-turns overrides still win. Null = ship defaults.
|
||||||
|
public string? ModelPresets { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
namespace ClaudeDo.Data.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The reasoning-effort levels the claude CLI accepts for <c>--effort</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static class EffortRegistry
|
||||||
|
{
|
||||||
|
public static readonly IReadOnlyList<string> Levels = new[] { "low", "medium", "high", "xhigh", "max" };
|
||||||
|
|
||||||
|
public const string DefaultLevel = "high";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate an effort level from external input. Null/blank → null (don't pass --effort).
|
||||||
|
/// Returns the canonical lowercase level; throws on an unknown value.
|
||||||
|
/// </summary>
|
||||||
|
public static string? NormalizeLevel(string? effort)
|
||||||
|
{
|
||||||
|
var e = effort?.Trim();
|
||||||
|
if (string.IsNullOrEmpty(e)) return null;
|
||||||
|
foreach (var level in Levels)
|
||||||
|
if (string.Equals(level, e, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return level;
|
||||||
|
throw new ArgumentException($"Unknown effort '{effort}'. Allowed: {string.Join(", ", Levels)}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,9 @@ public sealed class ListEntity
|
|||||||
public string? WorkingDir { get; set; }
|
public string? WorkingDir { get; set; }
|
||||||
public string DefaultCommitType { get; set; } = CommitTypeRegistry.DefaultType;
|
public string DefaultCommitType { get; set; } = CommitTypeRegistry.DefaultType;
|
||||||
public int SortOrder { get; set; }
|
public int SortOrder { get; set; }
|
||||||
|
// A manual list holds reminders rather than Claude work: new tasks created here start out
|
||||||
|
// manual (see TaskEntity.IsManual).
|
||||||
|
public bool IsManual { get; set; }
|
||||||
|
|
||||||
// Navigation properties
|
// Navigation properties
|
||||||
public ListConfigEntity? Config { get; set; }
|
public ListConfigEntity? Config { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Models;
|
||||||
|
|
||||||
|
/// <summary>Per-model run defaults: which reasoning effort and turn budget a session gets when
|
||||||
|
/// it runs under <paramref name="Model"/> and nothing more specific overrides it.</summary>
|
||||||
|
public sealed record ModelPreset(string Model, string Effort, int MaxTurns);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serialization + lookup for <see cref="AppSettingsEntity.ModelPresets"/> (a JSON array on the
|
||||||
|
/// singleton settings row). One row per <see cref="ModelRegistry.Aliases"/> entry; the row supplies
|
||||||
|
/// the *global* effort and max-turns defaults, which list- and task-level overrides still beat.
|
||||||
|
/// </summary>
|
||||||
|
public static class ModelPresets
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
/// <summary>Shipping defaults — one row per known model alias.</summary>
|
||||||
|
public static IReadOnlyList<ModelPreset> Defaults { get; } = new[]
|
||||||
|
{
|
||||||
|
new ModelPreset("haiku", "medium", 20),
|
||||||
|
new ModelPreset("sonnet", "high", 30),
|
||||||
|
new ModelPreset("opus", "high", 40),
|
||||||
|
new ModelPreset("fable", "high", 25),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string SerializeDefaults() => Serialize(Defaults);
|
||||||
|
|
||||||
|
public static string Serialize(IEnumerable<ModelPreset> presets)
|
||||||
|
=> JsonSerializer.Serialize(Normalize(presets), Json);
|
||||||
|
|
||||||
|
/// <summary>Parses the stored JSON, falling back to <see cref="Defaults"/> for anything absent
|
||||||
|
/// or malformed — a bad settings row must never stop a run from starting.</summary>
|
||||||
|
public static IReadOnlyList<ModelPreset> Parse(string? json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json)) return Defaults;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parsed = JsonSerializer.Deserialize<List<ModelPreset>>(json, Json);
|
||||||
|
return parsed is { Count: > 0 } ? Normalize(parsed) : Defaults;
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return Defaults;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The preset for <paramref name="model"/>, or the default row for it. Never null so
|
||||||
|
/// callers don't have to special-case an unconfigured or unknown model.</summary>
|
||||||
|
public static ModelPreset For(IReadOnlyList<ModelPreset> presets, string? model)
|
||||||
|
{
|
||||||
|
var alias = (model ?? ModelRegistry.DefaultAlias).Trim();
|
||||||
|
foreach (var p in presets)
|
||||||
|
if (string.Equals(p.Model, alias, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return p;
|
||||||
|
foreach (var p in Defaults)
|
||||||
|
if (string.Equals(p.Model, alias, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return p;
|
||||||
|
return new ModelPreset(alias, EffortRegistry.DefaultLevel, 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ModelPreset For(string? json, string? model) => For(Parse(json), model);
|
||||||
|
|
||||||
|
// One row per known alias, canonical order, clamped values — so an edited or partial list
|
||||||
|
// still round-trips into something every consumer can rely on.
|
||||||
|
private static IReadOnlyList<ModelPreset> Normalize(IEnumerable<ModelPreset> presets)
|
||||||
|
{
|
||||||
|
var byModel = new Dictionary<string, ModelPreset>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var p in presets)
|
||||||
|
{
|
||||||
|
if (p is null || string.IsNullOrWhiteSpace(p.Model)) continue;
|
||||||
|
byModel[p.Model.Trim()] = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new List<ModelPreset>();
|
||||||
|
foreach (var fallback in Defaults)
|
||||||
|
{
|
||||||
|
var source = byModel.TryGetValue(fallback.Model, out var found) ? found : fallback;
|
||||||
|
var effort = TryNormalizeEffort(source.Effort) ?? fallback.Effort;
|
||||||
|
var turns = source.MaxTurns is >= 1 and <= 200 ? source.MaxTurns : fallback.MaxTurns;
|
||||||
|
result.Add(new ModelPreset(fallback.Model, effort, turns));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? TryNormalizeEffort(string? effort)
|
||||||
|
{
|
||||||
|
try { return EffortRegistry.NormalizeLevel(effort); }
|
||||||
|
catch (ArgumentException) { return null; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ namespace ClaudeDo.Data.Models;
|
|||||||
|
|
||||||
public static class ModelRegistry
|
public static class ModelRegistry
|
||||||
{
|
{
|
||||||
public static readonly IReadOnlyList<string> Aliases = new[] { "sonnet", "opus", "haiku" };
|
public static readonly IReadOnlyList<string> Aliases = new[] { "sonnet", "opus", "haiku", "fable" };
|
||||||
|
|
||||||
/// <summary>Model aliases ordered cheapest → most capable. Single source for prompt cost guidance.</summary>
|
/// <summary>Model aliases ordered cheapest → most capable. Single source for prompt cost guidance.</summary>
|
||||||
public static readonly IReadOnlyList<string> ByCostAscending = new[] { "haiku", "sonnet", "opus" };
|
public static readonly IReadOnlyList<string> ByCostAscending = new[] { "haiku", "sonnet", "opus" };
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ public sealed class TaskEntity
|
|||||||
public int? MaxTurns { get; set; }
|
public int? MaxTurns { get; set; }
|
||||||
public bool IsStarred { get; set; }
|
public bool IsStarred { get; set; }
|
||||||
public bool IsMyDay { get; set; }
|
public bool IsMyDay { get; set; }
|
||||||
|
// Manual = a reminder only the user can do. Automation skips it (queue picker, daily prep,
|
||||||
|
// list handler) and the Claude affordances are hidden; a hand-driven ConPTY session is still
|
||||||
|
// allowed. New tasks in a manual list default to true.
|
||||||
|
public bool IsManual { get; set; }
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
public int SortOrder { get; set; }
|
public int SortOrder { get; set; }
|
||||||
public string? SessionSkills { get; set; }
|
public string? SessionSkills { get; set; }
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
"defaultInstructionsPlaceholder": "Basis-Anweisungen, die auf jede Aufgabe angewendet werden",
|
"defaultInstructionsPlaceholder": "Basis-Anweisungen, die auf jede Aufgabe angewendet werden",
|
||||||
"model": "Modell",
|
"model": "Modell",
|
||||||
"maxTurns": "Max. Durchläufe",
|
"maxTurns": "Max. Durchläufe",
|
||||||
|
"effort": "Effort",
|
||||||
|
"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.",
|
||||||
"permission": "Berechtigung",
|
"permission": "Berechtigung",
|
||||||
"maxParallelExecutions": "Max. parallele Ausführungen",
|
"maxParallelExecutions": "Max. parallele Ausführungen",
|
||||||
"maxParallelExecutionsHint": "Wie viele Aufgaben aus der Warteschlange der Worker gleichzeitig ausführt.",
|
"maxParallelExecutionsHint": "Wie viele Aufgaben aus der Warteschlange der Worker gleichzeitig ausführt.",
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
"defaultInstructionsPlaceholder": "Baseline instructions applied to every task",
|
"defaultInstructionsPlaceholder": "Baseline instructions applied to every task",
|
||||||
"model": "Model",
|
"model": "Model",
|
||||||
"maxTurns": "Max turns",
|
"maxTurns": "Max turns",
|
||||||
|
"effort": "Effort",
|
||||||
|
"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.",
|
||||||
"permission": "Permission",
|
"permission": "Permission",
|
||||||
"maxParallelExecutions": "Max parallel executions",
|
"maxParallelExecutions": "Max parallel executions",
|
||||||
"maxParallelExecutionsHint": "How many queued tasks the worker runs at once.",
|
"maxParallelExecutionsHint": "How many queued tasks the worker runs at once.",
|
||||||
|
|||||||
@@ -607,7 +607,11 @@ public sealed record AppSettingsDto(
|
|||||||
string? ReportExcludedPaths,
|
string? ReportExcludedPaths,
|
||||||
int StandupWeekday,
|
int StandupWeekday,
|
||||||
int DailyPrepMaxTasks,
|
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(
|
public sealed record SessionSkillDto(
|
||||||
string Name,
|
string Name,
|
||||||
|
|||||||
@@ -47,7 +47,11 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
|||||||
[ObservableProperty] private string _effectiveSystemPromptHint = "";
|
[ObservableProperty] private string _effectiveSystemPromptHint = "";
|
||||||
|
|
||||||
private string _globalModel = ModelRegistry.DefaultAlias;
|
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 string? _listModel; // Task scope only
|
||||||
private int? _listMaxTurns; // Task scope only
|
private int? _listMaxTurns; // Task scope only
|
||||||
private string? _listAgentName; // Task scope only
|
private string? _listAgentName; // Task scope only
|
||||||
@@ -56,7 +60,7 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
|||||||
private CancellationTokenSource? _saveCts;
|
private CancellationTokenSource? _saveCts;
|
||||||
|
|
||||||
public int EffectiveMaxTurns =>
|
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<string> ModelOptions { get; } = new(ModelRegistry.Aliases);
|
||||||
public ObservableCollection<AgentInfo> Agents { get; } = new();
|
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;
|
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)
|
partial void OnMaxTurnsChanged(decimal? value)
|
||||||
{
|
{
|
||||||
@@ -130,8 +141,8 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
|||||||
{
|
{
|
||||||
var own = MaxTurns?.ToString();
|
var own = MaxTurns?.ToString();
|
||||||
var (value, source) = _scope == AgentConfigScope.Task
|
var (value, source) = _scope == AgentConfigScope.Task
|
||||||
? InheritanceResolver.Resolve(own, _listMaxTurns?.ToString(), _globalMaxTurns.ToString())
|
? InheritanceResolver.Resolve(own, _listMaxTurns?.ToString(), GlobalMaxTurns.ToString())
|
||||||
: InheritanceResolver.ResolveList(own, _globalMaxTurns.ToString());
|
: InheritanceResolver.ResolveList(own, GlobalMaxTurns.ToString());
|
||||||
TurnsInheritedHint = value;
|
TurnsInheritedHint = value;
|
||||||
TurnsBadge = BadgeFor(source, MaxTurns is not null);
|
TurnsBadge = BadgeFor(source, MaxTurns is not null);
|
||||||
}
|
}
|
||||||
@@ -284,7 +295,9 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
|||||||
{
|
{
|
||||||
var app = await _worker.GetAppSettingsAsync();
|
var app = await _worker.GetAppSettingsAsync();
|
||||||
_globalModel = app?.DefaultModel ?? ModelRegistry.DefaultAlias;
|
_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)
|
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();
|
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() { }
|
||||||
|
|
||||||
public GeneralSettingsTabViewModel(ILocalizer localizer, Action<string> persist)
|
public GeneralSettingsTabViewModel(ILocalizer localizer, Action<string> persist)
|
||||||
@@ -50,10 +67,11 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
|||||||
|
|
||||||
public string? Validate()
|
public string? Validate()
|
||||||
{
|
{
|
||||||
if (DefaultMaxTurns < 1 || DefaultMaxTurns > 200)
|
|
||||||
return "Max turns must be between 1 and 200.";
|
|
||||||
if (MaxParallelExecutions < 1 || MaxParallelExecutions > 20)
|
if (MaxParallelExecutions < 1 || MaxParallelExecutions > 20)
|
||||||
return "Max parallel executions must be between 1 and 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,3 +91,21 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
|||||||
return names.Count == 0 ? null : names;
|
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 OnlineInbox.LoadAsync();
|
||||||
await SessionSkills.LoadAsync();
|
await SessionSkills.LoadAsync();
|
||||||
await General.LoadSessionSkillsAsync(_worker, dto?.SessionSkills);
|
await General.LoadSessionSkillsAsync(_worker, dto?.SessionSkills);
|
||||||
|
General.LoadModelPresets(dto?.ModelPresets);
|
||||||
}
|
}
|
||||||
finally { IsBusy = false; }
|
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()),
|
.Split('\n').Select(l => l.Trim().TrimEnd('\r')).Where(l => l.Length > 0).ToList()),
|
||||||
General.StandupWeekday,
|
General.StandupWeekday,
|
||||||
Prime.DailyPrepMaxTasks,
|
Prime.DailyPrepMaxTasks,
|
||||||
General.SelectedSessionSkillNames());
|
General.SelectedSessionSkillNames(),
|
||||||
|
General.ModelPresetDtos());
|
||||||
await _worker.UpdateAppSettingsAsync(dto);
|
await _worker.UpdateAppSettingsAsync(dto);
|
||||||
await Prime.SaveAsync();
|
await Prime.SaveAsync();
|
||||||
await OnlineInbox.SaveAsync();
|
await OnlineInbox.SaveAsync();
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
PlaceholderText="{loc:Tr settings.general.defaultInstructionsPlaceholder}"
|
PlaceholderText="{loc:Tr settings.general.defaultInstructionsPlaceholder}"
|
||||||
Text="{Binding General.DefaultClaudeInstructions, Mode=TwoWay}"/>
|
Text="{Binding General.DefaultClaudeInstructions, Mode=TwoWay}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Grid ColumnDefinitions="*,12,*,12,*">
|
<Grid ColumnDefinitions="*,12,*">
|
||||||
<StackPanel Grid.Column="0" Spacing="4">
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.model}"/>
|
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.model}"/>
|
||||||
<ComboBox ItemsSource="{Binding General.Models}"
|
<ComboBox ItemsSource="{Binding General.Models}"
|
||||||
@@ -75,17 +75,38 @@
|
|||||||
HorizontalAlignment="Stretch"/>
|
HorizontalAlignment="Stretch"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Grid.Column="2" Spacing="4">
|
<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}"/>
|
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.permission}"/>
|
||||||
<ComboBox ItemsSource="{Binding General.PermissionModes}"
|
<ComboBox ItemsSource="{Binding General.PermissionModes}"
|
||||||
SelectedItem="{Binding General.DefaultPermissionMode, Mode=TwoWay}"
|
SelectedItem="{Binding General.DefaultPermissionMode, Mode=TwoWay}"
|
||||||
HorizontalAlignment="Stretch"/>
|
HorizontalAlignment="Stretch"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</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">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.maxParallelExecutions}"/>
|
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.maxParallelExecutions}"/>
|
||||||
<NumericUpDown Value="{Binding General.MaxParallelExecutions, Mode=TwoWay}"
|
<NumericUpDown Value="{Binding General.MaxParallelExecutions, Mode=TwoWay}"
|
||||||
|
|||||||
@@ -40,7 +40,11 @@ public record AppSettingsDto(
|
|||||||
string? ReportExcludedPaths,
|
string? ReportExcludedPaths,
|
||||||
int StandupWeekday,
|
int StandupWeekday,
|
||||||
int DailyPrepMaxTasks,
|
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 record ModelPresetDto(string Model, string Effort, int MaxTurns);
|
||||||
|
|
||||||
public record SessionSkillDto(
|
public record SessionSkillDto(
|
||||||
string Name,
|
string Name,
|
||||||
@@ -318,7 +322,9 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
row.ReportExcludedPaths,
|
row.ReportExcludedPaths,
|
||||||
row.StandupWeekday,
|
row.StandupWeekday,
|
||||||
row.DailyPrepMaxTasks,
|
row.DailyPrepMaxTasks,
|
||||||
SkillsFromJson(row.SessionSkills));
|
SkillsFromJson(row.SessionSkills),
|
||||||
|
Data.Models.ModelPresets.Parse(row.ModelPresets)
|
||||||
|
.Select(p => new ModelPresetDto(p.Model, p.Effort, p.MaxTurns)).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateAppSettings(AppSettingsDto dto)
|
public async Task UpdateAppSettings(AppSettingsDto dto)
|
||||||
@@ -341,6 +347,11 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
StandupWeekday = dto.StandupWeekday is >= 0 and <= 6 ? dto.StandupWeekday : (int)DayOfWeek.Wednesday,
|
StandupWeekday = dto.StandupWeekday is >= 0 and <= 6 ? dto.StandupWeekday : (int)DayOfWeek.Wednesday,
|
||||||
DailyPrepMaxTasks = dto.DailyPrepMaxTasks,
|
DailyPrepMaxTasks = dto.DailyPrepMaxTasks,
|
||||||
SessionSkills = SkillsToJson(dto.SessionSkills),
|
SessionSkills = SkillsToJson(dto.SessionSkills),
|
||||||
|
// Normalized on the way in (unknown models dropped, effort validated, turns clamped).
|
||||||
|
ModelPresets = dto.ModelPresets is { Count: > 0 }
|
||||||
|
? Data.Models.ModelPresets.Serialize(
|
||||||
|
dto.ModelPresets.Select(p => new ModelPreset(p.Model, p.Effort, p.MaxTurns)))
|
||||||
|
: Data.Models.ModelPresets.SerializeDefaults(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ public sealed record ClaudeRunConfig(
|
|||||||
string? PermissionMode = null,
|
string? PermissionMode = null,
|
||||||
string? McpConfigPath = null,
|
string? McpConfigPath = null,
|
||||||
string? AllowedTools = 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>();
|
public IReadOnlyList<string> SkillNames { get; init; } = SkillNames ?? Array.Empty<string>();
|
||||||
@@ -53,6 +54,12 @@ public sealed class ClaudeArgsBuilder
|
|||||||
args.Add(config.Model);
|
args.Add(config.Model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(config.Effort))
|
||||||
|
{
|
||||||
|
args.Add("--effort");
|
||||||
|
args.Add(config.Effort);
|
||||||
|
}
|
||||||
|
|
||||||
if (config.MaxTurns is int turns && turns > 0)
|
if (config.MaxTurns is int turns && turns > 0)
|
||||||
{
|
{
|
||||||
args.Add("--max-turns");
|
args.Add("--max-turns");
|
||||||
|
|||||||
@@ -96,6 +96,11 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
|||||||
? WindowsTerminalLauncher.BuildResumeArgs(sessionId)
|
? WindowsTerminalLauncher.BuildResumeArgs(sessionId)
|
||||||
: BuildFreshPromptArgs(task);
|
: 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
|
// 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
|
// AskUser MCP tool call caps at 60s unless raised, and lifting it is harmless for
|
||||||
// every other tool.
|
// every other tool.
|
||||||
@@ -123,7 +128,10 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
|||||||
};
|
};
|
||||||
|
|
||||||
return new LaunchSpec(
|
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)
|
public LaunchSpec BuildPlanningResume(PlanningSessionResumeContext ctx)
|
||||||
@@ -138,10 +146,13 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
|||||||
};
|
};
|
||||||
|
|
||||||
return new LaunchSpec(
|
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))
|
if (!Directory.Exists(directory))
|
||||||
throw new InvalidOperationException($"Directory does not exist: {directory}");
|
throw new InvalidOperationException($"Directory does not exist: {directory}");
|
||||||
@@ -149,12 +160,18 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
|||||||
var resolvedClaude = WindowsTerminalLauncher.Resolve(_claudePath)
|
var resolvedClaude = WindowsTerminalLauncher.Resolve(_claudePath)
|
||||||
?? throw new InvalidOperationException($"claude executable not found: {_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>
|
var env = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["MCP_TOOL_TIMEOUT"] = "200000",
|
["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,
|
// 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
|
// (--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
|
// 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.
|
// 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>
|
var args = new List<string>
|
||||||
{
|
{
|
||||||
|
"--effort", EffortFor(settings, listConfig?.Model),
|
||||||
"--permission-mode", "default",
|
"--permission-mode", "default",
|
||||||
"--allowedTools", MergeHelperAllowedTools,
|
"--allowedTools", MergeHelperAllowedTools,
|
||||||
"--add-dir", sessionDir, repoDir,
|
"--add-dir", sessionDir, repoDir,
|
||||||
@@ -227,6 +248,26 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
|||||||
return new LaunchSpec(repoDir, resolvedClaude, args, env);
|
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)
|
// The positional prompt claude opens the interactive session on. Empty (no positional arg)
|
||||||
// if the task has neither a title nor a description.
|
// if the task has neither a title nor a description.
|
||||||
private static IReadOnlyList<string> BuildFreshPromptArgs(TaskEntity task)
|
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 requestedSkills = UnionSkillNames(task.SessionSkills, listConfig?.SessionSkills, global.SessionSkills);
|
||||||
var skillNames = await FilterToInstalledSkillsAsync(task.Id, requestedSkills, ct);
|
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(
|
return new ClaudeRunConfig(
|
||||||
Model: task.Model ?? listConfig?.Model ?? global.DefaultModel,
|
Model: model,
|
||||||
SystemPrompt: string.IsNullOrWhiteSpace(instructions) ? null : instructions,
|
SystemPrompt: string.IsNullOrWhiteSpace(instructions) ? null : instructions,
|
||||||
AgentPath: task.AgentPath ?? listConfig?.AgentPath,
|
AgentPath: task.AgentPath ?? listConfig?.AgentPath,
|
||||||
ResumeSessionId: resumeSessionId,
|
ResumeSessionId: resumeSessionId,
|
||||||
MaxTurns: ResolveMaxTurns(task.MaxTurns, listConfig?.MaxTurns, global.DefaultMaxTurns),
|
MaxTurns: ResolveMaxTurns(task.MaxTurns, listConfig?.MaxTurns, preset.MaxTurns),
|
||||||
PermissionMode: global.DefaultPermissionMode,
|
PermissionMode: global.DefaultPermissionMode,
|
||||||
SkillNames: skillNames);
|
SkillNames: skillNames,
|
||||||
|
Effort: preset.Effort);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IReadOnlyList<string>> FilterToInstalledSkillsAsync(
|
private async Task<IReadOnlyList<string>> FilterToInstalledSkillsAsync(
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Tests;
|
||||||
|
|
||||||
|
public class ModelPresetsTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Defaults_cover_every_known_alias()
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
ModelRegistry.Aliases.OrderBy(a => a),
|
||||||
|
ModelPresets.Defaults.Select(p => p.Model).OrderBy(m => m));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null)]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData(" ")]
|
||||||
|
[InlineData("not json at all")]
|
||||||
|
public void Parse_falls_back_to_defaults_when_unusable(string? json)
|
||||||
|
{
|
||||||
|
Assert.Equal(ModelPresets.Defaults, ModelPresets.Parse(json));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Serialize_roundtrips()
|
||||||
|
{
|
||||||
|
var edited = new[] { new ModelPreset("opus", "max", 55) };
|
||||||
|
var parsed = ModelPresets.Parse(ModelPresets.Serialize(edited));
|
||||||
|
|
||||||
|
Assert.Equal("max", ModelPresets.For(parsed, "opus").Effort);
|
||||||
|
Assert.Equal(55, ModelPresets.For(parsed, "opus").MaxTurns);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Serialize_fills_missing_aliases_from_defaults()
|
||||||
|
{
|
||||||
|
var parsed = ModelPresets.Parse(ModelPresets.Serialize(new[] { new ModelPreset("opus", "max", 55) }));
|
||||||
|
|
||||||
|
Assert.Equal(ModelRegistry.Aliases.Count, parsed.Count);
|
||||||
|
Assert.Equal(ModelPresets.For(ModelPresets.Defaults, "haiku"), ModelPresets.For(parsed, "haiku"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("nonsense", 30)] // unknown effort → the default row's effort
|
||||||
|
[InlineData("high", 0)] // turns out of range → the default row's turns
|
||||||
|
[InlineData("high", 999)]
|
||||||
|
public void Serialize_rejects_out_of_range_values(string effort, int turns)
|
||||||
|
{
|
||||||
|
var fallback = ModelPresets.For(ModelPresets.Defaults, "sonnet");
|
||||||
|
var parsed = ModelPresets.Parse(ModelPresets.Serialize(new[] { new ModelPreset("sonnet", effort, turns) }));
|
||||||
|
var row = ModelPresets.For(parsed, "sonnet");
|
||||||
|
|
||||||
|
if (effort == "nonsense") Assert.Equal(fallback.Effort, row.Effort);
|
||||||
|
if (turns is 0 or 999) Assert.Equal(fallback.MaxTurns, row.MaxTurns);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void For_unknown_model_yields_a_usable_row()
|
||||||
|
{
|
||||||
|
var row = ModelPresets.For(ModelPresets.Defaults, "some-future-model");
|
||||||
|
|
||||||
|
Assert.Equal("some-future-model", row.Model);
|
||||||
|
Assert.Contains(row.Effort, EffortRegistry.Levels);
|
||||||
|
Assert.True(row.MaxTurns > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void For_null_model_uses_the_default_alias()
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
ModelPresets.For(ModelPresets.Defaults, ModelRegistry.DefaultAlias),
|
||||||
|
ModelPresets.For(ModelPresets.Defaults, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("low", "low")]
|
||||||
|
[InlineData("XHIGH", "xhigh")]
|
||||||
|
[InlineData(" max ", "max")]
|
||||||
|
public void NormalizeLevel_canonicalizes(string input, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, EffortRegistry.NormalizeLevel(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NormalizeLevel_blank_means_unset_and_unknown_throws()
|
||||||
|
{
|
||||||
|
Assert.Null(EffortRegistry.NormalizeLevel(null));
|
||||||
|
Assert.Throws<ArgumentException>(() => EffortRegistry.NormalizeLevel("gigantic"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,11 +26,15 @@ public class AgentConfigEditorViewModelTests
|
|||||||
public override Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) { SavedTaskSettings = dto; return Task.CompletedTask; }
|
public override Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) { SavedTaskSettings = dto; return Task.CompletedTask; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The global turn default is per-model now, so the preset row for `model` is what carries
|
||||||
|
// `turns` — DefaultMaxTurns is no longer consulted.
|
||||||
private static AppSettingsDto AppWith(string model, int turns) =>
|
private static AppSettingsDto AppWith(string model, int turns) =>
|
||||||
new(DefaultClaudeInstructions: "", DefaultModel: model, DefaultMaxTurns: turns,
|
new(DefaultClaudeInstructions: "", DefaultModel: model, DefaultMaxTurns: turns,
|
||||||
DefaultPermissionMode: "auto", MaxParallelExecutions: 1, WorktreeStrategy: "sibling",
|
DefaultPermissionMode: "auto", MaxParallelExecutions: 1, WorktreeStrategy: "sibling",
|
||||||
CentralWorktreeRoot: null, WorktreeAutoCleanupEnabled: false, WorktreeAutoCleanupDays: 30,
|
CentralWorktreeRoot: null, WorktreeAutoCleanupEnabled: false, WorktreeAutoCleanupDays: 30,
|
||||||
ReportExcludedPaths: null, StandupWeekday: 3, DailyPrepMaxTasks: 5);
|
ReportExcludedPaths: null, StandupWeekday: 3, DailyPrepMaxTasks: 5,
|
||||||
|
SessionSkills: null,
|
||||||
|
ModelPresets: new List<ModelPresetDto> { new(model, "high", turns) });
|
||||||
|
|
||||||
private static TaskEntity TaskWith(string? model, int? turns, string? sp, string? agentPath) =>
|
private static TaskEntity TaskWith(string? model, int? turns, string? sp, string? agentPath) =>
|
||||||
new() { Id = "t1", ListId = "l1", Title = "t", CreatedAt = DateTime.UtcNow,
|
new() { Id = "t1", ListId = "l1", Title = "t", CreatedAt = DateTime.UtcNow,
|
||||||
|
|||||||
@@ -22,6 +22,25 @@ public sealed class ClaudeArgsBuilderTests
|
|||||||
Assert.DoesNotContain("--append-system-prompt", args);
|
Assert.DoesNotContain("--append-system-prompt", args);
|
||||||
Assert.DoesNotContain("--agents", args);
|
Assert.DoesNotContain("--agents", args);
|
||||||
Assert.DoesNotContain("--resume", args);
|
Assert.DoesNotContain("--resume", args);
|
||||||
|
Assert.DoesNotContain("--effort", args);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Effort_Adds_Effort_Flag()
|
||||||
|
{
|
||||||
|
var args = _builder.Build(new ClaudeRunConfig(null, null, null, null, Effort: "xhigh"));
|
||||||
|
Assert.Contains("--effort", args);
|
||||||
|
Assert.Equal("xhigh", args[args.ToList().IndexOf("--effort") + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null)]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData(" ")]
|
||||||
|
public void Blank_Effort_Omits_The_Flag(string? effort)
|
||||||
|
{
|
||||||
|
var args = _builder.Build(new ClaudeRunConfig(null, null, null, null, Effort: effort));
|
||||||
|
Assert.DoesNotContain("--effort", args);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
Assert.True(Directory.Exists(wtRow.Path));
|
Assert.True(Directory.Exists(wtRow.Path));
|
||||||
|
|
||||||
Assert.Equal(wtRow.Path, spec.Cwd);
|
Assert.Equal(wtRow.Path, spec.Cwd);
|
||||||
Assert.Equal(new[] { "T" }, spec.Args); // fresh session seeds the task title as the prompt
|
Assert.Equal(new[] { "T" }, ArgsAfterEffort(spec)); // fresh session seeds the task title as the prompt
|
||||||
Assert.Equal(_claudeStubPath, spec.Exe);
|
Assert.Equal(_claudeStubPath, spec.Exe);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +230,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
|
|
||||||
Assert.Equal(_worktreeDir, spec.Cwd);
|
Assert.Equal(_worktreeDir, spec.Cwd);
|
||||||
Assert.Equal(_claudeStubPath, spec.Exe);
|
Assert.Equal(_claudeStubPath, spec.Exe);
|
||||||
Assert.Equal(new[] { "--resume", "sess-123" }, spec.Args);
|
Assert.Equal(new[] { "--resume", "sess-123" }, ArgsAfterEffort(spec));
|
||||||
Assert.Equal("200000", spec.Env["MCP_TOOL_TIMEOUT"]);
|
Assert.Equal("200000", spec.Env["MCP_TOOL_TIMEOUT"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +246,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var spec = await svc.BuildForTaskAsync(taskId, CancellationToken.None);
|
var spec = await svc.BuildForTaskAsync(taskId, CancellationToken.None);
|
||||||
|
|
||||||
Assert.Equal(new[] { "--resume", "sess-kept" }, spec.Args);
|
Assert.Equal(new[] { "--resume", "sess-kept" }, ArgsAfterEffort(spec));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -261,7 +261,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var spec = await svc.BuildForTaskAsync(taskId, CancellationToken.None);
|
var spec = await svc.BuildForTaskAsync(taskId, CancellationToken.None);
|
||||||
|
|
||||||
Assert.Equal(new[] { "T" }, spec.Args); // fresh: seeds the task title as the prompt
|
Assert.Equal(new[] { "T" }, ArgsAfterEffort(spec)); // fresh: seeds the task title as the prompt
|
||||||
Assert.Equal(_worktreeDir, spec.Cwd);
|
Assert.Equal(_worktreeDir, spec.Cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,7 +277,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var spec = await svc.BuildForTaskAsync(taskId, CancellationToken.None);
|
var spec = await svc.BuildForTaskAsync(taskId, CancellationToken.None);
|
||||||
|
|
||||||
Assert.Equal(new[] { "T" }, spec.Args); // fresh: seeds the task title as the prompt
|
Assert.Equal(new[] { "T" }, ArgsAfterEffort(spec)); // fresh: seeds the task title as the prompt
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -297,7 +297,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var spec = await svc.BuildForTaskAsync(taskId, CancellationToken.None);
|
var spec = await svc.BuildForTaskAsync(taskId, CancellationToken.None);
|
||||||
|
|
||||||
Assert.Equal(new[] { "T\n\nDo the thing" }, spec.Args);
|
Assert.Equal(new[] { "T\n\nDo the thing" }, ArgsAfterEffort(spec));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -333,7 +333,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
|
|
||||||
Assert.Equal(_tempDir, spec.Cwd);
|
Assert.Equal(_tempDir, spec.Cwd);
|
||||||
Assert.Equal(_claudeStubPath, spec.Exe);
|
Assert.Equal(_claudeStubPath, spec.Exe);
|
||||||
Assert.Empty(spec.Args);
|
Assert.Empty(ArgsAfterEffort(spec));
|
||||||
Assert.Equal("200000", spec.Env["MCP_TOOL_TIMEOUT"]);
|
Assert.Equal("200000", spec.Env["MCP_TOOL_TIMEOUT"]);
|
||||||
Assert.Empty(_seeder.Calls);
|
Assert.Empty(_seeder.Calls);
|
||||||
}
|
}
|
||||||
@@ -348,6 +348,17 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
|
|
||||||
// ── Merge helper ──
|
// ── Merge helper ──
|
||||||
|
|
||||||
|
/// Every ConPTY spec now leads with `--effort <level>` from the per-model preset (the seeded
|
||||||
|
/// settings row has no overrides, so the shipped default for the default model applies).
|
||||||
|
/// Asserts that pair and returns the rest of the args for the test's own comparison.
|
||||||
|
private static string[] ArgsAfterEffort(LaunchSpec spec)
|
||||||
|
{
|
||||||
|
var args = spec.Args.ToList();
|
||||||
|
Assert.Equal("--effort", args[0]);
|
||||||
|
Assert.Equal(ModelPresets.For(ModelPresets.Defaults, ModelRegistry.DefaultAlias).Effort, args[1]);
|
||||||
|
return args.Skip(2).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
private readonly List<string> _mergeHelperSessionDirs = new();
|
private readonly List<string> _mergeHelperSessionDirs = new();
|
||||||
|
|
||||||
/// The session dir is the value right after --add-dir; register it for cleanup
|
/// The session dir is the value right after --add-dir; register it for cleanup
|
||||||
@@ -498,11 +509,12 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
|
|
||||||
var spec = BuildService().BuildPlanningResume(ctx);
|
var spec = BuildService().BuildPlanningResume(ctx);
|
||||||
|
|
||||||
Assert.Equal("--permission-mode", spec.Args[0]);
|
var args = ArgsAfterEffort(spec);
|
||||||
Assert.Equal("default", spec.Args[1]);
|
Assert.Equal("--permission-mode", args[0]);
|
||||||
Assert.Equal("--allowedTools", spec.Args[2]);
|
Assert.Equal("default", args[1]);
|
||||||
Assert.Contains("mcp__claudedo__", spec.Args[3]);
|
Assert.Equal("--allowedTools", args[2]);
|
||||||
Assert.Equal(new[] { "--resume", "sess-42" }, spec.Args.Skip(4).ToArray());
|
Assert.Contains("mcp__claudedo__", args[3]);
|
||||||
|
Assert.Equal(new[] { "--resume", "sess-42" }, args.Skip(4).ToArray());
|
||||||
Assert.Equal("tok-2", spec.Env["CLAUDEDO_PLANNING_TOKEN"]);
|
Assert.Equal("tok-2", spec.Env["CLAUDEDO_PLANNING_TOKEN"]);
|
||||||
Assert.Equal(_worktreeDir, spec.Cwd);
|
Assert.Equal(_worktreeDir, spec.Cwd);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user