ZenNotes 2.52.0: panels each note can remember, undo history that stays with each note (and can survive quitting), a note that keeps its width next to the side panels, and ten fixes, most of them for keys that went somewhere other than where you were looking
Released on September 17, 2026 as v2.52.0, at f90161fa. The release branch v2.52.0 was fast-forwarded into main through PR #807. It started as the 2.51.2 patch branch and grew into a minor. Five reports came from @uNyanda (#793, #794, #804, zennotesandroid#65 and the Play Store request behind the Browse move), one from @vlcinsky (#803) and one from @danjessen (#108); #805 and four more fixes were found while verifying those in the running app. Every issue is closed with a commit-linked comment. Channel and validation details are in the sections at the end.
✨ Features
- Panels that each note can remember (#794). The Connections, Outline, Comments and Calendar panels have always belonged to the pane: open the Outline and it stays open on every note you move to. That is still the default. Turn off Settings → Editor → Keep panels when switching notes (or run Remember Panels per Note from the command palette) and each note keeps its own set instead: a note comes back with the panels you left it with, and a note you have not opened yet starts with none. It mirrors how each note already remembers its Edit / Split / Preview mode, with one difference: the panels come back after a restart. They are saved with the workspace (the same record that restores tabs and layout, and
.zennotes/workspace.jsonfor a synced vault), never in the note itself. A note's panels follow it through a rename or a move, and a new split inherits them. With the automatic calendar on, a daily or weekly note whose calendar you closed stays closed when you come back to it; in the default mode the calendar keeps auto-opening on every arrival, as before. Flipping the setting never rearranges the screen: the panels on view are carried over and only later note switches behave differently. It travels inconfig.tomlaskeep_panels_across_notesunder[view]. - Each note keeps its own undo history (#793). A pane has one editor, and since the #247 fix every tab switch emptied its undo history on purpose: without that,
Cmd+Zon the new note pasted the previous note over it (#247). The price was that leaving a note for a second threw away every undo step on it. Now the history is set aside under the note it belongs to and handed back when that note returns: switch tabs, visit Trash or Tasks, or close the note and open it again, andu/Mod+Zand redo still walk through your edits, in Vim mode and out of it. #247 stays fixed, because a history only goes back onto the note it came from, and only while that note still reads exactly as it did. If it was changed somewhere else in the meantime (another pane, sync, an external editor) it starts a clean history, since old undo steps would no longer fit the text. It is always on; there is no setting for this part. - Undo history after quitting: Vim's
undofile(#793). Turn on Settings → Editor → Keep undo history after quitting (or:set undofilein Vim mode, or the command palette) and a note's undo history also survives quitting ZenNotes: reopen it tomorrow andu/Mod+Zand redo still step through yesterday's edits. It is off by default, because undo history contains text you deleted. It is stored with the app on this computer, in the app's own data folder, never in the vault, so it does not sync and never lands in a git-backed vault. The same safety rule applies as in a session: a saved history is only used while the note still reads exactly as it did when it was saved, so a note that changed while ZenNotes was closed (sync, git, another editor) starts clean. Histories nobody returns to expire after 90 days, a vault keeps at most 400, and turning the setting off erases all of them. Desktop only: the web and mobile apps keep undo history while they are open, as above. It travels inconfig.tomlaspersist_undo_historyunder[editor]. - Move a folder or a database from Browse, for the mobile apps. The iOS and Android apps could rename and delete a folder or database from Browse, and move a note, but not move a folder: desktop does that by dragging in the sidebar, which has no equivalent on a phone. The shared core now has the action (a Play Store review asked for it): it offers existing folders only, never the folder itself, anything under it, or a database, refuses a destination that already holds the name, and uses the same move the sidebar drag uses, so open tabs, folder icons and colors, favorites and manual order follow. It reaches phones with the iOS and Android releases that adopt this core; nothing changes on desktop.
🐛 Fixes
- Escape ends a Vim block edit in normal mode (#803). After a visual-block
I,Aorc,Escput the text on every row and left Vim in insert mode. The editor's keymaps see every key before the Vim layer does, and two of them were spending Escape. CodeMirror's default keymap uses it to collapse multiple cursors, and since 2.51.0 made block edits real multi-cursors (#792) that fired every time. The autocomplete keymap takes it while a completion is pending, and every typed character counts as pending for about 100 ms even when no popup ever appears, so anEscpressed right after the last character was lost with a single cursor too (easy to hit with Escape on Caps Lock). Now a block edit ends in normal mode with one cursor anduundoes it in one step; leaving visual block takes one press instead of two; aftervorV,Escleaves the cursor on the last selected character, where Vim puts it, instead of one position off; andEscis never swallowed by a completion nobody can see. Unchanged: with a popup actually on screen, the firstEsccloses the popup and stays in insert mode. - The sidebar's Vim cursor survives a trip to the editor and back. In a folder large enough to be windowed (240 notes and up), the sidebar cursor could vanish as soon as the sidebar took focus:
jshowed nothing, the next one jumped to Assets at the bottom of the list, and Enter opened whatever was there instead of a note. Walking the list, opening a note with Enter and coming back withCtrl+w hhit it every time. Row numbers were handed out by a counter that only the sidebar's top-level render restarts, while parts of the tree re-render on their own, and they do so on every focus change, so the rows under them were renumbered and the stored cursor pointed at nothing. Each part of the tree now keeps its numbering stable when it re-renders alone. - Typing goes to the search a command opened, not the note (zennotesandroid#65). Running Search Text in Vault…, Search Notes… or Open Note Outline… from the command palette with the caret in a note opened the search and then handed focus back to the editor behind it, so the query was typed into the note. It was reported on Android in Edit mode, but the cause was in the shared core and desktop had it too; the first run after launch worked by luck (the overlay loaded late) and every later one failed. Publish Note had the same fault through a different path. All four now keep focus where the command put it, and a test requires every overlay the app can open to be accounted for.
- Settings takes the keyboard. With Settings open, typing still edited the note behind it, whether Settings was opened with
Mod+,or from the command palette. Settings draws its own window instead of using the shared dialog shell, so it never got the focus handling every other dialog has. Opening it now lands on the settings search, so typing filters settings straight away; Tab stays inside the window;Esccloses it and the caret is back in the note. On a touch device focus lands on the Settings window itself rather than its search field, so the on-screen keyboard does not cover the list. - The Settings search works from the keyboard (#108). Settings opened on its search, but the search still needed the mouse:
Enterin the field did nothing, the arrows did not move through the results, and/andCmd+F, the keys that search everywhere else, were ignored (before Settings took the keyboard,/even opened the editor's search behind the window, which is what the report from 2.2.0 describes). NowEnteropens the picked result (right category, right sub-tab, the setting scrolled into view and highlighted);↑/↓, orCtrl+J/Ctrl+KandCtrl+N/Ctrl+Plike the palettes, walk the results without leaving the field, stopping at both ends and keeping the picked row on screen; and from anywhere in SettingsMod+F, or/in Vim mode, returns to the search with its text selected./is a search key only in Vim mode, and a/typed into a field is just a character. The key recorders in Settings (Keymaps, Ignored keys) still capture both keys while recording. - Renaming or moving the note you are editing keeps your place. Rename the open note and the caret jumped to the top of it, the scroll reset, and its undo history was gone, although you never left the note. The same happened when you moved it to another folder or renamed the folder it lives in. The editor has one document per pane and took any new path for a different note, which is the moment it deliberately drops all three (#247). Now it can tell: the store logs each rename, move and folder rename in the same update that changes the paths, and a path change that log explains is the same note. The caret stays on its character (it shifts with the text when the rename rewrites the title heading), the scroll stays, and
u/Mod+Zstill undo your last edit while the renamed heading stays renamed. A side benefit reaches further than renames: whenever a note changes underneath the editor (another pane typed in it, a rename elsewhere rewrote a link in it, the file changed on disk), the editor now applies only what changed instead of replacing the whole document, so the caret stays on its text and your undo history survives, where it used to be silently emptied. - The note keeps a readable width when side panels do not fit (#805). In a split pane, Outline plus Connections left the note a strip a few characters wide, and a third panel left it none: the panels have fixed widths and nothing reserved room for the note. Now the note comes first (320 px, 520 in Split mode). The panels shrink in proportion down to the smallest width they can be dragged to, and if that is still too much, the ones opened longest ago are tucked into a slim rail at the pane's right edge. They stay open: a click in the rail, or the panel's usual shortcut, brings one forward (so
Cmd+3on a tucked Outline shows it, and a second press closes it), and the toolbar tooltips say so. A full-width window behaves exactly as before, and widening a pane brings the panels back. Found while hand testing #794; not new in 2.52.0. |in the wikilink picker starts the display text (#804). The picker's own tip reads "Type | to change display text", but|was never one of its keys: it was typed as plain text, the highlighted note was thrown away, and you were left with[[|]]. Now|takes the highlighted suggestion, the wayEnterdoes, and leaves the caret behind a|inside the brackets:[[, pick Linux,|gives[[Linux|]]ready for the display text, and a half-typed name is completed first ([[projthen|gives[[Project plan|]]). It works for notes, assets (![[photo.png|300]]widths) and databases, on any keyboard layout, and from the@note picker too. Repicking the note of a link that already has display text adds no second|: the existing text is selected, ready to be typed over. Everywhere else|is still just a character: in plain text, in a table row, and after a name that is not in the list, so[[Brand new idea|alias]]types as before (with suggestions showing, pressEscfirst).- "Keep view mode when switching notes" travels in
config.toml. The setting has been on the list of portable preferences since it shipped, but it had no place in the file, so it never reached it: set it on one machine and no other machine heard about it. It is nowkeep_view_mode_across_notesunder[editor]. A key missing from an existing file keeps the local value, so nobody's choice is reset, and a test now requires every portable preference to come back out of a freshly written config. - Panel shortcuts leave a pane alone when it is not showing a note. With Trash, Tasks, Help or an asset in the active tab,
Cmd+2,Cmd+3andShift+Cmd+Cstill flipped that pane's Connections, Outline and Comments. Nothing appeared to happen, and the change turned up on the next note instead: a panel you never asked for, or the one you had open gone. The toggles now act only when the active tab is a note. Found by pressing the wrong key at the wrong time while hand testing per-note panels.
🧰 For contributors
- #794: 007f52c7 and, for restarts, 53c339ce.
lib/pane-panels.tsand the hookusePanePanels, which replaced fouruseStates inEditorPanewith the same names and setter shapes. Off, the panels live in the store'spanePanels[paneId][path], remapped on rename and copied to a new split;WorkspaceSnapshot.panePanelscarries them across a restart, written compactly (only what is open, live panes only, the 200 most recently set notes per pane) and read back trusting nothing, since that file syncs and can be edited by hand. The store action runs its updater outside zustand'sset: a write nested in asetcallback is clobbered when the callback returns. - #793: 27c09c98, and
undofilein bd0dc111.lib/note-undo-history.tskeeps the value of CodeMirror'shistoryFieldplus theTextit belongs to, keyed by vault root and path, and seeds it back withhistoryField.init; it is only used while the text still matches, which is what keeps #247 fixed.undofileaddsapps/desktop/src/main/undo-history-store.ts(hash-named files under<userData>/undo-history/, so nothing the renderer sends can steer a write outside that folder; 2 MB cap, temp file plus rename, a sweep once per launch), three OPTIONAL bridge methods and the capabilitysupportsUndoFile, so the web bridge and the mobile shells need no change.Vim.defineOption('undofile', …)gives:set undofilenatively. - Rename keeps caret and undo: 5b39bc8c. The store appends to
recentPathRewritesin the same update that rewrites paths (lib/path-rewrites.ts), and the editor's doc-sync effect treats a path change that log explains as the same note. Each editor remembers the newest entry it has accounted for, so an old rename can never make a real switch look like one. In-place text changes now dispatchminimalTextChangeinstead of a whole-document replace; a control test shows the whole replace is what emptied undo. - #805: 7537be3c. Pure
lib/side-panel-fit.ts,components/TuckedPanelsRail.tsx, an optionalfitWidthon the four panels. Tucked panels are not rendered, so Vim pane navigation, which discovers panels from the DOM, skips them by construction. - #804: 7d8e7316. A
|branch in thePrec.highestkey handler oflib/cm-completion-nav.ts, matched as a character rather than a chord (Shift+\, Option+7, AltGr+<); the edit lives withclosedLinkTailin the new store-freelib/cm-wikilink-tail.ts. - #803: 81798b43.
deferEscapeToVimincm-vim-default-keymap.tsandcloseVisibleCompletionincm-completion-nav.ts. A harness lesson: CodeMirror runs every keymap from one DOM handler placed at the FIRST keymap provider, and the editor mounts one ahead ofvim(); the #792 suite mountedvim()first and passed falsely. It now mountskeymap.of([])first, and nine tests fail without the fix. - Sidebar: 6f3b623e.
useStableSidebarIdxBaseinlib/sidebar-idx-counter.ts; any new tree component that reads the shared row counter and holds state must call it first. - zennotesandroid#65: 50ab2560.
COMMAND_OVERLAY_FLAGSandshouldRefocusEditorAfterCommandincommand-palette-mode.ts. Settings focus: 9a6f5b63, the shell's focus logic exported fromui/Modal.tsxasuseDialogFocusandtrapDialogTab. Settings search keys (#108): eb090882, handled on the Settings panel, not at the window, because the three key recorders listen at the window and must win while recording. Panel shortcuts: c048134e. Config mapping: ecbc0109. - Browse move: f8f967c9.
requestMoveBrowseDirectory(host, directory)in the public Browse API, calling the samerenameFolderstore action as the sidebar drag. Verified by tests only (ten cases); the shells add their "Move to…" rows when they adopt this core. - Release gates: f90161fa reads a saved undo history through one file handle, after CodeQL flagged the size check and the read as a file-system race on the release PR; d836b1ce makes the caret smoke check wait for a caret that has been laid out (it failed one run in five at the gate with a caret rect of all zeros, a test race, not a product bug).
- Tooling: 86d95760. The CDP smoke scripts resolve
electronfromapps/desktop/package.json, andsidebar-vim-smoke.mjsisolatesZENNOTES_CONFIG_DIRlike the rest.
Documentation: the manual (help.ts) and the website docs describe per-note panels, undo history and undofile, the rename behavior, the Settings search keys, the panel rail and the | key. The website side is the branch website/docs-2.52, merged with this release.
Demos
Twelve clips, 1920 × 1080 at 30 fps, with burned-in captions, key badges and a .vtt sidecar, recorded from the built app over CDP with real key and mouse events on a clean demo vault. The first four fix clips play the same keystrokes on a build of 2.51.1 and then on the fix; the rest show the released build.
- 794-panels-per-note.mp4: 28 s. Panels follow every note (the default); the setting is switched off; each note then comes back with its own panels.
- 793-undo-survives-tab-switch.mp4: 27 s. Two edits, a switch to another note and back,
u uandCtrl+rtwice; then the tab closed and reopened, andu ustill works. - 793-undofile-survives-restart.mp4: 21 s, two real launches over the same data.
:set undofile, two edits, quit; launched again,u uandCtrl+rtwice. - rename-keeps-caret-and-undo.mp4: 20 s. An edit, Rename Note, the heading follows the new name while the caret stays on the same character,
uandCtrl+r. - 805-note-keeps-its-width.mp4: 17 s. A split pane with three panels open: the note keeps its width, one panel shows, the others sit in the rail and come forward with a shortcut or a click.
- 804-pipe-starts-display-text.mp4: 21 s.
[[, arrow to Linux,|gives[[Linux|]]; then[[projand|completes to[[Project plan|]]. - 803-block-edit-escape.mp4: 26 s.
Ctrl+v,j j j,I,-,Esc. Before: still in insert mode. After: normal mode, anduundoes the whole block. - sidebar-cursor-round-trip.mp4: 33 s. A 300-note folder,
Ctrl+w h,j j j, Enter,Ctrl+w h. Before: the cursor is gone and the nextjjumps to Assets. After: it is still on the opened note. - android-65-search-focus.mp4: 25 s. Search Text in Vault… from the palette, then typing. Before: the word lands in the note. After: in the search.
- settings-takes-keyboard.mp4: 22 s.
Cmd+,then "font". Before: the note gets the text. After: the settings list filters. - 108-settings-search-keys.mp4: 24 s. Enter, arrows and
Ctrl+Kin the Settings search;/andCmd+Freturn to it from the controls. - panel-shortcuts-non-note-tab.mp4: 14 s.
Cmd+3andCmd+2on the Trash tab do nothing, and the note comes back as it was left.
Verification
- Unit, on the release commit:
npm run typecheck7 of 7;npm run test:runshared-domain 1,666, app-core 2,498 (1 skipped), desktop 860 (4 skipped). New suites this release:pane-panels(13) anduse-pane-panels(8),note-undo-history(15),note-undo-file(7), desktopundo-history-store(9),path-rewrites(9),minimal-text-change(5),side-panel-fit(9),cm-wikilink-display-text(10),settings-search-keys(6),sidebar-idx-counter(4), ten Browse move cases, and the guard that every portable preference round-trips throughconfig.toml. - Smoke, against the built app:
test:vim-editor12 of 12,test:editor-improvements19 of 19,test:sidebar-vim12 of 12 (it was 8 of 12 on 2.51.1, and the four failures were the sidebar bug). - Live, in the built desktop app over CDP, one instance at a time, both stores isolated, real key events. Every bug was first reproduced on the previous build. Highlights: fifteen Escape scenarios matching the
Ctrl-[control; per-note panels across four launches, including a daily note's dismissed calendar; undo after a tab switch, after the Trash tab (which destroys the editor), after closing and reopening a note, and withu u Cmd+Zon the OTHER note leaving it byte-identical (#247);undofileacross real relaunches, afterkill -9, after another program edited the note while the app was closed, and erased when the setting is turned off; a rename that shortened the heading and one that lengthened it with the caret staying on its character; a 632 px split pane holding the note at 320 px with one, two and three panels; and[[, a highlighted note,|. - Not exercised live: the Browse move (tests only, no desktop caller); Android and iOS themselves for the focus fixes (desktop with touch emulation); the web client (same core, covered by typecheck and tests); the
@picker and asset picks for the|key (same code path, unit-tested).
Distribution channels
In progress. The release workflow is building the installers; AUR, Nix, Homebrew, nixpkgs and the website follow as the assets land, and this section is replaced with the final details.
Release validation
Release PR #807's CI passed on macOS, Ubuntu, Ubuntu arm64 and Windows, plus CodeQL, the production dependency audit and the browser and candidate artifact checks, before the fast-forward.