diff --git a/docs/superpowers/plans/2026-04-22-agent-settings-ui.md b/docs/superpowers/plans/2026-04-22-agent-settings-ui.md new file mode 100644 index 0000000..1c8077f --- /dev/null +++ b/docs/superpowers/plans/2026-04-22-agent-settings-ui.md @@ -0,0 +1,1223 @@ +# Agent Settings UI Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Restore the ability to configure Model / SystemPrompt / AgentPath per list (via a new modal) and per task (via an expander in DetailsIsland), persisting through SignalR hub methods to the existing DB schema. + +**Architecture:** UI → new `WorkerHub` methods (`UpdateList`, `UpdateListConfig`, `UpdateTaskAgentSettings`, `GetListConfig`) → existing repositories in `ClaudeDo.Data` (schema already in place). Worker broadcasts `ListUpdated` so the lists island refreshes. Per-task settings auto-save on change, debounced. The DB columns/tables already exist; `TaskRunner` + `ClaudeArgsBuilder` already consume them. + +**Tech Stack:** .NET 8, Avalonia 12, CommunityToolkit.Mvvm, EF Core, SignalR (hub on `127.0.0.1:47821`), xUnit integration tests with real SQLite. + +**Build tip (from project memory):** `dotnet build ClaudeDo.slnx` fails on .NET 8. Build individual csproj files: +``` +dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj +dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj +dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj +dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj +dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj +``` + +--- + +## File Structure + +New files: +- `src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml` +- `src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml.cs` +- `src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs` +- `tests/ClaudeDo.Worker.Tests/Repositories/ListRepositoryDeleteConfigTests.cs` +- `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryAgentSettingsTests.cs` +- `tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs` + +Modified files: +- `src/ClaudeDo.Data/Repositories/ListRepository.cs` — add `DeleteConfigAsync` +- `src/ClaudeDo.Data/Repositories/TaskRepository.cs` — add `UpdateAgentSettingsAsync` +- `src/ClaudeDo.Worker/Hub/WorkerHub.cs` — add 4 methods + DTOs +- `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs` — add `ListUpdatedAsync` +- `src/ClaudeDo.Ui/Services/WorkerClient.cs` — add 4 methods + `ListUpdatedEvent` +- `src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs` (no changes expected, only for reference) +- `src/ClaudeDo.App/App.axaml.cs` (or DI extension file) — register `ListSettingsModalViewModel` +- `src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml` — add context menu + gear button +- `src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs` — add `OpenSettingsCommand`, subscribe `ListUpdated` +- `src/ClaudeDo.Ui/ViewModels/Islands/ListNavItemViewModel.cs` — expose list fields needed by modal +- `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` — add agent-settings fields + auto-save +- `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml` — add "Agent settings" expander +- `src/ClaudeDo.Data/CLAUDE.md` — refresh with new repo methods + ListConfigEntity +- `src/ClaudeDo.Worker/CLAUDE.md` — document new hub methods + `ListUpdated` +- `src/ClaudeDo.Ui/CLAUDE.md` — document list settings modal + details expander + +--- + +## Task 1: Add `ListRepository.DeleteConfigAsync` + +**Files:** +- Modify: `src/ClaudeDo.Data/Repositories/ListRepository.cs` +- Test: `tests/ClaudeDo.Worker.Tests/Repositories/ListRepositoryDeleteConfigTests.cs` + +The existing `ListRepository.SetConfigAsync` upserts but never deletes. The UI needs a way to fully remove a list config row (when the user clears all three agent fields). + +- [ ] **Step 1: Write the failing test** + +Create `tests/ClaudeDo.Worker.Tests/Repositories/ListRepositoryDeleteConfigTests.cs`: + +```csharp +using ClaudeDo.Data.Models; +using ClaudeDo.Data.Repositories; +using ClaudeDo.Worker.Tests.Infrastructure; +using Xunit; + +namespace ClaudeDo.Worker.Tests.Repositories; + +public sealed class ListRepositoryDeleteConfigTests : IAsyncLifetime +{ + private readonly TempDatabase _db = new(); + + public async Task InitializeAsync() => await _db.InitializeAsync(); + public async Task DisposeAsync() => await _db.DisposeAsync(); + + [Fact] + public async Task DeleteConfigAsync_RemovesExistingRow() + { + await using var ctx = _db.CreateContext(); + var repo = new ListRepository(ctx); + + var listId = Guid.NewGuid().ToString(); + await repo.AddAsync(new ListEntity + { + Id = listId, + Name = "L", + CreatedAt = DateTime.UtcNow, + }); + await repo.SetConfigAsync(new ListConfigEntity + { + ListId = listId, + Model = "opus", + SystemPrompt = "hello", + AgentPath = "/tmp/a.md", + }); + + var removed = await repo.DeleteConfigAsync(listId); + + Assert.True(removed); + Assert.Null(await repo.GetConfigAsync(listId)); + } + + [Fact] + public async Task DeleteConfigAsync_ReturnsFalseWhenAbsent() + { + await using var ctx = _db.CreateContext(); + var repo = new ListRepository(ctx); + + var removed = await repo.DeleteConfigAsync(Guid.NewGuid().ToString()); + + Assert.False(removed); + } +} +``` + +> If `TempDatabase` / `Infrastructure` namespace differs, match the existing test infrastructure pattern seen in other `*RepositoryTests` files in `tests/ClaudeDo.Worker.Tests/Repositories/`. Read one existing test file first to confirm the exact helper class name and SQLite setup call. + +- [ ] **Step 2: Run test to verify it fails** + +``` +dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter "FullyQualifiedName~ListRepositoryDeleteConfigTests" +``` +Expected: FAIL with `'ListRepository' does not contain a definition for 'DeleteConfigAsync'`. + +- [ ] **Step 3: Add the method** + +In `src/ClaudeDo.Data/Repositories/ListRepository.cs`, add below `SetConfigAsync`: + +```csharp +public async Task DeleteConfigAsync(string listId, CancellationToken ct = default) +{ + var affected = await _context.ListConfigs + .Where(c => c.ListId == listId) + .ExecuteDeleteAsync(ct); + return affected > 0; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +``` +dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter "FullyQualifiedName~ListRepositoryDeleteConfigTests" +``` +Expected: 2 tests passed. + +- [ ] **Step 5: Commit** + +``` +git add src/ClaudeDo.Data/Repositories/ListRepository.cs tests/ClaudeDo.Worker.Tests/Repositories/ListRepositoryDeleteConfigTests.cs +git commit -m "feat(data): add ListRepository.DeleteConfigAsync" +``` + +--- + +## Task 2: Add `TaskRepository.UpdateAgentSettingsAsync` + +**Files:** +- Modify: `src/ClaudeDo.Data/Repositories/TaskRepository.cs` +- Test: `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryAgentSettingsTests.cs` + +Focused method so the UI can update the three override columns without loading the entity. + +- [ ] **Step 1: Write the failing test** + +Create `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryAgentSettingsTests.cs`: + +```csharp +using ClaudeDo.Data.Models; +using ClaudeDo.Data.Repositories; +using ClaudeDo.Worker.Tests.Infrastructure; +using Xunit; + +namespace ClaudeDo.Worker.Tests.Repositories; + +public sealed class TaskRepositoryAgentSettingsTests : IAsyncLifetime +{ + private readonly TempDatabase _db = new(); + + public async Task InitializeAsync() => await _db.InitializeAsync(); + public async Task DisposeAsync() => await _db.DisposeAsync(); + + private async Task SeedTaskAsync() + { + await using var ctx = _db.CreateContext(); + var listId = Guid.NewGuid().ToString(); + var taskId = Guid.NewGuid().ToString(); + await new ListRepository(ctx).AddAsync(new ListEntity + { + Id = listId, Name = "L", CreatedAt = DateTime.UtcNow, + }); + await new TaskRepository(ctx).AddAsync(new TaskEntity + { + Id = taskId, ListId = listId, Title = "T", CreatedAt = DateTime.UtcNow, + }); + return taskId; + } + + [Fact] + public async Task UpdateAgentSettingsAsync_SetsAllThreeFields() + { + var taskId = await SeedTaskAsync(); + + await using var ctx = _db.CreateContext(); + var repo = new TaskRepository(ctx); + + await repo.UpdateAgentSettingsAsync(taskId, "opus", "system!", "/tmp/a.md"); + + var entity = await repo.GetByIdAsync(taskId); + Assert.NotNull(entity); + Assert.Equal("opus", entity!.Model); + Assert.Equal("system!", entity.SystemPrompt); + Assert.Equal("/tmp/a.md", entity.AgentPath); + } + + [Fact] + public async Task UpdateAgentSettingsAsync_NullsClearColumns() + { + var taskId = await SeedTaskAsync(); + + await using (var ctx = _db.CreateContext()) + { + await new TaskRepository(ctx).UpdateAgentSettingsAsync(taskId, "opus", "s", "/a.md"); + } + + await using (var ctx = _db.CreateContext()) + { + var repo = new TaskRepository(ctx); + await repo.UpdateAgentSettingsAsync(taskId, null, null, null); + + var entity = await repo.GetByIdAsync(taskId); + Assert.NotNull(entity); + Assert.Null(entity!.Model); + Assert.Null(entity.SystemPrompt); + Assert.Null(entity.AgentPath); + } + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +``` +dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter "FullyQualifiedName~TaskRepositoryAgentSettingsTests" +``` +Expected: FAIL with `'TaskRepository' does not contain a definition for 'UpdateAgentSettingsAsync'`. + +- [ ] **Step 3: Add the method** + +In `src/ClaudeDo.Data/Repositories/TaskRepository.cs`, add at the end of the `#region Status transitions` block (or a new `#region Agent settings` right after): + +```csharp +public async Task UpdateAgentSettingsAsync( + string taskId, + string? model, + string? systemPrompt, + string? agentPath, + CancellationToken ct = default) +{ + await _context.Tasks + .Where(t => t.Id == taskId) + .ExecuteUpdateAsync(s => s + .SetProperty(t => t.Model, model) + .SetProperty(t => t.SystemPrompt, systemPrompt) + .SetProperty(t => t.AgentPath, agentPath), ct); +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +``` +dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter "FullyQualifiedName~TaskRepositoryAgentSettingsTests" +``` +Expected: 2 tests passed. + +- [ ] **Step 5: Commit** + +``` +git add src/ClaudeDo.Data/Repositories/TaskRepository.cs tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryAgentSettingsTests.cs +git commit -m "feat(data): add TaskRepository.UpdateAgentSettingsAsync" +``` + +--- + +## Task 3: Hub methods + DTOs + `ListUpdated` broadcast + +**Files:** +- Modify: `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs` +- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs` +- Test: `tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs` + +Add four hub methods (`UpdateList`, `UpdateListConfig`, `UpdateTaskAgentSettings`, `GetListConfig`) plus a `ListUpdated` broadcast event. + +- [ ] **Step 1: Write the failing test** + +Create `tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs`: + +> Pattern notes: the worker test project usually calls hub methods by invoking repositories/services directly when no SignalR host is spun up. Read an existing hub-adjacent test (if one exists) to confirm. If no precedent, assert the underlying persistence via repo calls — the hub methods are thin wrappers. + +```csharp +using ClaudeDo.Data.Models; +using ClaudeDo.Data.Repositories; +using ClaudeDo.Worker.Tests.Infrastructure; +using Xunit; + +namespace ClaudeDo.Worker.Tests.Hub; + +// These tests exercise the hub behavior by invoking the underlying repository +// chain the hub calls. The hub itself is a thin wrapper around these repos; +// full SignalR integration testing is covered manually per the spec. +public sealed class AgentSettingsHubTests : IAsyncLifetime +{ + private readonly TempDatabase _db = new(); + public async Task InitializeAsync() => await _db.InitializeAsync(); + public async Task DisposeAsync() => await _db.DisposeAsync(); + + [Fact] + public async Task UpdateListConfig_AllNull_DeletesRow() + { + await using var ctx = _db.CreateContext(); + var repo = new ListRepository(ctx); + + var listId = Guid.NewGuid().ToString(); + await repo.AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow }); + await repo.SetConfigAsync(new ListConfigEntity + { + ListId = listId, Model = "opus", SystemPrompt = null, AgentPath = null, + }); + + // Simulate hub: all three null => delete. + const string? model = null; const string? sp = null; const string? ap = null; + if (model is null && sp is null && ap is null) + await repo.DeleteConfigAsync(listId); + else + await repo.SetConfigAsync(new ListConfigEntity + { + ListId = listId, Model = model, SystemPrompt = sp, AgentPath = ap, + }); + + Assert.Null(await repo.GetConfigAsync(listId)); + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +It actually passes since it only calls existing code. The purpose of this test is to encode the hub's "all-null → delete" contract so that Task 3's code changes don't break it. + +``` +dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter "FullyQualifiedName~AgentSettingsHubTests" +``` +Expected: 1 passed (the test is a spec/guardrail, not a red-green). + +- [ ] **Step 3: Add the broadcaster event** + +In `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs`, add a new method alongside the existing `TaskUpdatedAsync`: + +```csharp +public Task ListUpdatedAsync(string listId) => + _hub.Clients.All.SendAsync("ListUpdated", listId); +``` + +- [ ] **Step 4: Add DTOs + hub methods** + +In `src/ClaudeDo.Worker/Hub/WorkerHub.cs`, add these `record` DTOs at the top (alongside `ActiveTaskDto`, `AppSettingsDto`, etc.): + +```csharp +public record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType); +public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath); +public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath); +public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath); +``` + +Then add these methods to the `WorkerHub` class (place at the end, before the closing brace): + +```csharp +public async Task UpdateList(UpdateListDto dto) +{ + using var ctx = _dbFactory.CreateDbContext(); + var repo = new ListRepository(ctx); + var entity = await repo.GetByIdAsync(dto.Id); + if (entity is null) throw new HubException("list not found"); + + entity.Name = dto.Name; + entity.WorkingDir = string.IsNullOrWhiteSpace(dto.WorkingDir) ? null : dto.WorkingDir; + entity.DefaultCommitType = string.IsNullOrWhiteSpace(dto.DefaultCommitType) ? "chore" : dto.DefaultCommitType; + await repo.UpdateAsync(entity); + + await _broadcaster.ListUpdatedAsync(dto.Id); +} + +public async Task UpdateListConfig(UpdateListConfigDto dto) +{ + using var ctx = _dbFactory.CreateDbContext(); + var repo = new ListRepository(ctx); + + var model = Nullify(dto.Model); + var systemPrompt = Nullify(dto.SystemPrompt); + var agentPath = Nullify(dto.AgentPath); + + if (model is null && systemPrompt is null && agentPath is null) + { + await repo.DeleteConfigAsync(dto.ListId); + } + else + { + await repo.SetConfigAsync(new ListConfigEntity + { + ListId = dto.ListId, + Model = model, + SystemPrompt = systemPrompt, + AgentPath = agentPath, + }); + } + + await _broadcaster.ListUpdatedAsync(dto.ListId); +} + +public async Task GetListConfig(string listId) +{ + using var ctx = _dbFactory.CreateDbContext(); + var repo = new ListRepository(ctx); + var config = await repo.GetConfigAsync(listId); + if (config is null) return null; + return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath); +} + +public async Task UpdateTaskAgentSettings(UpdateTaskAgentSettingsDto dto) +{ + using var ctx = _dbFactory.CreateDbContext(); + var repo = new TaskRepository(ctx); + await repo.UpdateAgentSettingsAsync( + dto.TaskId, + Nullify(dto.Model), + Nullify(dto.SystemPrompt), + Nullify(dto.AgentPath)); + + await _broadcaster.TaskUpdatedAsync(dto.TaskId); +} + +private static string? Nullify(string? s) => string.IsNullOrWhiteSpace(s) ? null : s; +``` + +If `_broadcaster.TaskUpdatedAsync` has a different signature in the existing broadcaster, adapt the call to match — read `HubBroadcaster.cs` for the exact name. + +- [ ] **Step 5: Build worker** + +``` +dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj +``` +Expected: build succeeded, 0 errors. + +- [ ] **Step 6: Run the guardrail test** + +``` +dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter "FullyQualifiedName~AgentSettingsHubTests" +``` +Expected: passed. + +- [ ] **Step 7: Commit** + +``` +git add src/ClaudeDo.Worker/Hub/WorkerHub.cs src/ClaudeDo.Worker/Hub/HubBroadcaster.cs tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs +git commit -m "feat(worker): add hub methods for list and task agent settings" +``` + +--- + +## Task 4: WorkerClient — 4 new methods + `ListUpdatedEvent` + +**Files:** +- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs` + +Add client-side DTOs (mirroring the hub DTOs, or import from `ClaudeDo.Worker` if cross-project references allow — else redeclare as simple records in the UI project), client methods, and subscribe to the new `ListUpdated` event. + +- [ ] **Step 1: Inspect current WorkerClient shape** + +Open `src/ClaudeDo.Ui/Services/WorkerClient.cs` and find where `UpdateAppSettings`, `TaskUpdatedEvent`, and the hub event subscriptions are declared. The new code will mirror the exact pattern used there (naming, event signature, async wrapper). + +- [ ] **Step 2: Add client DTOs** + +At the top of `WorkerClient.cs` (or in a nested namespace if the file already has DTO records, e.g. `AppSettingsDto`), add: + +```csharp +public sealed record UpdateListClientDto(string Id, string Name, string? WorkingDir, string DefaultCommitType); +public sealed record UpdateListConfigClientDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath); +public sealed record UpdateTaskAgentSettingsClientDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath); +public sealed record ListConfigClientDto(string? Model, string? SystemPrompt, string? AgentPath); +``` + +Use the existing file's naming convention instead of "ClientDto" if the file already has a convention (e.g., `AppSettingsDto` with no suffix — then follow suit and just name them `UpdateListDto`, `UpdateListConfigDto`, etc. in the UI namespace). + +- [ ] **Step 3: Add the four client methods** + +Inside the `WorkerClient` class, alongside `UpdateAppSettings`: + +```csharp +public async Task UpdateListAsync(UpdateListClientDto dto, CancellationToken ct = default) +{ + await _hub.InvokeAsync("UpdateList", dto, ct); +} + +public async Task UpdateListConfigAsync(UpdateListConfigClientDto dto, CancellationToken ct = default) +{ + await _hub.InvokeAsync("UpdateListConfig", dto, ct); +} + +public async Task GetListConfigAsync(string listId, CancellationToken ct = default) +{ + return await _hub.InvokeAsync("GetListConfig", listId, ct); +} + +public async Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsClientDto dto, CancellationToken ct = default) +{ + await _hub.InvokeAsync("UpdateTaskAgentSettings", dto, ct); +} +``` + +- [ ] **Step 4: Subscribe to `ListUpdated` event** + +Find where other hub events are subscribed (usually in `WorkerClient`'s `ConfigureHub` / `StartAsync` / constructor-equivalent setup). Add a line mirroring the pattern used for `TaskUpdated`: + +```csharp +_hub.On("ListUpdated", listId => ListUpdatedEvent?.Invoke(listId)); +``` + +And declare the event alongside the other events (e.g. `TaskUpdatedEvent`): + +```csharp +public event Action? ListUpdatedEvent; +``` + +- [ ] **Step 5: Build UI** + +``` +dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj +``` +Expected: build succeeded, 0 errors. + +- [ ] **Step 6: Commit** + +``` +git add src/ClaudeDo.Ui/Services/WorkerClient.cs +git commit -m "feat(ui): WorkerClient supports list/task agent settings + ListUpdated event" +``` + +--- + +## Task 5: `ListSettingsModalViewModel` + +**Files:** +- Create: `src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs` +- Modify: `src/ClaudeDo.App/App.axaml.cs` (or wherever DI is registered — look for `AddTransient` / `AddSingleton` to find the registration block) + +- [ ] **Step 1: Create the ViewModel** + +Write `src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs`: + +```csharp +using System.Collections.ObjectModel; +using ClaudeDo.Data.Models; +using ClaudeDo.Ui.Services; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace ClaudeDo.Ui.ViewModels.Modals; + +public sealed partial class ListSettingsModalViewModel : ViewModelBase +{ + private readonly WorkerClient _worker; + + // Set by caller before Show + public string ListId { get; set; } = ""; + + [ObservableProperty] private string _name = ""; + [ObservableProperty] private string _workingDir = ""; + [ObservableProperty] private string _defaultCommitType = "chore"; + + [ObservableProperty] private string _selectedModel = "(default)"; + [ObservableProperty] private string _systemPrompt = ""; + [ObservableProperty] private AgentInfo? _selectedAgent; + + public ObservableCollection ModelOptions { get; } = new() + { + "(default)", "sonnet", "opus", "haiku", + }; + + public ObservableCollection CommitTypeOptions { get; } = new() + { + "chore", "feat", "fix", "refactor", "docs", "test", "ci", "perf", "style", "build", + }; + + public ObservableCollection Agents { get; } = new(); + + public Action? CloseAction { get; set; } + + public ListSettingsModalViewModel(WorkerClient worker) + { + _worker = worker; + } + + public async Task LoadAsync( + string listId, + string name, + string? workingDir, + string defaultCommitType, + CancellationToken ct = default) + { + ListId = listId; + Name = name; + WorkingDir = workingDir ?? ""; + DefaultCommitType = string.IsNullOrWhiteSpace(defaultCommitType) ? "chore" : defaultCommitType; + + Agents.Clear(); + Agents.Add(new AgentInfo("(none)", "", "")); + var agents = await _worker.GetAgentsAsync(); + foreach (var a in agents) Agents.Add(a); + + var config = await _worker.GetListConfigAsync(listId, ct); + SelectedModel = string.IsNullOrWhiteSpace(config?.Model) ? "(default)" : config!.Model!; + SystemPrompt = config?.SystemPrompt ?? ""; + SelectedAgent = string.IsNullOrWhiteSpace(config?.AgentPath) + ? Agents[0] + : (Agents.FirstOrDefault(a => a.Path == config!.AgentPath) ?? Agents[0]); + } + + [RelayCommand] + private async Task SaveAsync() + { + var model = SelectedModel == "(default)" ? null : SelectedModel; + var sp = string.IsNullOrWhiteSpace(SystemPrompt) ? null : SystemPrompt; + var ap = SelectedAgent is null || string.IsNullOrWhiteSpace(SelectedAgent.Path) ? null : SelectedAgent.Path; + + await _worker.UpdateListAsync(new UpdateListClientDto( + ListId, + string.IsNullOrWhiteSpace(Name) ? "Untitled" : Name, + string.IsNullOrWhiteSpace(WorkingDir) ? null : WorkingDir, + DefaultCommitType)); + + await _worker.UpdateListConfigAsync(new UpdateListConfigClientDto( + ListId, model, sp, ap)); + + CloseAction?.Invoke(); + } + + [RelayCommand] + private void Cancel() => CloseAction?.Invoke(); + + [RelayCommand] + private void ResetAgentSettings() + { + SelectedModel = "(default)"; + SystemPrompt = ""; + SelectedAgent = Agents.Count > 0 ? Agents[0] : null; + } +} +``` + +> Match the exact DTO record names used in `WorkerClient.cs` from Task 4. If you named them `UpdateListDto` (without `Client` suffix), update the `new UpdateListClientDto(...)` calls above to `new UpdateListDto(...)`. + +- [ ] **Step 2: Register in DI** + +Find the file that registers modal ViewModels (search the repo for `AddTransient` or `AddSingleton` — likely in `src/ClaudeDo.App/App.axaml.cs` or a DI extension). Add: + +```csharp +services.AddTransient(); +``` + +- [ ] **Step 3: Build UI** + +``` +dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj +dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj +``` +Expected: both build succeeded, 0 errors. + +- [ ] **Step 4: Commit** + +``` +git add src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs src/ClaudeDo.App/App.axaml.cs +git commit -m "feat(ui): add ListSettingsModalViewModel" +``` + +--- + +## Task 6: `ListSettingsModalView` + +**Files:** +- Create: `src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml` +- Create: `src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml.cs` + +- [ ] **Step 1: Create the view XAML** + +Write `src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml`: + +```xml + + + +