From 68f461d0e1ef2dbef0dfa9acc4b5f6d8bb5f578f Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Wed, 22 Apr 2026 12:01:20 +0200 Subject: [PATCH] docs(specs): agent settings per list and per task UI reimplementation --- .../2026-04-22-agent-settings-ui-design.md | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-22-agent-settings-ui-design.md diff --git a/docs/superpowers/specs/2026-04-22-agent-settings-ui-design.md b/docs/superpowers/specs/2026-04-22-agent-settings-ui-design.md new file mode 100644 index 0000000..fe02cf8 --- /dev/null +++ b/docs/superpowers/specs/2026-04-22-agent-settings-ui-design.md @@ -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: )` 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.