docs(specs): agent settings per list and per task UI reimplementation
This commit is contained in:
162
docs/superpowers/specs/2026-04-22-agent-settings-ui-design.md
Normal file
162
docs/superpowers/specs/2026-04-22-agent-settings-ui-design.md
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
# 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.
|
||||||
Reference in New Issue
Block a user