# Phase 4 — AgentConfigEditor (A2)

Date: 2026-06-23 (picked up after reordering Phase 3 ↔ 4)
Umbrella: `docs/superpowers/plans/2026-06-19-feature-unification-plan.md`
Design: `docs/superpowers/specs/2026-06-19-feature-unification-design.md` (A2)

## Reordering note

Phase 3 (WorktreeActions) was deferred. Its premise — overview rows and the Details
merge section each owning duplicate worktree commands — only half-holds: Details has
no Discard/Keep/ForceRemove, and the two Diff doors open different VMs (`WorktreeModal`
vs `DiffModal`) that only Phase 5 unifies. So Phase 3's clean form depends on Phase 5
(Diff) and a fuller MergeCoordinator (Merge); doing it now would build throwaway
per-surface delegates. **Phase 3 is folded into Phase 5.** Phase 4 (independent, clean
dedup) runs now.

## Scope decision: List + Task only (global left as-is)

The design names three scopes (Global | List | Task). Verified against the tree on
2026-06-23, only **List and Task genuinely duplicate**:

- **List** (`ListSettingsModalViewModel`, "AGENT" section): Model / MaxTurns /
  SystemPrompt / AgentFile, each with `InheritedBadge` + `↺` reset; 2-tier
  (list→global) badges computed with inline logic (does **not** use the existing
  `InheritanceResolver.ResolveList` — which is currently dead code); explicit Save.
- **Task** (`AgentSettingsSectionViewModel`, TaskHeaderBar gear flyout): same four
  fields; 3-tier (task→list→global) badges via `InheritanceResolver.Resolve`;
  `EffectiveMaxTurns` + `EffectiveSystemPromptHint`; `IsRunning` gate; debounced
  auto-save.

**Global** (`GeneralSettingsTabViewModel`, Settings → General) is the root: no
inheritance, no badges, no agent file, no reset — three plain controls (model combo,
max-turns numeric, instructions textbox) plus a global-only PermissionMode, interleaved
with unrelated settings (Language, parallelism, report paths, standup weekday) and
saved batched into one `AppSettingsDto` via the modal Save. Embedding the shared editor
there buys ~3 plain fields at the cost of a degenerate no-badges/no-agent/no-reset mode
plus surgery on the settings save path and a relayout of the most settings-dense view.
**Not worth it — global stays as-is.** (Confirmed with Mika 2026-06-23.)

The real maintenance hazard is the **VM logic** (two copies of badge/reset/inheritance
that already drifted), and the **view** (3 of 4 field blocks are pixel-identical). Both
collapse cleanly for List+Task.

## Target

One `AgentConfigEditorViewModel` + one `AgentConfigEditor` UserControl, instantiated
per surface with a scope. The two host VMs keep only their non-agent concerns and host
the editor as a child.

### `ViewModels/Agent/AgentConfigEditorViewModel.cs` (new)

- `enum AgentConfigScope { List, Task }`
- ctor `(IWorkerClient worker, AgentConfigScope scope)`
- Unified bindable surface (single names both views bind to):
  `Model` (string?), `MaxTurns` (decimal?), `SystemPrompt` (string),
  `SelectedAgent` (AgentInfo?); `ModelOptions`, `Agents`;
  `ModelBadge`/`TurnsBadge`/`AgentBadge`, `ModelInheritedHint`/`TurnsInheritedHint`,
  `EffectiveSystemPromptHint`; `EffectiveMaxTurns` (int), `IsRunning`/`IsEnabled`.
- Reset commands: `ResetModel`, `ResetTurns`, `ResetAgent`, `ResetAll`.
- Badges via `InheritanceResolver`: scope==Task → `Resolve(own, list, global)`;
  scope==List → `ResolveList(own, global)` (adopts the dead method). One `BadgeFor`
  helper covers both (List scope never yields the `List` source).
- Load: `LoadForListAsync(listId)` and `LoadForTaskAsync(TaskEntity entity)` — both
  pull agents + app-settings (global defaults); Task also pulls the list tier +
  `EffectiveSystemPromptHint`. Localizer-change re-badges (port the `Loc.LanguageChanged`
  handler + `IDisposable`).
- Save: `SaveAsync()` is scope-aware — List builds `UpdateListConfigDto` →
  `UpdateListConfigAsync`; Task builds `UpdateTaskAgentSettingsDto` →
  `UpdateTaskAgentSettingsAsync`. Task scope also auto-saves debounced (300ms) on field
  changes; List does not (the modal Save button calls `SaveAsync`). `SaveAsync` is
  directly callable (tests bypass the debounce).
- Task-only `Clear()` + `TaskId`.

### `Views/Controls/AgentConfigEditor.axaml` (+ .axaml.cs) (new)

- `x:DataType` = `AgentConfigEditorViewModel`; host sets `DataContext="{Binding Agent}"`.
- The four field blocks (model/turns/systemprompt/agent) with `InheritedBadge` + `↺`
  reset, lifted verbatim from the existing two views (they already match). Agent combo
  shows Name + Description (both scopes; harmless for task). `EffectiveSystemPromptHint`
  line gated on non-empty (hides for List).
- `StyledProperty<bool> ShowAgentBrowse` (default false). True → render the Browse
  button + path line; the browse file-picker code-behind lives here (moved from
  `ListSettingsModalView`).
- Shared localization namespace `settings.agentEditor.*` (model/maxTurns/systemPrompt/
  agentFile/promptPrepended). Reset tooltip reuses `settings.inherit.resetToInherited`.

### Re-point hosts

- `ListSettingsModalViewModel`: drop the agent fields/badges/resets/option-lists; add
  `public AgentConfigEditorViewModel Agent { get; }` (scope=List). `LoadAsync` →
  `Agent.LoadForListAsync(listId)`. `SaveAsync` keeps `UpdateListAsync` (name/dir) and
  adds `await Agent.SaveAsync()`. Keep working-dir browse (`BrowseClicked`).
- `ListSettingsModalView.axaml`: replace the AGENT section body with
  `<ctl:AgentConfigEditor DataContext="{Binding Agent}" ShowAgentBrowse="True"/>`; the
  section-header "Reset agent settings" button binds `Agent.ResetAllCommand`. Remove the
  agent browse code-behind (moved into the control).
- `DetailsIslandViewModel`: `AgentSettings` becomes `AgentConfigEditorViewModel`
  (scope=Task). Preserve the call sites: ctor, `EffectiveMaxTurns`→`TurnsText`
  PropertyChanged hook, `IsRunning` push, `Dispose`, `Clear`, `TaskId`,
  `LoadForTaskAsync(entity, ct)`.
- `TaskHeaderBar.axaml`: replace the flyout field blocks with
  `<ctl:AgentConfigEditor DataContext="{Binding AgentSettings}"/>` (ShowAgentBrowse=false).
  Keep the gear button + heading.
- Delete `AgentSettingsSectionViewModel.cs`.

## Tests

- New `tests/ClaudeDo.Ui.Tests/ViewModels/AgentConfigEditorViewModelTests.cs`:
  - List scope: badges resolve override-vs-global; resets clear; `SaveAsync` builds the
    right `UpdateListConfigDto` (via `StubWorkerClient`).
  - Task scope: badges resolve override/list/global; `EffectiveMaxTurns`/
    `EffectiveSystemPromptHint` from list tier; resets clear; `SaveAsync` builds the right
    `UpdateTaskAgentSettingsDto`.
- `InheritanceResolverTests` unchanged (resolver untouched).
- Existing DetailsIsland* tests must stay green (they construct the VM but don't name the
  moved members).

## Acceptance

- `dotnet build -c Release` clean for Ui (+ App).
- `Ui.Tests` + `Localization.Tests` green.
- One editor VM + one control drive both List and Task; duplicated field/badge/reset
  logic deleted; `ResolveList` now has a real caller.
- Visual gap flagged: open List Settings → Agent, and a task's gear flyout — verify
  badges, ↺ resets, reset-all, agent browse (list only), system-prompt hint (task), and
  that list Save persists + task auto-saves.

## Commit

`refactor(agent-config): single AgentConfigEditor for list + task scopes`. Stage by
path. Commit this plan with it.
