Background
Clean-code review of PR #135 found the overall direction sound (domain boundary, ChangePresentation adapter, RefreshState) but flagged that SourceControlViewModel is starting to absorb workflow state. This issue tracks the three "must-fix" items before the ViewModel becomes a god object — the review notes "now is easy, two-three PRs later it starts to hurt."
Must-fix
1. Rename selectedItems → syncQueue
selectedItems is a UI-interaction term; the domain concept is "the pending sync operation queue." Rename the SourceControlViewState.selectedItems projection field to syncQueue (aligns with the SYNC QUEUE UI label already landed). Touches SourceControlViewModel, SourceControlView, and tests. Purely a rename.
2. Extract DiffStatProvider
The +/- diff-stat cache (Map<ChangeId, ChangeStat | null>), eager local-only load, eager selected load, lazy two-sided load, and clear-on-refresh currently all live inside SourceControlView.ts. Extract them into a DiffStatProvider (get / eagerLocal / eagerSelected / lazyLoad / clear) so the view stops owning cache+load+invalidate and the data concern is isolated. UI-layer class (the loader reads view-side diff content).
3. Extract SelectionController
PushSelectionStore already exists and is clean, but SourceControlView holds a direct reference to it and reaches past the ViewModel (this.selection.includeForPush/excludeFromPush/getSelectedChangeIds), and the View implements batch ops (clearSelection, toggleFolderSelect) inline. Consolidate selection behind a SelectionController exposed via viewModel.selection so the view no longer bypasses the ViewModel and batch ops live in one domain-named controller. (Exact shape — wrap vs rename vs minimal-expose — to be confirmed; preserves the buildSummary/matchesFilter isIncluded contract.)
Constraints
- Domain-untouched invariant must hold:
src/logic/source-control/ may only gain the new SelectionController/DiffStatProvider-related files and ViewModel edits; no sync-pipeline changes.
- TDD: update/extend Vitest tests alongside source.
- DoD:
npx eslint . 0 errors; npm run build passes (tsc + Obsidian 1.11.0 compat + esbuild); npx vitest run passes.
Out of scope (next PR)
Notification UX, full operation history, diff viewer.
Background
Clean-code review of PR #135 found the overall direction sound (domain boundary,
ChangePresentationadapter,RefreshState) but flagged thatSourceControlViewModelis starting to absorb workflow state. This issue tracks the three "must-fix" items before the ViewModel becomes a god object — the review notes "now is easy, two-three PRs later it starts to hurt."Must-fix
1. Rename
selectedItems→syncQueueselectedItemsis a UI-interaction term; the domain concept is "the pending sync operation queue." Rename theSourceControlViewState.selectedItemsprojection field tosyncQueue(aligns with theSYNC QUEUEUI label already landed). TouchesSourceControlViewModel,SourceControlView, and tests. Purely a rename.2. Extract
DiffStatProviderThe +/- diff-stat cache (
Map<ChangeId, ChangeStat | null>), eager local-only load, eager selected load, lazy two-sided load, and clear-on-refresh currently all live insideSourceControlView.ts. Extract them into aDiffStatProvider(get / eagerLocal / eagerSelected / lazyLoad / clear) so the view stops owning cache+load+invalidate and the data concern is isolated. UI-layer class (the loader reads view-side diff content).3. Extract
SelectionControllerPushSelectionStorealready exists and is clean, butSourceControlViewholds a direct reference to it and reaches past the ViewModel (this.selection.includeForPush/excludeFromPush/getSelectedChangeIds), and the View implements batch ops (clearSelection,toggleFolderSelect) inline. Consolidate selection behind aSelectionControllerexposed viaviewModel.selectionso the view no longer bypasses the ViewModel and batch ops live in one domain-named controller. (Exact shape — wrap vs rename vs minimal-expose — to be confirmed; preserves thebuildSummary/matchesFilterisIncludedcontract.)Constraints
src/logic/source-control/may only gain the newSelectionController/DiffStatProvider-related files and ViewModel edits; no sync-pipeline changes.npx eslint .0 errors;npm run buildpasses (tsc + Obsidian 1.11.0 compat + esbuild);npx vitest runpasses.Out of scope (next PR)
Notification UX, full operation history, diff viewer.