# 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**: - `Model` — `opus` / `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 `ListsIslandView` → `ContextMenu` 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: )` option as the unset state - `System prompt` — TextBox with watermark showing effective inherited value when empty - `Agent file` — ComboBox prepended with `(inherit: )` 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.