ZenNotes 2.48.0: a Cloud conflict decision saves one note and stays saved, queued conflicts merge on their own when they safely can, interrupted uploads clean up, and Escape cancels a Quick Capture selection without closing the window
A follow-up to the 2.47.0 Cloud conflict work from Unyanda's reports: choosing a version no longer waits on a download of the whole vault, a saved decision stays saved when the sync that follows it is slow or fails, a queued conflict that has become a clean three-way merge resolves itself on the next sync, an interrupted large upload closes its file reader instead of reporting "Controller is already closed", and Cloud Settings follows the live sync state. Also: Escape cancels a Quick Capture selection without closing the window (#765), and the Nix package builds against nixpkgs 25.11 and later (#763). Release PR: #766.
🐛 Fixes
-
Resolving a Cloud conflict no longer downloads the whole vault first. Choosing your version, the other device's version, both, or a combined note used to fetch the content manifest of the entire vault before it would save one note, so on a vault with many attachments the decision could time out with nothing written. The server already checks the base revision and the destination path atomically, and a vault-wide download cannot strengthen that check, so the save now goes against the reviewed revision alone. Taking the Cloud version, or keeping both, checks freshness from metadata and fetches only that file's retained bytes when the snapshot lacks them. Hosts without revision reads keep the content-manifest path. A save that times out stays retryable with the local file untouched; a stale revision is reported as The Cloud version changed. Sync again before choosing a version.
How to test locally: launch the isolated desktop against the local Cloud fixture as described below, on a vault with a few dozen attachments, and create a conflict by editing the same line on both test devices. Open Review now and pick a version. Before: the decision waits on every unrelated asset and can time out with the conflict still open. After: the note saves at once. Edit the Cloud side again before deciding: the review reports the stale revision and asks for a fresh sync instead of writing over it.
-
A saved conflict decision stays saved. The whole-vault sync that runs after a decision used to own the outcome: a slow or failed run left the review open on a note that was already resolved and invited a second save, which asked the server to resolve it twice. The review now moves to the next conflict, or closes, the moment the save succeeds; the remaining vault sync runs in the background, and a failure is reported as Note saved. Remaining vault sync failed: plus the reason, so the note is known to be safe. A resolver that has saved ignores further clicks, and Settings adopts the shared summary when the later run replaces it.
How to test locally: create a conflict as above, then stop the local Cloud server and choose a version. Before: the review stays open on the resolved note and pressing the same choice again sends a second resolution. After: the review advances and the status reads Note saved. Remaining vault sync failed: with the network error; the local file holds the chosen content, and the next Sync now after restarting the server completes cleanly.
-
Queued conflicts merge on their own once they safely can. A content conflict that could not be merged when it was found (one side's bytes not yet retained, for example) stayed queued until opened, even after the two sides had become a clean three-way merge, such as a daily note where one device changed the date at the top and the other appended at the bottom. Every sync now reconsiders those conflicts when the Cloud side is unchanged since the conflict was recorded, all three paths agree, there is no draft and no paused path, and the current local file merges cleanly against base and Cloud; the merged text is written to disk and uploaded, and the conflict clears. Overlapping edits, unfinished drafts, renames, paused paths and a file changing mid-write stay queued for you. Identical files keep the stricter convergence check from 2.47.0.
How to test locally: on two linked test devices, change the first line of the same note on one and append a paragraph on the other, sync both, and leave the conflict queued without opening it. Press Sync now. Before: the conflict waits until you open it. After: the note holds both edits, the merge is uploaded, and the queue is empty. Repeat with both devices editing the same line: the conflict stays queued and the review still asks.
-
Interrupted large-asset uploads clean up properly. A failed, cancelled or early-rejected upload now disposes its native file reader before releasing the upload reservation. That removes the stream-adapter path behind the Controller is already closed error Unyanda hit on a large image, and a retry starts from a clean reservation. The existing per-file limit is unchanged.
How to test locally: paste an image of several megabytes into a synced note, then stop the local Cloud server while the upload is in flight. Before: the sync error reads "Controller is already closed" and the retry fails the same way. After: the error names the network failure, and Sync now after restarting the server uploads the file with matching bytes.
-
Cloud Settings shows the current sync state. An upload or a failure no longer leaves an old Everything is up to date result on screen, and a successful retry clears the previous sync error, including a retry started from the editor. Unrelated backup and publishing errors, and unfinished conflict drafts, stay visible.
How to test locally: open Settings > Cloud and press Sync now with the local Cloud server stopped. Before: the summary from the previous run stays on screen. After: the failure shows immediately; restart the server, retry from the editor status, and the panel reports the successful run with the old error gone.
-
Escape cancels a Quick Capture selection without closing the window (#765, reported by Unyanda). Quick Capture treated every Escape as "save and hide", so leaving a Vim visual selection, or an ordinary editor selection, closed the window and saved a draft you were still writing. When the editor handles the Escape, Quick Capture now leaves the draft open; the next unhandled Escape still saves and hides the window, and the explicit save shortcut is unchanged. Insert mode, the completion popup and the picker dismiss the same way.
How to test locally: open Quick Capture with its hotkey, type a few words, and select some of them (
vand a motion in Vim mode, or Shift+Left otherwise). Press Escape. Before: the window saves and hides. After: the selection clears and the window stays open; a second Escape saves and hides it as before.
🧰 For contributors
- Nix packaging: #763, by @blueagledev, replaces the 12 deprecated
xorg.*names inpackage-desktop.nixwith their top-level libraries; the derivation is unchanged on the flake's pinned nixpkgs. Requires nixpkgs 25.11 or later; 25.05, end-of-life since December 2025, is no longer supported by the copy-the-file route. - Dependencies: #760 moved
smol-tomlto 1.8.0 andhonoto 4.13.7 for the day's advisories.npm audit --omit=dev --audit-level=highis clean; the only remaining finding is a moderate, development-onlyvitestadvisory. - Sources:
06dc99e0(conflict decisions and queued merges),55cafa08(upload streams and sync status),edd55e03(Quick Capture Escape),ba4ae683(Nix). New tests:cloud-sync-resolution.test.ts, the "safely merging queued conflicts" block incloud-sync-coordinator.test.ts,cloud-sync-upload-network.test.ts(real HTTP uploads of 8.1 MB and 10 MB with byte and hash checks, disconnects, timeouts, retries and restart recovery), and the Quick Capture Escape cases inQuickCaptureApp.test.ts. - Verified: the Cloud fixes and the Quick Capture change were driven in the built desktop app on macOS against the local fixture. The Arch Linux upload report was reproduced through the synthetic upload fixtures only; a retest on the reporter's device is welcome.
- Build with
npm run build --workspace @zennotes/desktop. Create a scratch root withmktemp -d /tmp/zennotes-248.XXXXXX, then launchZEN_PERF=1 ZENNOTES_USER_DATA_PATH=<scratch>/userdata ZENNOTES_CONFIG_DIR=<scratch>/config ZENNOTES_CLOUD_BASE_URL=http://127.0.0.1:43183 apps/desktop/node_modules/.bin/electron apps/desktop/out/main/index.js --remote-debugging-port=9326. Use a scratch vault and a local test account only.node tooling/scripts/cloud-conflict-demo-fixture.mjssupplies the local Cloud server. - Release gates run fresh at the cut:
apps/desktopbuild:prod(typecheck, tests, build, packaged CLI isolation), Go vet and tests, the app-core suite (2,003 tests) and the shared-domain suite (1,593 tests), the signed packaged app launched in an isolated profile and reaching a CDP page target, and the website suite with the release page entry in place.
Local-first and keyboard-first, as always.