Skip to content

Commit 9fd1789

Browse files
committed
feat(source-control): split sync queue/repository regions and extract DiffStatProvider + SelectionController
Two intertwined improvements to the Source Control view. UX — queue/repository separation: - Upper action region becomes "SYNC QUEUE" with an "N files selected" subtitle (always a flat list); lower source region becomes "Repository Changes" with a Tree/List view toggle scoped to it. - List view renders flat rows with a dim right-aligned folder-path suffix. - Mobile collapses the Sync Queue by default so the repository tree stays primary. - Drops 6 dead sourceControl.section.* keys; adds queueSubtitle/ repositoryChanges/view.* keys (en/zh-cn/zh-tw). Architecture (issue #136, PR #135 clean-code review): - Rename the selectedItems projection -> syncQueue (domain term, not UI). - Extract DiffStatProvider (cache + eager/lazy load + clear) out of the view so the view owns no diff cache; +9 unit tests. - Expose PushSelectionStore via viewModel.selection; add selectMany/ deselectMany batch methods; the view drops its direct store reference and routes all selection through the ViewModel (no reach-past-ViewModel), consolidating inline toggle/clear loops. Domain-untouched: sync pipeline untouched; only PushSelectionStore (+batch methods) and SourceControlViewModel (rename + selection getter) change in src/logic/source-control. Closes #136
1 parent 069653f commit 9fd1789

18 files changed

Lines changed: 695 additions & 240 deletions

progress.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Completed work is archived in [archive/](./archive/), one file per calendar mont
55
## Current State
66

77
**Last Updated:** 2026-08-23
8-
**Active Feature:** sync-status-workflow-ui plan (`.kilo/plans/1787412338771-sync-status-workflow-ui.md`) on `feat/sync-status-workflow-ui` — core 4 commits + 2 follow-up UX commits + 1 queue-refactor commit + 1 UX-convergence commit (8 total); automated checks green; manual Obsidian desktop/mobile UI verification remains. The convergence pass (renamed `SELECTED FOR SYNC``SYNC QUEUE`, 5-chip filter `All/Needs Sync/Remote/Conflict/Synced` with "All" composing actionable+synced view-side, default `Needs Sync`, `Added/Modified locally` badge tooltips, mobile bar `N files selected`+`Sync`) stayed entirely in the view layer — domain-untouched invariant still holds. Prior active feature feat-026 / issue #105 (sync architecture refactor on `refactor/sync-domain-pipeline`) still has manual Obsidian move smoke tests outstanding.
8+
**Active Feature:** sync-status-workflow-ui plan (`.kilo/plans/1787412338771-sync-status-workflow-ui.md`) on `feat/sync-status-workflow-ui` — core 4 commits + 2 follow-up UX commits + 1 queue-refactor commit + 1 UX-convergence commit (8 total); automated checks green; manual Obsidian desktop/mobile UI verification remains. Latest pass (uncommitted): queue/repository role-label separation (`SYNC QUEUE` + `Repository Changes`), Tree/List view toggle on the repository region, flat list mode with folder-path suffix, mobile queue collapse-by-default — view-layer only, domain untouched. The convergence pass (renamed `SELECTED FOR SYNC`→`SYNC QUEUE`, 5-chip filter `All/Needs Sync/Remote/Conflict/Synced` with "All" composing actionable+synced view-side, default `Needs Sync`, `Added/Modified locally` badge tooltips, mobile bar `N files selected`+`Sync`) stayed entirely in the view layer — domain-untouched invariant still holds. Architecture must-fixes from the PR #135 review (`selectedItems`→`syncQueue`, `DiffStatProvider`, `SelectionController`) deferred to a separate issue. Prior active feature feat-026 / issue #105 (sync architecture refactor on `refactor/sync-domain-pipeline`) still has manual Obsidian move smoke tests outstanding.
99
**Parallel Work:** PR #87 (4x Dependabot security alerts via npm overrides) and Issue #57 (live-credential smoke test).
1010

1111
## Outstanding Items
@@ -20,6 +20,10 @@ Completed work is archived in [archive/](./archive/), one file per calendar mont
2020

2121
## Latest Evidence
2222

23+
- [x] refactor(source-control): slim SourceControlViewModel via SyncQueue rename, DiffStatProvider, SelectionController (issue #136, 2026-08-23), branch `feat/sync-status-workflow-ui`, uncommitted: the three clean-code must-fixes from the PR #135 review, domain-untouched (sync pipeline untouched). (1) **Rename `selectedItems`→`syncQueue`**: `SourceControlViewState.selectedItems`→`syncQueue` (aligns with the `SYNC QUEUE` UI label); updated ViewModel projection, View (`renderSelectedSection`/`eagerLoadSelectedStats`), and `SourceControlViewModel.test.ts`. (2) **Extract `DiffStatProvider`** (`src/ui/source-control/DiffStatProvider.ts`, +9 unit tests): pulled the +/- diff-stat cache (`Map<ChangeId, ChangeStat|null>`), `eagerLocal`/`eagerSelected`/`lazyLoad` policies, and `clear()` out of `SourceControlView.ts` into its own class; the View now holds a `DiffStatProvider` (constructed in its ctor with `callbacks.loadDiffStat` + a rerender `settle`) and owns no diff cache — three private view methods removed. Null results stay cached (no retry); `lazyLoad` only settles when a stat is produced. (3) **SelectionController (minimal-expose option)**: added `PushSelectionStore.selectMany`/`deselectMany` batch methods (no boolean selector param — satisfies `sonarjs/no-selector-parameter`); exposed the store via a new `viewModel.selection` getter (ctor param renamed `selection`→`selectionStore`); `SourceControlView` dropped its direct `PushSelectionStore` constructor arg and now routes all selection through `viewModel.selection` (`includeForPush`/`excludeFromPush`/`selectMany`/`deselectMany`/`getSelectedChangeIds`) — the view no longer reaches past the ViewModel, and `clearSelection`/`toggleFolderSelect` use the batch methods instead of inline loops. `SourceControlItemView` construction updated. Verification: `npx eslint .` — 0 errors; `npm run build` — clean incl. Obsidian 1.11.0 compat; `npx vitest run` — 62 files / 654 tests pass. Issue #136 created on GitHub; board add pending (token lacks `project` scopes — `gh auth refresh -s read:project,write:project` needed). Manual Obsidian verification remains.
24+
25+
- [x] sync-status-workflow-ui queue/repository separation + List view (2026-08-23), branch `feat/sync-status-workflow-ui`, uncommitted: separated the two regions by role label per UX review, view-layer only. (1) Renamed the upper action region `CHECKED CHANGES (N)`→`SYNC QUEUE` with a `N files selected` subtitle (replaced the count badge), i18n en/zh-cn/zh-tw; dropped the now-unused `sourceControl.section.{all,readyToPush,changes,remoteChanges,conflicts,synced}` keys + the `FILTER_HEADER_KEYS` map. (2) Renamed the lower region's header from the per-filter name (`ALL`/`NEEDS SYNC`) to a single role label `Repository Changes (N)` (the active filter is conveyed by the chips above); renamed `renderActiveFilterHeader`→`renderRepositoryHeader` and `scv-active-filter-*`→`scv-repository-*` CSS. (3) Added a Tree/List segmented view toggle scoped to Repository Changes only (the queue is always a flat list) — `viewMode` state + `renderViewToggle`/`setViewMode`, icons `folder-tree`/`list`, click stopPropagation so switching presentation doesn't collapse the region; mobile hides the labels. (4) New `renderChangeList` flat renderer in `ChangeTree.ts` (sorted by path) + `ChangeItem` `folderPath`/`listMode` options rendering a dimmed right-aligned `.scv-change-path` suffix so flat rows stay disambiguated; `eagerLoadLocalStats` runs in both modes. (5) Mobile: the Sync Queue starts collapsed to a header bar (`mobileQueueExpanded` flag) so it doesn't push the repository tree off-screen; tapping the header expands it; the bottom sync bar still carries the count. Domain-untouched invariant holds (no `src/logic/**` changes). Verification: `npx eslint .` — 0 errors; `npm run build` — clean incl. Obsidian 1.11.0 compat; `npx vitest run` — 61 files / 642 tests pass. Manual Obsidian desktop/mobile UI verification remains. Follow-up: architecture must-fixes from the PR #135 review (rename `selectedItems`→`syncQueue`, extract `DiffStatProvider`, extract `SelectionController`) deferred to a separate issue per one-feature-at-a-time — to be filed via firstsun-pm.
26+
2327
- [x] CI parallel-validation DAG refactor (2026-08-23), branch `feat/sync-status-workflow-ui` (uncommitted working-tree change to `.github/workflows/ci.yml` + `docs/testing/real-provider-e2e.md`): restructured the CI DAG so all four validation jobs start in parallel after a push instead of lint/test/build waiting behind the real-provider E2E matrix. Removed `preflight` (duplicated the reusable workflow's lint/test/build), `e2e-gate`, the shared reusable `firstsun-dev/.github` `CI` workflow call (its internal release fired before any E2E gate, so it couldn't be made to release-after-gate without inlining), and the standalone `build-artifact` job (its raw-artifact upload is now folded into `CI / Build`, same job as the build, so upload state can't drift from build state — Phase 5). New layout: `CI / Detect Changes` → (`CI / Lint`, `CI / Unit Test (Node 22|24)`, `CI / Build`, `CI / Provider E2E / <provider>`) all parallel → `CI / Required Checks` (`if: always()`, passes only when every validation job is `success`/`skipped`; a cancelled E2E leg is treated as a failure since the surviving run owns the latest-commit gate) → `Release / Package` (zip artifact, all branches) and `Release / Publish` (semantic-release + build-provenance attestation, `main`/`master` only). `provider-e2e` is unchanged internally (gitea-disabled notice, per-source/provider concurrency group `e2e-<branch>-<provider>`, run-scoped `E2E_WORKDIR`, retry, `if: always()` cleanup) — only its `name` and `needs: [changes]` (dropped `preflight`) changed, so the cleanup workflows' shared concurrency-group naming still matches. All third-party actions pinned to full commit SHAs (checkout/setup-node/paths-filter/retry unchanged; `actions/upload-artifact@`→`043fb46…` v7, `actions/attest-build-provenance@`→`4d10147…` v4.2.2). Verification: `actionlint v1.7.7 .github/workflows/ci.yml` — 0 errors (only the known `32gb-ram` custom self-hosted-label false positive); `python3 -c yaml.safe_load(...)` — parses. Source tree (`eslint`/`build`/`vitest`) untouched by this change. Follow-ups: real PR run + 3-5 execution-time comparisons (rollout), and the manual branch-protection switch to `CI / Required Checks`.
2428

2529
- [x] sync-status-workflow-ui UX convergence (2026-08-23), branch `feat/sync-status-workflow-ui`, commit (pending): converged the PR's mixed workflow concepts per review feedback, view-layer only. (1) Renamed `SELECTED FOR SYNC`→`SYNC QUEUE` (i18n en/zh-cn/zh-tw); the queue stays a compact read-only action preview (badge+name+diff-stat, no checkbox). (2) Filter chips redesigned to `All / Needs Sync / Remote / Conflict / Synced` via a UI chip model mapping `(domain filter, showSynced)` — `Needs Sync` (domain `all`, actionable) is the default; `All` composes actionable+synced by concatenating `getState('all',false)` + `getState('synced',true)` in the view (the domain `all` filter still returns actionable-only, so no domain change); `Synced` re-surfaces (domain `synced`, showSynced=true); `Local` dropped. (3) Badge tooltip wording `Added`→`Added locally`, `Modified`→`Modified locally`. (4) Mobile sync bar → `N files selected` + `Sync` button (was a single full-width `SELECTED FOR SYNC (N)` button). Domain-untouched invariant verified: `git diff claude/source-control-foundation -- src/logic/source-control/` shows only `RefreshState.ts` (new) + `SourceControlViewModel.ts` (edited). Verification: `npx eslint .` — 0 errors; `npm run build` — clean incl. Obsidian 1.11.0 compat; `npx vitest run` — 632/633 (1 unrelated pre-existing failure in `ci-workflow.test.ts` from an uncommitted `ci.yml`, not touched by this work).

session-handoff.md

Lines changed: 24 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,41 @@
11
# Session Handoff
22

33
**Date:** 2026-08-23
4-
**Branch:** `feat/sync-status-workflow-ui` (4 commits ahead of `claude/source-control-foundation` @ `f449125`)
5-
**Active Feature:** sync-status-workflow-ui plan (`.kilo/plans/1787412338771-sync-status-workflow-ui.md`) — code complete; manual Obsidian verification remains.
6-
**Side task done this session:** CI parallel-validation DAG refactor (uncommitted working-tree change on this branch).
4+
**Branch:** `feat/sync-status-workflow-ui`
5+
**Active Feature:** sync-status-workflow-ui plan (`.kilo/plans/1787412338771-sync-status-workflow-ui.md`) — UX queue/repository separation + List view landed (uncommitted); manual Obsidian verification remains.
76

8-
## Done This Session — CI DAG refactor
7+
## Done This Session — Queue/Repository UX separation + List view
98

10-
Refactored `.github/workflows/ci.yml` (and `docs/testing/real-provider-e2e.md`) so all four
11-
validation jobs start in parallel after a push instead of lint/test/build waiting behind the
12-
real-provider E2E matrix (the user's 8-phase plan).
9+
Separated the two Source Control regions by role label per UX review (view-layer only, no `src/logic/**` changes):
1310

14-
New DAG:
15-
```
16-
changes ──► provider-e2e ──┐
17-
lint ──────────────────────┤
18-
unit-test (Node 22|24) ─────┤──► required-checks ──► package
19-
build ──────────────────────┘ └────► publish (main only)
20-
```
11+
1. **SYNC QUEUE** (upper, action region): renamed `CHECKED CHANGES (N)``SYNC QUEUE`, replaced the count badge with a `N files selected` subtitle (`sourceControl.section.queueSubtitle`). Always a flat list (no tree) — matches its "what I'm about to push" role.
12+
2. **Repository Changes** (lower, source region): renamed the per-filter header (`ALL`/`NEEDS SYNC`) to a single role label `Repository Changes (N)` — the active filter is conveyed by the chips above. Renamed `renderActiveFilterHeader``renderRepositoryHeader`, `scv-active-filter-*``scv-repository-*`.
13+
3. **Tree/List view toggle** on the repository region only (the queue needs none): new `viewMode` ('tree'|'list'), `renderViewToggle`/`setViewMode`, icons `folder-tree`/`list`; click stopPropagation so switching presentation doesn't collapse the region; mobile hides labels.
14+
4. **List view**: new `renderChangeList` in `ChangeTree.ts` (sorted by path); `ChangeItem` gains `folderPath`/`listMode` options rendering a dimmed right-aligned `.scv-change-path` suffix so flat rows stay disambiguated. `eagerLoadLocalStats` runs in both modes.
15+
5. **Mobile**: the Sync Queue starts collapsed to a header bar (`mobileQueueExpanded`) so it doesn't push the repository tree off-screen; tapping expands it; the bottom sync bar still carries the count.
2116

22-
Removed: `preflight`, `e2e-gate`, the shared reusable `firstsun-dev/.github` `CI` workflow call
23-
(its internal release fired before any E2E gate, so it couldn't release-after-gate without
24-
inlining), and the standalone `build-artifact` job (raw-artifact upload folded into `CI / Build`).
25-
Added: `CI / Lint`, `CI / Unit Test (Node 22|24)`, `CI / Build`, `CI / Provider E2E / <provider>`,
26-
`CI / Required Checks` (aggregate gate), `Release / Package`, `Release / Publish`. The
27-
`provider-e2e` job's internals are unchanged (gitea-disabled notice, concurrency group
28-
`e2e-<branch>-<provider>`, run-scoped `E2E_WORKDIR`, retry, `if: always()` cleanup) — only `name`
29-
and `needs: [changes]` (dropped `preflight`) changed, so the cleanup workflows' shared concurrency
30-
naming still matches. All third-party actions pinned to commit SHAs.
17+
i18n: en/zh-cn/zh-tw updated; dropped the now-unused `sourceControl.section.{all,readyToPush,changes,remoteChanges,conflicts,synced}` keys + the `FILTER_HEADER_KEYS` map.
3118

3219
Verification evidence:
3320
```text
34-
actionlint v1.7.7 .github/workflows/ci.yml -> 0 errors (only known 32gb-ram false positive)
35-
python3 -c yaml.safe_load(ci.yml) -> parses
21+
npx eslint . -> 0 errors
22+
npm run build -> clean (tsc + Obsidian 1.11.0 compat + esbuild)
23+
npx vitest run -> 61 files / 642 tests pass
3624
```
37-
Source tree (eslint/build/vitest) is untouched by this change.
3825

39-
## Prior Session — Sync Status Workflow UI (code complete, manual verify pending)
26+
## Deferred — architecture must-fixes (separate issue, per one-feature-at-a-time)
4027

41-
Implemented the full four-commit "Sync Status Workflow UI" feature on this branch, each commit
42-
passing the husky pre-commit hook (`npm run lint && npm run build`):
28+
From the PR #135 clean-code review. To be filed via firstsun-pm as a follow-up issue, NOT bundled here:
29+
- Rename `SourceControlViewModel.selectedItems``syncQueue` (aligns domain naming with the `SYNC QUEUE` UI label).
30+
- Extract `DiffStatProvider` (cache + load + invalidate) out of the ViewModel so the diff-stat cache stops being a ViewModel concern.
31+
- Extract `SelectionController` from the ViewModel so selection state isn't absorbed into the ViewModel long-term.
4332

44-
1. `8c69cc8``SourceControlViewModel` gains `selectedItems` + `refreshStatus` projections and a
45-
`refresh()` delegate backed by a new `RefreshState` holder (idle/loading/failed).
46-
2. `625fad2` — Filter chips drop `ready-to-push` (now 4: All/Local/Remote/Conflict). New
47-
`renderSelectedSection()` shows "SELECTED FOR SYNC (N)".
48-
3. `754b717` — Refresh button (idle/loading/failed); `OperationIndicator` icon+text labels.
49-
4. `dd8ddd5` — New `ChangePresentation` UI adapter (`remote-only` badged `D`). Diff-stat threaded
50-
through rows (eager local-only + lazy two-sided); responsive mobile (filter dropdown, sticky
51-
bottom sync bar, flatter tree).
33+
## Exact Next Steps
5234

53-
Domain-untouched invariant verified:
54-
`git diff claude/source-control-foundation -- src/logic/source-control/` shows ONLY
55-
`RefreshState.ts` (new) + `SourceControlViewModel.ts` (edited).
35+
1. **Commit this UX pass** (user has not yet requested commit — wait for explicit ask). The change is view-layer only; stage `src/ui/source-control/`, `src/ui/components/icons.ts`, `src/i18n/locales/*`, `styles.css`, `tests/ui/source-control/SourceControlView.test.ts`, `progress.md`, `session-handoff.md`. NOTE: an unrelated uncommitted `ci.yml` change from a prior session is still in the working tree — stage selectively.
36+
2. **Manual Obsidian verification** (desktop + mobile): the new `SYNC QUEUE` + subtitle, `Repository Changes` header, Tree/List toggle (folder nesting vs flat list with path suffix), mobile queue collapse-by-default + bottom sync bar.
37+
3. **File the architecture follow-up issue** via the firstsun-pm skill (the three deferred must-fixes above) before starting that work.
5638

57-
## Exact Next Steps
39+
## Prior Session — Sync Status Workflow UI (code complete, manual verify pending)
5840

59-
1. **CI refactor follow-ups** (see `progress.md` 0b/0c):
60-
- Real PR run of the new workflow + 3-5 PR-execution-time comparisons (rollout plan) before
61-
deleting old assumptions. NOTE: the `ci.yml` change is currently an **uncommitted** working-
62-
tree change mixed with the UI feature's in-progress edits — stage/select carefully before
63-
committing (`git add .github/workflows/ci.yml docs/testing/real-provider-e2e.md`).
64-
- Manual GitHub branch-protection switch to require `CI / Required Checks` (needs repo admin).
65-
2. **sync-status-workflow-ui manual Obsidian verification** (desktop + mobile): refresh button
66-
states, "SELECTED FOR SYNC" section, per-row subtitles/badges (esp. `remote-only``D`),
67-
diff-stat `+N -M` spans, mobile filter dropdown + bottom sync bar. Then open a PR against
68-
`claude/source-control-foundation` (confirm base branch name with the user first).
41+
Implemented the four-commit "Sync Status Workflow UI" feature on this branch, each commit passing the husky pre-commit hook. See `progress.md` "Latest Evidence" for the full commit-by-commit detail. Domain-untouched invariant: `git diff claude/source-control-foundation -- src/logic/source-control/` shows ONLY `RefreshState.ts` (new) + `SourceControlViewModel.ts` (edited).

src/i18n/locales/en.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -259,15 +259,11 @@ const en = {
259259
'sourceControl.filter.conflicts': 'Conflicts',
260260
'sourceControl.filter.synced': 'Synced',
261261
'sourceControl.filter.showSynced': 'Show synced',
262-
'sourceControl.section.all': 'ALL',
263-
'sourceControl.section.readyToPush': 'READY TO PUSH',
264-
'sourceControl.section.selectedForSync': 'Checked Changes',
262+
'sourceControl.section.selectedForSync': 'Sync Queue',
263+
'sourceControl.section.queueSubtitle': '{count} files selected',
264+
'sourceControl.section.repositoryChanges': 'Repository Changes',
265265
'sourceControl.section.clearSelection': 'Clear',
266266
'sourceControl.section.clearSelection.tooltip': 'Deselect all changes',
267-
'sourceControl.section.changes': 'CHANGES',
268-
'sourceControl.section.remoteChanges': 'REMOTE CHANGES',
269-
'sourceControl.section.conflicts': 'CONFLICTS',
270-
'sourceControl.section.synced': 'SYNCED',
271267
'sourceControl.push': ' Sync ({count})',
272268
'sourceControl.push.tooltip': 'Push {count} ready file(s)',
273269
'sourceControl.refresh.tooltip': 'Refresh',
@@ -297,6 +293,9 @@ const en = {
297293
'sourceControl.diff.switchToUnified': 'Switch to single-column diff',
298294
'sourceControl.diff.split': 'Split',
299295
'sourceControl.diff.unified': 'Unified',
296+
'sourceControl.view.toggleLabel': 'View',
297+
'sourceControl.view.tree': 'Tree',
298+
'sourceControl.view.list': 'List',
300299
};
301300

302301
export default en;

0 commit comments

Comments
 (0)