Files
ClaudeDo/docs/superpowers/plans/2026-06-19-unify-agent-config.md

7.2 KiB

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 UpdateListConfigDtoUpdateListConfigAsync; Task builds UpdateTaskAgentSettingsDtoUpdateTaskAgentSettingsAsync. 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). LoadAsyncAgent.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, EffectiveMaxTurnsTurnsText 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.