github ZenNotes/zennotes v2.42.0
ZenNotes 2.42.0

3 hours ago

ZenNotes 2.42.0: an update that lands under a running ZenNotes asks for a restart, Ctrl+P reaches the search palette from a Vim editor, links work in a note opened from Finder, a renamed note stays open, Escape leaves the Arrow tool with the sidebar open, and a trashed note's bin icon deletes for good

Six fixes. On Linux and Windows with Vim mode on, Ctrl+P now opens Search Notes and Ctrl+N makes a new note from inside the editor, Ctrl+F pages forward like Ctrl+B, and the help finally says which Ctrl chords Vim keeps (#510, reported by @corpserot). A note opened from Finder outside any vault can finally follow its own links: relative links resolve from the file's folder and wikilinks find their page in that folder's tree (#626, reported by @ashebanow). Renaming the open note no longer closes it on Linux: the file watcher's report of the move was arriving before the rename finished and was being read as a deletion (#713, reported by @uNyanda). The editor header of a note that is already in the Trash no longer offers to move it to the Trash again; the bin icon there now deletes the note permanently after a confirmation, next to Restore (#712, reported by @uNyanda). Escape in an Excalidraw drawing now leaves the Arrow tool (or any tool) even while the sidebar is open: with Vim mode on, the sidebar's keyboard router was taking Escape before the canvas saw it (#721, reported by @Toha764). And the one that looked worse than it was: Upgrade ZenNotes with your package manager while it is open, and the next window the old process opened came up as a page of raw JavaScript: the "white screen of death" unyanda hit on Arch after updating to 2.41.0. The install was fine; the running copy was reading the new files through the layout it had memorized from the old ones. ZenNotes now notices that its own archive was replaced on disk and asks you to restart, instead of loading a page it cannot possibly get right.

πŸ› Fixes

  • An update installed under a running ZenNotes asks for a restart. (reported on Discord by unyanda, Arch + Niri) Electron reads the layout of app.asar once, when the app starts, and keeps it for the life of the process; every later load opens the archive on disk by path and reads at the remembered offsets. That is fine until a package manager (pacman, dpkg, rpm, a Homebrew cask upgrade) swaps the archive underneath a ZenNotes that is still running. From then on the old process reads the new file at the old offsets, and the next window it opens, whether from Reload, a new window, opening a note from the file manager, or reopening ZenNotes from the launcher while the quick-capture panel kept the process alive, renders whatever bytes now sit there. In unyanda's screenshot that was the tail of one Excalidraw locale chunk and the head of the next, displayed as HTML. Replaying the 2.40.0 archive's layout against the published 2.41.0 archive produces that text byte for byte, so this was never a broken build. Now a packaged ZenNotes records its archive's identity and header digest at boot and checks them when a window regains focus, on a slow poll, and right before any renderer load or reload. Once the archive on disk is a different layout, the load is refused: the window shows a short plain-text explanation instead of raw code, and a dialog says which version is installed and which one this window is still running, with "Restart ZenNotes" (relaunches into the new version) and "Not Now". Declining keeps the passive checks quiet; only an action that would actually load through the stale layout asks again. A byte-identical reinstall of the same version is recognized and never prompts. Installs that never rewrite the running path are never asked either: an AppImage's mount pins the old file, a Nix store path is immutable, and Windows locks the files while the app runs. Deliberately not done: restarting on its own. You may be mid-sentence, and notes are saved as you type, so the restart is yours to take. How to test locally: npm run pack, then 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 it with npx asar pack, and copy it over Contents/Resources/app.asar while the app is running. Now press Cmd+R, or open a note by launching the binary a second time with a .md path. Before: a window full of minified JavaScript with an empty title. After: the window says "ZenNotes Was Updated", the dialog names both versions and offers "Restart ZenNotes", and the main process logs [install] app.asar was replaced on disk.

  • App shortcuts beat Vim's Ctrl chords, and the exceptions are written down. (#510, reported by @corpserot) 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. codemirror-vim aliases those chords to k and j and stops propagation of every key it handles, and the app was listening for the two shortcuts in the bubble phase, after the editor. Both now run in the capture phase next to the pane shortcuts that were moved there for the same reason (#124, #579), so they win from anywhere. Ctrl+F was the opposite case: CodeMirror's find panel took it from Vim's page-forward while Ctrl+B paged back as Vim intended. With Vim mode on and Mod resolving to Ctrl, the find binding is dropped, so Ctrl+F pages and / searches, the Vim way; macOS keeps Cmd+F because it never collided, and Vim-off keeps the panel everywhere. Ctrl+W stays Vim's pane prefix while a tab is open, as it has since 2.4.0, and the help now says so instead of listing "Close active tab" without the caveat. The rule, stated in the Global shortcuts help and in the shortcut descriptions: with Vim mode on and the editor focused, an app shortcut on a Ctrl chord wins over Vim's, except Ctrl+W (pane prefix, close a tab with :q or :bd), Ctrl+O / Ctrl+I (jumplist) and Ctrl+D / Ctrl+U (half page). One behavior change to know about: Linux and Windows Vim users who reached the find panel with Ctrl+F now get page-forward there; the panel is still one Escape and / away, or Cmd/Ctrl+F with Vim off. 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. On a Mac the same collision can be staged by rebinding Search Notes to Ctrl+P under Settings, Keymaps.

  • Links work in a note opened from Finder. (#626, reported by @ashebanow) Open a .md that lives outside every vault, from Finder, a double-click or zn open, 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. The app's link resolvers are all vault-bound (the note index, the vault root, "open this note in the workspace"), and a loose file has none of that. It does have a directory, so the window now hands links to the host, which resolves them the way the file's author meant: a relative or absolute href from the file's own folder, adding .md when the link leaves it off, and a [[wikilink]] by name in that folder or below it (a generated wiki keeps its pages together; the search stops at four levels, skips dot-folders and node_modules, and prefers the shallowest match), with alias and #heading stripped and Obsidian's path-like [[sub/page]] form taken relative to the file. A markdown target opens the way Finder would have opened it, in its vault when it belongs to one and in a standalone window of its own otherwise, and any other file opens with the OS. Clicking works in the preview, a plain click on a rendered link or a Cmd/Ctrl-click on any link works in the editor, and gd works in Vim mode, the same rules as the main editor. A link that names nothing on disk says so in the window's banner instead of staying silent. Web and mail links keep opening in the browser. 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 with npm run dev -- path/to/index.md (or from Finder), and click each link in edit and preview. Before: nothing happens. After: each target opens in its own window, and a link to a missing file explains itself in the banner.

  • A renamed note stays open. (#713, reported by @uNyanda) 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. 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 that unlink reaches the app within milliseconds, but the reply to the rename waits until the app has rewritten every inbound [[wikilink]] in the vault, so the unlink arrived first, was read as "this note was deleted", and closed the tab; the reply then moved a tab that no longer existed. macOS delivers file events late enough that the reply usually won, which is why this was a Linux report and why it showed up on macOS too the moment the rename had enough links to rewrite (it reproduced here with 400 of them). The app now remembers which renames it has asked for: while one is in flight, the unlink of its old path is recognised as the rename's own echo and ignored, and a background refresh keeps the 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. 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 with the cursor where it was.

  • Escape leaves the Arrow tool while the sidebar is open. (#721, reported by @Toha764) In an Excalidraw drawing with Vim mode on, pressing Escape with the sidebar open did nothing: the Arrow tool (or whichever tool was active) stayed selected, while the same key worked the moment the sidebar was hidden or Zen mode was on. VimNav's window-level key router runs in the capture phase and, when 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 before Excalidraw's own handler ever sees it. Excalidraw is deliberately not one of the surfaces VimNav ignores outright, because the leader on a Space tap, gt/gT and the Ctrl+W pane prefix are meant to work from inside a drawing. So the router now lets those global bindings run and yields to the canvas right before it would route into a panel: a canvas holding focus owns Escape, tool letters, arrow nudges and Delete. The handoff in the other direction is covered too: giving the keyboard to the sidebar blurs the canvas, the same rule the database grid already follows, so Ctrl+W h from a drawing still moves you into the sidebar. Clicking into a drawing also claims the focused panel the way the editor does, so the sidebar stops painting its cursor on the row that opened the drawing. Vim mode off was never affected. How to test locally: npm run dev, Vim mode on, sidebar open, open a .excalidraw note, click the canvas, press a (Arrow tool), then Escape. Before: the Arrow tool stays selected. After: the toolbar is back on Selection, and the same key still cancels a pending leader sequence first.

  • A trashed note's bin icon deletes for good. (#712, reported by @uNyanda) Open a note from the Trash view and the editor header already swapped Archive for Restore, but the bin icon next to it still said "Move to Trash" and, pressed, asked whether to move to the Trash a note that was already there. A confirmed yes moved nothing and closed the tab, which is not what the prompt promised. The header now knows where the note is: in the Trash, the bin icon reads "Delete permanently" and asks the same question the Trash view's own row action asks ("Delete "Old idea" permanently? This cannot be undone."), then removes the file and closes its tab. Outside the Trash nothing changes. The command palette follows: a trashed note offers Restore Note and Delete Note Permanently, and hides Move Note to Trash. The one wording for permanent deletion now lives in a single helper shared by the Trash view, the header and the palette, so the three cannot drift apart. 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.

🧰 For contributors

  • #510: the two shortcuts moved from the bubble-phase handler into focusPaneHandler (capture) in App.tsx, under the same modal, recorder and autocomplete guards, with one carve-out so Search Notes keeps toggling its own palette closed. Verified live on macOS with the two shortcuts rebound to Ctrl+P / Ctrl+N: before, each press moved the cursor and nothing opened; after, the palette opens and toggles and Ctrl+N creates the note, while Cmd+F still opens the find bar. The Linux-only half (Ctrl+F paging instead of the find bar) is covered by the keymap unit test, not driven live, since the host reports the real platform. vimAwareSearchKeymap(vimMode, mac) in lib/cm-vim-default-keymap.ts filters Mod-f out of searchKeymap when Vim is on and Mod is Ctrl; all five editors (EditorPane, PinnedReferencePane, FloatingNoteApp, ExternalFileApp, QuickCaptureApp) use it. Reproducing the Linux collision on a Mac: a CDP user-agent override does not do it, because isMacPlatform() asks the host (window.zen.platformSync()) before it looks at navigator.platform, so only CodeMirror's own Mod detection flips. Rebind the two shortcuts to Ctrl+P / Ctrl+N in config.toml's [keymaps] instead: that puts the app shortcut on the exact chord codemirror-vim aliases to k / j, which is the Linux situation.
  • #626: apps/desktop/src/main/standalone-links.ts is the resolver (pure, injectable io, tests on a real temp tree); the handler for APP_FOLLOW_EXTERNAL_FILE_LINK resolves against the path main holds for that window (externalFileWindows), never one the renderer sends, and routes markdown through openMarkdownFileFromOS. In app-core, lib/standalone-links.ts decides what the window sends (wikilink by the data-wikilink the pipeline stamps, hrefs by shape); ExternalFileApp intercepts preview clicks in the capture phase ahead of Preview's vault-bound handler, adds the editor mousedown rule from EditorPane, and maps gd. pointerOverRange moved out of EditorPane into lib/cm-pointer-range.ts so both editors share it. Not done here: relative images in that window's preview still go through the vault asset path and stay broken; same shape of fix, separate change.
  • #713: renamesInFlight in store.ts, next to lastWrittenByPath (the existing echo suppression for our own writes; this is the same idea for our own moves). renameNote adds the old path before the IPC call and removes it in a finally; closeUnlinkedNote (both the live unlink branch and the resync sweep) and the keep rule in refreshNotes consult it. Main's renameNote in vault.ts does fs.rename, then moveNoteComments, then updateInboundWikilinks before replying, which is the window the unlink slips through; chokidar's awaitWriteFinish delays add/change events, never unlink. A Go-server workspace reports moves the same way, so the web build gets the fix for free.
  • #712: deleteActivePermanently / deleteNotePermanently(path) in the store, next to trashNote, with the tab-and-buffer cleanup that trashing, archiving and deleting all need factored into one withoutNoteInWorkspace(state, path) (it was two hand copies before). confirmDeletePermanently in lib/confirm-trash.ts is the shared wording; deleteNotePermanently in lib/trash-note.ts is the IPC call with the toast on refusal, mirroring moveNoteToTrash. Command id note.delete-permanently.
  • #721: the yield lives in VimNav.tsx right before the "Sidebar navigation (explicit)" routing, keyed on DOM focus inside [data-excalidraw-view] (the EXCALIDRAW_SURFACE constant in lib/self-keyed-surfaces.ts). It is placed after the Ctrl+W, leader, gt/gT and half-page blocks on purpose; moving it up would kill those inside a drawing (#309's Space-tap leader in particular). releaseSelfKeyedSurfaceFocus now blurs the canvas as well as the grids, and ExcalidrawView claims focusedPanel = 'editor' on click and focus, mirroring ArchiveView. Driven live over CDP: with the sidebar open, a then Escape went from Arrow to Selection only after the change; a document-capture probe showed the key never reached the document before it.
  • The guard lives in apps/desktop/src/main/installed-bundle.ts with its own tests. Every read it does treats the archive as a plain file: Electron's Node fs hooks present an .asar path as a directory and would answer from the same stale header, so withRawFs sets process.noAsar for the duration of each read. Electron's side of the story is shell/common/asar/asar_util.cc (GetOrCreateAsarArchive, a process-lifetime cache keyed by path) and shell/browser/net/asar/asar_url_loader.cc (base::File file(archive->path(), ...), a fresh open by path at the cached offset).
  • All five renderer loads in main/index.ts (workspace window, external file window, floating note, quick capture, PDF export) now go through one loadRenderer(win, query), which is where the refusal lives. The View menu's Reload and Force Reload are explicit items for the same reason; the stock roles reload straight through the stale header.
  • Diagnosing this from a screenshot: download the previous release's archive, parse its header, and read the new archive at the old index.html offset. If the bytes match the screenshot, it is this bug. The asar header is two Pickle records (uint32 4, header pickle size, payload size, JSON length, then the JSON); file data starts at 8 + header pickle size.
  • The bug reproduces on macOS with the shipped 2.41.0 build: launch it, copy a repacked archive over Contents/Resources/app.asar, open a note from a second instance, and the new window renders a mermaid parser table as its document. Screenshots are in media/.
  • The restart question is always a sheet on a visible window, never an app-level alert, and that is not a style choice. On macOS a parentless dialog.showMessageBox runs a nested modal session, and anything that ends that session from outside reports NSModalResponseCancel, which is 0, which Electron hands back as the first button: during verification the app relaunched itself with nobody at the keyboard. A sheet maps an outside dismissal to cancelId. With no visible window there is nobody to ask, so the question waits for the next window, whose page carries the same explanation.
  • Verification: npm run typecheck clean, desktop suite green (691 tests, 18 new), the packaged macOS app driven over CDP before and after the archive swap: the old build renders garbage in the new window, the fixed build renders the explanation page, logs the refusal, raises the sheet, and stays up waiting. The passive poll caught a swap 28 seconds after it happened. Not exercised by automation (AppleScript access is not granted in the verification session): the click on "Restart ZenNotes", which is the same app.relaunch() + app.quit() pair the Linux package installer has used since 2.0.6 (#60), and the guarded View > Reload item, which cannot be triggered through CDP on macOS because menu accelerators are matched on native key events.

Local-first and keyboard-first, as always.

Don't miss a new zennotes release

NewReleases is sending notifications on new releases.