50 KiB
Inherited Settings Display, Overrides, and Turns — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Make inherited config values visible in-place with a source-aware marker (inherited · List / inherited · Global), let users override them, and add Max turns as a list- and task-level overridable setting.
Architecture: Add a nullable MaxTurns override to ListConfigEntity and TaskEntity resolved task → list → global in TaskRunner. Thread it through repositories, SignalR DTOs, the SignalR hub, and the MCP config tools. In the UI, a pure resolution helper computes (value, source); a small reusable badge control plus per-field reset buttons render the marker in both the List settings modal and the task overrides flyout.
Tech Stack: .NET 8, EF Core (SQLite), ASP.NET Core SignalR, Avalonia 12 + CommunityToolkit.Mvvm, xUnit. ModelContextProtocol MCP server.
Build/test reminders (see CLAUDE.md): build individual csproj with -c Release (a running Worker locks Debug). Subagents use the sonnet model and stage files explicitly by path — never git add -A. locales/en.json and locales/de.json keys must stay in parity. Changing IWorkerClient / DTOs breaks hand-rolled fakes — update tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs.
File Structure
Created:
src/ClaudeDo.Data/Migrations/<timestamp>_InheritableMaxTurns.cs(+.Designer.cs, generated bydotnet ef)src/ClaudeDo.Ui/Services/InheritanceResolver.cs— pure helper:(value, source)resolution +InheritSourceenumsrc/ClaudeDo.Ui/Views/Controls/InheritedBadge.axaml(+.axaml.cs) — label + source badge controltests/ClaudeDo.Ui.Tests/InheritanceResolverTests.cstests/ClaudeDo.Worker.Tests/Runner/MaxTurnsResolutionTests.cs(or extend an existing runner-config test file)tests/ClaudeDo.Data.Tests/MaxTurnsRoundTripTests.cs
Modified:
src/ClaudeDo.Data/Models/ListConfigEntity.cs,TaskEntity.cssrc/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs,TaskEntityConfiguration.cssrc/ClaudeDo.Data/Repositories/ListRepository.cs,TaskRepository.cssrc/ClaudeDo.Worker/Runner/TaskRunner.cssrc/ClaudeDo.Worker/Hub/WorkerHub.cssrc/ClaudeDo.Worker/External/ConfigMcpTools.cssrc/ClaudeDo.Ui/Services/WorkerClient.cs,Services/Interfaces/IWorkerClient.cstests/ClaudeDo.Ui.Tests/StubWorkerClient.cssrc/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs,Views/Modals/ListSettingsModalView.axamlsrc/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs,Views/Islands/DetailsIslandView.axamllocales/en.json,locales/de.json
Task 1: Add MaxTurns to entities, EF config, and migration
Files:
-
Modify:
src/ClaudeDo.Data/Models/ListConfigEntity.cs -
Modify:
src/ClaudeDo.Data/Models/TaskEntity.cs -
Modify:
src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs -
Modify:
src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs:83-85 -
Create: migration (generated)
-
Step 1: Add the property to
ListConfigEntity
In src/ClaudeDo.Data/Models/ListConfigEntity.cs, after the AgentPath line:
public sealed class ListConfigEntity
{
public required string ListId { get; init; }
public string? Model { get; set; }
public string? SystemPrompt { get; set; }
public string? AgentPath { get; set; }
public int? MaxTurns { get; set; }
// Navigation property
public ListEntity List { get; set; } = null!;
}
- Step 2: Add the property to
TaskEntity
In src/ClaudeDo.Data/Models/TaskEntity.cs, beside the existing Model / SystemPrompt / AgentPath override properties, add:
public int? MaxTurns { get; set; }
- Step 3: Map the columns in both entity configurations
In ListConfigEntityConfiguration.cs, after the AgentPath mapping:
builder.Property(c => c.MaxTurns).HasColumnName("max_turns");
In TaskEntityConfiguration.cs, after line 85 (AgentPath mapping):
builder.Property(t => t.MaxTurns).HasColumnName("max_turns");
- Step 4: Generate the migration
Run:
dotnet ef migrations add InheritableMaxTurns --project src/ClaudeDo.Data --startup-project src/ClaudeDo.Worker
Expected: creates src/ClaudeDo.Data/Migrations/<timestamp>_InheritableMaxTurns.cs (+ Designer) and updates ClaudeDoDbContextModelSnapshot.cs. The Up method should AddColumn<int>("max_turns", "list_config", nullable: true) and AddColumn<int>("max_turns", "tasks", nullable: true).
If the dotnet ef tool is unavailable, hand-author the migration mirroring Migrations/20260603141020_DailyPrepMaxTasks.cs but with nullable: true and no defaultValue, adding both columns, and add matching Property<int?>("MaxTurns").HasColumnName("max_turns") entries to the snapshot for the ListConfigEntity and TaskEntity builders.
- Step 5: Verify the migration is sane
Open the generated _InheritableMaxTurns.cs and confirm both AddColumn calls use nullable: true and there is no defaultValue (null = inherit). Confirm Down drops both columns.
- Step 6: Build Data
Run: dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj -c Release
Expected: build succeeds.
- Step 7: Commit
git add src/ClaudeDo.Data/Models/ListConfigEntity.cs src/ClaudeDo.Data/Models/TaskEntity.cs src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs src/ClaudeDo.Data/Migrations/
git commit -m "feat(data): add nullable max_turns override to list_config and tasks"
Task 2: Repositories persist MaxTurns
Files:
-
Modify:
src/ClaudeDo.Data/Repositories/ListRepository.cs:56-70 -
Modify:
src/ClaudeDo.Data/Repositories/TaskRepository.cs:157-170 -
Test:
tests/ClaudeDo.Data.Tests/MaxTurnsRoundTripTests.cs -
Step 1: Write the failing test
Create tests/ClaudeDo.Data.Tests/MaxTurnsRoundTripTests.cs. Use the same in-memory/temp-SQLite context setup as a neighboring Data.Tests file (open one, e.g. ListRepositoryTests.cs, and copy its context-creation helper exactly — it builds a ClaudeDoDbContext against a temp SQLite file and calls Database.Migrate() or EnsureCreated()).
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using Xunit;
namespace ClaudeDo.Data.Tests;
public class MaxTurnsRoundTripTests
{
[Fact]
public async Task ListConfig_persists_max_turns()
{
await using var ctx = TestDb.NewContext(); // use the helper pattern from existing Data.Tests
var lists = new ListRepository(ctx);
await ctx.Lists.AddAsync(new ListEntity { Id = "L1", Name = "L1" });
await ctx.SaveChangesAsync();
await lists.SetConfigAsync(new ListConfigEntity { ListId = "L1", MaxTurns = 42 });
var cfg = await lists.GetConfigAsync("L1");
Assert.Equal(42, cfg!.MaxTurns);
}
[Fact]
public async Task Task_agent_settings_persist_and_clear_max_turns()
{
await using var ctx = TestDb.NewContext();
var tasks = new TaskRepository(ctx);
await ctx.Tasks.AddAsync(new TaskEntity { Id = "T1", ListId = "L1", Title = "t" });
await ctx.SaveChangesAsync();
await tasks.UpdateAgentSettingsAsync("T1", model: null, systemPrompt: null, agentPath: null, maxTurns: 7);
var t = await tasks.GetByIdAsync("T1");
Assert.Equal(7, t!.MaxTurns);
await tasks.UpdateAgentSettingsAsync("T1", null, null, null, maxTurns: null);
var cleared = await tasks.GetByIdAsync("T1");
Assert.Null(cleared!.MaxTurns);
}
}
NOTE: replace
TestDb.NewContext()with whatever the existing Data.Tests use (they may have a fixture or a static helper). Match the existing pattern exactly; do not invent a new harness.ListEntityrequires onlyId+Name(other fields have defaults) — verify against the existing tests.
- Step 2: Run the test to verify it fails
Run: dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release --filter MaxTurnsRoundTripTests
Expected: FAIL — UpdateAgentSettingsAsync has no maxTurns parameter (compile error), and SetConfigAsync does not copy MaxTurns onto existing rows.
- Step 3: Update
ListRepository.SetConfigAsync
In src/ClaudeDo.Data/Repositories/ListRepository.cs, in the else branch that updates an existing row (after existing.AgentPath = config.AgentPath;):
existing.MaxTurns = config.MaxTurns;
(The Add branch already stores the full entity, so no change there.)
- Step 4: Update
TaskRepository.UpdateAgentSettingsAsync
Replace the method body in src/ClaudeDo.Data/Repositories/TaskRepository.cs:157-170 with:
public async Task UpdateAgentSettingsAsync(
string taskId,
string? model,
string? systemPrompt,
string? agentPath,
int? maxTurns = null,
CancellationToken ct = default)
{
await _context.Tasks
.Where(t => t.Id == taskId)
.ExecuteUpdateAsync(s => s
.SetProperty(t => t.Model, model)
.SetProperty(t => t.SystemPrompt, systemPrompt)
.SetProperty(t => t.AgentPath, agentPath)
.SetProperty(t => t.MaxTurns, maxTurns), ct);
}
maxTurnsis placed beforectwith a default so existing 4-arg callers still compile; we update those callers explicitly in Task 4.
- Step 5: Run the test to verify it passes
Run: dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release --filter MaxTurnsRoundTripTests
Expected: PASS (2 tests).
- Step 6: Commit
git add src/ClaudeDo.Data/Repositories/ListRepository.cs src/ClaudeDo.Data/Repositories/TaskRepository.cs tests/ClaudeDo.Data.Tests/MaxTurnsRoundTripTests.cs
git commit -m "feat(data): persist max_turns in list and task repositories"
Task 3: Runner resolves MaxTurns task → list → global
Files:
- Modify:
src/ClaudeDo.Worker/Runner/TaskRunner.cs:388-394 - Test:
tests/ClaudeDo.Worker.Tests/Runner/MaxTurnsResolutionTests.cs
Context:
BuildRunConfigcurrently returnsMaxTurns: global.DefaultMaxTurns.ClaudeRunConfig.MaxTurnsisint?andClaudeArgsBuilderalready emits--max-turnswhen> 0. Only the resolution line changes.
- Step 1: Inspect the method signature
Open src/ClaudeDo.Worker/Runner/TaskRunner.cs around line 360-395 and confirm the names of the in-scope variables: the task entity (task), the list config (listConfig), and the app settings (global). If the config-building logic is not directly unit-testable (private method, lots of dependencies), extract a tiny pure static helper for just the MaxTurns precedence and test that:
internal static int? ResolveMaxTurns(int? taskTurns, int? listTurns, int globalDefault)
=> taskTurns ?? listTurns ?? globalDefault;
Place it in TaskRunner as an internal static method.
- Step 2: Write the failing test
Create tests/ClaudeDo.Worker.Tests/Runner/MaxTurnsResolutionTests.cs:
using ClaudeDo.Worker.Runner;
using Xunit;
namespace ClaudeDo.Worker.Tests.Runner;
public class MaxTurnsResolutionTests
{
[Fact]
public void Task_override_wins()
=> Assert.Equal(5, TaskRunner.ResolveMaxTurns(taskTurns: 5, listTurns: 20, globalDefault: 100));
[Fact]
public void List_override_used_when_no_task_override()
=> Assert.Equal(20, TaskRunner.ResolveMaxTurns(taskTurns: null, listTurns: 20, globalDefault: 100));
[Fact]
public void Falls_back_to_global_default()
=> Assert.Equal(100, TaskRunner.ResolveMaxTurns(taskTurns: null, listTurns: null, globalDefault: 100));
}
- Step 3: Run the test to verify it fails
Run: dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter MaxTurnsResolutionTests
Expected: FAIL — ResolveMaxTurns not defined.
- Step 4: Add the helper and use it in
BuildRunConfig
Add the ResolveMaxTurns helper from Step 1 to TaskRunner. Then change the MaxTurns: argument in the ClaudeRunConfig construction (currently MaxTurns: global.DefaultMaxTurns,) to:
MaxTurns: ResolveMaxTurns(task.MaxTurns, listConfig?.MaxTurns, global.DefaultMaxTurns),
- Step 5: Run the test to verify it passes
Run: dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter MaxTurnsResolutionTests
Expected: PASS (3 tests).
- Step 6: Build Worker
Run: dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
Expected: build succeeds.
- Step 7: Commit
git add src/ClaudeDo.Worker/Runner/TaskRunner.cs tests/ClaudeDo.Worker.Tests/Runner/MaxTurnsResolutionTests.cs
git commit -m "feat(worker): resolve max-turns from task then list then global default"
Task 4: Thread MaxTurns through Worker transport (DTOs, Hub, MCP)
Files:
-
Modify:
src/ClaudeDo.Worker/Hub/WorkerHub.cs:58-60, 334-368, 406-417 -
Modify:
src/ClaudeDo.Worker/External/ConfigMcpTools.cs -
Step 1: Extend the Worker-side DTO records
In src/ClaudeDo.Worker/Hub/WorkerHub.cs (lines 58-60), add a trailing nullable int? to each record (defaulted so existing positional usages elsewhere keep compiling):
public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
- Step 2: Persist
MaxTurnsinUpdateListConfig
In WorkerHub.UpdateListConfig (lines 334-359): include MaxTurns in the "is everything empty?" delete check and in the SetConfigAsync call.
public async Task UpdateListConfig(UpdateListConfigDto dto)
{
using var ctx = _dbFactory.CreateDbContext();
var repo = new ListRepository(ctx);
var model = dto.Model.NullIfBlank();
var systemPrompt = dto.SystemPrompt.NullIfBlank();
var agentPath = dto.AgentPath.NullIfBlank();
if (model is null && systemPrompt is null && agentPath is null && dto.MaxTurns is null)
{
await repo.DeleteConfigAsync(dto.ListId);
}
else
{
await repo.SetConfigAsync(new ListConfigEntity
{
ListId = dto.ListId,
Model = model,
SystemPrompt = systemPrompt,
AgentPath = agentPath,
MaxTurns = dto.MaxTurns,
});
}
await _broadcaster.ListUpdated(dto.ListId);
}
- Step 3: Return
MaxTurnsfromGetListConfig
In WorkerHub.GetListConfig (line 367), change the return to include MaxTurns:
return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath, config.MaxTurns);
- Step 4: Persist
MaxTurnsinUpdateTaskAgentSettings
In WorkerHub.UpdateTaskAgentSettings (lines 406-417), pass dto.MaxTurns to the repository (note the new maxTurns parameter sits before ct):
await repo.UpdateAgentSettingsAsync(
dto.TaskId,
dto.Model.NullIfBlank(),
dto.SystemPrompt.NullIfBlank(),
dto.AgentPath.NullIfBlank(),
dto.MaxTurns);
- Step 5: Extend the MCP config tools
In src/ClaudeDo.Worker/External/ConfigMcpTools.cs:
Update the DTO (line 9):
public sealed record TaskConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns);
Update GetListConfig (line 29):
return cfg is null ? null : new TaskConfigDto(cfg.Model, cfg.SystemPrompt, cfg.AgentPath, cfg.MaxTurns);
Update SetListConfig signature + body — add an int? maxTurns parameter (before CancellationToken), include it in the clear-check and the upsert:
[McpServerTool, Description("Set a list's default model/system prompt/agent path/max turns. Passing model, systemPrompt, agentPath, and maxTurns all null clears the list config.")]
public async Task SetListConfig(
string listId, string? model, string? systemPrompt, string? agentPath, int? maxTurns, CancellationToken cancellationToken)
{
_ = await _lists.GetByIdAsync(listId, cancellationToken)
?? throw new InvalidOperationException($"List {listId} not found.");
var m = model.NullIfBlank();
var sp = systemPrompt.NullIfBlank();
var ap = agentPath.NullIfBlank();
if (m is null && sp is null && ap is null && maxTurns is null)
await _lists.DeleteConfigAsync(listId, cancellationToken);
else
await _lists.SetConfigAsync(new ListConfigEntity
{
ListId = listId, Model = m, SystemPrompt = sp, AgentPath = ap, MaxTurns = maxTurns,
}, cancellationToken);
await _broadcaster.ListUpdated(listId);
}
Update SetTaskConfig signature + body — add int? maxTurns (before CancellationToken) and pass it through:
[McpServerTool, Description("Set per-task config overrides (model/system prompt/agent path/max turns). Pass null for any field to clear that override.")]
public async Task SetTaskConfig(
string taskId, string? model, string? systemPrompt, string? agentPath, int? maxTurns, CancellationToken cancellationToken)
{
_ = await _tasks.GetByIdAsync(taskId, cancellationToken)
?? throw new InvalidOperationException($"Task {taskId} not found.");
await _tasks.UpdateAgentSettingsAsync(taskId, model.NullIfBlank(), systemPrompt.NullIfBlank(), agentPath.NullIfBlank(), maxTurns, cancellationToken);
await _broadcaster.TaskUpdated(taskId);
}
- Step 6: Build Worker
Run: dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
Expected: build succeeds. (If any other caller constructed these DTOs positionally and now mismatches, fix it — the new params are defaulted, so this should be clean.)
- Step 7: Commit
git add src/ClaudeDo.Worker/Hub/WorkerHub.cs src/ClaudeDo.Worker/External/ConfigMcpTools.cs
git commit -m "feat(worker): expose max-turns override over signalr and mcp config tools"
Task 5: Mirror DTO changes in the UI transport layer
Files:
- Modify:
src/ClaudeDo.Ui/Services/WorkerClient.cs:524-526 - Modify:
tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs(only if a signature/record change breaks it)
The UI keeps its own copies of
UpdateListConfigDto,UpdateTaskAgentSettingsDto,ListConfigDto. They must mirror the Worker exactly or SignalR (de)serialization drops the field.IWorkerClientmethod signatures don't change (they take whole DTOs), so the interface and stub need no new members — but verify the stub still compiles.
- Step 1: Extend the UI DTO records
In src/ClaudeDo.Ui/Services/WorkerClient.cs (lines 524-526):
public sealed record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
public sealed record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
public sealed record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
- Step 2: Build the UI project
Run: dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
Expected: build succeeds (this pulls in Ui + Data).
- Step 3: Build the UI test project
Run: dotnet build tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
Expected: build succeeds. StubWorkerClient returns null for GetListConfigAsync and ignores UpdateTaskAgentSettingsAsync(dto), so the defaulted record params keep it compiling. If it breaks, fix only the affected line.
- Step 4: Commit
git add src/ClaudeDo.Ui/Services/WorkerClient.cs
git commit -m "feat(ui): mirror max-turns field on signalr config dtos"
Task 6: Localization keys (en + de parity)
Files:
- Modify:
locales/en.json - Modify:
locales/de.json
Localization.Tests enforces key parity. Find the right nesting by searching for the existing sibling keys (
modals.listSettings.model,details.modelLabel,vm.details.effectiveIfInherited).
- Step 1: Add the new keys to
locales/en.json
Add (place each next to its siblings; values shown):
-
under
modals.listSettings:"maxTurns": "Max turns" -
under
details:"maxTurnsLabel": "Max turns","systemPromptPrepended": "Prepended automatically:" -
a new/extended
settings.inheritgroup (or wherever shared UI strings live — match existing convention):"inheritedFromList": "inherited · List""inheritedFromGlobal": "inherited · Global""overrideBadge": "override""resetToInherited": "Reset to inherited"
-
Step 2: Add the same keys to
locales/de.json
Same key paths, German values:
-
modals.listSettings.maxTurns:"Max. Turns" -
details.maxTurnsLabel:"Max. Turns" -
details.systemPromptPrepended:"Wird automatisch vorangestellt:" -
settings.inherit.inheritedFromList:"geerbt · Liste" -
settings.inherit.inheritedFromGlobal:"geerbt · Global" -
settings.inherit.overrideBadge:"überschrieben" -
settings.inherit.resetToInherited:"Auf geerbt zurücksetzen" -
Step 3: Remove the now-unused
effectiveIfInheritedkeys
Remove vm.details.effectiveIfInherited from BOTH en.json and de.json (it is replaced by the badge approach in Tasks 8-9; this plan deletes its only usages there). Removing from both keeps parity.
- Step 4: Run the localization tests
Run: dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
Expected: PASS (en/de key parity holds).
- Step 5: Commit
git add locales/en.json locales/de.json
git commit -m "feat(i18n): add inherited-marker, turns, and prepended-prompt strings"
Task 7: InheritanceResolver helper (pure, unit-tested)
Files:
-
Create:
src/ClaudeDo.Ui/Services/InheritanceResolver.cs -
Test:
tests/ClaudeDo.Ui.Tests/InheritanceResolverTests.cs -
Step 1: Write the failing test
Create tests/ClaudeDo.Ui.Tests/InheritanceResolverTests.cs:
using ClaudeDo.Ui.Services;
using Xunit;
namespace ClaudeDo.Ui.Tests;
public class InheritanceResolverTests
{
[Fact]
public void Task_value_is_an_override()
{
var (value, source) = InheritanceResolver.Resolve("opus", "sonnet", "haiku");
Assert.Equal("opus", value);
Assert.Equal(InheritSource.Override, source);
}
[Fact]
public void Falls_through_to_list()
{
var (value, source) = InheritanceResolver.Resolve(null, "sonnet", "haiku");
Assert.Equal("sonnet", value);
Assert.Equal(InheritSource.List, source);
}
[Fact]
public void Falls_through_to_global()
{
var (value, source) = InheritanceResolver.Resolve(" ", null, "haiku");
Assert.Equal("haiku", value);
Assert.Equal(InheritSource.Global, source);
}
[Fact]
public void List_scope_treats_list_value_as_override()
{
var (value, source) = InheritanceResolver.ResolveList("sonnet", "haiku");
Assert.Equal("sonnet", value);
Assert.Equal(InheritSource.Override, source);
var (value2, source2) = InheritanceResolver.ResolveList(null, "haiku");
Assert.Equal("haiku", value2);
Assert.Equal(InheritSource.Global, source2);
}
}
- Step 2: Run the test to verify it fails
Run: dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter InheritanceResolverTests
Expected: FAIL — type does not exist.
- Step 3: Implement the helper
Create src/ClaudeDo.Ui/Services/InheritanceResolver.cs:
namespace ClaudeDo.Ui.Services;
public enum InheritSource { Override, List, Global }
public static class InheritanceResolver
{
// Task-scope fields: task -> list -> global.
public static (string Value, InheritSource Source) Resolve(
string? taskValue, string? listValue, string? globalValue)
{
if (!string.IsNullOrWhiteSpace(taskValue)) return (taskValue!, InheritSource.Override);
if (!string.IsNullOrWhiteSpace(listValue)) return (listValue!, InheritSource.List);
return (globalValue ?? "", InheritSource.Global);
}
// List-scope fields: list -> global (lists have no tier above them).
public static (string Value, InheritSource Source) ResolveList(
string? listValue, string? globalValue)
{
if (!string.IsNullOrWhiteSpace(listValue)) return (listValue!, InheritSource.Override);
return (globalValue ?? "", InheritSource.Global);
}
}
- Step 4: Run the test to verify it passes
Run: dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter InheritanceResolverTests
Expected: PASS (4 tests).
- Step 5: Commit
git add src/ClaudeDo.Ui/Services/InheritanceResolver.cs tests/ClaudeDo.Ui.Tests/InheritanceResolverTests.cs
git commit -m "feat(ui): add inheritance resolver returning value and source"
Task 8: InheritedBadge reusable control
Files:
- Create:
src/ClaudeDo.Ui/Views/Controls/InheritedBadge.axaml - Create:
src/ClaudeDo.Ui/Views/Controls/InheritedBadge.axaml.cs
A tiny
UserControlrendering a single badge whose text comes from a bound string. The consuming VM supplies the already-localized badge text (e.g. "inherited · List" / "override") and a bool for muted styling. Keeping localization in the VM avoidsLoclookups inside the control. NoICommandlives in the control — reset buttons are placed by each consumer (Tasks 9-10).
- Step 1: Create the control XAML
src/ClaudeDo.Ui/Views/Controls/InheritedBadge.axaml:
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ClaudeDo.Ui.Views.Controls.InheritedBadge"
x:Name="Root">
<Border Background="{DynamicResource SubtleFillBrush}"
CornerRadius="4" Padding="6,1"
IsVisible="{Binding #Root.BadgeText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
VerticalAlignment="Center" HorizontalAlignment="Left">
<TextBlock Classes="meta" FontSize="11" Opacity="0.75"
Text="{Binding #Root.BadgeText}"/>
</Border>
</UserControl>
If
SubtleFillBrushis not a defined token, use an existing subtle background brush fromDesign/Tokens.axaml(open it and pick the closest — e.g. a hover/overlay brush). The user will do the final visual pass.
- Step 2: Create the code-behind with a
BadgeTextStyledProperty
src/ClaudeDo.Ui/Views/Controls/InheritedBadge.axaml.cs:
using Avalonia;
using Avalonia.Controls;
namespace ClaudeDo.Ui.Views.Controls;
public partial class InheritedBadge : UserControl
{
public static readonly StyledProperty<string?> BadgeTextProperty =
AvaloniaProperty.Register<InheritedBadge, string?>(nameof(BadgeText));
public string? BadgeText
{
get => GetValue(BadgeTextProperty);
set => SetValue(BadgeTextProperty, value);
}
public InheritedBadge() => InitializeComponent();
}
- Step 3: Build the UI project
Run: dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
Expected: build succeeds (control compiles, even though nothing consumes it yet).
- Step 4: Commit
git add src/ClaudeDo.Ui/Views/Controls/InheritedBadge.axaml src/ClaudeDo.Ui/Views/Controls/InheritedBadge.axaml.cs
git commit -m "feat(ui): add reusable inherited-source badge control"
Task 9: List settings modal — Turns field + inherited markers
Files:
- Modify:
src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs - Modify:
src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml
Lists inherit only from Global. The ViewModel loads the global defaults (via
GetAppSettingsAsync) to render the muted resolved value +inherited · Globalbadge. For Model we drop the(default)sentinel row and useSelectedItem == nullto mean "inherited", withPlaceholderTextshowing the resolved value. Turns uses aNumericUpDownwhoseValueisdecimal?(null = inherit) with aWatermarkshowing the global default.
- Step 1: Add fields + load logic to the ViewModel
In ListSettingsModalViewModel.cs:
- Add a
using ClaudeDo.Ui.Services;if not present (forInheritanceResolver/InheritSource). - Change the Model collection to not include the sentinel:
public ObservableCollection<string> ModelOptions { get; } = new(ModelRegistry.Aliases);
- Change
SelectedModelto nullable (null = inherit) and add the badge + Turns properties:
[ObservableProperty] private string? _selectedModel; // null = inherit from global
[ObservableProperty] private decimal? _maxTurns; // null = inherit from global
[ObservableProperty] private string _modelInheritedHint = ""; // muted resolved value, e.g. "sonnet"
[ObservableProperty] private string _modelBadge = ""; // localized badge text
[ObservableProperty] private string _turnsInheritedHint = ""; // muted resolved value, e.g. "100"
[ObservableProperty] private string _turnsBadge = "";
[ObservableProperty] private string _agentBadge = "";
[ObservableProperty] private string _agentInheritedHint = "";
- Add badge recompute helpers and wire them to property changes:
partial void OnSelectedModelChanged(string? value) => RecomputeModelBadge();
partial void OnMaxTurnsChanged(decimal? value) => RecomputeTurnsBadge();
partial void OnSelectedAgentChanged(AgentInfo? value) => RecomputeAgentBadge();
private string _globalModel = ModelRegistry.DefaultAlias;
private int _globalMaxTurns = 100;
private void RecomputeModelBadge()
{
var overridden = !string.IsNullOrWhiteSpace(SelectedModel);
ModelInheritedHint = _globalModel;
ModelBadge = overridden ? Loc.T("settings.inherit.overrideBadge") : Loc.T("settings.inherit.inheritedFromGlobal");
}
private void RecomputeTurnsBadge()
{
var overridden = MaxTurns is not null;
TurnsInheritedHint = _globalMaxTurns.ToString();
TurnsBadge = overridden ? Loc.T("settings.inherit.overrideBadge") : Loc.T("settings.inherit.inheritedFromGlobal");
}
private void RecomputeAgentBadge()
{
var overridden = SelectedAgent is not null && !string.IsNullOrWhiteSpace(SelectedAgent.Path);
// Agent has no global default; inherited means "(none)".
AgentInheritedHint = "(none)";
AgentBadge = overridden ? Loc.T("settings.inherit.overrideBadge") : Loc.T("settings.inherit.inheritedFromGlobal");
}
- In
LoadAsync, load globals and seed the new fields. After theGetListConfigAsyncblock, replace the Model/Agent seeding with the null-sentinel approach and add MaxTurns + globals:
var app = await _worker.GetAppSettingsAsync();
_globalModel = app?.DefaultModel ?? ModelRegistry.DefaultAlias;
_globalMaxTurns = app?.DefaultMaxTurns ?? 100;
SelectedModel = string.IsNullOrWhiteSpace(config?.Model) ? null : config!.Model!;
MaxTurns = config?.MaxTurns is int mt ? mt : (decimal?)null;
SystemPrompt = config?.SystemPrompt ?? "";
SelectedAgent = string.IsNullOrWhiteSpace(config?.AgentPath)
? Agents[0]
: (Agents.FirstOrDefault(a => a.Path == config!.AgentPath) ?? Agents[0]);
RecomputeModelBadge();
RecomputeTurnsBadge();
RecomputeAgentBadge();
(Remove the old line that set SelectedModel = ... ListDefaultSentinel ....)
- In
SaveAsync, compute the override values and include MaxTurns:
var model = string.IsNullOrWhiteSpace(SelectedModel) ? null : SelectedModel;
var sp = string.IsNullOrWhiteSpace(SystemPrompt) ? null : SystemPrompt;
var ap = SelectedAgent is null || string.IsNullOrWhiteSpace(SelectedAgent.Path) ? null : SelectedAgent.Path;
var turns = MaxTurns is decimal d ? (int?)d : null;
await _worker.UpdateListAsync(new UpdateListDto(
ListId,
string.IsNullOrWhiteSpace(Name) ? Loc.T("vm.listSettings.untitled") : Name,
string.IsNullOrWhiteSpace(WorkingDir) ? null : WorkingDir,
DefaultCommitType));
await _worker.UpdateListConfigAsync(new UpdateListConfigDto(
ListId, model, sp, ap, turns));
- Add reset commands and update
ResetAgentSettings:
[RelayCommand] private void ResetModel() => SelectedModel = null;
[RelayCommand] private void ResetTurns() => MaxTurns = null;
[RelayCommand] private void ResetAgent() => SelectedAgent = Agents.Count > 0 ? Agents[0] : null;
And in the existing ResetAgentSettings, replace SelectedModel = ModelRegistry.ListDefaultSentinel; with SelectedModel = null; and add MaxTurns = null;.
- Step 2: Update the modal XAML — Model field
In ListSettingsModalView.axaml, add xmlns:ctl="using:ClaudeDo.Ui.Views.Controls" to the root if not already present (it is). Replace the Model StackPanel (lines ~76-81) with a label-row that carries the badge + reset, and a ComboBox using null-selection + placeholder:
<StackPanel Spacing="4">
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr modals.listSettings.model}" VerticalAlignment="Center"/>
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding ModelBadge}"/>
<Button Grid.Column="3" Classes="btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
Command="{Binding ResetModelCommand}" Padding="6,1"/>
</Grid>
<ComboBox ItemsSource="{Binding ModelOptions}"
SelectedItem="{Binding SelectedModel, Mode=TwoWay}"
PlaceholderText="{Binding ModelInheritedHint}"
HorizontalAlignment="Left" MinWidth="160" />
</StackPanel>
- Step 3: Update the modal XAML — add the Turns field
Add a new StackPanel inside the AGENT section's inner StackPanel (after the Model field, before System prompt):
<StackPanel Spacing="4">
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr modals.listSettings.maxTurns}" VerticalAlignment="Center"/>
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding TurnsBadge}"/>
<Button Grid.Column="3" Classes="btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
Command="{Binding ResetTurnsCommand}" Padding="6,1"/>
</Grid>
<NumericUpDown Value="{Binding MaxTurns, Mode=TwoWay}"
Watermark="{Binding TurnsInheritedHint}"
Minimum="1" Maximum="200" Increment="1" FormatString="0"
HorizontalAlignment="Left" Width="160"/>
</StackPanel>
- Step 4: Update the modal XAML — Agent field badge + reset
Wrap the existing Agent field-label (line ~91) in the same label-row pattern so it shows AgentBadge and a reset button bound to ResetAgentCommand. Keep the existing ComboBox + browse button + path TextBlock unchanged below it.
- Step 5: Build the UI project
Run: dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
Expected: build succeeds. Fix any binding/compile errors (compiled bindings require the VM properties to exist — they were added in Step 1).
- Step 6: Commit
git add src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml
git commit -m "feat(ui): show inherited markers and max-turns override in list settings"
Task 10: Task overrides flyout — Turns field + inherited markers
Files:
- Modify:
src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs - Modify:
src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml:90-122
Task scope falls through task → list → global. Model resolves to global default; Agent has no global tier (inherited = list value or "(none)"); Turns resolves to list value or global default. Reuse
InheritanceResolver.Resolve. Drop the(inherit)sentinel from the Model dropdown and the Agent dropdown's first row; null selection = inherited.
- Step 1: Replace the effective-hint fields with badge fields in the ViewModel
In DetailsIslandViewModel.cs:
- Remove
EffectiveModelLabel/EffectiveAgentLabelcomputed props and theOnEffective*HintChangedpartials (lines ~155-159), and theOnPropertyChanged(nameof(EffectiveModelLabel))calls at lines ~141-142 and ~291-292. - Change the Model options to drop the sentinel:
public System.Collections.ObjectModel.ObservableCollection<string> TaskModelOptions { get; } = new(ModelRegistry.Aliases);
- Change
TaskModelSelectionto nullable and add new observable fields:
[ObservableProperty] private string? _taskModelSelection; // null = inherit
[ObservableProperty] private decimal? _taskMaxTurns; // null = inherit
[ObservableProperty] private string _modelBadge = "";
[ObservableProperty] private string _modelInheritedHint = "";
[ObservableProperty] private string _turnsBadge = "";
[ObservableProperty] private string _turnsInheritedHint = "";
[ObservableProperty] private string _agentBadge = "";
// keep existing _taskSystemPrompt; EffectiveSystemPromptHint stays as the prepended-prompt hint
Keep EffectiveSystemPromptHint (used as the read-only "prepended automatically" hint). Remove EffectiveModelHint and EffectiveAgentHint if nothing else references them after this task.
4. Track globals and recompute badges:
private string _globalModel = ModelRegistry.DefaultAlias;
private int _globalMaxTurns = 100;
partial void OnTaskModelSelectionChanged(string? value) => RecomputeModelBadge();
partial void OnTaskMaxTurnsChanged(decimal? value) => RecomputeTurnsBadge();
partial void OnTaskSelectedAgentChanged(AgentInfo? value) => RecomputeAgentBadge();
private string? _listModel, _listAgentName;
private int? _listMaxTurns;
private void RecomputeModelBadge()
{
var (value, source) = InheritanceResolver.Resolve(TaskModelSelection, _listModel, _globalModel);
ModelInheritedHint = value;
ModelBadge = BadgeFor(source, taskSet: !string.IsNullOrWhiteSpace(TaskModelSelection));
}
private void RecomputeTurnsBadge()
{
var (value, source) = InheritanceResolver.Resolve(
TaskMaxTurns?.ToString(), _listMaxTurns?.ToString(), _globalMaxTurns.ToString());
TurnsInheritedHint = value;
TurnsBadge = BadgeFor(source, taskSet: TaskMaxTurns is not null);
}
private void RecomputeAgentBadge()
{
var taskSet = TaskSelectedAgent is not null && !string.IsNullOrWhiteSpace(TaskSelectedAgent.Path);
var (_, source) = InheritanceResolver.Resolve(
taskSet ? TaskSelectedAgent!.Path : null, _listAgentName, null);
AgentBadge = BadgeFor(source, taskSet);
}
private static string BadgeFor(InheritSource source, bool taskSet) => taskSet
? Loc.T("settings.inherit.overrideBadge")
: source == InheritSource.List
? Loc.T("settings.inherit.inheritedFromList")
: Loc.T("settings.inherit.inheritedFromGlobal");
- Step 2: Update
LoadAgentSettingsAsyncto seed the new fields
Replace the seeding block (lines ~504-514) with:
TaskModelSelection = string.IsNullOrWhiteSpace(entity.Model) ? null : entity.Model!;
TaskMaxTurns = entity.MaxTurns is int tmt ? tmt : (decimal?)null;
TaskSystemPrompt = entity.SystemPrompt ?? "";
TaskSelectedAgent = string.IsNullOrWhiteSpace(entity.AgentPath)
? TaskAgentOptions[0]
: (TaskAgentOptions.FirstOrDefault(a => a.Path == entity.AgentPath) ?? TaskAgentOptions[0]);
var listCfg = await _worker.GetListConfigAsync(entity.ListId);
var app = await _worker.GetAppSettingsAsync();
_globalModel = app?.DefaultModel ?? ModelRegistry.DefaultAlias;
_globalMaxTurns = app?.DefaultMaxTurns ?? 100;
_listModel = listCfg?.Model;
_listMaxTurns = listCfg?.MaxTurns;
_listAgentName = string.IsNullOrWhiteSpace(listCfg?.AgentPath)
? null : System.IO.Path.GetFileName(listCfg!.AgentPath!);
EffectiveSystemPromptHint = string.IsNullOrWhiteSpace(listCfg?.SystemPrompt) ? "" : listCfg!.SystemPrompt!;
RecomputeModelBadge();
RecomputeTurnsBadge();
RecomputeAgentBadge();
Also drop the (inherit) sentinel row added to TaskAgentOptions (line ~500): replace
TaskAgentOptions.Add(new AgentInfo(ModelRegistry.TaskInheritSentinel, "", ""));
with a "(none)"/inherit placeholder row (path empty) so TaskAgentOptions[0] still means "inherited":
TaskAgentOptions.Add(new AgentInfo("(inherited)", "", ""));
- Step 3: Update the save + clear paths
In the agent-save method (line ~481), include MaxTurns:
var model = string.IsNullOrWhiteSpace(TaskModelSelection) ? null : TaskModelSelection;
var sp = string.IsNullOrWhiteSpace(TaskSystemPrompt) ? null : TaskSystemPrompt;
var ap = TaskSelectedAgent is null || string.IsNullOrWhiteSpace(TaskSelectedAgent.Path)
? null : TaskSelectedAgent.Path;
var turns = TaskMaxTurns is decimal d ? (int?)d : null;
await _worker.UpdateTaskAgentSettingsAsync(
new ClaudeDo.Ui.Services.UpdateTaskAgentSettingsDto(Task.Id, model, sp, ap, turns));
In the Bind(null) reset block (lines ~583-596), set the new fields:
TaskModelSelection = null;
TaskMaxTurns = null;
TaskSystemPrompt = "";
TaskSelectedAgent = null;
and remove the now-deleted EffectiveModelHint/EffectiveAgentHint assignments; keep EffectiveSystemPromptHint = "";.
- Step 4: Add reset commands
[RelayCommand] private void ResetTaskModel() => TaskModelSelection = null;
[RelayCommand] private void ResetTaskTurns() => TaskMaxTurns = null;
[RelayCommand] private void ResetTaskAgent() => TaskSelectedAgent = TaskAgentOptions.Count > 0 ? TaskAgentOptions[0] : null;
- Step 5: Update the flyout XAML
In DetailsIslandView.axaml, add xmlns:ctl="using:ClaudeDo.Ui.Views.Controls" to the root if absent. Replace the Model block (lines ~90-98) with the badge+reset label row + null-select ComboBox:
<StackPanel Spacing="2">
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr details.modelLabel}" VerticalAlignment="Center"/>
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding ModelBadge}"/>
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
Command="{Binding ResetTaskModelCommand}"/>
</Grid>
<ComboBox ItemsSource="{Binding TaskModelOptions}"
SelectedItem="{Binding TaskModelSelection, Mode=TwoWay}"
PlaceholderText="{Binding ModelInheritedHint}"
HorizontalAlignment="Stretch"/>
</StackPanel>
<StackPanel Spacing="2">
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr details.maxTurnsLabel}" VerticalAlignment="Center"/>
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding TurnsBadge}"/>
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
Command="{Binding ResetTaskTurnsCommand}"/>
</Grid>
<NumericUpDown Value="{Binding TaskMaxTurns, Mode=TwoWay}"
Watermark="{Binding TurnsInheritedHint}"
Minimum="1" Maximum="200" Increment="1" FormatString="0"
HorizontalAlignment="Stretch"/>
</StackPanel>
For the System prompt block (lines ~100-105), keep the TextBox but change the placeholder hint label to use the new "prepended automatically" key by adding a small caption above/below it:
<StackPanel Spacing="2">
<TextBlock Classes="field-label" Text="{loc:Tr details.systemPromptLabel}"/>
<TextBox Text="{Binding TaskSystemPrompt, Mode=TwoWay}"
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="70"/>
<TextBlock Classes="meta" Opacity="0.6"
Text="{loc:Tr details.systemPromptPrepended}"
IsVisible="{Binding EffectiveSystemPromptHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<TextBlock Classes="meta" Opacity="0.6" TextWrapping="Wrap"
Text="{Binding EffectiveSystemPromptHint}"
IsVisible="{Binding EffectiveSystemPromptHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
For the Agent block (lines ~107-121), wrap the field-label in the badge+reset row (badge bound to AgentBadge, reset to ResetTaskAgentCommand); remove the old EffectiveAgentLabel <TextBlock> at lines ~118-120; keep the ComboBox.
- Step 6: Build the UI project
Run: dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
Expected: build succeeds. Resolve any leftover references to the removed Effective*Label/Effective*Hint members.
- Step 7: Run the full UI test suite
Run: dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
Expected: PASS. If StubWorkerClient.GetAppSettingsAsync returning null causes a VM test NRE, the VM already null-coalesces (app?.DefaultModel ?? ...) — but verify any DetailsIsland VM test still passes; adjust the stub to return a minimal AppSettingsDto if a test needs non-null globals.
- Step 8: Commit
git add src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml
git commit -m "feat(ui): show inherited markers and max-turns override in task flyout"
Task 11: Full build + test sweep, docs, visual-verification flag
Files:
-
Modify:
src/ClaudeDo.Ui/CLAUDE.md(one-line note on the new flyout/modal fields),src/ClaudeDo.Data/CLAUDE.md(notemax_turnson ListConfig/TaskEntity) -
Step 1: Build everything that matters
Run, in order:
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
Expected: both succeed.
- Step 2: Run the test suites
Run:
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
Expected: all PASS.
- Step 3: Update the two CLAUDE.md notes
In src/ClaudeDo.Data/CLAUDE.md, update the ListConfigEntity and TaskEntity lines to mention MaxTurns (nullable override). In src/ClaudeDo.Ui/CLAUDE.md, update the ListSettingsModalView and DetailsIslandView descriptions to mention the Max-turns field and source-aware inherited markers.
- Step 4: Commit docs
git add src/ClaudeDo.Data/CLAUDE.md src/ClaudeDo.Ui/CLAUDE.md
git commit -m "docs: note max-turns override and inherited markers in module docs"
- Step 5: Flag the visual-verification gap (do NOT claim the UI works)
The badge styling, muted placeholder appearance, NumericUpDown watermark, and reset-button layout in both the List settings modal and the task overrides flyout require a human visual pass — they cannot be asserted programmatically. Report to the user: build + automated tests pass; the visual rendering of the inherited markers, placeholders, and reset buttons needs manual confirmation in the running app (open a list's settings and a task's ⚙ flyout, toggle override vs inherited for Model / Turns / Agent).
Self-Review notes
- Spec coverage: marker shows resolved value muted (placeholder/watermark) + source-aware badge (Tasks 9-10 via Task 7 resolver) ✓; override semantics with reset (reset commands, Tasks 9-10) ✓; Turns at list + task levels (Tasks 1-5, 9-10) ✓; SystemPrompt stays plain + "prepended automatically" hint (Task 10 Step 5) ✓; MCP parity (Task 4) ✓; localization parity + retire old key (Task 6) ✓; test-fake sync (Task 5) ✓.
- Naming consistency: badge VM props named
ModelBadge/TurnsBadge/AgentBadge; hints*InheritedHint; reset commandsReset*Command; resolverResolve/ResolveListreturning(string Value, InheritSource Source); control propertyBadgeText. These names are used identically across tasks. - Known risk:
AppSettingsDtoalready carriesDefaultModelandDefaultMaxTurns(confirmed) — UI globals load works.NumericUpDown.Valueisdecimal?, hence thedecimal?VM properties withint?conversion at save.