feat(source-control): replace sync status panel with the source control workflow - #129
Merged
Merged
Conversation
PushSelectionStore and OperationState were keyed by file path, so a rename/move would silently drop a pending selection or in-flight operation status. Introduce a branded ChangeId type and rekey both stores on it so state stores are keyed by SyncChange identity instead of file path, preserving user intent across rename/move. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ClaudiaFang
force-pushed
the
claude/source-control-foundation
branch
from
August 22, 2026 04:56
19dedbc to
5d5645e
Compare
Phase 1 of the Source Control refactor: ChangeRepository, SourceControlFilter, SourceControlViewModel, and ChangeTreeBuilder. Combines SyncChange[], PushSelectionStore, and OperationState into UI-ready state, keyed by ChangeId so renames/moves keep identity. Does not touch SyncManager/SyncPlanner/SyncExecutor or add UI. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add SourceControlView and its component tree (ChangeTree, FilterMenu, ChangeSection, ChangeItem, PushButton, OperationIndicator, SourceControlHeader) built on top of the Phase 1 SourceControlViewModel. Reuses the existing DiffPanel for diff rendering. Not yet registered in main.ts; push/diff actions are injected via callbacks pending Phase 2. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add SourceControlActionService wrapping the existing SyncWorkspace facade to unify push, pull, delete-remote, delete-local, and resolve-conflict actions behind a single ChangeId-keyed API. Resolves ChangeId to SyncChange via the Phase 1 ChangeRepository and reports per-change outcomes through OperationState. push()/loadDiffContent() are directly assignable to Phase 3's SourceControlView callback types. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e legacy UI Phase A integration + Phase E cleanup. The new SourceControlView (Phase 3) was built but never registered; this makes it the sole entry point and deletes the legacy SyncStatusView UI it replaces. Wiring (src/main.ts): - Register SourceControlItemView under the legacy view type string 'sync-status-view' so already-open/pinned leaves resolve into the new view instead of an 'unrecognized view type' placeholder. - Ribbon icon, 'open-sync-status' command, and startup refresh now route through activateSourceControlView(). - Construct ChangeRepository / PushSelectionStore / OperationState / SourceControlViewModel / SourceControlActionService on the plugin; the ribbon/command no longer reach GitService directly. - Subscribe sync.status -> ChangeRepository.replace(toSyncChanges(...)) so the Source Control tree stays in sync with the same SyncStatusService the sync domain already publishes to (no separate refresh/polling path); unsubscribe in onunload. - File modify/rename events now go through syncStatusRefresh (shared service republishes to the open view) instead of per-view notifySyncStatusViews. New: - src/ui/source-control/SourceControlItemView.ts: thin ItemView host that delegates rendering to SourceControlView and routes onPush/loadDiffContent to plugin.sourceControlActions. - src/logic/source-control/FileStatusAdapter.ts: toSyncChanges() projects FileStatus[] -> SyncChange[] for ChangeRepository. Documents two pre-existing FileStatus limits it inherits (not introduces): 'modified' can't tell which side changed (maps to local-modified), and FileStatus never yields 'conflict' (conflicts are detected only during push via SyncPlanner, not pre-computed for display) -- so the CONFLICTS section won't populate from the status map alone until Phase B. Removed (legacy UI replaced by the Source Control layer): - src/ui/SyncStatusView.ts, src/ui/DiffView.ts, src/ui/components/* (4), src/ui/sync-status/* (7), and their tests; styles.css trimmed -547/+174. Verification: npx eslint . -- 0 errors; npm run build -- PASS incl. Obsidian 1.11.0 compatibility; npx vitest run -- 55 files / 531 tests. Manual Obsidian verification in a real vault remains (DoD for UI surfaces): ribbon opens the new panel, tree/filter/push render, live modify+rename refresh, pinned-leaf migration to the new view type, onunload cleanup. Plan: docs/source-control-refactor/roadmap.md (Phases A-E).
Introduce a presentation layer so the UI no longer reads Git status
directly: SourceControlSummary is the single source for every count
(all/changes/remote-changes/ready-to-push/conflicts/synced) and the
ViewModel only forwards its counts.
Filter semantics:
- all = actionable (kind !== 'synced') so All no longer duplicates the
Synced bucket.
- changes = local-side only (local-only/local-modified/moved).
- ready-to-push excludes synced.
Rendering:
- Every filter (including All) renders one flat tree + an active-filter
header; the old section breakdown under All is removed, so SYNCED never
leaks into All.
- Synced hidden by default behind a Show synced toggle; the synced chip
only surfaces when opted in, and hiding it while on synced falls back
to All.
Tree grouping:
- ChangeTreeBuilder gains TreeDisplayOptions { maxDepth,
collapseSingleChild }; the view enables collapseSingleChild so
single-child folder chains collapse to one path node instead of an
Explorer-like deep nest.
ChangeSection.ts deleted (no longer used). i18n keys + styles added.
Verification: npx eslint . 0 errors; npm run build PASS (tsc + Obsidian
1.11.0 compat + esbuild); npx vitest run 56 files / 547 tests. Manual
Obsidian verification in a real vault remains.
Scope excludes diff viewer, conflict resolution UI, push/pull pipeline,
and view migration per the fix plan's后续順序.
Add e2e/support/sync-manager-fixture.ts (real-provider service + verifier + TFile shim + auto-confirming plan/conflict modals, steered by a per-test conflict resolver) and e2e/support/source-control- scenarios.ts (high-level seed/modify/assert verbs + the Source Control selection stack wiring), so workflow suites read as seed -> modify -> push -> expect instead of 50 lines of setup per test. Add a non-breaking removeLocal to FakeVault for delete-local conflict scenarios. No production code touched; existing provider/SyncManager E2E unchanged.
Add e2e/suites/source-control-flows.e2e.test.ts and wire it into scripts/run-e2e.sh. Phase 2 covers: rename+modify (one commit, metadata moved to the new path, old path metadata cleared), multi-rename+modify batch (two moves in one commit), and the Extended nested-directory move and A->B->C rename-chain collapse (GitHub only, since they exercise SyncManager rename tracking rather than provider APIs).
Phase 3 locks the current SyncPlanner conflict contract: modify/modify with a stored baseline IS a conflict (asserted with strengthened side-effect checks — both sides + baseline + HEAD untouched on skip); delete/modify (unrelated push leaves the modified remote intact, metadata not advanced), modify/delete (blind re-create from local), rename with a remotely-edited source (move drops the old-path edit), and no-baseline add/add (local overwrites remote) are NOT conflicts today and are locked as such, so a future change to surface them as conflicts is an intentional, test-updating decision. No production behavior changed.
Phase 4 verifies the end-to-end resolution paths: keep-local pushes local content over the remote in one commit and advances metadata to the new sha; keep-remote pulls the remote blob into the vault (no remote mutation, no new commit) and updates metadata; skip is retained as a regression lock confirming local, remote, baseline metadata, and HEAD are all untouched.
Phase 5: create+modify+rename in one commit (GitHub only), a full create+modify+pure-rename+rename-with-modify lifecycle batch in one commit (all providers), and a safe+conflict batch that locks the current non-atomic contract — safe files land in one commit while the conflict is skipped and the remote stays on the remote side.
Phase 6 drives the real SourceControlActionService + PushSelectionStore + ChangeRepository over the real SyncManager (via BoundarySyncWorkspace): selected-subset push leaves unselected files untouched (core); subset- then-remaining push yields two separate commits (GitHub only); and a rename yields a path-derived ChangeId so selecting the new path's change pushes the move (GitHub only), locking the current status-model assumption. Add listCommitShas to the scenario helper.
Phase 7: remote-ahead pull advances metadata (core); a remote-ahead change and an unrelated local change coexist without cross-contamination (GitHub); a concurrent remote write surfaces as a conflict then reconciles with no lost update (GitHub); an all-unchanged batch reports zero work and zero commits (core); repeating a push makes no second mutation and corrupts no metadata (GitHub); and a skipped conflict can be resolved then re-synced cleanly with no stale operation state (GitHub).
Phase 8: unicode filename create+modify+rename, spaces-and-symbols create+modify, deeply-nested move+modify (GitHub only); 100-file batch create in one commit and a 100-file mixed modify+create+rename batch in one commit (GitHub only); plus an opt-in 1000-file stress create behind E2E_STRESS=1 (never a required CI check).
The provider-e2e job hard-coded the vitest suite list and omitted
source-control-flows.e2e.test.ts, so the new suite never ran in CI
(a "fake green" — the job passed without exercising the new coverage).
Make scripts/e2e-suites.txt the single source of truth: scripts/run-e2e.sh
reads it, expands ${provider}, and runs the listed suites; CI now calls
scripts/run-e2e.sh --provider <provider> (same command local dev uses),
collapsing the separate provision/seed/vitest/verify steps into the one
retry-wrapped entry point. Adding a shared suite now only requires editing
scripts/e2e-suites.txt.
Also make the Gitea-disabled state explicit: the gate step emits a notice
and a step-summary ("Gitea E2E: disabled — runner Docker networking") so a
green gitea leg is never mistaken for three-provider coverage.
Add scripts/check-e2e-suite-registration.mjs and wire it into `npm run lint`
(both the husky pre-commit hook and CI). It fails when an
e2e/suites/*.e2e.test.ts file exists but isn't registered in
scripts/e2e-suites.txt — provider-specific suites (github/gitlab/gitea) are
covered by the ${provider} line; every other shared suite must be listed
explicitly. So adding a suite without wiring CI now breaks the build instead
of silently passing (the original fake-green failure mode).
Replace the standalone check-e2e-suite-registration.mjs (wired into `npm run lint`) with forward/reverse checks inside scripts/run-e2e.sh itself, so suite manifest validation lives in the same script CI already calls instead of a separate Node checker. Also harden GitVerifier.git() to surface stderr on unexpected git failures while keeping expected missing-path lookups silent. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
vitest's default reporter only prints once a whole file finishes, and these suites do real network round trips per test — in CI that reads as a silent hang. Switch to the verbose reporter so each test prints as it completes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
headAfterFirst was captured before the first push instead of after, so expectNoCommitSince compared against the pre-push head — failing on the commit the first push itself legitimately created, not on any duplicate mutation from the second push. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each remote-read verifier call (getFile/fileMissing/listCommitShas) does its own `git fetch origin <branch>` even when nothing has mutated the remote since the last read in the same test — most tests do 3-5 such reads per push. Cache them in SourceControlScenario, invalidated on any call to manager.pushFiles/pullFile or service.pushFile/deleteFile. The invalidation hooks onto the manager/service instances themselves (via a thin Proxy), not this class's own push()/baseline() wrappers, so it stays correct even for mutations this class doesn't mediate directly — e.g. the selection stack's `actionService.push()`, which calls manager.pushFiles through BoundarySyncWorkspace. Scoped to source-control-flows.e2e.test.ts only (the sole consumer of SourceControlScenario); the shared GitVerifier and the other real-provider suites are untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Missing i18n keys (settings.releaseHistory.name/desc/button) referenced by settings-implementation.ts's renderReleaseHistorySetting broke npm run build. Add them to all three locales. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- convergence-assertions.ts: give both path .sort() calls an explicit localeCompare comparator instead of relying on default string sort (typescript:S2871, reliability bug). - sync-manager-fixture.ts: suppress typescript:S2245 on the Math.random() run-namespace generator with a NOSONAR + comment — it's a test-only disambiguator with no security context, not an unsafe PRNG use. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f5vayBwAVMfZxBXokTnay
This was referenced Aug 31, 2026
… static runtime files
Relocates e2e/{config,shim,suites,support} to e2e-tests/provider/ and
replaces scripts/e2e-harness.sh's per-run generation of
obsidian-request-url.ts/window-timers.ts/git-verifier.ts (into
$E2E_RUNTIME_DIR, never committed) with committed static files under
e2e-tests/provider/runtime/ and e2e-tests/provider/support/git-verifier.ts.
GitVerifier now resolves its clone path from $E2E_WORKDIR at call time
instead of a shell-baked constructor default.
Updates scripts/run-e2e.sh, scripts/e2e-suites.txt, vitest.e2e.config.ts,
tsconfig.json, eslint.config.mts, and .github/workflows/ci.yml's
e2e-relevant path filter for the new layout, and extends
tests/ci-workflow.test.ts with contract assertions for it. The path filter
also gains src/logic/sync/** and src/logic/source-control/** (exercised by
the E2E suites but not previously watched) and scripts/e2e-suites.txt /
vitest.e2e.config.ts (previously not path-filtered, so a manifest-only
change could silently skip E2E).
Side effect: removing the generated-file heredoc also removed the file's
only ${var@Q} bash-4-ism, which previously blocked
scripts/e2e-harness.sh provision under macOS system bash 3.2.
Known gap, documented in docs/testing/real-provider-e2e.md and
docs/obsidian-scanner-audit.md: this PR's premise -- that committing these
files under e2e-tests/ (vs. e2e/) is scanner-safe -- is unverified against
the actual Obsidian scanner, and contradicts this repo's own prior audit
finding that the scanner flagged these same APIs regardless of directory.
A real rescan is required before relying on this.
Filed #143 separately for E2E CI retry/tiering
(rate-limit pressure) -- out of scope here, not touched by this commit.
Closes #142
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RQYgJEPDDLNpBxiW7pzibY
…-modified SyncStatusService.classify() collapsed every two-sided diff into a direction-blind 'modified', which FileStatusAdapter then mapped to 'local-modified' regardless of which side actually changed. When only the remote side had moved (the exact shape a second client produces), the change was routed to the push bucket instead of pull, so it was silently skipped/conflicted instead of being pulled — caught by the new two-client-sync E2E suite (P0-1, P0-2) failing against the disposable Gitea provider in CI. classify() now takes optional localChanged/remoteChanged facts (derived from the tracked lastSyncedSha baseline) so it can tell "only remote changed" apart from "local changed" or "no baseline on record", and emits the already-modeled but previously unreachable 'remote-modified' status. FileStatusAdapter, ChangeActionPolicy.canDownload, and ChangeItem's inline Download button are wired through so a remote-modified row now routes to pull by default and offers manual download, same as remote-only. Also relabels the Source Control "Remote" filter chip to "Incoming" in all three locales: with remote-modified now reachable, "Remote Changes" read as "an existing file changed on the remote" and obscured that the bucket also holds brand-new remote-only files — "Incoming" covers both without implying a direction that isn't there.
4 tasks
CI (gitea provider) still failed after the classify() fix: expectConverged compares trackedPaths(context) (the union of both clients' local paths) against the remote listing filtered by context.runPrefix, but trackedPaths itself was never filtered — a real client sync pulls the whole remote tree, so once a client's local vault also picks up every other suite's fixtures on the shared disposable-provider branch, the two sides stop matching by construction. trackedPaths now filters by runPrefix too, per its own (previously unenforced) doc comment. Also fixes an off-by-one in P0-3: BatchConflictResolutionModal's constructor is (app, conflicts, safeCount, ...), so the conflicts array sits at mock.calls[n][1], not [2] (which is the safeCount number) — indexing [2] threw `.map is not a function` before the array of conflicted paths was ever read.
…ture Last piece of the gitea CI failure: after fixing classification and convergence scoping, P0-1/P0-2 still failed expectClean — a client's own just-pushed file stayed 'modified' in its status map instead of flipping to 'synced'. In production (main.ts), SyncManager's default-constructed `status` is threaded into SyncStatusRefreshService as the single shared SyncStatusService, so a push's SyncMetadataStore.update -> status.markSynced(path, sha) updates the exact instance the UI reads. TwoClient instead built refreshService around its own separate `new SyncStatusService()`, while `newManager()` gave the SyncManager a second, orphaned instance (also defaulted, since the fixture passes `undefined` for that constructor param) — so a push's markSynced call landed on an instance nobody read from, and the row only flipped to 'synced' on the next full refresh(), which never came before the test's convergence check. TwoClient now reuses manager.status, matching main.ts.
The real-provider E2E suite (run 33358507732) repeatedly hangs the two-client-sync P0 tests for exactly their 120s testTimeout with no error or log output at all — the shim's fetch() call has no network timeout, so a stalled connection on the CI runner blocks silently until vitest kills the whole test, indistinguishable from a real deadlock in the sync logic under test. Bounding the request lets a stall fail fast with a clear cause instead of masquerading as a hang.
TwoClient's refresh() bypassed vault-folder filtering entirely
(filterFilesByVaultFolder/filterPathByVaultFolder were no-ops) and the
fixture left rootPath/vaultFolder empty, so every refresh() classified
the WHOLE shared branch's remote tree, not just this run's
e2e-tc-<runId>/ namespace. Other suites' leftover fixtures inflated
tree-listing/refresh time and risked the 120s per-test timeout.
Scope both sides via the real production rootPath/vaultFolder model
instead of a test-only filter: createSyncManagerFixture({ scoped: true })
configures the git service's own rootPath (env.ts contexts now accept
one) and settings.vaultFolder to the same e2e-tc-<runId> value, so the
vaultFolder-strip / rootPath-readd round trip cancels out and push/pull
targets stay unchanged while remote-tree classification is actually
scoped. TwoClient's refresh/filter wiring now mirrors main.ts's real
filterFilesByVaultFolder/filterPathByVaultFolder/getNormalizedPath/
getVaultPath instead of bypassing them.
Also adds a fail-fast scope-leakage assertion after every refresh() and
opt-in timing diagnostics (E2E_TIMING_DEBUG=1) around refresh/sync/
baseline, so a future regression or slow run is attributable instead of
surfacing only as a suite timeout.
E2E fixture/support/diagnostics only — no changes to E2E_TEST_TIMEOUT_MS,
retry policy, or production sync code.
npx eslint . — 0 errors (1 pre-existing unrelated warning)
npm run build — passed
npx vitest run — 68 files / 862 tests passed
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNTkwXym3gvHDn8rhhSPap
Follow-up to the scope-isolation commit, per review feedback: - Extracted the vaultFolder path-mapping rules (filterPathByVaultFolder/ filterFilesByVaultFolder/getNormalizedVaultPath/ getVaultPathFromNormalized) into a pure module src/logic/sync/vault-folder-scope.ts, shared by src/main.ts, SyncScanner.toRepoPath (both now delegate, behavior unchanged), and the E2E TwoClient wiring (now imports the same functions instead of a hand-copied duplicate) — production and the E2E fixture can no longer silently drift apart on this logic. - P0-1: removed the redundant second baseline (`other` was baselined then immediately treated as "A creates a new file", which was really exercising modify, not create); `other` is now a genuine create, one fewer real provider push + verifier read. - P0-2: removed its trailing expectIdempotent + second expectTwoClientConvergence — idempotency-under-repeated-sync is already covered by P0-1's own expectIdempotent; P0-2's actual contract (concurrent edits on different files both survive) is already proven by the first convergence check + explicit remote-content assertions. - convergence-assertions.ts: added captureRemoteSnapshot/RemoteSnapshot so expectConverged/expectMetadataConsistent share one getFile-per-path + one listFiles instead of each independently re-fetching the same remote files; expectTwoClientConvergence now captures once and passes it to both. - Wrapped captureRemoteSnapshot in the existing opt-in timed() helper as "remote snapshot (verifier)" for the same E2E_TIMING_DEBUG=1 breakdown. Not done this round (per plan): no GitLab server-side rootPath listing optimization, no timeout/retry changes, no production sync semantics changes — the main.ts/SyncScanner.ts edits are a pure logic-preserving extraction only. npx eslint . — 0 errors (1 pre-existing unrelated warning) npm run build — passed npx vitest run — 68 files / 862 tests passed Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SNTkwXym3gvHDn8rhhSPap
ClaudiaFang
force-pushed
the
claude/source-control-foundation
branch
6 times, most recently
from
August 31, 2026 10:15
5fb4c96 to
a589045
Compare
ClaudiaFang
force-pushed
the
claude/source-control-foundation
branch
from
August 31, 2026 10:17
a589045 to
ac7b466
Compare
|
ClaudiaFang
pushed a commit
that referenced
this pull request
Sep 1, 2026
## [1.6.0](1.5.9...1.6.0) (2026-09-01) ### Features * **conflict-modal:** wire SyncDiffService.getConflictStat as the batch conflict diff-stat loader ([246a59f](246a59f)) * **conflict-modal:** wire View Diff data through SyncDiffService ([ec9d356](ec9d356)) * **source-control:** add Phase 2 action service ([70f6c9e](70f6c9e)) * **source-control:** add Phase 3 source control UI ([7cec661](7cec661)) * **source-control:** add push-selection and operation-state foundation ([2d72022](2d72022)), closes [#128](#128) * **source-control:** add remote-only download action and queue upload/download routing ([f9eb81b](f9eb81b)) * **source-control:** add ViewModel foundation layer ([76db082](76db082)) * **source-control:** full-width diff tab and mobile view-title dedup ([f449125](f449125)) * **source-control:** presentation adapter, diff stat, responsive mobile ([dd8ddd5](dd8ddd5)) * **source-control:** selected-section rows, drop show-synced toggle, colored diff-stat ([853793c](853793c)) * **source-control:** split sync queue/repository regions and extract DiffStatProvider + SelectionController ([9fd1789](9fd1789)), closes [#136](#136) [#135](#135) [#136](#136) * **source-control:** wire Source Control view as the entry and remove legacy UI ([4e647fb](4e647fb)) * **sync-status:** add refresh and operation feedback ([759b717](759b717)) * **sync-status:** add selection workflow and sync action UI ([625fad2](625fad2)) * **sync:** auto-refresh status on local vault changes and distinguish local deletes ([4009f1d](4009f1d)), closes [#66](#66) * **whats-new:** add onboarding layout for the Source Control workflow ([88e08dd](88e08dd)) ### Bug Fixes * **ci:** continue after intentionally skipped E2E jobs ([18de6e0](18de6e0)) * **ci:** fold E2E suite registration check into run-e2e.sh ([c2bfeb0](c2bfeb0)) * **ci:** isolate manual E2E concurrency ([b5884fc](b5884fc)) * **ci:** run E2E suites through shared runner ([e9f0d28](e9f0d28)) * **ci:** serialize branch validation workflows ([acd2046](acd2046)) * **conflict-modal:** apply modal sizing CSS and add filename-first rows with progressive diff stats ([ac2bd2a](ac2bd2a)) * **deps:** bump undici and ip-address overrides to patched versions ([44c17ba](44c17ba)), closes [#43-45](#43) [#42](#42) [#34-35](#34) * **e2e:** bound the requestUrl shim to a 30s timeout ([8d09aad](8d09aad)) * **e2e:** constrain generated runtime imports ([5dcd87e](5dcd87e)) * **e2e:** invalidate SourceControlScenario's remote cache on commitResolvedBatch ([257b2a4](257b2a4)) * **e2e:** replace unsafe dynamic imports and align Obsidian lint ([3e9f709](3e9f709)) * **e2e:** resolve SonarCloud quality gate findings on new code ([38a5d9e](38a5d9e)) * **e2e:** scope two-client convergence checks to the run's own namespace ([2c7a473](2c7a473)) * **e2e:** share the manager's SyncStatusService in the two-client fixture ([120dfe5](120dfe5)) * **i18n:** remove duplicate releaseHistory keys from concurrent fixes ([de55653](de55653)) * **settings:** keep release history accessible after dismiss ([c37e37c](c37e37c)) * **settings:** keep release history accessible after dismiss ([d6cdc36](d6cdc36)) * **source-control:** apply keep-remote-only batch plans and harden resolution tests ([2591e05](2591e05)) * **source-control:** correct mobile queue and diff presentation ([b3720f7](b3720f7)) * **source-control:** correct one-sided diff stat direction ([fbe0787](fbe0787)) * **source-control:** correct status grouping and filter semantics ([0bcc800](0bcc800)) * **source-control:** harden scroll, diff-stat and create lifecycles ([05f6628](05f6628)) * **source-control:** key selection and operation state by ChangeId ([4b09425](4b09425)) * **source-control:** make keep-remote resolution authoritative ([c73c9cc](c73c9cc)) * **source-control:** make sync actions actually sequential and clean up on remote delete ([2dfe78b](2dfe78b)), closes [#129](#129) * **source-control:** make whole view scroll, add clear-selection, click-to-collapse folders ([a9d3e98](a9d3e98)) * **source-control:** pin Checked Changes, independent scroll for Changes tree ([d7606ff](d7606ff)) * **source-control:** preserve Changes tree scroll position on rerender ([10e344f](10e344f)) * **source-control:** preserve mobile list position after diff ([709905a](709905a)) * **source-control:** prevent duplicate sync status views ([2bbd042](2bbd042)) * **source-control:** refresh the open diff tab when its backing status changes ([10f9ead](10f9ead)) * **source-control:** repair diff stat cache lifecycle ([2d6cf91](2d6cf91)) * **source-control:** route local-deleted to delete-remote, not pull-restore ([ebd8cb6](ebd8cb6)), closes [#129](#129) * **source-control:** track diff stat requests by generation token ([78a78e8](78a78e8)) * **source-control:** unify sync completion notification ([17b361f](17b361f)) * **sync-status:** preserve refreshed state across live modifications ([49f3033](49f3033)) * **sync:** classify remote-only changes as remote-modified, not local-modified ([264cb47](264cb47)) * **sync:** report added/updated counts in push/pull toasts, i18n them ([b5eb1ae](b5eb1ae)) * **sync:** stop remote-op failures from masking each other's outcome ([02bd3e3](02bd3e3)) * **test:** capture post-push head before asserting no-op repeat push ([039588f](039588f)) * **test:** show per-test progress in real-provider E2E CI logs ([54e3fb7](54e3fb7)) ### Performance Improvements * **ci:** gate and tier real-provider E2E ([b1d2208](b1d2208)) * **test:** memoize remote reads in source-control-flows scenarios ([b9a90b2](b9a90b2)) ### Documentation * add source control refactor roadmap ([5d5645e](5d5645e)) * align guides with source control workflow ([d4ce756](d4ce756)) * **claude:** remove session-handoff references from agent workflow ([a4ccf9f](a4ccf9f)) * **imgs:** add sync-status screenshot ([a07a895](a07a895)) * **progress:** record CI run 33358507732 triage ([9528528](9528528)) * record CI green evidence for the whole-run concurrency ([8c07011](8c07011)) * record final-fix round in session handoff and progress ([17d241c](17d241c)) * record Gitea E2E CI verification ([bf33cd2](bf33cd2)) * record lifecycle hardening round in session handoff and progress ([9eb3713](9eb3713)) * record sync-status-workflow-ui completion in progress + handoff ([8aa8fdf](8aa8fdf)) * record unified sync notification verification ([25dfbc8](25dfbc8)) * restore README demo media ([daf7c81](daf7c81)) ### Code Refactoring * **diff:** extract shared DiffViewer, gfs-conflict-modal shell, and gfs-diff-surface tokens ([769f82d](769f82d)) * **source-control:** converge UI to sync-intent workflow ([639840a](639840a)), closes [#135](#135) * **source-control:** Selected section becomes a read-only action queue ([7538e0a](7538e0a)) * **source-control:** staged/Changes split with collapsible sections ([60790a3](60790a3)) * **source-control:** unify Sync into one plan, one commit ([264ae4b](264ae4b)) * **sync-status:** integrate source control view model ([8c69cc8](8c69cc8)) * **sync:** compact batch-conflict header and drop totalFiles from the interaction port ([a7dcda7](a7dcda7)) * **test:** move real-provider E2E to e2e-tests/provider/, commit static runtime files ([376901f](376901f)), closes [#143](#143) [#142](#142)
Member
Author
|
🎉 This PR is included in version 1.6.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This was referenced Sep 18, 2026
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.




Summary
Umbrella PR for the Source Control foundation: this branch (75 commits) carries the sync-status UI from a legacy read-only panel to the full Source Control workflow — selection → Sync Queue → one-plan/one-commit sync — plus the real-provider E2E harness that gates it.
Closes #128, and delivers the issues listed below.Core: state foundation (#128)
PushSelectionStore— tracks which pending sync changes are "Ready to Push" (includeForPush()/excludeFromPush()), independent of theSyncPlanner/SyncExecutorchange model. Deliberately avoids VCS stage/unstage terminology; keyed by brandedChangeIdso rename/move preserves selection and in-flight intent.OperationState— per-change in-flight operation status (idle/running/success/failed), independent of selection.SyncManager/SyncPlanner/SyncExecutorbehavior.Source Control UI (Phase 1–3 + user workflow)
SourceControlViewModel+ChangeRepository+OperationState/RefreshStatefoundation layer (76db082), Phase 2 action service (70f6c9e), Phase 3 UI (7cec661), then legacy UI removal — the Source Control view is now the single entry (4e647fb).9fd1789,60790a3).local-deletedroutes to delete-remote, not pull-restore (ebd8cb6,264ae4b).264ae4b), with a single aggregated completion toast, i18n'd (17b361f,b5eb1ae).f449125).88e08dd) + README/docs alignment (d4ce756).Diff stats (+N/−N) (#93)
DiffStatProvider— three-state load result (ready/pending/unavailable).pending(content not yet in memory) is never cached, so rows retry and late-arriving content lands;unavailable(binary/symlink) is cached permanently. Per-rowinvalidate(id);clear()reserved for full refreshes (2d6cf91).local-onlyfirst, batched settle → one progressive re-render.handleFileCreatedreads content before publishing, so a freshArow shows +N immediately.Mobile UX
MainScrollState+ anchor-ChangeId re-anchoring absorbs height/order changes from live status and background stat updates; onlyselectedChangeIdresets on Back (709905a,10e344f,d7606ff)。Live status (
4009f1d)create/modify/delete/rename) update the shared status store live; legacy UI is dead and duplicate-leaf normalization handles old saved workspaces (2bbd042).Real-provider E2E (#115, #139)
8ed5df9…119ba03).mktempworkdir, hardened hosted runners).395b87e), E2E tiering + concurrency isolation (b1d2208,b5884fc).Lifecycle hardening + legacy cleanup (final round)
restoreNavigationScrollOnNextRender); checkbox/diff-stat/status rerenders restore their own captured DOM positions and never re-anchor. Regression matrix covers scroll=900 through every rerender class, Back→900→scroll 1400 fidelity, and row-vanished-while-in-detail.invalidate()/clear()both drop the cache AND deny in-flight old-content responses the cache write; in-flight markers are evicted so the reload is immediate; background loader rejections are retryable (never cachedunavailable) and cannot escape as unhandled rejections.handleFileCreatedpublishes the row immediately (pending, uncached stat) then lands content async under a per-path revision guard — a slow create read can't clobber a raced modify, and a read failure leaves the row visible/retryable.SyncDiffService(local-only ⇒ remote '', remote-only/local-deleted ⇒ local '') so stats render +N/−N without per-kind UI branching, plus in-flight remote-blob deduplication (stat loader racing user-opened diff ⇒ one fetch).ui/sync-status,src/ui/source-control/**included in coverage (~90% lines) with conservative thresholds.Related issues
selectedItems→syncQueuerename,DiffStatProvider/selection extraction landed (9fd1789)Test plan
npx eslint .— 0 errorsnpm run build— clean, including Obsidian 1.11.0 compat typechecknpx vitest run— 66 files / 788 tests passed🤖 Generated with Claude Code