Commit Graph

2 Commits

Author SHA1 Message Date
Michael Jolley
4337f8e5ff CmdPal: Make settings and app state immutable (#46451)
## Summary
 
 This PR refactors CmdPal settings/state to be immutable end-to-end.
 
 ### Core changes
 - Convert model types to immutable records / init-only properties:
   - `SettingsModel`
   - `AppStateModel`
   - `ProviderSettings`
   - `DockSettings`
   - `RecentCommandsManager`
- supporting settings types (fallback/hotkey/alias/top-level
hotkey/history items, etc.)
- Replace mutable collections with immutable equivalents where
appropriate:
   - `ImmutableDictionary<,>`
   - `ImmutableList<>`
 - Move mutation flow to atomic service updates:
- `ISettingsService.UpdateSettings(Func<SettingsModel, SettingsModel>)`
   - `IAppStateService.UpdateState(Func<AppStateModel, AppStateModel>)`
- Update ViewModels/managers/services to use copy-on-write (`with`)
patterns instead of in-place
mutation.
- Update serialization context + tests for immutable model graph
compatibility.
 
 ## Why
 
Issue #46437 is caused by mutable shared state being updated from
different execution paths/threads,
leading to race-prone behavior during persistence/serialization.
 
By making settings/app state immutable and using atomic swap/update
patterns, we remove in-place
mutation and eliminate this class of concurrency bug.
 
 ## Validation
 
 - Built successfully:
   - `Microsoft.CmdPal.UI.ViewModels`
   - `Microsoft.CmdPal.UI`
   - `Microsoft.CmdPal.UI.ViewModels.UnitTests`
 - Updated unit tests for immutable update patterns.
 
 Fixes #46437

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-27 17:54:58 +00:00
Michael Jolley
86115a54f6 CmdPal: Extract persistence services from SettingsModel and AppStateModel (#46312)
## Summary of the Pull Request

Extracts persistence (load/save) logic from `SettingsModel` and
`AppStateModel` into dedicated service classes, following the
single-responsibility principle. Consumers now interact with
`ISettingsService` and `IAppStateService` instead of receiving raw model
objects through DI.

**New services introduced:**
- `IPersistenceService` / `PersistenceService` — generic `Load<T>` /
`Save<T>` with AOT-compatible `JsonTypeInfo<T>`, ensures target
directory exists before writing
- `ISettingsService` / `SettingsService` — loads settings on
construction, runs migrations, exposes `Settings` property and
`SettingsChanged` event
- `IAppStateService` / `AppStateService` — loads state on construction,
exposes `State` property and `StateChanged` event

**Key changes:**
- `SettingsModel` and `AppStateModel` are now pure data models — all
file I/O, migration, and directory management removed
- Raw `SettingsModel` and `AppStateModel` removed from DI container;
consumers receive the appropriate service
- `IApplicationInfoService.ConfigDirectory` injected into services for
config path resolution (no more hardcoded `Utilities.BaseSettingsPath`)
- ~30 consumer files updated across `Microsoft.CmdPal.UI.ViewModels` and
`Microsoft.CmdPal.UI` projects
- All `#pragma warning disable SA1300` suppressions removed —
convenience accessors replaced with direct `_settingsService.Settings` /
`_appStateService.State` access
- Namespace prefixes (`Services.ISettingsService`) replaced with proper
`using` directives

## PR Checklist

- [ ] **Communication:** I've discussed this with core contributors
already.
- [x] **Tests:** Added/updated and all pass
- [ ] **Localization:** N/A — no end-user-facing strings changed
- [ ] **Dev docs:** N/A — internal refactor, no public API changes
- [ ] **New binaries:** N/A — no new binaries introduced

## Detailed Description of the Pull Request / Additional comments

### Architecture

Services are registered as singletons in `App.xaml.cs`:
```csharp
services.AddSingleton<IPersistenceService, PersistenceService>();
services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton<IAppStateService, AppStateService>();
```

`PersistenceService.Save<T>` writes the serialized model directly to
disk, creating the target directory if it doesn't exist. It also does
not attempt to merge existing and new settings/state. `SettingsService`
runs hotkey migrations on load and raises `SettingsChanged` after saves.
`AppStateService` always raises `StateChanged` after saves.

### Files changed (41 files, +1169/−660)

| Area | Files | What changed |
|------|-------|-------------|
| New services | `Services/IPersistenceService.cs`,
`PersistenceService.cs`, `ISettingsService.cs`, `SettingsService.cs`,
`IAppStateService.cs`, `AppStateService.cs` | New service interfaces and
implementations |
| Models | `SettingsModel.cs`, `AppStateModel.cs` | Stripped to pure
data bags |
| DI | `App.xaml.cs` | Service registration, removed raw model DI |
| ViewModels | 12 files | Constructor injection of services |
| UI | 10 files | Service injection replacing model access |
| Settings | `DockSettings.cs` | `Colors.Transparent` replaced with
struct literal to avoid WinUI3 COM dependency |
| Tests | `PersistenceServiceTests.cs`, `SettingsServiceTests.cs`,
`AppStateServiceTests.cs` | 38 unit tests covering all three services |
| Config | `.gitignore` | Added `.squad/`, `.github/agents/` exclusions
|

## Validation Steps Performed

- Built `Microsoft.CmdPal.UI` with MSBuild (x64/Debug) — exit code 0,
clean build
- Ran 38 unit tests via `vstest.console.exe` — all passing
- Verified no remaining `#pragma warning disable SA1300` blocks
- Verified no remaining `Services.` namespace prefixes

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 18:58:27 -05:00