Conversation
… restart Upgrade ZenNotes with a package manager while it is open, and the next window the old process opened came up as a page of raw JavaScript. That is the "white screen of death" unyanda reported on Discord after 2.41.0 landed on Arch, and the install was never at fault: the published archive is intact, offsets and integrity hashes included. Electron parses the app.asar header once per process and keeps it for that process's lifetime (asar_util.cc, GetOrCreateAsarArchive), but every file:// load reopens the archive on disk by path and reads at the cached offset (asar_url_loader.cc). pacman, dpkg, rpm and a Homebrew cask all put a new archive at the same path, so from that moment the old process reads the new file at the old offsets. Replaying the 2.40.0 header's index.html entry against the 2.41.0 archive produces the text in the screenshot byte for byte: the tail of the zh-HK Excalidraw locale chunk and the head of zh-TW, rendered as HTML. The same swap reproduces on macOS with the shipped 2.41.0 build. Nothing in a process can be repaired once its header is stale, so the fix does not try. A packaged ZenNotes records its archive's identity and a digest of the raw header at boot (installed-bundle.ts), reading the file with process.noAsar because Electron's fs hooks present an .asar path as a directory and would answer from the same stale header. The status is checked when a window regains focus, on a 30 second poll, and right before any renderer load or reload; once the archive on disk is a different layout, that status is sticky. All five renderer loads now go through one loadRenderer, which refuses a stale load: the window shows a plain explanation page instead of whatever bytes sit at the old offsets, and a sheet names the installed and running versions with "Restart ZenNotes" and "Not Now". Declining silences the passive checks; only an action that would actually load through the stale header asks again. View > Reload and Force Reload are explicit items for the same reason, since the stock roles reload straight through the stale header. Three edges are deliberate. A byte-identical reinstall keeps every offset valid, so the header digest decides, not the inode. The restart is offered, never taken: the user may be mid-sentence. And the sheet only ever attaches to a visible window. A parentless macOS alert runs a nested modal session, and anything ending that session from outside comes back as NSModalResponseCancel, which is 0, which is the Restart button: the app relaunched itself with nobody at the keyboard during verification. AppImage, Nix and Windows installs never rewrite the running path and are never asked. Verified on the packaged macOS app over CDP with isolated userData and config dirs: the shipped build renders garbage in a window opened after the swap, the fixed build renders the explanation page, logs the refusal, raises the sheet and keeps waiting, and the poll caught a swap 28 seconds after it happened. Not driven by automation: the Restart click (the same app.relaunch plus app.quit the updater uses) and the Reload item, which CDP key events cannot reach on macOS. How to test locally: npm run pack, launch dist/mac-arm64/ZenNotes.app/Contents/MacOS/ZenNotes with ZEN_PERF=1, ZENNOTES_USER_DATA_PATH and ZENNOTES_CONFIG_DIR pointing at scratch folders, and --remote-debugging-port=9333. Extract the app's own app.asar in a scratch directory, change the version in its package.json, repack with npx asar pack, and copy it over Contents/Resources/app.asar while the app runs. Press Cmd+R, or open a note by launching the binary a second time with a .md path. Before: a window of minified JavaScript with an empty title. After: "ZenNotes Was Updated", the sheet names both versions, and the main process logs "[install] app.asar was replaced on disk".
In an Excalidraw drawing with Vim mode on, pressing Escape while the sidebar was open did nothing: the Arrow tool, or whichever tool was active, stayed selected, and the same key worked the moment the sidebar was hidden or Zen mode was on. That is #721, and it only ever affected Vim mode. VimNav's window-level key router runs in the capture phase and, once no text editor holds focus, hands every key it does not recognise to the open sidebar, where Escape means "back to the editor" and is consumed with stopImmediatePropagation. Excalidraw binds its keys on its own container in the bubble phase, so it never saw the Escape at all: a document-capture probe counted zero arrivals while the toolbar stayed on Arrow. The obvious fix, adding the canvas to SELF_KEYED_SURFACES so VimNav ignores it outright, is wrong on purpose: the leader on a Space tap (#309), gt/gT between buffers and the Ctrl+W pane prefix are meant to work from inside a drawing, and all three live in the same handler. So the router lets its global bindings run and yields to a DOM-focused canvas right before it would route into a panel. A canvas holding focus owns Escape, the tool letters, arrow nudges and Delete. The handoff in the other direction is covered too. Giving the keyboard to the sidebar now blurs the canvas, the rule the database grid already follows, so Ctrl+W h from a drawing still lands in the sidebar and j/k drive it. And clicking into a drawing claims focusedPanel the way the editor and the archive list do, so the sidebar stops painting its cursor on the row that opened the drawing and pane navigation measures from the right panel. Verified over CDP on the built renderer with a seeded vault: with the sidebar open, `a` then Escape went from Arrow to Selection only after the change; Space tap then `e` inside the canvas still toggles the sidebar; Ctrl+W h from the drawing blurs the canvas, lights the sidebar cursor, and j/k move it exactly as they do from a markdown note. Vim mode off was unaffected before and after. How to test locally: npm run dev, Vim mode on, sidebar open, open a .excalidraw note, click the canvas, press `a`, then Escape. Before: the Arrow tool stays selected. After: the toolbar is back on Selection. Closes #721
Open a note from the Trash view and the editor header already knew where it was: Archive had become Restore. The bin icon next to it had not been told. It still ran Move to Trash, so pressing it asked whether to move to the Trash a note that was already there, and a confirmed yes moved nothing and closed the tab, which is not what the prompt promised. That is #712. The command palette was already right (it hides Move to Trash for a trashed note); only the header lagged. In the Trash, the bin icon now reads "Delete permanently" and does exactly that: the same question the Trash view's own row action asks, then the file is removed and its tab closed. Outside the Trash nothing changes. The palette gains Delete Note Permanently for a trashed note, so the action is reachable without the mouse. Two things are shared on purpose. The wording for deleting a note for good now lives in one confirmDeletePermanently, used by the Trash view, the header and the palette, so three prompts cannot drift apart. And the workspace cleanup that trashing, archiving and deleting all need (close the tabs, drop the buffer and dirty flag, unpin the reference pane) was two hand copies in the store; it is now withoutNoteInWorkspace and all three go through it. A refused delete keeps the note open and says so in a toast, mirroring Move to Trash. Verified over CDP on the built renderer: trash a note from the header, open it from Trash, the header shows Restore and Delete permanently, the prompt reads 'Delete "Doomed" permanently? This cannot be undone.', and confirming removes the file from the vault's trash folder, empties the Trash list and closes the tab. Both docs surfaces describe the header's two actions for a trashed note. How to test locally: npm run dev, open any note, press the bin icon and confirm, then open Trash from the sidebar and click the note. Before: the header's bin icon is titled "Move to trash" and its prompt offers to move the note to the Trash. After: it is titled "Delete permanently", the prompt says so, and confirming removes the note from the Trash list and closes the tab. Closes #712
Rename the note you are editing, from the command palette or anywhere else, and on Linux it closed the moment you confirmed: the tab vanished and the editor went blank while the file itself was renamed correctly. That is #713. A rename is a move on disk, and the vault watcher reports a move as an unlink of the old path followed by an add of the new one. On Linux (inotify) that unlink reaches the renderer within milliseconds, but the reply to the rename waits until main has done fs.rename, moved the note's comments and rewritten every inbound [[wikilink]] in the vault. So the unlink arrived first, applyChange read it as a deletion and closed the tab, and the reply then renamed a tab that was no longer there. macOS delivers file events late enough that the reply usually won, which is why this was a Linux report; it reproduced on macOS the moment the rename had 400 links to rewrite, with the tab gone 300ms after Enter. The store now remembers the renames it has asked for: renamesInFlight, next to lastWrittenByPath, which is the same idea for our own writes. renameNote adds the old path before the IPC call and removes it in a finally. While a path is in flight, closeUnlinkedNote treats its unlink as the rename's own echo (both the live branch and the resync sweep), and the keep rule in refreshNotes holds its tab instead of pruning it, so the reply finds the tab where it left it and moves it to the new name. Every other unlink still closes its note, including one that arrives after a refused rename. Three tests pin the event order: unlink and add delivered before the reply, then the reply moving the tab; a genuine deletion still closing; a refused rename forgetting its path. The first fails with the guard disabled. Verified live on the built renderer with 400 inbound links: the tab stays open through the rename and ends under the new name with its heading synced. How to test locally: npm run dev on Linux, or on any platform give a note a few hundred inbound [[links]], open it, run Rename Note… from the command palette, type a new name and press Enter. Before: the tab disappears and the editor goes blank; the renamed file is in the sidebar. After: the same tab stays open under its new name. Closes #713
Open a markdown file that lives outside every vault (Finder "Open With", a double-click, `zn open <file>`) and it comes up in the standalone "Not in a vault" window, where every link was dead: `[[topic]]` did nothing, `[README](../../README.md)` did nothing, in edit and preview alike, with no error anywhere. That is #626, and it held through 2.41.0. The app's link resolvers are all vault-bound. The preview looks a wikilink up in the vault's note index, resolves a relative link against a vault-relative note path, and ends in "open this note in the workspace"; the standalone window has none of those, and its own editor never had the main editor's link-follow handler at all. A loose file does have a directory, though, which is the base its author meant. So the window now hands links to the host. `resolveStandaloneLink` takes a relative or absolute href from the file's own folder, adding `.md` when a link leaves it off, and finds a `[[wikilink]]` by name in that folder or below it: four levels deep, dot-folders and node_modules skipped, the shallowest match wins, alias and anchors stripped, and Obsidian's path-like `[[sub/page]]` taken relative to the file. The IPC handler resolves against the path main already holds for that window, never one the renderer supplies, and routes a markdown target through the Finder opener, so it lands in its vault when it has one and in a standalone window of its own otherwise; any other file opens with the OS, and a miss comes back as a reason the window shows in its banner. In the renderer, `standaloneLinkForAnchor` reads the target the markdown pipeline stamps on a wikilink and judges everything else by href shape, keeping web and mail links in the browser and in-page anchors with the browser's scrolling. ExternalFileApp intercepts preview clicks in the capture phase, ahead of Preview's vault-bound handler, gives its editor the same mousedown rule as EditorPane (a plain click follows a rendered link in live preview, Cmd/Ctrl-click follows any link, #201) and maps `gd`. `pointerOverRange` moves out of EditorPane into lib/cm-pointer-range.ts so both editors share one copy. Deliberately left alone: relative images in that window's preview still resolve through the vault asset path and stay broken. Same shape of fix, separate change. Verified over CDP on the built app with the layout from the report (proj/README.md, proj/docs/wiki/index.md, pages beside and below it): in edit mode, plain and Cmd-clicks opened topic-name.md, the converter page through its alias, README.md two levels up and the report in a subfolder, each in its own window; preview mode did the same on a fresh launch; a link to a missing file explains itself in the banner. The resolver has 11 tests on a real temp tree, the classifier 5. How to test locally: outside any vault, make proj/README.md and proj/docs/wiki/index.md containing `[[topic]]`, `[README](../../README.md)` and `[report](out/REPORT.md)` with the targets present, open index.md from Finder or with the file as an argument, and click each link in edit and preview. Before: nothing happens. After: each target opens in its own window. Closes #626
…tions are written down On Linux and Windows, where Mod is Ctrl, pressing Ctrl+P inside a Vim-mode editor moved the cursor up and the Search Notes palette never opened; Ctrl+N moved down and New Note Here never fired. Meanwhile Ctrl+F opened CodeMirror's find panel over Vim's page-forward while Ctrl+B paged back as Vim intended, and Ctrl+W did nothing visible with no word anywhere about why. That is #510, in three parts. The first is a bug. codemirror-vim aliases <C-p> and <C-n> to k and j and stops propagation of every key it handles, and the app listened for those two shortcuts in the window's bubble-phase handler, after the editor. Everything the app already wanted to win over Vim runs in the capture phase for exactly this reason (pane focus #124, pane modes #579, the inline-format chords, the jumplist #488); these two were the ones left behind. They move there now, under the same modal, recorder and autocomplete guards, with one carve-out so Search Notes keeps closing the palette it opened. The second is a gap in the docs. Ctrl+W has been Vim's pane prefix while a tab is open since 2.4.0, on purpose, with :q and :bd as the way to close a tab, and the help said "Mod+W closes the tab" without the caveat. The Global shortcuts help now states the rule: with Vim mode on and the editor focused, an app shortcut on a Ctrl chord wins over Vim's, except the chords Vim mode keeps, Ctrl+W (pane prefix), Ctrl+O and Ctrl+I (jumplist) and Ctrl+D and Ctrl+U (half page). The shortcut descriptions say the same where each applies. The third is a judgment call, made in Vim's favour. The find panel's Mod-f binding was the one motion chord the editor still took from Vim, and only because CodeMirror's search keymap happens to claim it. With Vim mode on and Mod resolving to Ctrl, vimAwareSearchKeymap drops it, so Ctrl+F pages forward like Ctrl+B and / searches, the Vim way; every other search binding stays. macOS keeps Cmd+F because it never collided, and Vim-off keeps the panel everywhere. Linux and Windows Vim users who reached the find panel with Ctrl+F now get page-forward there; the release notes say so. Verified live before and after, by stashing the fix and rebuilding. A browser user-agent override does not flip the app's Mod key (the app asks the host for its platform), so the Linux collision was staged on a Mac by rebinding the two shortcuts to Ctrl+P and Ctrl+N. Before: each Ctrl+P moved the cursor up a line and nothing opened, Ctrl+N created nothing. After: Ctrl+P opens the palette and a second press closes it, Ctrl+N creates the note with its title focused, Cmd+F still opens the find bar with Vim on. The Ctrl+F paging half is covered by the keymap unit tests, not driven live. How to test locally: on Linux or Windows, Vim mode on, click into a note, press Ctrl+P. Before: the cursor moves up. After: the search palette opens, and Ctrl+P again closes it. Then Ctrl+F. Before: the find bar. After: the view pages forward, and Ctrl+B pages back. Closes #510
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.42.0. Work accumulates here;
mainfast-forwards at release time.Shipped so far
Fix(desktop): an update installed under a running ZenNotes asks for a restart (fa16dfd, reported on Discord by unyanda)
Upgrade ZenNotes with a package manager while it is open and the next window the old process opened came up as a page of raw JavaScript. Electron caches the
app.asarheader for the life of the process but reopens the archive by path for every load, so once pacman, dpkg, rpm or a Homebrew cask swapped the archive, the old process read the new file at the old offsets. Replaying the 2.40.0 header against the published 2.41.0 archive reproduces the reported screenshot byte for byte; the install itself was never broken. A packaged ZenNotes now records its archive's identity and header digest at boot and checks them on focus, on a slow poll and before any renderer load or reload. A stale load is refused: the window shows a plain explanation and a sheet names both versions with "Restart ZenNotes" and "Not Now". A byte-identical reinstall never prompts; AppImage, Nix and Windows installs never see a change. The sheet only ever attaches to a visible window, because a parentless macOS alert ended from outside reports the Restart button (observed during verification). Reproduced and fixed on the packaged macOS app over CDP.Fix(keymaps): app shortcuts win over Vim's Ctrl chords, and the exceptions are written down (443a210, closes #510)
On Linux and Windows with Vim mode on, Ctrl+P moved the cursor up and Search Notes never opened; Ctrl+N moved down and New Note Here never fired. codemirror-vim aliases those chords to
k/jand stops propagation, and the app was listening in the bubble phase. Both move to the capture phase next to the pane shortcuts that were moved there for the same reason (#124, #579). Ctrl+F was the opposite case: CodeMirror's find panel took it from Vim's page-forward while Ctrl+B paged back; with Vim on and Mod resolving to Ctrl the find binding is dropped, so Ctrl+F pages and/searches (macOS keeps Cmd+F, Vim-off keeps the panel). Ctrl+W stays Vim's pane prefix as it has since 2.4.0, and the help now says so, with the rule: an app shortcut on a Ctrl chord wins over Vim's, except Ctrl+W, Ctrl+O / Ctrl+I and Ctrl+D / Ctrl+U. Verified before and after on macOS with the shortcuts rebound to Ctrl chords.Fix(desktop): links work in a note opened from Finder (be38419, closes #626)
A markdown file opened from outside every vault lands in the standalone window, where every link was dead: the app's resolvers are all vault-bound. The window now hands links to the host, which resolves them from the file's own directory: a relative or absolute href (adding
.mdwhen omitted), a[[wikilink]]by name in that folder or below it (four levels, dot-folders andnode_modulesskipped, shallowest match wins, alias and anchors stripped). A markdown target opens the way Finder would (its vault, else its own standalone window); other files open with the OS; a miss shows in the window's banner. Click in preview, plain click on a rendered link or Cmd/Ctrl-click in the editor, andgdin Vim mode. Verified on the reporter's layout in both modes. Relative images in that window remain a separate change.Fix(editor): a renamed note stays open (4f13af1, closes #713)
Rename the note you are editing and on Linux it closed the moment you confirmed. The watcher reports a move as unlink then add, and on inotify the unlink reached the renderer before the rename reply (which waits for inbound wikilinks to be rewritten), so the tab was closed as a deletion and the reply renamed a tab that was gone. Reproduced on macOS with 400 inbound links. The store now tracks renames in flight: the old path's unlink is recognised as the rename's own echo and refreshes keep the tab, so the reply moves it to the new name. Three tests pin the event order; the race test fails with the guard disabled.
Fix(editor): a trashed note's bin icon deletes for good (2c0fcf0, closes #712)
Open a note from the Trash and the header already showed Restore, but the bin icon still ran Move to Trash and asked to move an already-trashed note. In the Trash it now reads "Delete permanently", asks the same question the Trash view asks, removes the file and closes the tab; the command palette gains Delete Note Permanently. The wording lives in one helper shared by the Trash view, the header and the palette, and the tab cleanup that trashing, archiving and deleting all need is one function instead of two hand copies. Verified over CDP end to end.
Fix(excalidraw): Escape leaves the Arrow tool while the sidebar is open (8444bb3, closes #721)
With Vim mode on and the sidebar open, Escape in a drawing did nothing. VimNav's capture-phase router hands every key no text editor holds to the open sidebar, where Escape means "back to the editor", so Excalidraw never saw it. The router now yields to a focused canvas right before the panel routing, after the leader, gt/gT and Ctrl+W blocks so those still work inside a drawing; giving the keyboard to the sidebar blurs the canvas, and clicking into a drawing claims the focused panel like the editor does. Verified over CDP with vim on and off.
Docs
Both surfaces updated for #510, #626 and #712 (in-app help and the website's docs page, plus the feature index), committed in the website repo and ready to push with the release.
Verification
npm run typecheckclean; desktop suite 684 tests, app-core suite 1862 tests, all green