Skip to content

feat: WPF Timeline + Compare shell with Application layer extraction - #7

Open
nicolasestrem wants to merge 4 commits into
mainfrom
feat/wpf-timeline-shell
Open

feat: WPF Timeline + Compare shell with Application layer extraction#7
nicolasestrem wants to merge 4 commits into
mainfrom
feat/wpf-timeline-shell

Conversation

@nicolasestrem

Copy link
Copy Markdown
Owner

Summary

Implements the approved Timeline + Comparison Workspace design as a new WPF application running side-by-side with the existing WinForms shell. Both share the same Core engine and a new WinRestoreKit.Application library.

What shipped

New projects

  • WinRestoreKit.Application — orchestration, backup presets, scope groups, snapshot event catalog, comparison service, update/theme/settings services extracted from WinForms and shared by both shells
  • WinRestoreKit.Wpf — Timeline home, Compare/Confirm restore workflow, Backup workspace, Progress, Results, Advanced History, Settings, About, App Restore dialog

Visual system

  • Neutral Windows surfaces, mineral-blue actions, restrained coral warnings
  • Light / Dark / Follow-system themes with a shared ~35-colour token set
  • Segoe UI Variable + packaged IBM Plex Mono
  • Vector status glyphs (eliminates raw ErrorCircle enum text at source)
  • Full implicit WPF control templates, visible keyboard focus, UI Automation names/roles/states
  • 6–10 px corner radii, flat panes, no gradients/glow/blur

Crash fixes

Two read-only property bindings caused InvalidOperationException on view render:

View Binding Property Fix
About Run.Text CurrentVersion (read-only) Mode=OneWay
Progress ProgressBar.Value Percent (read-only) Mode=OneWay

Both have regression tests that render the view through the layout dispatcher.

Verification

  • Build: 0 warnings, 0 errors (Release)
  • Tests: 980 passed, 0 failed, 0 skipped
  • Desktop smoke: Timeline renders with real snapshots; Create snapshot workflow reaches Results without crashing; Settings and About render correctly
  • Published EXE: Self-contained single-file WinRestoreKit.Wpf.exe launches and renders elevated

Not in scope

  • WinForms removal (both shells remain; clean cutover is a follow-up)
  • Semantic comparison adapters (module-level comparison only in first release)
  • The publish-*/ directories are gitignored (self-contained executables, ~70 MB each)

…sh fixes

Add a new WPF application (WinRestoreKit.Wpf) implementing the approved
Timeline + Comparison Workspace design, running side-by-side with the
existing WinForms shell. Both share the same Core engine and a new
WinRestoreKit.Application library.

Architecture:
- WinRestoreKit.Application: orchestration, backup presets, scope groups,
  snapshot event catalog, comparison service, update/theme services extracted
  from the WinForms project and shared by both shells
- WinRestoreKit.Wpf: Timeline home, Compare/Confirm restore workflow, Backup
  workspace with presets, Progress with live metrics, Results, Advanced
  History, Settings, About, App Restore dialog
- WinRestoreKit.Core: unchanged isolation guarantee (no UI dependency)
- WinRestoreKit (WinForms): updated to reference Application layer

Visual system:
- Neutral Windows surfaces, mineral-blue actions, restrained coral warnings
- Light/Dark/Follow-system themes with shared token set (~35 colours each)
- Segoe UI Variable + IBM Plex Mono typography
- Vector status glyphs (eliminates raw ErrorCircle enum text at source)
- Full implicit control templates, visible keyboard focus, accessible names
- 6-10px radii, flat panes, no gradients/glow/blur

Fixed:
- AboutView: Run.Text bound CurrentVersion read-only (TwoWay default) -> Mode=OneWay
- ProgressWorkspaceView: ProgressBar.Value bound Percent read-only -> Mode=OneWay
- Both crash with InvalidOperationException on view render; regression tests added

Tests: 980 passed, 0 failed, 0 skipped (Release)
Build: 0 warnings, 0 errors
@nicolasestrem

Copy link
Copy Markdown
Owner Author

@claude review

@nicolasestrem

Copy link
Copy Markdown
Owner Author

/oc review

@claude

claude Bot commented Aug 9, 2026

Copy link
Copy Markdown

Claude finished @nicolasestrem's task in 5m 9s —— View job


Review complete

  • Gather context (PR body, changed files, CLAUDE.md)
  • Review Core restore-safety changes (FileModule, FolderModule, WPowerPlans, RegFile, RestoreContents, RestoreLog, SnapshotGate)
  • Review Application layer extraction (orchestration, snapshots, comparison, updates, settings, app-restore)
  • Review WPF views/viewmodels for binding issues similar to the two fixed crashes
  • Sanity-check restore-safety declarations for new/moved code against CLAUDE.md invariants

This is a large, well-executed PR — the extraction respects the codebase's core invariants (Application layer is genuinely UI-agnostic, IRunUi/RestoreDialog seams stay toolkit-neutral, the Aggregate/StepResult/bool?-probing discipline is preserved end-to-end). One real gap found; everything else below is minor or informational.

1. Test coverage regression on AppStoreApps (worth fixing before merge)
src/WinRestoreKit.Tests/AppRestoreDialogTests.cs shrank from 690 → ~170 lines, and two invariants this very PR touches lost their tests with no replacement anywhere in the suite:

  • AppStoreApps.Verify(ProcessOutcome, string) (src/WinRestoreKit.Core/Conf/AppStoreApps.cs:128) — the "exit code is not evidence" ladder (bad exit code, missing file, empty file, malformed Sources[0].Packages) — is now exercised by zero tests (Verify_NamesEveryWayTheExportCanFail, Verify_AGoodExport_Succeeds, Verify_AnExportWithNoPackagesArray_Fails were deleted and not moved).
  • The new owner == nullFailed branch and the pre-existing "no RestoreDialog registered → Failed, not Skipped" branch (AppStoreApps.cs:224-260) are also untested now — Restore_WithNoDialogRegistered_FailsRatherThanClaimingSkipped was deleted; only the happy-path RestoreDialogCoreSeamRemainsInteractive test remains.

Concrete failure scenario: a future change could flip either branch to Skipped, or break the Verify exit-code/JSON-shape checks, and the suite would stay green while a backup or restore that did nothing gets reported as succeeded/skipped — exactly the "announcing success it had not verified" failure mode this codebase's CLAUDE.md calls out by name. AppRestoreServiceTests.cs and WpfAppRestoreDialogTests.cs cover the newly-extracted AppRestoreService well, but neither exercises AppStoreApps.Verify or the Failed-not-Skipped branches directly.

Fix this →

2. Minor asymmetry — WinForms owner cast (informational, not currently reachable)
src/WinRestoreKit/Program.cs's RegisterUiSeams casts the restore-dialog owner unconditionally: (IWin32Window)owner, with no type check — unlike the WPF side (WpfAppRestoreDialog.CreateCallback), which defensively checks owner is not Window window. Today owner always comes from ProgressPageView.DialogOwner (a Control, always IWin32Window-compatible), so this isn't reachable with a bad type, and a mismatch would degrade to Failed via BackupRestoreOrchestrator's outer catch rather than crash — but it's worth a defensive check for consistency with the WPF bridge and to fail with a clearer diagnostic than a generic cast exception.

3. Everything else checked out clean:

  • SnapshotComparisonService, RestoreContents, RestoreLog, SnapshotGate changes are safety-tightening (e.g. PartiallyCaptured and non-empty NothingCaptured now correctly require an override, where before they silently proceeded) and consistent with the bool?-never-crying-wolf and "claim only what was verified" rules.
  • FileModule/FolderModule/WPowerPlans now clear the previous backup artifact before writing a fresh one — directly addresses the stale-artifact class of bug CLAUDE.md documents (the WThemes incident).
  • UpdateCheckService preserves the GitHub-Releases-then-AssemblyInfo.cs-fallback logic and Version.ToString(3) normalization faithfully.
  • Swept every two-way-bindable control property (Text, Value, IsChecked, SelectedItem) across all new WPF views against their view-model properties — found no further instances of the read-only-property binding crash pattern the PR already fixed twice (About/CurrentVersion, Progress/Percent). Cross-thread collection mutation and CanExecute re-evaluation also look correctly handled.
  • BackupModuleCatalog is a thin wrapper over Conf.ModuleCatalog, so the two shells are structurally guaranteed to show the same module list — no divergence risk.
    · branch: feat/wpf-timeline-shell

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

Here's my review of PR #7.


Review: WPF Timeline + Compare Shell with Application Layer Extraction

Architecture: Strong

The five-project solution (Core ← Application ← WinForms/WPF ← Tests) is clean and well-motivated. Key architectural wins:

  • WinRestoreKit.Application has no WinForms/WPF references, enforced structurally by ApplicationBoundaryTests.cs:23-32 which asserts at test time that the assembly never loads System.Windows.Forms, PresentationFramework, or WindowsBase.
  • IRunUi.DialogOwner is typed as object, not IWin32Window, keeping the seam framework-neutral.
  • BackupRestoreOrchestrator moved verbatim from the WinForms view into Application, preserving behavior without rewrite.
  • DataTemplate-based view resolution in MainWindow.xaml is textbook WPF MVVM - the shell only switches ViewModels, never references Views.

Issues

Medium: Implicit Newtonsoft.Json dependency

AppRestoreService.cs:2 uses Newtonsoft.Json.Linq, but WinRestoreKit.Application.csproj declares no <PackageReference> for it - it arrives transitively through Core. If Core ever drops its Newtonsoft.Json reference, this file breaks with a confusing error. Consider adding an explicit <PackageReference> in the Application csproj.

Medium: ScopeGroups.cs hardcodes all 30 module types

Lines 100-141 use is pattern matching against every concrete BackupBase subclass in Core. Any new module added to Core that should appear in a scope group requires editing this Application-layer file. The throw on line 76-77 catches unmatched modules at runtime, but only during a live Build() call. The ModuleCatalogTests cross-check catalog registration, but no equivalent test enforces scope group coverage. Consider a test that verifies every registered module matches at least one scope definition.

Low: RunSeverity.Error is declared but never produced

RunSummary.Severity at line 65-68 maps only to Information or Warning. The Error enum value exists but is unreachable from this property. This is harmless (modules produce per-step failures that fold into Warning) but could confuse future contributors.

Low: ViewModel base class inconsistency

TimelineViewModel and AdvancedHistoryViewModel implement INotifyPropertyChanged manually instead of inheriting from ObservableObject, which every other ViewModel uses. The implementations are functionally identical but create a maintenance seam where a bug fix to ObservableObject wouldn't reach these two classes.

Low: ConfirmView.xaml.cs temporal coupling

ConfirmView_Loaded calls viewModel.AttachRunSurfaces(Dispatcher, ...), meaning the ViewModel is partially constructed (missing UI dependencies) until the View loads. The CanStartRestore property gates on dialogService != null, which works, but the pattern is fragile if a future code path reads a UI-dependent property before Loaded fires.

Informational: RestoreConsentDialog is both View and ViewModel

DataContext = this makes the dialog untestable in isolation. Acceptable for a simple consent dialog, but noted for consistency.

Informational: BrowseDestination_Click is a code-behind handler

Not unit-testable through the ViewModel. Acceptable since WPF has no built-in folder dialog MVVM binding.

What's Done Well

  • 980 tests, 0 failures. Test quality is high - real measured data as fixtures (powercfg output), proper isolation with temp directories and registry cleanup, concurrency tests with thread-safe counters, accessibility tests with visual tree inspection.
  • ApplicationBoundaryTests is the kind of structural test that prevents regressions in the layering boundary. Good addition.
  • Crash fixes for read-only property bindings (Mode=OneWay on CurrentVersion and Percent) with regression tests that render through the dispatcher. Correct fix for a real WPF binding pitfall.
  • Theme system is properly DynamicResource-based throughout, with WpfThemeService swapping dictionaries at runtime. Controls.xaml comments this requirement explicitly.
  • Commands are used everywhere except the Browse button (unavoidable) and the consent restore button (sets DialogResult). AsyncDelegateCommand guards against re-entrancy.
  • The design spec, plans, and CHANGELOG are consistent with each other. The migration roadmap is well-structured across five stages.
  • Core module changes (FileModule, FolderModule, AppStoreApps, WPowerPlans, RegFile, RestoreContents, RestoreLog, SnapshotGate) are minimal and additive. No breaking changes to existing module behavior.

Verdict

This is a well-structured PR. The architecture is sound, the test coverage is strong, and the MVVM implementation is solid. The two medium issues (transitive Newtonsoft.Json dependency, untested scope group coverage) are worth addressing but not blockers. The low/informational items are code cleanliness concerns for a follow-up.

Recommendation: Approve with the two medium suggestions noted as follow-up items.

New%20session%20-%202026-08-09T19%3A28%3A51.810Z
opencode session  |  github run

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 34a9bab938

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
{
if (Mode == ThemeMode.FollowSystem)
ApplyEffectiveMode();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Marshal system-theme updates to the WPF dispatcher

When the default FollowSystem mode is active, SystemEvents.UserPreferenceChanged can invoke this handler from the SystemEvents thread. Calling ApplyEffectiveMode() there loads and replaces WPF resource dictionaries off the UI dispatcher, which can raise a cross-thread exception and terminate the application when a Windows preference or theme changes; dispatch this work to the application dispatcher as the existing WinForms handler does.

Useful? React with 👍 / 👎.

if (isDefaultRoot)
return new BackupFolders(backups, snapshots, ex.Message);
{
return new BackupFolders(backups, snapshots, ex.Message, unreadableRoots);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Continue scanning custom roots after a default-root error

If the executable-adjacent default backup root exists but cannot be enumerated—for example because of its ACL—this return exits the entire multi-root scan before any remembered custom destination is inspected. Valid snapshots on external or user-selected roots consequently disappear from Timeline and cannot be selected for restore; record the default-root error but continue through the remaining roots.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d9cbcac3d7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +61 to +62
PreviewKeyDown="OnPreviewKeyDown"
SelectionChanged="OnSelectionChanged">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add a mouse activation path for timeline entries

A mouse click only changes SelectedEvent and focuses the list; OpenSelectedAsync is invoked exclusively from the Enter-key handler. Because the row template contains no button and the list has no double-click handler, mouse users cannot open either Compare or a failed-entry diagnostic from the Timeline. Bind a row activation gesture or explicit action to the same open operation.

Useful? React with 👍 / 👎.

Comment on lines +116 to +117
ShowSettingsCommand = new DelegateCommand(_ => NavigateTo(settings, "Settings"));
ShowAboutCommand = new DelegateCommand(_ => NavigateTo(about, "About"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep run controls reachable during active operations

When a backup or restore is running, these commands remain enabled even though only CreateSnapshotCommand observes RunCoordinator.IsRunning. Clicking Settings or About replaces the progress/confirmation workspace, and the shell exposes no command for returning to the active operation, so the user loses access to pause, cancellation, logs, and status while a potentially destructive run continues. Disable these navigation commands during a run or provide an active-run navigation route.

Useful? React with 👍 / 👎.

{
CreateSnapshotCommand = new DelegateCommand(_ => ShowCreateSnapshot(),
_ => !RunCoordinator.IsRunning);
ShowTimelineCommand = new DelegateCommand(_ => ShowTimeline(), _ => timelineWorkspace != null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Expose the Timeline command after entering Compare

ShowTimelineCommand is created but is not bound anywhere in the WPF views, and the Compare workspace itself has no back action. After opening any snapshot, users therefore cannot return to Timeline to choose another snapshot without restarting the application or starting an unrelated backup, which also makes the implemented restore-set discard confirmation unreachable through the normal UI.

Useful? React with 👍 / 👎.

Comment on lines +53 to +56
internal Task RefreshAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
IReadOnlyList<SnapshotEvent> snapshots = catalog.Read();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Move timeline discovery off the UI dispatcher

Every Timeline load calls catalog.Read() synchronously on the WPF dispatcher. That call recursively enumerates and stats every file in every discovered snapshot to calculate sizes, including remembered external or network roots, so a normal collection of large snapshots can prevent the window from painting or responding for a long time each time Timeline is revisited. Perform discovery on a worker thread and marshal only the resulting collection update back to the dispatcher.

Useful? React with 👍 / 👎.

Codex P1:
- WpfThemeService: marshal system-theme reapply onto the application
  dispatcher (SystemEvents fires off the UI thread; resource-dictionary
  swaps there can terminate the process). Mirrors the WinForms handler.
- BackupFolders: a bad default root no longer aborts the whole multi-root
  scan; custom/external roots are still enumerated so their backups remain
  restorable. The reason surfaces only when no root was readable.

Tests (Claude):
- Restore AppStoreApps.Verify ladder coverage (bad exit code, no file,
  empty file, no Packages array, invalid JSON) and the good-export path.
- Restore Failed-not-Skipped coverage for Restore (no dialog registered,
  null owner).

OpenCode:
- Declare Newtonsoft.Json explicitly in WinRestoreKit.Application
  (AppRestoreService uses it directly; was arriving only transitively).
- Timeline/AdvancedHistory ViewModels now inherit ObservableObject like
  every other ViewModel (made internal sealed to match the siblings and
  satisfy base-class accessibility).

986 passed, 0 failed, 0 skipped; Release build 0 warnings, 0 errors.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ef1b4f60c1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +22 to +23
ShellViewModel shell = new ShellViewModel(themes, updates,
VersionInfo.GetCurrentVersion(typeof(App).Assembly));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Read the canonical product version for update checks

The WPF project does not set or link the product's AssemblyFileVersion, so the SDK gives WinRestoreKit.Wpf the default 1.0.0 while the canonical WinForms assembly is currently 0.0.1. Passing the WPF assembly here consequently makes About display 1.0.0, and UpdateCheckService.Decide treats release tags below that value as older, suppressing every normal 0.0.x update. Read the shared canonical version or apply the same version metadata to the WPF executable.

Useful? React with 👍 / 👎.

Comment on lines +205 to +208
else
{
usableArtifact = true;
artifactSummary = "The module verified a captured artifact.";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject artifacts omitted from a present manifest

When a manifest exists but has no entry for this module, a positive artifact probe reaches this branch and marks the artifact usable. Two backups made in one process reuse the same Data.NowShort directory; if an earlier uncompressed run selected the module and a later run omits it, its loose artifact remains while the new manifest excludes it. Compare then enables restoring stale data from the earlier run, contrary to the manifest for the selected snapshot. Treat any module absent from a present manifest as not captured and reserve artifact probing for legacy manifestless backups.

Useful? React with 👍 / 👎.

xmlns:views="clr-namespace:WinRestoreKit.Wpf.Views"
xmlns:vm="clr-namespace:WinRestoreKit.Wpf.ViewModels"
xmlns:timeline="clr-namespace:WinRestoreKit.Wpf.ViewModels.Timeline"
Title="WinRestoreKit" MinWidth="1024" MinHeight="640"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Block window shutdown while a run is active

When a backup or restore is active, closing this sole WPF window still uses WPF's default last-window shutdown behavior; there is no Closing handler here or elsewhere in the shell to cancel or defer closure based on RunCoordinator.IsRunning. Clicking the title-bar close button therefore terminates the process in the middle of file or registry work, bypassing the run's orderly cancellation and cleanup and potentially leaving a partial restore or an incomplete backup. Prevent shutdown during a run or request cancellation and wait for the operation to settle.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant