From 37ce673a57f270b2abef1ac4e32b3f9f69b96c58 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 4 Jun 2026 12:04:06 +0200 Subject: [PATCH] docs: spec for inherited-settings display, overrides, and Turns Co-Authored-By: Claude Opus 4.7 --- ...-04-inherited-settings-and-turns-design.md | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-04-inherited-settings-and-turns-design.md diff --git a/docs/superpowers/specs/2026-06-04-inherited-settings-and-turns-design.md b/docs/superpowers/specs/2026-06-04-inherited-settings-and-turns-design.md new file mode 100644 index 0000000..0f4c6ef --- /dev/null +++ b/docs/superpowers/specs/2026-06-04-inherited-settings-and-turns-design.md @@ -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).