Files
ClaudeDo/docs/superpowers/specs/2026-04-22-agent-settings-ui-design.md

8.1 KiB

Design: Agent settings per list and per task (UI reimplementation)

Date: 2026-04-22 Status: Approved by user, implementation pending

Problem

During the recent UI rework, the editors for per-list and per-task agent settings were lost. The data layer and Worker still support them (TaskEntity.Model/SystemPrompt/AgentPath, ListConfigEntity, TaskRunner + ClaudeArgsBuilder), but the UI has zero references to these fields. Users currently cannot set model, custom system prompt, or agent file from the app.

Goal

Restore the ability to configure, per list and per task:

  • Modelopus / sonnet / haiku / inherit
  • SystemPrompt — free-text, appended to Claude's system prompt
  • AgentPath — selection from agent files discovered by the Worker under ~/.todo-app/agents/*.md

Per-task values override per-list values. Per-list values override global defaults from worker.config.json. This cascade is already implemented in TaskRunner.

Non-goals

  • Agent file CRUD in the UI (read-only picker only)
  • --allowedTools, --bare, permission modes (deferred, matches existing worker design notes)
  • Any schema migration — the DB already has the required columns/tables

Approach

Update-and-broadcast over SignalR. UI never touches the DB directly; all writes go through new WorkerHub methods. Worker persists via repositories, then broadcasts ListUpdated / TaskUpdated so connected clients refresh.

Sections

1. Data layer additions

New repository src/ClaudeDo.Data/Repositories/ListConfigRepository.cs:

  • GetByListIdAsync(string listId, CancellationToken) -> ListConfigEntity?
  • UpsertAsync(ListConfigEntity, CancellationToken) -> ListConfigEntity
  • DeleteAsync(string listId, CancellationToken) -> bool

New method on ListRepository:

  • UpdateAsync(ListEntity, CancellationToken) — updates Name, WorkingDir, DefaultCommitType. Included because the consolidated list-settings modal edits these alongside agent fields.

New method on TaskRepository:

  • UpdateAgentSettingsAsync(string taskId, string? model, string? systemPrompt, string? agentPath, CancellationToken) -> bool
    • null values mean "inherit" (column is nulled out in DB).
    • Kept as a narrow method to avoid widening UpdateAsync.

DI: register ListConfigRepository alongside other repos.

No migration — all columns/tables already exist.

2. SignalR hub surface

New DTOs in src/ClaudeDo.Data/Dtos/ (project existing DTO pattern):

  • UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType)
  • UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath)
  • UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath)

New methods on WorkerHub:

  • UpdateList(UpdateListDto dto) — calls ListRepository.UpdateAsync, then broadcasts ListUpdated(listId).
  • UpdateListConfig(UpdateListConfigDto dto) — upserts via ListConfigRepository.UpsertAsync, broadcasts ListUpdated(listId). If all three fields are null, calls DeleteAsync instead so the row doesn't linger empty.
  • UpdateTaskAgentSettings(UpdateTaskAgentSettingsDto dto) — calls TaskRepository.UpdateAgentSettingsAsync, broadcasts TaskUpdated(taskId) (existing event).

New broadcast method on HubBroadcaster:

  • Task ListUpdatedAsync(string listId) => _hub.Clients.All.SendAsync("ListUpdated", listId);

Loader endpoint to add:

  • GetListConfig(string listId) — returns (string? Model, string? SystemPrompt, string? AgentPath) record, or null if no row. Used by ListSettingsModal and by DetailsIslandViewModel for effective-value inheritance display. Existing GetLists / GetTasks already cover the rest.

3. UI — ListSettingsModal

New files:

  • src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml + .axaml.cs
  • src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs

Entry points:

  • Right-click on a list row in ListsIslandViewContextMenu with "Settings…" item
  • Gear button on the list row (visible on hover/selected)

Layout: vertical stack with two grouped sections.

General

  • Name — TextBox (required, non-empty)
  • Working directory — TextBox + "Browse…" button (folder picker)
  • Default commit type — ComboBox populated with chore, feat, fix, refactor, docs, test, ci, perf, style, build

Agent

  • Model — ComboBox: (default), sonnet, opus, haiku (selecting (default) sends null)
  • System prompt — multi-line TextBox with 4-row min height; empty = null
  • Agent file — ComboBox populated via WorkerClient.GetAgentsAsync(), first item (none); tooltip shows each agent's Description. Empty selection = null.
  • Reset agent settings button — clears Model/SystemPrompt/AgentPath in the form (save then sends null triple → backend DeleteAsync).

Commands:

  • SaveCommand — validates, calls UpdateList then UpdateListConfig, closes modal on success.
  • CancelCommand — closes without saving.

Loading: on open, ViewModel calls GetListConfig and populates fields; missing row means all three agent fields start empty.

ViewModel uses [ObservableProperty] / [RelayCommand] per project convention.

4. UI — DetailsIsland per-task agent section

Modify DetailsIslandView.axaml + DetailsIslandViewModel.cs.

Add an Expander titled "Agent settings (overrides)", collapsed by default, below the existing task detail content.

Fields (same control types as ListSettingsModal's Agent section):

  • Model — ComboBox prepended with (inherit: <effective>) option as the unset state
  • System prompt — TextBox with watermark showing effective inherited value when empty
  • Agent file — ComboBox prepended with (inherit: <effective>)

Effective-value computation:

  • Server-side would be more accurate but requires a new hub call. For v1, UI computes locally: if task field is null, show the list's config value; if that's also null, show (global default).
  • DetailsIslandViewModel already has access to the selected TaskDto + list; add list-config loading when task selection changes.

Persistence: auto-save on field change (debounced 300ms) calling UpdateTaskAgentSettings. No separate Save button — matches "settings" feel.

If the task is currently Running, fields are read-only (disabling controls). Agent settings only apply to the next invocation.

5. Testing

xUnit integration tests in tests/ClaudeDo.Worker.Tests against a real SQLite temp DB:

  • ListConfigRepositoryTests
    • UpsertAsync_InsertsWhenAbsent
    • UpsertAsync_UpdatesWhenPresent
    • DeleteAsync_RemovesRow
    • GetByListIdAsync_ReturnsNullWhenAbsent
  • ListRepositoryTests.UpdateAsync_UpdatesMutableFields
  • TaskRepositoryTests.UpdateAgentSettingsAsync_NullsClearColumns
  • WorkerHubTests (if present pattern; otherwise via direct service call):
    • UpdateListConfig_AllNull_DeletesRow
    • UpdateTaskAgentSettings_PersistsAndBroadcasts

No UI tests — project has no UI test project. Build-time compile check is the only UI gate.

Manual verification checklist

  1. Open app, right-click a list → "Settings…" opens modal with correct current values.
  2. Change model to opus, save, reopen → model persists.
  3. Set system prompt on list, create task in list, run it → log confirms --append-system-prompt was passed.
  4. Select task, set per-task Model = haiku, run → log confirms --model haiku overrides list value.
  5. Unset per-task Model → effective falls back to list's model.
  6. Click "Reset agent settings" on list → row removed, tasks fall back to global defaults.
  7. Running task: DetailsIsland agent fields disabled.

Risks / open questions

  • Refresh propagation: ListUpdated is a new event; IslandsShellViewModel must subscribe and re-fetch. Any missed subscriber means stale UI. Mitigated by following the existing TaskUpdated pattern exactly.
  • Working-dir browser: Avalonia folder picker API needs a TopLevel; pass via StorageProvider. Standard pattern in Avalonia 12.
  • Conventional-commit-type list: hardcoded in ComboBox — acceptable, matches existing CommitType defaults.