5.9 KiB
Settings Modal — Design
Date: 2026-04-21 Status: Approved for planning
Goal
Add a general-settings modal reachable from the ⋯ button in the user footer of the Lists island (ListsIslandView.axaml:68). The modal exposes app-wide defaults for Claude runs, worktree behavior, and maintenance actions (cleanup / force-remove worktrees), plus a read-only "About" section with paths.
Scope
In scope
- Claude defaults: instructions, model, max turns, permission mode
- Worktree defaults: strategy, central root, auto-cleanup toggle + days
- Worktree maintenance actions: cleanup finished, force-remove all
- About section: version, data/logs/config paths with "Open in Explorer"
- Single settings row persisted in SQLite
- SignalR surface for read/update + maintenance
ClaudeArgsBuildermerge behavior for per-task overrides
Out of scope
- Worker-side infrastructure settings (hub URL, auto-start worker) — stays in
worker.config.json - Per-task "inherit defaults" toggle (always inherit; task values override per rule below)
- Any UI-layer tests (project has none today)
Architecture
Persistence
New single-row entity AppSettingsEntity (Id = 1) in SQLite. Access via new AppSettingsRepository with GetAsync / UpdateAsync. Seeded by a new EF migration AddAppSettings.
Fields:
| Column | Type | Seed default |
|---|---|---|
DefaultClaudeInstructions |
text | "" |
DefaultModel |
string | sonnet |
DefaultMaxTurns |
int | 100 |
DefaultPermissionMode |
string | acceptEdits |
WorktreeStrategy |
string | sibling |
CentralWorktreeRoot |
string? | null |
WorktreeAutoCleanupEnabled |
bool | false |
WorktreeAutoCleanupDays |
int | 7 |
Rationale for DB over worker.config.json: transactional writes from the UI, no file-watcher dance, and the Worker already uses IDbContextFactory<ClaudeDoDbContext>.
Merge rules for per-task overrides
ClaudeArgsBuilder gains a dependency on AppSettingsRepository and merges at build time:
- Instructions:
global + "\n\n" + task(skip separator if either side empty) - Model / MaxTurns / PermissionMode:
task ?? global(task value wins when set)
No TaskEntity schema change.
SignalR surface (new hub methods)
| Method | Returns | Notes |
|---|---|---|
GetAppSettings() |
AppSettingsDto |
Single row |
UpdateAppSettings(dto) |
void | Full-row replace |
CleanupFinishedWorktrees() |
int removedCount |
Skips Active |
ResetAllWorktrees() |
{ removed, tasksAffected } |
Fails if any task is Running |
Maintenance logic lives in a new WorktreeMaintenanceService in the Worker; the hub stays thin. Service uses existing GitService + WorktreeRepository.
Running-task guard: ResetAllWorktrees() checks for any Running tasks before touching anything. If present, returns an error — the modal surfaces "Cannot force-remove: N task(s) still running. Cancel them first."
Affected worktrees after force-remove are marked Discarded in WorktreeRepository.
UI
Entry point
ListsIslandView.axaml:68 ⋯ button binds to a new OpenSettingsCommand on IslandsShellViewModel. Command resolves SettingsModalViewModel and shows SettingsModalView via the existing modal pattern (TaskCompletionSource<bool> on save/cancel — same as WorktreeModalView / DiffModalView).
Layout
Single scrollable modal, ~560 px wide, matches existing modal chrome (header eyebrow, monospace labels, close affordance). No tabs.
Sections (top to bottom):
- CLAUDE DEFAULTS — instructions textarea (6 lines), model picker, max-turns numeric, permission-mode picker
- WORKTREES — strategy picker, central-root folder picker, auto-cleanup toggle + days, then the two maintenance buttons
- ABOUT — version (read-only), data folder / logs folder / worker.config path, each with "Open in Explorer" icon button
Footer: [ Cancel ] [ Save ], right-aligned. Save button disabled while the form is invalid.
Destructive action UX
- Cleanup finished — single click, inline result line under the button (
"Removed 3 worktree(s)."), auto-clears ~4 s - Force-remove all — click reveals an inline confirm row: "Remove ALL N worktrees? Uncommitted work will be lost." with red
Remove Alland neutralCancel. Two-click confirm, no typed string (matches the delete-task confirm already in the app)
Both run against the worker over SignalR and leave the modal open on completion.
Open-in-Explorer
Uses Process.Start("explorer.exe", path). Windows-only is acceptable (app ships Windows-only via WPF installer).
Validation
Modal-side only, block Save:
- Max turns: integer 1–200
- Auto-cleanup days: integer 1–365 (required only when toggle is on)
- Central root: required when Strategy = Central; must be an existing directory
- Instructions: no length cap
Invalid fields get a red eyebrow with the specific error text.
Testing (ClaudeDo.Worker.Tests)
AppSettingsRepositoryTests— round-tripGet/Updateon real SQLiteClaudeArgsBuilderTests— four merge cases: both empty, only global, only task, both set (prepend + separator behavior)WorktreeMaintenanceServiceTests— real git worktree fixtures:- cleanup skips
Active, removesMerged/Discarded/Kept - force-remove fails while any task is
Running - force-remove succeeds otherwise and flips affected worktrees to
Discarded
- cleanup skips
No UI-layer tests (project has none today).
Build order (high level)
- Data: entity + configuration + migration + repository
- Worker:
WorktreeMaintenanceService+ClaudeArgsBuilderwiring + hub methods + DTO - UI: SignalR client methods on
WorkerClient,SettingsModalViewModel,SettingsModalView - Wire ⋯ button on
ListsIslandView→IslandsShellViewModel.OpenSettingsCommand - Tests (Worker.Tests)
Detailed step-by-step sequencing belongs in the implementation plan, not here.