Conversation
…recognize On Linux, Chromium picks the safeStorage keyring backend from XDG_CURRENT_DESKTOP instead of probing the bus for a Secret Service. On compositors it does not recognize (Niri, Hyprland, Sway) it falls back to the plaintext basic_text backend, safeStorage reports encryption as unavailable, and cloud sign-in dies with "could not store the cloud credential securely" even though gnome-keyring is running and healthy. Reported on Discord by a Niri user whose secret-tool round trip worked, which is the confusing part: nothing the user can check locally is consulted by Chromium's detection. The fix appends --password-store=gnome-libsecret before app ready, only when the session variables identify no desktop Chromium recognizes and the user did not pass the switch themselves. Safe by construction: when the Secret Service is genuinely absent, Chromium falls back to basic_text, the same outcome as today. On recognized desktops, including XFCE and LXQt where Chromium chose plaintext deliberately, ZenNotes defers to Chromium's own choice. The storage failure dialog and the remote-workspace console warning now point at the switch instead of leaving a dead end.
…ve echo (#585) Typing a multi-line mermaid block (or editing bullet lists) could erase the entire note, with undo unable to restore it. Four small defects had to line up, and all four are fixed here. writeNote was truncate-then-write, so for a moment every save leaves an empty file on disk. The watcher's awaitWriteFinish delays the echo of one save into exactly that moment of the next, and the echoed readNote comes back empty. The store's live change path then pushed that read over the open buffer without checking the dirty flag (the resync path has the check, with a comment explaining why; the live path did not). The editor applies external content as a non-undoable doc swap (#247), which is why undo could not bring anything back. And persistNote cleared the dirty flag even when keystrokes landed during the write, so the follow-up save that would have healed the disk bailed on its dirty check, making the wipe permanent. Mermaid and bullet lists are only amplifiers: per-line save cycles plus renderer stalls from the first mermaid render push the echo into the truncate window. Note saves now go through writeFileAtomic so no reader can observe a half-written note; the watcher ignores the atomic-write scratch files (which also stops every database save from firing an asset refresh); a dirty buffer is never replaced by a watcher read, the pending save reconciles disk instead; and the dirty flag survives a save whenever the buffer has moved past what hit disk. Verified by driving the built app over CDP: forcing the empty read mid-edit wiped the editor and disk with undo dead before the change, and leaves buffer, disk, and undo history intact after it. Regression tests cover both store defects in store-note-integrity.test.ts. The Go server's WriteNote shares the truncate-then-write shape; the renderer guard already protects web buffers, and the Go write gets its own follow-up once its watcher's rename semantics are verified.
Typing `\su` inside `$…$`, `$$…$$`, or a ```math fence pops KaTeX
commands with the rendered symbol in the completion row's icon slot.
~250 curated commands (greek, operators, accents, fonts, relations,
arrows, functions, delimiters, environments); argument-taking commands
insert as snippets — accepting `\frac` lands the cursor in the
numerator and Tab moves to the denominator — and big operators
scaffold their usual bounds (`\sum` → `\sum_{i=1}^{n}` with each bound
selectable in turn).
Math detection counts unmatched `$`/`$$` delimiters rather than closed
pairs, so a formula still being typed — the moment completion matters —
already counts as math. Escaped dollars and non-math code regions are
excluded; a fence with the `math` info string is a math region in its
own right, matching how remark-math renders it.
One more source in the editor's existing autocompletion stack; no new
dependencies (katex and @codemirror/autocomplete are already there).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…symlink (#585) The desktop stopped wiping notes in 376245e by writing them atomically, but the self-hosted server still wrote truncate-then-write, so a browser or a second device could read a note in the moment its own save had emptied it. A Go test that reads the file while WriteNote runs proves it: against the old implementation a reader sees 0 bytes, the same empty read that erased notes on the desktop. The server writes atomically now. Verifying that turned up three things a rename does that a plain write did not, two of them regressions the desktop change had already shipped. A rename replaces the directory entry, not the file. Pointed at a symlinked note it leaves a regular file where the link was, so a note kept in one place and linked into the vault silently becomes two files. The same call writes config.toml, so a stow or chezmoi managed ~/.config/zennotes/config.toml was being replaced by a regular file on startup with the dotfiles copy never seeing another change, and that one predates this cycle. Both writers now resolve the link and do the atomic dance at the target, which is what workflow-apply.ts had already worked out for workflow edits. A rename also carries the temp file's permissions, so a note chmod'ed to 0600 came back at the default: an existing file's mode is now reproduced exactly, while files these calls create are left umasked exactly as they were. The event kinds changed too, and clients read them. Linux maps a rename into place to IN_MOVED_TO, which fsnotify folds into Create, so an atomic save reaches clients as "add" rather than "change", and the renderer ignored "add" and would have gone on showing content that no longer existed on disk. It now treats "add" for a note it holds open as content to read, which also repairs edits made by git, rsync, Syncthing or vim, none of which ever refreshed an open tab. macOS reports the same rename as a delete followed by a create, and a client told its open note was deleted closes the tab, so the watcher stats the path and never calls an existing file unlinked. Both watchers drop the scratch file itself, which otherwise made every keystroke-driven save re-list the whole asset tree. Verified past the unit tests: the real server binary driven over its change feed the way a web client subscribes (no scratch events, symlink intact and written through, 0600 kept), the full Go suite and an inotify probe inside a Linux container, and the #585 reproductions replayed against the rebuilt desktop app.
…ices A prompt autofocused its input as soon as it opened, which on a phone summons the soft keyboard over the very list the user is about to tap. The folder picker was the worst of it: the suggestions it exists to offer were hidden behind the keyboard the moment it appeared. On a coarse pointer, a prompt that actually has suggestions now opens tap-first: no autofocus, no hint line about arrow keys and Ctrl+J that a phone has no way to press, and taller rows to aim at. Typing is still one tap away in the input itself, and a prompt with nothing to tap keeps its autofocus. Nothing changes for a mouse or a trackpad.
Extracts the decision from 3c6b5f0 into shouldAutofocusPrompt() so it can be asserted the way activeSuggestionAfterInput() is, and pins the three cases that matter: touch with a list opts out, touch with nothing to tap keeps its keyboard, and a fine pointer is never affected. Behavior is unchanged.
…k first A vault could reach a state where every sync ended with "Cloud sync stopped because .zennotes/vault.json has unsynced local edits", naming a file that had not been touched in days and whose hash never changed. Reported on Discord with the diagnostics that made it findable: no write events on the file, the same error after every restart, and a stack pointing at assertUnchanged. Sync refuses to write over a file it cannot vouch for, which is the right instinct, but it never asked whether the file it was refusing to overwrite was already the same file. When the change feed carried an item this device had never tracked and the path existed on disk, it threw without comparing content, and throwing stopped the run before the cursor was saved, so the next run fetched the same change and stopped in the same place, forever. The file it names is not special: the scan sorts by path and a dot sorts first, so .zennotes/vault.json simply got there ahead of every note. Three rules replace the refusal. A file already holding exactly what the change carries is adopted, because both sides agree and there is nothing to resolve. A file that differs is kept where it is while the incoming version lands beside it as "Note (cloud conflict).md", numbered when that name is taken. A delete or a move whose file cannot be vouched for keeps the local file instead, since nothing arrives with those to park and the local file is the version being preserved. Each of those reports a conflict and lets the run continue, so one file can never wedge sync again. Vault settings are answered rather than merged. A conflict copy inside a hidden folder is not something anyone can act on, so the cloud's settings wait at .zennotes/vault.cloud-conflict.json, never synced themselves and replaced rather than numbered when a newer one arrives, while Cloud settings asks which side to keep. This device's settings stay in use until the question is answered, so doing nothing keeps what already works, and taking the cloud's writes them through the vault's own normalizer, which refuses a file it cannot read instead of applying half of it. Two smaller wedges went with it: a remote delete for a file already deleted locally used to fail the run with a raw ENOENT, and so did a move whose source had gone. Both repositories change, the desktop one and the shared portable one behind mobile and web, since they are a deliberately synced pair. The portable one already adopted identical content, which is why this only ever bit desktop.
#591) On a soft-wrapped line, Home and End could land short of the row edge or on a neighboring row, exactly as a bare $ did before a16300f. The reporter spotted that it was the same bug wearing different keys, and they were right: Home and End were never our bindings at all. They fell through to CodeMirror's cursorLineBoundaryBackward/Forward, which locate the row edge by hit-testing an x coordinate at the editor's edge, the same resolution that #575 removed from $ because it walks sub-pixel glyph rects under fractional display scaling. ZenNotes gives that hit-test further to travel than plain CodeMirror does. It probes view.dom, the whole editor element, while the text column is centered inside it, so the probed x sits well outside the text: 87 pixels past it on the machine this was measured on. codemirror-vim maps neither key, so Vim users were on the same CodeMirror path rather than a motion. Both keys are now bound ahead of defaultKeymap to commands that compute the boundary from row geometry through displayRowEdge, the helper $, g0, A and I already use, so no x coordinate is resolved anywhere. Shift extends the selection the same way, and Mod-Home/Mod-End still fall through to document start and end. A caret resting exactly on a wrap point belongs to two rows at once, so, like CodeMirror's own boundary motion, the character before a backward-associated caret is what gets measured; without that a second End press walked on to the next row. displayRowEdge moves out of cm-vim-display-line.ts into cm-display-row.ts, since it is now shared by the Vim motions and by keys that have nothing to do with Vim, and this repo would rather move a function than keep a second copy of it. The mislanding does not reproduce on a pixel-accurate display, as in #575, and emulating fractional device scale factors did not provoke it either, so the verification is structural: unit tests assert exact landings under jittered row coordinates and that posAtCoords is never called at all, and in the running app the true wrap points were measured from DOM text rects, with Home and End landing exactly on every display row in both modes.
Requested after Zed, which maps the same keys, and the keys Vim itself uses to move between sections. In a note the sections are the headings, so `]]` goes to the next one and `[[` to the one before. It is a motion rather than a command, which is what makes it worth having: `d]]` deletes to the next heading, `v]]` selects to it, `3]]` skips three, and `Ctrl+O` comes back, none of which needs writing. The headings come from the parser the outline panel and `Space p` already use, so a `# comment` inside a code fence or a `#` line in frontmatter is never a destination, and what the motion stops on is exactly what the outline lists. With no heading left that way the cursor runs to the end or start of the note, like Vim's section motions, so the key never sits there doing nothing. The keys turned out to be unreachable before they could work. VimNav carries `[b`/`]b` buffer switching as a global fallback for when focus is outside the editor, and it consumed the first `[` or `]` to arm that sequence, with preventDefault and stopImmediatePropagation, so codemirror-vim never saw either press. A bare `]]` did nothing while `2]]` worked, because a pending count already had an exception carved out for it: the same problem was found once before for `f[` and patched narrowly. That fallback now stands down for the whole focused editor, which is where it never belonged: codemirror-vim has `[b`, `]b`, `gt` and `gT` mapped itself. Any future `[x` or `]x` motion would have hit the same wall. Buffer and tab keys are mapped in visual context too, so nothing that used to reach the fallback from a standing selection loses its binding. Two suites cover it: the motion directly, and the keys pressed for real through codemirror-vim, which is the one that matters because `]<character>` is a built-in Vim motion that `]]` also matches, and only a real keypress proves which mapping wins. `d]]` leaves a blank line behind, which is Vim's own rule for an exclusive motion ending in column one, so the test asserts that rather than pretending otherwise.
…kchvtr) Typing `\su` inside `$…$`, `$$…$$`, or a ```math fence now completes KaTeX commands with the rendered symbol in the row, and argument-taking commands land as snippets: accepting `\frac` puts the cursor in the numerator, `\sum` scaffolds its bounds a Tab apart. Merged as sent. Review findings follow in their own commit.
…s in context Review follow-up to #594, kept out of the contributor's commit. A note set to the Typst typesetter takes different syntax entirely, so offering `\frac{}{}` in one was wrong every time. The completion source now reads the renderer the pane is configured with and stays silent unless it is KaTeX. The facet already carried the answer; it just had no accessor. Dollars inside code were being counted as delimiters. A note with `echo $$` in a shell block flipped the parity for everything below it, so `\` popped LaTeX commands in plain prose from there on, and a `$5` in inline code did the same for the rest of its line. Both counts now skip anything the syntax tree says is code, which is the same question the cursor position was already being asked. The block scan also ran from the start of the document on every `\` typed, allocating the whole prefix as a string. It now looks back a bounded window: a display block opened further up than that is not a formula anyone is still typing. The rendered previews are cached between popups. A bare `\` opens the entire table at once, and typesetting every visible row through KaTeX on each open was the one cost this feature could be felt through.
…tags (by @junereycasuga) Frontmatter `tags:` values now behave like inline `#tags`: typing in one suggests tags already in the vault, and each value renders as a chip that opens the tag view. Inline `#tag` completion stands down inside frontmatter so the two never compete. Merged as sent. Review findings follow in their own commit.
…r too Review follow-up to #595, kept out of the contributor's commit. `parseFrontmatterFields` lowercases keys, so `Tags:` and `TAGS:` are the tags field everywhere the vault is indexed: those notes show up under their tags in the Tags view, in search, in the CLI. The editor read the key case-sensitively for an inline `tags: a, b` line, so a note written with a capital T had tags the rest of the app knew about and the editor would neither chip nor complete. Its own block-list branch already lowercased, which is how the two halves of one feature came to disagree. Both places now ask one question, `frontmatterTagsValue`, phrased the way the shared parser phrases it. The deeper risk is that two parsers now decide what a frontmatter tag is: the shared one, which is what gets indexed, and the editor's own scan, which exists because the shared one returns tags without positions and decorations need offsets. They cannot be collapsed, so a test pins them together instead: a matrix of frontmatter shapes (inline list, comma scalar, space scalar, block list, quoted, `#`-prefixed, capitalized key, and two fields that only look like tags) asserts the chips a note renders are exactly the tags `frontmatterTags` returns for it. Whichever side moves next, that test fails.
Two saves for one note could overlap, and the filesystem was free to finish the older body after the newer one. The newer completion had already marked the buffer clean, leaving memory ahead of disk with no retry scheduled. Queue writes per path while keeping different notes independent. Each queued turn snapshots the latest dirty buffer only after the prior write finishes, so disk order now matches edit order.
A first sync classified differing vault settings as an ordinary bootstrap conflict, so it never parked the cloud copy and repeated forever. Established syncs had the opposite problem: they parked the copy, then immediately uploaded local settings before showing the question. Treat settings specially during bootstrap and expose durable pending paths from each filesystem repository. Mutation planning now leaves those paths untouched while continuing to sync every unrelated file.
Windows can deny a destination rename while a watcher, indexer, or antivirus scanner has the file open. The atomic writer treated that transient sharing window as a permanent save failure, and the new concurrent-reader test exposed it in CI. Retry only permission and sharing failures with a short bounded backoff. Both the Go server and Electron writer keep the old file visible until a complete replacement can land, and all other errors still fail immediately.
The display-math scan started at an arbitrary 20,000-character boundary. If that boundary split a long formula from its opening delimiter, completion either disappeared inside the formula or appeared in prose after its closing delimiter. Count display delimiters from the stable start of the document. Long closed and still-open formulas now preserve the same parity as short ones, while code-region filtering remains unchanged.
vscode-textmate includes one-time scanner compilation in its per-line deadline. Slower Windows runners could therefore mark a tiny valid preview as stopped before any token was emitted. Retry only a stopped first line once, after compilation has completed, while charging both attempts to the existing wall-clock caps. A deterministic cold-start regression now covers the behavior.
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.
Release branch for 2.28.2. Cycle is open: more fixes may land here before the tag.
Fixes
A note being edited can no longer be wiped by its own save echo (376245e + 8926e34, closes #585)
Four defects had to line up: non-atomic truncate-then-write note saves; the watcher's delayed echo of save N reading the file inside save N+1's truncate window; the store pushing that empty read over a dirty buffer with no guard (the resync path had the guard, the live path did not); and the editor applying external content as a non-undoable doc swap (#247), with persistNote clearing the dirty flag so the healing save bailed. Fixed at every link:
writeNoterideswriteFileAtomic, the watcher ignores the scratch files,applyChangenever replaces a dirty buffer, and the dirty flag survives a save when keystrokes landed mid-write.The follow-up commit takes the same fix to the Go server and repairs what a rename quietly changes:
atomicwrite.goreplacesos.WriteFileinWriteNote. Its test reads the file concurrently and fails against the old implementation with "a reader saw 0 bytes", the same empty read that erased notes on the desktop.config.toml, which predates this cycle: a stow/chezmoi-symlinked~/.config/zennotes/config.tomlwas replaced by a regular file on startup and the dotfiles copy stopped updating (verified by rebuilding with the old behavior). Both writers resolve the link and write at the target now, matching whatworkflow-apply.tshad already worked out.os.WriteFile/fs.writeFileonly applied a mode when creating a file, so a note chmod'ed to 0600 came back at the default. An existing file's mode is reproduced exactly; created files stay umasked as before.Create, so an atomic save arrives asadd, notchange; the renderer ignoredaddand would have shown content that no longer existed. It now treatsaddfor an open note as content to read, which also repairs edits by git/rsync/Syncthing/vim that never refreshed an open tab. macOS reports the rename as delete-then-create, so the watcher now stats the path and never reports an existing note as unlinked (which used to close the tab of the note being saved).Cloud sign-in stores its credential on compositors Chromium does not recognize (c804038, reported by @uNyanda on Discord, confirmed fixed by the reporter)
Chromium picks the safeStorage keyring backend from
XDG_CURRENT_DESKTOPinstead of probing the bus, so on Niri, Hyprland and Sway it falls back to plaintext and cloud sign-in died with "could not store the cloud credential securely" despite a healthy gnome-keyring. ZenNotes now appends--password-store=gnome-libsecretwhen the session variables identify no desktop Chromium recognizes and the user did not pass the switch themselves. Safe by construction: with no Secret Service present Chromium falls back to plaintext, the same outcome as today. The reporter confirmed the flag fixes sign-in on Niri, which is exactly what this automates.Pickers with a list are tappable one-handed on touch devices (3c6b5f0)
A prompt autofocused its input on open, so on a phone the soft keyboard covered the list the user was about to tap (worst in the move-note folder picker). On a coarse pointer a prompt with suggestions now opens tap-first: no autofocus, no hint line about keys a phone cannot press, and taller rows. Mouse and trackpad behavior is unchanged.
One file can no longer stop Cloud sync forever, and vault settings ask first (46152f7, reported by @uNyanda on Discord)
Every sync ended with "Cloud sync stopped because .zennotes/vault.json has unsynced local edits", on a file that had not changed in days.
assertUnchangedthrew whenever the change feed carried an item this device had never tracked and the path existed on disk, without ever comparing content, and throwing stopped the run before the cursor was saved, so every later run replayed the same change and died in the same place.vault.jsonwas not special:scan()sorts by path and a dot sorts first, so it was simply first in line.Three rules replace the refusal: a file already holding exactly what the change carries is adopted; a file that differs is kept while the incoming version lands beside it as
Note (cloud conflict).md; a delete or move over an unvouchable file keeps the local file. All three report a conflict and let the run continue, so one file can never wedge sync again. Two smaller wedges went with it (a remote delete for an already-deleted file, and a move with a missing source, both used to fail with a raw ENOENT).Vault settings are answered rather than merged: the cloud's version waits at
.zennotes/vault.cloud-conflict.json(never synced, replaced rather than numbered) while Settings, Cloud offers "Keep this device's" or "Use the cloud's". Local settings stay in use until answered, and applying the cloud's runs them through the vault's normalizer so an unreadable file is refused rather than half-applied.Both
CloudSyncRepositoryimplementations change, desktop and the shared portable one behind mobile and web. The portable one already adopted identical content, which is why this only ever bit desktop.Home and End reach the real edge of a wrapped display row (d1f8fd2, #591, reported by @corpserot)
The same defect a16300f fixed for
$, surviving becauseHome/Endwere never our bindings: they fell through to CodeMirror'scursorLineBoundaryBackward/Forward, which hit-test an x coordinate at the editor's edge. ZenNotes gives that probe further to travel than plain CodeMirror (it usesview.domwhile the text column is centered inside it, 87px past the text on the machine measured), and codemirror-vim maps neither key, so Vim users were on the CodeMirror path too. Both keys are now bound ahead ofdefaultKeymapto commands built ondisplayRowEdge, the helper$/g0/A/Ialready use, so no x coordinate is resolved. Shift extends;Mod-Home/Mod-Endstill fall through.displayRowEdgemoved to a newcm-display-row.tsnow that it is shared by Vim motions and non-Vim keys.Not reproducible on a pixel-accurate display (as with #575) and emulated fractional device scale factors did not provoke it, so verification is structural: tests assert exact landings under jittered coordinates and that
posAtCoordsis never called, plus an in-app run measuring true wrap points from DOM text rects with Home/End landing exactly on all 5 rows in both modes. Left open pending the reporter's confirmation.]]/[[jump to the next and previous heading (d7acdc6, closes #578, requested by @corpserot)A real Vim motion, so
d]],v]],3]]andCtrl+Oall work without extra code. Headings come fromparseOutline, so fences and frontmatter are skipped and the motion agrees with the outline panel. Registered in all three renderers.Getting the keys to arrive needed a fix underneath: VimNav's global
[b/]bfallback consumed the first bracket withpreventDefault+stopImmediatePropagation, so codemirror-vim never saw it and no Vim sequence starting with[or]could run. A bare]]did nothing while2]]worked, because a pending count was already excepted (patched narrowly once forf[). The fallback now stands down for the whole focused editor, where codemirror-vim already owns[b,]b,gt,gT; those are additionally mapped in visual context so nothing regresses with a selection standing. Verified in-app that buffer switching still works.Two suites: the motion directly, and the keys pressed for real through codemirror-vim (which matters, since the built-in
]<character>also matches]]).LaTeX command completion in math regions (#594 by @flokchvtr, merged 8cd147d, follow-up 5eb6d5c)
Merged as sent, verified in the app first (13 options inside
$$…$$for\su,\sumboosted, Enter lands the snippet, inert in prose). Review findings in their own commit per the contributor flow: gated on themath_rendererfacet so Typst notes are left alone;$/$$counts now skip anything the syntax tree calls code (a note withecho $$in a shell block used to flip parity for everything below it); the block scan is bounded instead of slicing the whole prefix on every\; and KaTeX previews are cached between popups.Frontmatter tag autocomplete and clickable tags (#595 by @junereycasuga, merged 80a90a9, follow-up a342da2)
Merged as sent, verified in the app first (chips for inline lists/scalars/block lists, click opens the tag view). After the follow-up, all four completion paths were exercised in the running app:
tags:andTags:both suggest vault tags,#inside frontmatter stays silent, and#in the body still completes. Chips went from[beta]to[alpha, beta]on a note carrying both key casings. It extractsfrontmatterRangeand reuses it in the existing decoration builder rather than adding a second frontmatter scan, and its click handler matches the inline hashtag one exactly.Follow-up finding:
parseFrontmatterFieldslowercases keys, soTags:/TAGS:are the tags field everywhere the vault indexes, but the editor's inline branch matched case-sensitively (its own block-list branch already lowercased, so the two halves disagreed). Both now share onefrontmatterTagsValue. Since two parsers now decide what a frontmatter tag is (the shared one gets indexed; the editor's scan exists because decorations need positions), a matrix test asserts the chips a note renders equalfrontmatterTags(body)across inline lists, scalars, block lists, quoted,#-prefixed, capitalized keys, and lookalike fields.Verification
npm run typecheckclean; fullnpm run test:rungreen (1451 shared-domain, 1526 app-core, 577 desktop)go vet ./...clean and the full Go suite green, on macOS and inside agolang:1.25Linux containeratomicwrite_test.go(concurrent reader, symlink, mode, scratch files),watcher_test.go(scratch files ignored, replaced note never reported deleted),vault.test.ts(atomic-save fidelity),store-note-integrity.test.ts(ZenNotes clears all text from a note while editing and undo does not restore it #585 block, plus theaddpath). The desktop and Go tests were each confirmed to fail against the pre-fix code./watchfeed the way a web client subscribes (zero scratch events, symlink intact and written through, 0600 preserved, no leftovers), an inotify probe on Linux, and the ZenNotes clears all text from a note while editing and undo does not restore it #585 reproductions replayed over CDP against the rebuilt desktop app.