docs: add design + plan for tabbed settings + Prime Claude
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2424
docs/superpowers/plans/2026-04-28-tabbed-settings-prime-claude.md
Normal file
2424
docs/superpowers/plans/2026-04-28-tabbed-settings-prime-claude.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,272 @@
|
||||
# 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 Mon–Fri 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).
|
||||
Reference in New Issue
Block a user