docs: spec for inherited-settings display, overrides, and Turns

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-04 12:04:06 +02:00
parent b9741ef38b
commit 37ce673a57

View File

@@ -0,0 +1,154 @@
# Inherited settings display, per-task overrides, and Turns
**Date:** 2026-06-04
**Status:** Approved (design)
## Problem
Config inheritance is three-tier (Task → List → Global app settings). Today the UI
only signals inheritance with a placeholder sentinel (`(inherit)` for tasks,
`(default)` for lists) and, for tasks, a faint "Effective if inherited: {value}"
hint under Model and Agent. Two gaps:
1. You can't see the *actual resolved value* an inherited field will use, nor where
it comes from (List vs Global).
2. **Max turns** is global-only (`AppSettingsEntity.DefaultMaxTurns` = 100). It is not
overridable per list or per task, unlike Model / SystemPrompt / AgentPath.
## Goals
- Show the real inherited value in-place, muted, with a **source-aware marker**
(`inherited · List` vs `inherited · Global`). Picking a value turns it into an
override; a reset affordance clears it back to inherited.
- Add **Turns** (max turns) as an overridable field at both List and Task levels,
inheriting from the global default. Numeric box; empty = inherit.
- Keep SystemPrompt as-is (it is additive, not override) but show what gets prepended.
## Non-goals
- No change to SystemPrompt merge semantics (stays additive/concatenated).
- No new global settings; `DefaultMaxTurns` already exists.
- No change to PermissionMode handling.
## Inheritance semantics (reference)
Resolved in `TaskRunner.BuildRunConfig` (~line 388):
| Field | Semantics | Resolution |
|--------------|------------|--------------------------------------------------------|
| Model | override | `task.Model ?? listConfig?.Model ?? global.DefaultModel` |
| AgentPath | override | `task.AgentPath ?? listConfig?.AgentPath` (no global) |
| MaxTurns | override | **new:** `task.MaxTurns ?? listConfig?.MaxTurns ?? global.DefaultMaxTurns` |
| SystemPrompt | additive | merged: global + list + task + agent (unchanged) |
Lists inherit only from Global (no tier above them), so a list's inherited marker is
always `inherited · Global`.
## Design
### 1. Data layer
- `ListConfigEntity`: add `int? MaxTurns`.
- `TaskEntity`: add `int? MaxTurns` (nullable override).
- EF Core migration adding `max_turns` column to `list_config` and `tasks`
(nullable, no default — null = inherit).
- `TaskRunner` BuildRunConfig: `MaxTurns: task.MaxTurns ?? listConfig?.MaxTurns ?? global.DefaultMaxTurns`.
`ClaudeRunConfig.MaxTurns` and `ClaudeArgsBuilder` already accept/emit `--max-turns`
when `> 0` — no change needed there.
- `ListRepository.SetConfigAsync` (upsert) and `TaskRepository.UpdateAgentSettingsAsync`
extend to carry `maxTurns`.
### 2. DTOs / transport
Add `int? MaxTurns` to (Worker + Ui copies kept in sync):
- `UpdateListConfigDto`, `ListConfigDto` (WorkerHub.cs + WorkerClient.cs)
- `UpdateTaskAgentSettingsDto` (WorkerHub.cs + WorkerClient.cs)
- `TaskConfigDto` (ConfigMcpTools.cs)
`WorkerHub.UpdateListConfig` / `UpdateTaskAgentSettings` persist the new field via the
repositories above. MCP `SetListConfig` / `SetTaskConfig` gain an optional `maxTurns`
parameter to keep the agent-facing API at parity with the UI.
### 3. Resolution helper (Ui)
A small helper that, given `(taskValue, listValue, globalValue)`, returns
`(effectiveValue, source)` where `source ∈ { Override, List, Global }`. Drives the
marker text and muted/normal styling for Model, Agent, and Turns so the logic isn't
duplicated per field or per editor. Lives in the Ui layer beside its consumers.
### 4. UI rendering — inherited marker (source-aware)
For **Model**, **Agent**, **Turns** in both `ListSettingsModalView` and the
DetailsIsland "Agent settings (overrides)" expander:
- Remove the `(inherit)` / `(default)` sentinel *row* from the control's item source.
- When no override is set: control shows the **resolved value muted/greyed** (dropdown
shows e.g. "sonnet" dimmed; Turns box shows e.g. "100" as a muted placeholder), and a
small badge beside the field label reads `inherited · List` or `inherited · Global`.
- On picking a value / typing a number: it becomes an override — text returns to normal
color, the badge flips to `override` (or hides), and a small **"↺ reset to inherited"**
affordance appears that clears the value back to null.
- List modal: source is always Global → badge reads `inherited · Global`; reset clears
to the global default.
- Turns: numeric box, empty = inherit (muted resolved number as placeholder); a typed
number is the override.
**Rendering approach:** a small reusable `InheritedFieldHeader` control (label + badge +
reset button), fed by the resolution helper's `source`, wraps each field. Keeps the three
fields consistent and avoids per-field XAML duplication. Badge / muted styling uses
existing design tokens. Visual polish pass is the user's.
### 5. SystemPrompt (stays plain)
SystemPrompt keeps its plain multi-line text box (additive, not override). Below it, a
small **read-only, collapsed-by-default** hint shows the inherited prompts that will be
prepended (global + list), labeled e.g. "Prepended automatically:". No marker, no reset —
it never replaces, only appends.
### 6. Localization
New keys in `locales/en.json` + `locales/de.json` (parity enforced by Localization.Tests):
marker text (`inherited · List`, `inherited · Global`, `override`), reset affordance
label, Turns field label, and the SystemPrompt "prepended automatically" hint. Retire the
now-unused `vm.details.effectiveIfInherited` key (and its German counterpart) if nothing
else references it.
## Affected files (indicative)
- `src/ClaudeDo.Data/Models/ListConfigEntity.cs`, `TaskEntity.cs`
- `src/ClaudeDo.Data/Migrations/` (new migration)
- `src/ClaudeDo.Data/Repositories/ListRepository.cs`, `TaskRepository.cs`
- `src/ClaudeDo.Data/Configuration/` (column mapping for `max_turns`)
- `src/ClaudeDo.Worker/Runner/TaskRunner.cs`
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs`
- `src/ClaudeDo.Worker/External/ConfigMcpTools.cs`
- `src/ClaudeDo.Ui/Services/WorkerClient.cs` (+ `Interfaces/IWorkerClient.cs`)
- `src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs` + view
- `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` + `DetailsIslandView.axaml`
- `src/ClaudeDo.Ui/Views/Controls/` (new `InheritedFieldHeader`)
- `src/ClaudeDo.Ui/` resolution helper
- `locales/en.json`, `locales/de.json`
## Testing
- Data: migration applies; `MaxTurns` round-trips through `ListRepository.SetConfigAsync`
and `TaskRepository.UpdateAgentSettingsAsync`.
- Worker: `BuildRunConfig` resolves MaxTurns via task → list → global precedence
(unit test on the resolution). Existing `ClaudeArgsBuilder` `--max-turns` behavior
unchanged.
- Ui: resolution helper returns correct `(value, source)` for each of the
override / list / global cases across Model, Agent, Turns.
- Localization: en/de key parity (existing Localization.Tests).
- Test fakes: update hand-rolled `IWorkerClient` fakes in both test projects for the new
DTO fields (per known gotcha).
- Visual verification of the marker / muted styling: flagged for the user (cannot be
asserted programmatically).
## Open risks
- DTO/ctor changes ripple into hand-rolled test fakes in Worker.Tests and Ui.Tests —
must be updated in the same change.
- Removing the sentinel row from dropdowns changes selection binding; ensure null/empty
override state is represented without a sentinel item (e.g. dropdown `SelectedItem`
null when inherited).