Feat/sync status workflow UI - #135
Merged
ClaudiaFang merged 17 commits intoAug 24, 2026
Merged
Conversation
Add selectedItems and refreshStatus projections to SourceControlViewState so later UI commits can render a 'SELECTED FOR SYNC (N)' section and refresh button states. selectedItems reuses buildSummary.readyToPush (selected + non-synced) so the section and Sync button count never drift. Add RefreshState holder (idle/loading/failed) mirroring OperationState's API shape but for a single view-wide refresh rather than per-change operations. Add ViewModel.refresh(), which delegates to an injected refresh callback wired to SyncWorkspace.refresh() in main.ts and drives the RefreshState lifecycle. Refresh republishes sync.status, so the existing subscription repopulates ChangeRepository — refresh never becomes a second population path, and no ChangeRepository.reload() is added. Wire RefreshState + the refresh delegate into the ViewModel in main.ts. Update the ViewModel docstring to acknowledge the refresh-delegation responsibility (it delegates; no provider/refresh logic lives in it). Domain untouched: only SourceControlViewModel.ts edited and RefreshState.ts new under src/logic/source-control/ (filter/summary/types/repository/store/ adapter unchanged).
Add the 'SELECTED FOR SYNC (N)' summary section above the change tree, rendered only when the user has at least one actionable change selected for push. Its count comes straight from the ViewModel's single-source selectedItems projection (the same selected + non-synced definition as the Sync button count), so the section and the Sync button can never drift. Synced changes are excluded even when selected. Drop the 'Ready to Push' chip from the filter row. The visible row is now four chips — All / Local / Remote / Conflict — backed by the unchanged domain filters (all / changes / remote-changes / conflicts). data-filter attributes keep the domain values; only the displayed labels change via new i18n keys (sourceControl.filter.local / .remote / .conflict). The ready-to-push and synced domain filters remain in the type (the former is just no longer exposed as a chip; the latter still surfaces via the 'Show synced' toggle). Add sourceControl.section.selectedForSync i18n key (en/zh-cn/zh-tw) and CSS for .scv-selected-section. FilterMenu/SourceControlView tests updated.
Add a refresh button to the Source Control header with three states
driven by the RefreshState holder: idle (icon-only), loading
('Refreshing...' with a spinning icon, disabled), and failed
('Refresh failed'). The button reads refreshStatus off the ViewModel
state and calls a new onRefresh callback. Wire onRefresh in
SourceControlViewCallbacks and SourceControlItemView to the ViewModel's
refresh() delegate via a runRefresh helper that renders immediately (so
the loading state shows once refresh() sets RefreshState to 'loading'
synchronously) then re-renders on settle (idle on success, failed on
rejection — the rejection is swallowed since the state was already
recorded on the holder).
Add text labels alongside the per-change OperationIndicator icon
('Syncing' / 'Synced' / 'Failed') via new sourceControl.op.* i18n keys,
so an in-flight operation is readable rather than icon-only.
Add sourceControl.refresh.* i18n keys (en/zh-cn/zh-tw) and CSS for
.scv-refresh-btn states and .scv-op-label. The push button's full-width
layout becomes flex:1 so the refresh button sits beside it. Refresh and
operation tests added.
Add ChangePresentation UI adapter so all kind-specific presentation
(badge letter, subtitle, rename display, deleted-locally tooltip) lives in
the UI layer, keeping the domain filters/summary semantics-only. remote-only
is badged 'D' (deleted locally) rather than 'A'; rename and subtitle move
out of ChangeItem into the adapter.
Thread an optional diff-stat through each row: local-only stats are
eagerly resolved from the in-memory sync.status (no provider call) on
render and cached; two-sided stats lazy-load on open and reuse the diff
content the pane already fetches. The cache clears on refresh. Null
results (binary/missing content) are cached too so they aren't retried
every rerender.
Responsive mobile layout: chips collapse to a single filter dropdown, the
header push button is hidden, and a sticky bottom sync bar appears when a
push selection exists. The mobile tree uses a flatter shape
(collapseSingleChild + maxDepth). No src/logic/source-control/ files
touched beyond ViewModel + RefreshState.
- New: src/ui/source-control/ChangePresentation.ts
- New i18n: sourceControl.status.{added,modified,renamed,deletedLocally,
modifiedRemotely,conflict,synced} + deletedLocally.tooltip (en/zh-cn/zh-tw)
- Tests: ChangePresentation (badge/subtitle/rename/stat), ChangeTree
(remote-only D, subtitle, diff-stat span), SourceControlView (stat
caching/clear-on-refresh/lazy-load, mobile dropdown + bottom sync bar)
- 629 tests pass, eslint clean, build + Obsidian 1.11 compat pass
… colored diff-stat - Selected section now lists actual selected change rows (full renderChangeItem rows with unselecting checkboxes) instead of a bare count; sits in a boxed region between the filter and the tree. - Tree keeps selected rows visible but muted via an is-selected class (italic name, reduced opacity) so context isn't lost. - Remove the 'Show synced' toggle and the synced chip from the UI; the domain synced filter/summary stay computed by the ViewModel but have no entry point. - Drop the inline status subtitle from each row; the kind label now lives on the badge tooltip, removing the M/Modified redundancy. - Split the diff stat into green additions / red deletions spans. - Use design tokens (--radius-s/--radius-m) for chip and section radii.
…ck-to-collapse folders
- Move the Selected section into the scroll body so the whole lower region
(selected rows + filter header + tree) scrolls as one. Previously the
selected section sat outside the scroll container with no height cap, so a
tall selection blew out the layout under .scv-root{overflow:hidden} and
nothing scrolled.
- Add a 'Clear' button to the Selected section header that deselects every
selected change in one shot, instead of unchecking rows one by one.
- Make the entire folder row toggle collapse (clicking the folder name or
chevron expands/collapses); only the select-all checkbox keeps its own
action. The chevron stops propagation so it doesn't double-fire.
… queue - Selected rows now render as queue items (badge + name + diff-stat, NO checkbox) instead of full tree rows, so the section reads as an action preview of the working push batch rather than a second active copy of the tree. Selection still happens in the tree below via checkboxes. - Eager-load diff stats for every selected change (any kind) so the queue previews +/- next to each row; tree two-sided rows stay lazy on open. - Drop the is-selected muting (opacity/italic) from tree rows: a checked checkbox is the only selection signal in the browser, matching the queue/ browser role split. - Export renderDiffStat and add renderSelectedQueueItem (reuses presentChange + the colored diff-stat spans).
PR #135 review flagged that the view mixed VS Code staged / Git status / sync-queue concepts. Converged to a single sync-intent workflow, view-layer only (no domain file touched; the domain-untouched invariant still holds). Filter chips redesigned to All / Needs Sync / Remote / Conflict / Synced via a UI chip model mapping (domain filter, showSynced): - Needs Sync (domain all, showSynced=false) = actionable set, default — keeps a quiet workspace quiet. - All (domain all, showSynced=true) 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 was needed. - Synced re-surfaces (domain synced, showSynced=true); Local dropped. SELECTED FOR SYNC renamed to SYNC QUEUE (the queue stays a compact read-only action preview). Badge tooltips read Added locally / Modified locally. Mobile sync bar becomes N files selected + Sync. Verification: eslint 0 errors; build clean (incl. Obsidian 1.11.0 compat); vitest 632/633 (1 unrelated pre-existing ci-workflow.test.ts failure from an uncommitted ci.yml). Domain diff shows only RefreshState.ts + SourceControlViewModel.ts.
Restructure .github/workflows/ci.yml so lint, unit-test (Node 22|24), build, and provider-e2e all start in parallel after a push instead of lint/test/build waiting behind the real-provider E2E matrix. - Remove 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 (raw-artifact upload folded into CI / Build, same job as the build, so upload state can't drift from build state). - Add CI / Lint, CI / Unit Test (Node 22|24), CI / Build, CI / Provider E2E / <provider>, CI / Required Checks (aggregate gate, if: always(), passes only when every validation job is success/skipped), Release / Package (zip artifact, all branches), Release / Publish (semantic- release + build-provenance attestation, main/master only). - provider-e2e internals unchanged (gitea-disabled notice, concurrency group e2e-<branch>-<provider>, run-scoped E2E_WORKDIR, retry, if: always() cleanup); only name and needs: [changes] (dropped preflight) changed, so cleanup workflows' shared concurrency naming still matches. - All third-party actions pinned to commit SHAs (upload-artifact@v7, attest-build-provenance@v4.2.2). Verification: actionlint v1.7.7 .github/workflows/ci.yml -- 0 errors (only the known 32gb-ram self-hosted-label false positive); YAML parses. Source tree (eslint/build/vitest) untouched by this change. Follow-ups (progress.md 0b/0c): real PR run + execution-time comparisons; manual branch-protection switch to require CI / Required Checks.
Replace the read-only SYNC QUEUE with a VS Code Staged/Changes model: checking a tree row moves it up into the CHECKED CHANGES region and OUT of the lower Changes tree (no duplication); unchecking it there moves it back down. Both regions are collapsible via their headers. - CHECKED CHANGES (N): selected items as normal rows (badge+name+diff-stat, checkbox checked). Uncheck here moves the row back to the tree. - CHANGES (M): active-filter items EXCLUDING selected, as the folder tree. The active-filter-header count now reflects remaining (unchecked) rows. - Both headers (Checked Changes + the active-filter header) toggle collapse; state persists across rerenders like collapsed folders. - Mobile sync bar still renders when the lower tree is empty but the queue is non-empty (no early return). i18n: section label 'SYNC QUEUE' -> 'Checked Changes' (zh: 已勾选的更改/ 已勾選的變更). Domain untouched (only RefreshState.ts + SourceControlViewModel.ts vs the base branch). Verification: eslint 0; build clean (incl. Obsidian 1.11.0 compat); vitest 634/635 (1 unrelated pre-existing ci-workflow.test.ts failure).
…ges tree The Checked Changes region and the Changes tree shared one scroll container, so expanding the tree (many files) pushed the pinned queue out of view. Split them: Checked Changes is flex-shrink:0 at the top (its list capped at 38vh with its own scroll), and the Changes region is a flex:1 column whose tree scrolls independently. Expanding folders in the tree now never hides the queue above it.
…cks gate The old contract asserted the removed e2e-gate cancelled-handling string and run-ci mechanism. Replace with assertions for the new DAG: validation jobs run in parallel (no preflight/e2e-gate), and release is gated behind a single CI / Required Checks aggregate (if: always(), needs all four validation jobs) that treats a cancelled matrix leg as a hard failure.
… 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
…d/download routing
The `remote-only` badge was `D` (read as "deleted locally"), which is
misleading — pushFiles never propagates a local deletion. Re-badge to `↓`
("Remote available") and `remote-modified` to `↕`. Add an inline Download
button on `remote-only` rows wired to `actionService.pull`, so a remote
file pulls into the vault without first entering the Sync Queue. Route
the Sync button per-kind via `changeOperation(kind)`: upload kinds
(local-only/local-modified/moved/conflict) -> onPush, download kinds
(remote-only/remote-modified) -> new onPull callback. Split the Sync
Queue into Upload/Download sub-groups (labels only when mixed) so a
queued batch reads as what the Sync button will actually do.
View-layer only; no src/logic/** changes. New optional onPull/onDownload
callbacks on SourceControlViewCallbacks, wired in SourceControlItemView
to sourceControlActions.pull.
Verification: npx eslint . -- 0 errors; npm run build -- clean (tsc +
Obsidian 1.11.0 compat + esbuild); npx vitest run -- 62 files / 668 tests
pass. Real-provider gitea E2E + manual Obsidian verification remain.
…h local deletes Add a 'local-deleted' SyncStatus/SyncChangeKind so a previously-tracked file removed locally classifies distinctly from a never-tracked 'remote-only' download candidate (badge D, 'Deleted locally', changeOperation download), driven by a new SyncStatusFacts.wasTracked flag in SyncStatusService.classify. Threaded through FileStatusAdapter/SourceControlFilter/SourceControlSummary/ ChangePresentation, and identifyExtraFiles + move reconciliation treat local-deleted like remote-only. Thread a RefreshReason (startup|manual|local-change|remote-change|sync-complete) through RefreshState.start -> SourceControlViewModel.refresh (default manual; startup refresh now goes through the ViewModel with 'startup'), recording the reason and a lastCheckedAt timestamp so the header can surface 'Last checked'. Wire app.vault create/delete events to new optimistic handlers (handleFileCreated -> unsynced; handleFileDeleted: synced/modified -> local-deleted, unsynced/moved -> drop) alongside the existing modify/rename handlers. All update the shared SyncStatusService which republishes through the existing pipeline (no direct filesystem->UI path; syncMetadata is not cleared on delete so out-of-band move reconciliation keeps working, #66). Add a 'Last checked: just now / <time>' line to the header info strip, fed by refreshState.getLastCheckedAt(); hidden before the first refresh. Tests: new SyncStatusRefreshService suite (handleFileCreated/handleFileDeleted/ identifyExtraFiles local-deleted classification), RefreshState reason/ lastCheckedAt, sync-status-service local-deleted matrix, FileStatusAdapter/ SourceControlFilter/SourceControlSummary/ChangePresentation local-deleted, SourceControlViewModel refresh-reason threading, SourceControlView header 'Last checked' line. i18n keys across en/zh-cn/zh-tw.
|
ClaudiaFang
merged commit Aug 24, 2026
66d7b8a
into
claude/source-control-foundation
18 of 23 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.




Description
Type of Change
Engineering Standards
Related Issues