Files
ClaudeDo/docs/superpowers/specs/2026-04-28-tabbed-settings-prime-claude-design.md
Mika Kuns 2ff0971dce docs: add design + plan for tabbed settings + Prime Claude
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 08:46:43 +02:00

273 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tabbed Settings + Prime Claude — Design
**Date:** 2026-04-28
**Status:** Draft for review
## Goal
Two related UI changes:
1. Restructure the existing **Settings modal** from a single scrollable stack into a `TabControl` with focused tabs. Move the read-only "About" content out of Settings entirely, into a new modal accessible from the existing Help menu.
2. Add a new **Prime Claude** tab where the user defines date-bounded daily schedules. At each scheduled time, the worker fires a single non-interactive `claude -p "ping" --max-turns 1` call to start Claude's 5-hour usage window early — "priming" the day.
## Scope
### In scope
- Settings tabbed UI with 4 tabs: General, Worktrees, Files, Prime Claude.
- New About modal opened from `MainWindow` Help menu.
- New `PrimeSchedules` table, repository, EF migration.
- New `PrimeScheduler` background service (event-driven, no polling).
- New SignalR hub methods + client wiring.
- Footer notification on prime fire (success/failure) via `StatusBarView`.
- 30-minute catch-up window on app launch / wake.
- Tests: scheduler unit tests, tab VM tests.
### Out of scope
- Auto-start ClaudeDo at OS boot.
- Multiple pings per day per schedule.
- Per-schedule prompt customization (schema reserves the column for future use).
- Holiday / calendar integration.
- Toast notifications, sound, OS-level notifications.
## Settings tab layout
| Tab | Contents (existing sections, no field changes) |
|---|---|
| **General** | Claude Defaults: instructions, model, max turns, permission mode |
| **Worktrees** | Strategy, central root, auto-cleanup, Cleanup button, Force-remove confirm flow |
| **Files** | Agents (Restore default agents) + Prompts (System / Planning / Agent open-in-editor rows) |
| **Prime Claude** | New — schedule list + add button (see below) |
- Window stays 580×760, custom title bar preserved.
- Footer (Save / Cancel) preserved; Save iterates per-tab VMs.
- Status / validation strip stays above the footer.
- Tab strip uses the existing section-label style for headers (mono, 10pt, letter-spacing 1.4) so it visually matches the current aesthetic.
## About modal
New `AboutModalView` + `AboutModalViewModel`:
- Same 4 rows as today's About section: Version, Data folder, Logs folder, Worker config — each with an Open button.
- Compact dialog (~480×280), same chrome as `SettingsModalView`.
- Wired into `MainWindow` Help menu as a new `<MenuItem Header="About…">` next to "Check for updates".
- About content removed from `SettingsModalView` entirely (cleaner: not a setting).
## Prime Claude tab — UI
```
┌────────────────────────────────────────────────────────────────┐
│ Prime your Claude usage window each morning by firing a single │
│ non-interactive `ping` call at a chosen time. Only runs while │
│ ClaudeDo is open. If the app starts within 30 min of the target │
│ time, the ping fires immediately (catch-up window). │
├────────────────────────────────────────────────────────────────┤
│ ☑ May 5, 2026 → Jun 30, 2026 07:00 MonFri last: today ✕│
│ ☐ Jul 1, 2026 → Jul 7, 2026 09:30 All days — ✕│
├────────────────────────────────────────────────────────────────┤
│ [+ Add schedule] │
└────────────────────────────────────────────────────────────────┘
```
Per-row controls:
- Enabled checkbox (`Enabled`)
- Start date picker (`StartDate`)
- End date picker (`EndDate`)
- Time-of-day field (`TimeOfDay`, 24h, e.g. `07:00`)
- Workdays-only checkbox (`WorkdaysOnly`)
- Last run label (`{LastRunAt:g}` or `—` if null)
- Delete button (✕, with inline confirm bar matching the Worktrees pattern)
`+ Add schedule` appends a new row pre-filled with: today, today + 30 days, `07:00`, `WorkdaysOnly = true`, `Enabled = true`.
Validation per row:
- `StartDate <= EndDate`
- `TimeOfDay` parses as `HH:mm`
- `EndDate >= today` (else mark row disabled-looking + tooltip "expired")
Persistence: rows save with the rest of the modal on **Save**. On Save, `PrimeClaudeTabViewModel` diffs in-memory rows against the loaded snapshot and emits one hub call per change: `UpsertPrimeSchedule` for new/edited rows, `DeletePrimeSchedule` for removed rows. Cancel discards in-memory edits. No per-row autosave.
## Data model
New EF Core entity `PrimeScheduleEntity` in `ClaudeDo.Data/Models/`:
```csharp
public class PrimeScheduleEntity
{
public Guid Id { get; set; }
public DateOnly StartDate { get; set; }
public DateOnly EndDate { get; set; }
public TimeSpan TimeOfDay { get; set; } // local clock
public bool WorkdaysOnly { get; set; }
public bool Enabled { get; set; }
public DateTimeOffset? LastRunAt { get; set; }
public string? PromptOverride { get; set; } // reserved, always null today
public DateTimeOffset CreatedAt { get; set; }
}
```
- New `PrimeScheduleConfiguration : IEntityTypeConfiguration<PrimeScheduleEntity>` in `Configuration/`.
- New repository `PrimeScheduleRepository` matching the existing async + CancellationToken pattern. Methods: `ListAsync`, `GetAsync(id)`, `UpsertAsync(entity)`, `DeleteAsync(id)`, `UpdateLastRunAsync(id, when)`.
- EF migration `AddPrimeSchedules` (auto-named per existing migration history).
## Worker scheduler — `PrimeScheduler`
New folder `ClaudeDo.Worker/Prime/`. Class hierarchy:
- `PrimeScheduler : BackgroundService` — event-driven loop.
- `IPrimeRunner` / `PrimeRunner` — fires the actual `claude -p "ping" --max-turns 1` call. Injected so tests can fake it.
- `IPrimeClock` / `PrimeClock``DateTimeOffset Now { get; }`. Faked in tests.
- `PrimeSchedulerOptions``CatchUpWindow = TimeSpan.FromMinutes(30)`. Hardcoded today; typed for swappability.
### Loop
```text
while not cancelled:
next = ComputeNextDue(now) # null if no enabled schedules
if next is null:
await wait-on-signal # blocks until schedules change
continue
delay = max(0, next.At - now)
try:
await Task.Delay(delay, linkedToken) # cancellable by signal
catch OperationCanceledException:
continue # schedules changed → recompute
await Fire(next.Schedule)
```
`ComputeNextDue(now)`:
- For each enabled schedule:
- Determine the next eligible date `d >= today` within `[StartDate, EndDate]`, honoring `WorkdaysOnly`.
- Skip the day if `LastRunAt.LocalDate == today` (already fired today).
- Build `target = d.At(TimeOfDay)` in local time.
- Apply catch-up: if `target < now <= target + 30min` and not already fired today, target = `now` (fire immediately).
- If `target < now` (past catch-up window) and `d == today`, advance `d` to next eligible date.
- Return the schedule with the smallest `target`.
### Signal source
`IPrimeScheduleSignal` — a thin abstraction wrapping a `CancellationTokenSource` reset. The hub calls `Signal()` on:
- App start (initial recompute is implicit — service first-run computes immediately).
- After `UpsertPrimeSchedule` / `DeletePrimeSchedule`.
- After a successful fire (so the next-due is recomputed without polling).
### Fire
`PrimeRunner.FireAsync(schedule, ct)`:
1. Resolve `claude` executable via existing `ClaudeProcess` discovery.
2. Spawn with `cwd = Paths.AppDataRoot()`, args `["-p", "ping", "--max-turns", "1"]`. No worktree, no task entity, no list/tag side effects.
3. Capture stdout/stderr; success = exit 0 within a 60s timeout.
4. On finish: `await PrimeScheduleRepository.UpdateLastRunAsync(id, now)`, append a one-line summary to `~/.todo-app/logs/prime.log`, broadcast `PrimeFired(success, message, timestamp)` via `HubBroadcaster`.
Failure modes (network, auth, executable missing) → broadcast a failure message; `LastRunAt` still stamped so the day doesn't keep retrying.
## SignalR / IPC
### Hub methods (`WorkerHub`)
```csharp
Task<IReadOnlyList<PrimeScheduleDto>> ListPrimeSchedules();
Task<PrimeScheduleDto> UpsertPrimeSchedule(PrimeScheduleDto dto);
Task DeletePrimeSchedule(Guid id);
```
DTO mirrors entity minus `CreatedAt` (server-managed).
### Hub events (broadcast)
```csharp
event PrimeFired(Guid scheduleId, bool success, string message, DateTimeOffset firedAt);
```
The `scheduleId` lets an open Settings modal update the matching row's `LastRunAt` without a full reload. No separate `PrimeSchedulesChanged` event — Settings is the only writer, so the modal's own VM state is authoritative until Save.
`WorkerClient` adds matching async methods + the event handler.
## UI wiring
### ViewModel split
`SettingsModalViewModel` stops holding field properties directly and becomes a coordinator:
```csharp
public sealed partial class SettingsModalViewModel
{
public GeneralSettingsTabViewModel General { get; }
public WorktreesSettingsTabViewModel Worktrees { get; }
public FilesSettingsTabViewModel Files { get; }
public PrimeClaudeTabViewModel Prime { get; }
[RelayCommand] private async Task Save() { ... iterate tabs, call SaveAsync on each ... }
}
```
Each tab VM:
- Owns its observable properties.
- Has `Task LoadAsync()` and `Task SaveAsync()` (or returns a partial DTO the coordinator merges).
- Owns its own validation, surfaces `ValidationError`.
`PrimeClaudeTabViewModel`:
- `ObservableCollection<PrimeScheduleRowViewModel> Rows`
- `[RelayCommand] AddSchedule()` / `RemoveSchedule(id)`
- Subscribes to `WorkerClient.PrimeSchedulesChanged` / `PrimeFired` to keep rows fresh while modal is open.
### Footer notification
`StatusBarViewModel`:
- New `string? PrimeStatus` property.
- Subscribes to `WorkerClient.PrimeFired`.
- On event: set `PrimeStatus`, start a `DispatcherTimer` for 5s, clear on tick.
- `StatusBarView` gets a `TextBlock` bound to `PrimeStatus`, right-aligned, dim-foreground, only visible when non-empty.
Format: `"✓ Primed Claude at 07:01"` or `"⚠ Prime failed: <reason>"`.
### About wiring
- `MainWindowViewModel` adds `[RelayCommand] OpenAbout()` — opens `AboutModalView` via the existing dialog factory pattern.
- `MainWindow.axaml` Help menu gains `<MenuItem Header="About…" Command="{Binding OpenAboutCommand}"/>`.
## Tests
### `ClaudeDo.Worker.Tests/Prime/PrimeSchedulerTests.cs`
Real SQLite, fake `IPrimeClock`, fake `IPrimeRunner`. Cases:
- Fires once at exact target time.
- Fires immediately on startup if within catch-up window.
- Skips firing if past catch-up window (waits for next eligible day).
- Honors `WorkdaysOnly` (no fire on Sat/Sun).
- Honors date range (no fire before StartDate, none after EndDate).
- Idempotent: doesn't double-fire if `LastRunAt` is today.
- Recomputes on signal (upsert mid-wait).
- Disabling a schedule mid-wait recomputes.
### `ClaudeDo.Ui.Tests/ViewModels/PrimeClaudeTabViewModelTests.cs`
Cases:
- Add row appends with sensible defaults.
- Remove row removes from collection.
- Validation: StartDate > EndDate flags row as invalid.
- Save serializes all rows to repository in one batch.
- `PrimeFired` event updates the matching row's `LastRunAt`.
### `ClaudeDo.Ui.Tests/ViewModels/StatusBarViewModelTests.cs` (extend existing if present, else new)
- `PrimeFired` sets `PrimeStatus` and clears it after 5s (use a fake `IDispatcherTimer` or an injectable delay).
## Migration / rollout
- Single EF migration `AddPrimeSchedules`. Existing DBs upgrade on next launch via the existing migration runner (no manual step).
- No data backfill — table starts empty. Users add schedules manually via the new tab.
- Backwards compatibility for `AppSettingsEntity`: untouched.
## Risks & mitigations
| Risk | Mitigation |
|---|---|
| App is closed at scheduled time | 30 min catch-up on launch; explicit copy in tab explains the limitation. |
| Clock/timezone change while waiting | `Task.Delay` fires on monotonic time; recompute after each fire catches drift on next iteration. Acceptable for a 5h-window primer. |
| Claude CLI hangs | 60s timeout on the spawn; failure stamped + broadcast. |
| Multiple ClaudeDo instances on same machine | Out of scope (existing app already assumes single instance via fixed SignalR port). |
| User edits schedule while scheduler is mid-fire | Fire completes, then signal triggers recompute. No race — `UpdateLastRunAsync` is the last write. |
## Open questions
None at design time. Implementation may surface small details (e.g. exact Avalonia controls for date/time pickers — likely `CalendarDatePicker` + a `TextBox` masked to `HH:mm` since Avalonia 12 has no built-in TimePicker on all platforms).