# 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: <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.
