github ZenNotes/zennotes v2.56.0
ZenNotes v2.56.0

latest release: web-2.56.0-web.h377717cfb31ad477
2 hours ago

ZenNotes 2.56.0: three features asked for on the tracker, seven fixes, and a manual that reads as one page. Making a note from a template, or inserting one into the open note, now has a shortcut of its own that works with Vim mode on or off: ⌘⌥T on macOS, and any chord you set in Settings → Keymap (#847, asked for by @Wordius). A Markdown table that has outgrown its note converts into a database in place: Convert Table to Database… (the command palette, :table_to_database, Space l d, or the table's cell menu) takes a name prefilled from the heading above the table, writes a <Name>.base database with its columns typed the way an opened CSV's are, and leaves a [[Name]] link where the table stood, one undo away (#832, asked for by @junereycasuga). And a vault can go by a name of its own: Rename Vault… (the palette, :vault_rename, the sidebar's vault menu, or a Vault name field in Settings) names the vault in the sidebar header, the switcher and the title bar while its folder stays docs on disk; the name lives in the vault's own vault.json, so it travels with the folder (#692, asked for by @hotondo). Fixes: Settings search now finds where new task files, drawings and databases go (search "tasks folder" and it jumps to Vault → Notes → Default tasks location), and a specific folder's field names where files will really land, since that folder lives inside your primary notes area (Tasks is inbox/Tasks/ in an Inbox vault). Settings search also reaches fourteen settings it could not find before, from Use theme for PDF export to Roll over unfinished tasks to today, and every hit now opens the tab its setting is on. A task added from the calendar panel, or moved into another day's note from it, now joins that note's Tasks section instead of landing at the bottom (#851, from @uNyanda). In Preview, a code block inside a task stays under the task's text in a loose list, and a checked task no longer strikes its code block through (#849, from @Sullti). When the open note changes on disk (a sync tool, a script, an AI agent), undo no longer strips your own earlier typing out from under the new text and saves the mix; the note starts a clean history, as a reopened one already did (#852, also from @Sullti). And the in-app Help reads as one page instead of a wall of cards: guides at a comfortable width, shortcuts that lead with what they do, and a keycap for every key.

Released on September 24, 2026 as v2.56.0, at 2c3dd065. The release branch v2.56.0 was cut from main at e417342c (the 2.55.0 release, after its channel commits) and fast-forwarded into main through PR #855. Eleven commits from the cycle (three features, six fixes, the Help redesign and the merge of a contributor PR), the version bump 703c688f, and one test-only commit made at the gate, 2c3dd065, which the tag sits on; the three Packaging(...) commits that publish the channels follow it on main and v2.56.0.

✨ Features

  • The template pickers get their own shortcuts, ⌘⌥T on macOS (#847, asked for by @Wordius). Creating a note from a template, or inserting one into the note you are editing, had two keyboard routes: Vim's leader (Space t, Space i) and the command palette. With Vim mode off there was nothing to press, and rebinding the leader entries could not help, since a leader binding is the one key typed after Space: a chord like ⌘⌥T is not something it can hold. Two regular shortcuts now open the same pickers, with Vim mode on or off. New note from template is ⌘⌥T out of the box on macOS; on Linux and Windows it starts unbound, since Ctrl+Alt+T opens a terminal on most Linux desktops. Insert template into current note starts unbound everywhere (⌘⌥I is Electron's DevTools). Both are in Settings → Keymap (Change…, type the chord, Save) and in config.toml, the palette shows your chord next to the template commands once one is set, and like every app shortcut they stay quiet while Settings, a palette, a menu or a dialog is open. (7cb62b0e)
  • A Markdown table converts into a database, linked from the note (#832, asked for by @junereycasuga). A note often starts with a small pipe table that outgrows inline editing: the moment you want a sort, a filter or a board, the only route was New Database and retyping or exporting the rows into it, and the note lost track of where its data went. Now put the cursor in the table and run Convert Table to Database… from the command palette, :table_to_database on the ex line, or Space l d in Vim mode; in a rendered table the same entry sits at the bottom of the cell menu (right-click, or m in a cell). A prompt takes the name, prefilled from the heading above the table or the note's title. ZenNotes creates a <Name>.base database where new databases go (Settings → Vault → New Drawings, Databases & Tasks location) and replaces the table with a [[Name]] wikilink, the same link the [[ picker inserts for a database, so the note still points at its data and the grid is a gd or a click away; a name the vault already has becomes Name 2, and the link says so. Every column becomes a field typed the way an opened CSV's are (checkbox, number, date, text), so the Board and filters work at once; cell text travels verbatim, inline Markdown included, with the CSV layer quoting commas and quotes; column widths you dragged in the editor carry over, alignment does not, since a grid has none. The swap is an ordinary edit, autosaved and one u away, and the database stays behind when you undo; the confirmation toast offers Open for the grid. Names a wikilink cannot carry ([ ] # ^ | and slashes) are refused in the prompt rather than sanitized into a link that finds nothing, and the note is only touched if the table is still exactly where it was once you confirm; otherwise the database exists and the table stays, with a toast saying so. Not in this release: an inline database view inside the note, which would need an embed syntax databases do not have. (7d8b9507)
  • A vault can go by a name of its own, the folder untouched (#692, asked for by @hotondo). A vault was named after its folder, which is fine until the folders are all called docs: one notes vault per project repo, kept in the repo's docs/ so the app's inbox, quick and archive directories stay out of the source tree, and a vault switcher that read docs, docs, docs. Rename the vault without renaming the folder: Rename Vault… in the command palette (:vault_rename on the ex line), the same entry in the sidebar's vault menu (click the vault name at the top of the sidebar), or the Vault name field under Settings → Vault → Location, which Settings search finds under "vault name" and "rename vault". The prompt and the field show the folder's name as the placeholder and say it keeps that name on disk; an empty answer goes back to it. The name shows in the sidebar header, the vault switcher (Space v), the title bar, the note-list heading of a root-mode vault and zn vault list, and zn --vault docs still finds the vault after a rename, since the folder name is matched second. It is saved in the vault's own .zennotes/vault.json as displayName rather than in the app's config, so it travels with the folder: the repo's docs vault is "Acme API docs" on every machine that opens it, and an edit to vault.json from outside (a git pull, a sync) renames the open vault on the spot. A temporary folder session and a remote workspace do not offer it: the first writes nothing into its folder, the second's settings belong to its server. The server side carries the field too, so a web client's settings save keeps a name the desktop wrote and the web app shows it; that part ships with the next server release. (9d3d2b15)

🐛 Fixes

  • Settings search finds where new tasks, drawings and databases go, and the folder field says where that is (reported on Discord by Wordius). A user asked for a setting to choose where new tasks go, so they stop landing as loose notes in the vault. That setting has been there since 2.15, as Settings → Vault → Notes → Default tasks location, but nothing led to it: it sits under Vault rather than the Tasks section, and Settings search had no entry for it, so "task folder", "tasks folder", "tasks location" and "new task" all came back empty. The drawings and databases locations beside it had the same gap. All three are now in the search under the words people use for them, and a hit opens Vault → Notes with the row highlighted, like every other setting. The folder field under each row also said "Vault-relative subfolder", which only held in a vault whose notes live at the root: a specific folder lives inside your primary notes area, so Tasks is inbox/Tasks/ in an Inbox vault. The field now says that and names the real destination for the open vault (inbox/Tasks/, Tasks/ in a Vault root vault, or under a remapped inbox), worked out by the same code that decides where the file is written, so the two cannot disagree. Where files go is unchanged, and the in-app manual now gives the full path. (ef45691b)
  • Settings search reaches every setting, and a hit opens the tab the setting is on. Fourteen settings had no search entry, so searching for them came back empty: Use theme for PDF export, Sync clipboard with Vim registers, Keep the input method out of normal mode, Completed task style, Default view mode, the Workflows Event triggers switch, the Workflows guided tutorial and built-in recipes, the two daily-note task options (Tasks are due on the note's date, Roll over unfinished tasks to today), and the Inbox, Quick Notes, Archive and Trash paths. Each is now in the search under its own name and the words people use for it, such as "clipboard", "korean", "strikethrough", "rollover" or "trash folder". Seven more were found but opened the wrong tab: Math renderer, Math size, the two Typst rows and Relaxed $$ math delimiters live on Editor → Writing, yet a hit left you on whichever Editor tab was open with nothing highlighted, and Date-titled Quick Notes and Quick Note prefix opened Quick capture, where neither lives. Each of these now opens the right tab with the row highlighted. Four of the new entries only appear once something else is on (the two Vim options need Vim mode, the two daily-note task options need daily notes), so while that switch is off their hit highlights the switch instead of landing on nothing. (2c96e660)
  • Tasks added or moved from the calendar panel join the note's Tasks section (#851, PR #850 by @uNyanda). A task added with "Add a task for …" in the calendar panel, or moved into another day's note from its drop menu (by dragging it onto a day, or with m, h/j/k/l and Enter), was appended to the bottom of the note even when the note had a ## Tasks section, so it landed under whatever came last, often a ## Notes section. It now goes into the Tasks section, after its last line and before the next heading or a --- divider, the placement forwarding and rollover already use. A note without a Tasks heading still gets the task at the end. (f3226405, follow-up 73691f5c)
  • A code block inside a task stays under the task's text in Preview (#849, from @Sullti, who also pinned down the cause). In a loose task list (a blank line anywhere between or inside the items), a fenced code block inside a task was drawn above the task's text: checkbox, code, then the step it belonged to. The wrapper Preview puts around a task's own text, so that done and cancelled styling lands on it alone (#512), took every child of the item except the checkbox, paragraphs and sub-lists, and went in where the first of those sat; in a loose item that was the code block. In a tight list the order held, but a checked task's code block went into the wrapper and was struck through and grayed with the text. The wrapper now takes only the task's own text and leaves code blocks, paragraphs and sub-lists where they are, so both list shapes read in source order and a done task strikes its words, not its code. (e28c5aec)
  • Undo after the open note changed on disk no longer saves a text nobody wrote (#852, from @Sullti). Type Hello into a note, let it save, and have another program rewrite the file as Hello, changed outside: the editor showed the new text, which is right, but undo then removed Hello and left , changed outside, a text neither you nor the other program ever wrote, and the save that follows every edit put it on disk. Anyone syncing a vault (Syncthing, a cloud folder, ZenNotes Cloud pulling a change made on the phone) or editing notes with a script or an AI tool while ZenNotes is open could hit it. The editor applies a changed body as a minimal, non-undoable change and lets CodeMirror map the existing undo steps through it, which is what keeps undo working through a rename's heading rewrite or another pane typing; for a change from disk those steps land on text nobody here wrote, and nothing told the editor which of the two it was looking at. The store now marks a body that came from disk (a watcher event, or the re-read after a remote feed reconnects), and the editor, on such a change to the note it is showing, applies the new text in place as before (caret and scroll stay) and then starts the note's undo history clean, which is what a reopened note already did and what the manual promised. Undo does nothing afterwards; typing later undoes back to the text on disk. A rename's heading rewrite, the link rewrites after an asset rename, and another pane typing still keep the history. Making the disk change one undoable step was the other reading of the report; it would let a habitual undo revert a sync or an agent's edit and save that over it, so the clean history won. (a80a45a5)
  • Editor commands reach the : line too. Every command palette entry is meant to be an ex command under its id (:note_copy_wikilink, :fold_all, and so on), but the registration ran when the Editor mounted, before any pane had a view or a note, and dropped every command whose availability check said no at that instant. That was the whole editor-scoped family: :editor_reflow_paragraph, :task_forward, :task_toggle_checkbox, :fold_all and the rest answered "Not an editor command" for the life of the window. They are registered regardless now, and the check still runs when the name is typed, so nothing fires out of context. Found while wiring :table_to_database. (7d8b9507)
  • The in-app Help is one readable page instead of a wall of cards. Help opened on a gradient banner with four buttons, a stats line, a "Filter the manual" label and eight chips, and every guide, shortcut and setting below it sat in its own card. Guides ran the full width of the pane (about 125 characters a line in a 1440 px window), and a shortcut row led with its keys in a monospace box, asides included, so Shift+Enter (in Search notes) wrapped inside a keycap before you learned what it did. Now the page is a title, one sentence, a search field and a row of section links that stays pinned while you scroll (it wraps onto a second row in a narrow window). Sections and guides are set apart by space, and prose keeps to about 80 characters a line. Shortcut, Vim and Settings rows lead with the action and end the line with its keys, each key in its own keycap (⌘4 / ⌘5 / ⌘6 is three), with an aside like "(in Search notes)" beside them in plain text and Unbound as a word. Keys the manual spelled with the internal Mod now read the way your platform names them (⇧⌘T on macOS, Shift+Ctrl+T elsewhere), like the keys it reads from your own bindings. The command palette list is one line per command: its name, its shortcut and its : command. The manual's text is unchanged, and searching it narrows both the rows and the section links. (9184b48a)

🧰 For contributors

  • specificFolderDestination(folder, settings) in packages/app-core/src/lib/vault-layout.ts returns the vault-relative directory a "Specific folder" location creates files in ('' is the vault root). It is resolveCreateLocation plus vaultRelativeFolderPath, the same pair createNote resolves through, so any surface that needs to name a destination should call it rather than join paths itself.
  • A Settings row is only searchable if its category's searchItems has an entry for its settingId, and it only opens the right sub-tab if that sub-tab's searchIds lists it. The three location rows now have both (drawings-location, databases-location, tasks-location on Vault → Notes). The folder fields under them are reached through their location row, since they only render in folder mode.
  • Two guard tests in SettingsModal.test.ts now enforce both halves for every row. The first scans SettingsModal.tsx for every row it registers (settingId="…", settingsSearchTargetProps("…"), data-settings-search-id="…") and fails for any without a search item whose id or targetId names it; the three folder fields above are the only exception. The second renders the modal once with every gate on (Vim mode, Typst, Harper, undofile, remote workspaces, all periodic notes, the folder modes), reads the categories the modal hands getSettingsSearchResults through vi.mock("../lib/settings-search", { spy: true }), and walks each sub-tab's JSX: every row there needs a search item in its category and a place in that sub-tab's searchIds, and searchIds may not list a row the sub-tab does not render. It sees rows named by an expression, such as the ${key}-path folder rows, which a text scan cannot. A row behind a new kind of gate needs that gate switched on in the test's renderEveryRow.
  • A search item for a row that only renders behind a switch takes a targetId that falls back to the switch while it is off (vimYankToClipboardTargetId, dailyNotesRolloverTargetId and the other two), the way wrappedLineMotionsTargetId already did.
  • The live harness for the search fix is tools/check-settings-search.mjs before|after, same launcher as the tasks one: two sessions (Vim mode and daily notes on, then off), 20 checks. Before each search it parks Settings on a different category or sub-tab, so a check only passes if the hit itself opens the right place, highlights the row, and scrolls it into view. SHOTS_DIR=<dir> saves a few screenshots.
  • Tests: specificFolderDestination in vault-layout.test.ts (Inbox vault, root vault, remapped inbox, no folder named yet), and two in SettingsModal.test.ts: every location row is found and opened from search, and the tasks folder field names inbox/Tasks/ in an Inbox vault and Tasks/ in a root vault. Both SettingsModal tests fail on the code before ef45691b.
  • The live harness is tools/check-tasks-location.mjs before|after: before launches the packaged 2.55.0 app in dist/mac-arm64, after the build in apps/desktop/out, both stores isolated, one instance at a time. Two vaults: an Inbox vault (six searches, the jump, the folder field empty and with Tasks, and where New Task from the command palette writes) and a Vault root vault with tasksLocation already set (the field, and where New Task writes). 13 checks.
  • tools/burn-clip.py (the demo kit carried over from 2.55.0) now draws key badges in SF and falls back to SF inside captions for any character Helvetica lacks: Helvetica has no ⌘, ⇧, ⌥, ⌫ or →, and the 2.55.0 clips drew ⌘P as a box.
  • Contributor PR #850 (@uNyanda) merged as f3226405 onto v2.56.0, its base retargeted from main; their commit, 26dbed0f, stands as they wrote it. Calendar add (addTaskForDate) and calendar move (moveTaskToDate) now place tasks through insertTasksUnderTasksHeading, like forwarding and rollover. The follow-up 73691f5c folds the PR's second focus effect in ContextMenu into the existing one, which already focused the menu a frame after mount and handed focus back on close: the menu now takes focus on mount and again on the next frame, and ContextMenu.test.ts pins both halves (the mount-focus test fails on the code before #850 and passes on #850 as submitted).
  • The live harness for the calendar fix is tools/check-calendar-placement.mjs before|after, one launch with Vim mode and daily notes on, 9 checks: a keyboard move with the drop menu's focus sampled at 0, 50 and 300 ms, an add into a note with a Tasks section and one without, and a right-click menu's focus and its hand-back on Escape.
  • The task-text wrapper moved out of Preview.tsx into lib/preview-task-body.ts (wrapTaskItemOwnText(li, checkbox)): it wraps each run of the item's inline content in its own span.task-item-body, treats block tags (P, lists, DIV, PRE, BLOCKQUOTE, TABLE, headings and the like), the checkbox and the state marker as run boundaries, and does nothing when a wrapper is already there. The CSS was already written for this (> .task-item-body in tight items, > p in loose ones). Its tests run on renderMarkdown's real loose and tight output; the #849 ones fail on the old algorithm and the #512 ones (sub-list and state marker outside the wrapper) pass on both.
  • The live harness for #849 is tools/check-task-code-block.mjs before|after: one launch in Preview with completed tasks gray and struck, four notes (the issue's note verbatim, the same with the step checked, a tight checked variant, a checked parent with an open sub-task), 12 checks on the rendered DOM order and on what sits inside the struck wrapper. SHOTS_DIR=<dir> saves a screenshot per note.
  • Two app shortcuts, global.newNoteFromTemplate (defaultBinding: "", defaultBindingMac: "Alt+Mod+T") and global.insertTemplate (""), in both KEYMAP_DEFINITIONS and the shared-domain KEYMAP_CATALOG, dispatched in App.tsx right after tab selection and behind the same overlay guard. They are the first actions to ship with an empty default: "" already meant unbound for an override, and every surface (Settings, Help, the palette, the conflict check, the config.toml writer) reads the effective binding, so an empty default reads as Unbound with no special case. The portable-spelling test skips a deliberately unbound default. tools/check-template-shortcuts.mjs before|after drives it live: two launches (Vim mode off, then on), 8 checks, the keymap recorder driven with a real mouse click, since a scripted .click() leaves focus in the Settings search field, which the recorder rightly ignores.
  • createDatabase(folder, subpath, title?, seed?) on the bridge takes an optional DatabaseSeed (headers, rows, optional columnWidths; bridge-contract/databases.ts). What a new database is written with is decided in one place now, initialDatabaseContents(seed, genId) in shared-domain/database-csv.ts: without a seed the id + Name grid, with one the seed run through inferFields, the same inference that adopts a plain CSV, rows keyed by the resulting field ids. Both IO twins build from it (desktop main's databases.ts and the IO-abstracted database-ops.ts behind the web bridge, the CLI and remote workspaces), so the files stay byte-compatible across transports; the two hand-rolled id + Name blocks they each carried are gone. The seed crosses IPC from the renderer, so the helper coerces cells and headers to strings and keeps only positive finite widths.
  • The store's createDatabase gained a fifth argument, { seed?, open? }, and now resolves to the created DatabaseDoc (or undefined when nothing was created or the workspace moved on), so a caller can read the title the vault actually gave it. open: false leaves the caller in place; every existing caller is unchanged.
  • Finding the table: tableRangeAt(view, pos) in cm-table.ts is exported (syntax tree first, the #485 line scan as fallback, both extended over a zen:cols marker), parseTableBlock in markdown-table.ts lifts that marker into colWidths instead of reading it as a body row, and focusedTableAnchor(view) is the new piece: each table widget records itself on focusin (per editor, a WeakMap), and the editor forgets it as soon as focus lands back in the note text. A command that runs after the palette took focus can still tell which rendered table the user was in; when the caret is in the note, the caret wins. noteEditorPath(view) in note-editor-context.ts gives the note a registered editor shows, which the converter uses for placement and for the post-write guard.
  • The converter is lib/table-to-database.ts: tableUnderCursor, headingAbove (tree-based where parsed that far, so a # comment in a code fence is not a heading), defaultDatabaseTitle, validateDatabaseTitle, tableToSeed, and convertTableToDatabase(view, anchor?), which the palette (table.to-database, so :table_to_database), VimNav's Space l d (vim.leaderTableToDatabase, in KEYMAP_DEFINITIONS and the shared-domain catalog) and the widget's menu (TableMenuRequest.convertToDatabase, an optional callback the widget supplies; the menu shows the entry only when it is there) all call. The widget passes its captured anchor, since the focusout that opens the menu already committed any dirty cell and rebuilt the decorations.
  • registerCommandPaletteEx in Editor.tsx now builds from buildCommands({ includeUnavailable: true }); runCommand already re-checked when at invocation. Any new palette command with an editor-scoped when is an ex command from now on without further work.
  • Tests: initialDatabaseContents in database-domain.test.ts (empty grid, a typed seed keyed by field id and round-tripped through the CSV, an adopted id column, header renames and short rows, widths, a header-only seed), the seeded path on disk in databases.test.ts and in the memory vault in database-ops.test.ts, parseTableBlock in markdown-table.test.ts, and table-to-database.test.ts in a real EditorView (caret and focused-widget resolution across a focus change, the heading default, the validation, the seed, and the whole conversion: prompt prefill, the seed handed to the store, the deduped title in the link, undo, the Open toast, cancel, no table under the cursor, a table that moved while the prompt was up, a refused creation) and the menu entry's presence.
  • The live harness is tools/check-table-to-database.mjs before|after: three launches, both stores isolated, one at a time (Vim mode with rendered tables, Vim mode with raw tables, Vim mode off), 53 checks on the rendered DOM and the vault on disk. tools/demo-table-to-database.mjs before|after is the clip's recorder.
  • VaultSettings.displayName (bridge-contract) is the first top-level scalar in vault.json. normalizeVaultDisplayName and resolveVaultName in shared-domain/vault-display-name.ts own the rules (C0 and C1 controls other than the whitespace ones dropped, whitespace collapsed to one space, trimmed, cut at 64 UTF-16 units without splitting a surrogate pair, undefined when nothing is left so the key is omitted); znserver's internal/vault/display_name.go is the byte-for-byte mirror and its test runs the same inputs. Every enumeration of the sections gained the key: main's normalizeVaultSettings candidate and return, cloneVaultSettings, the renderer's normalizeVaultSettings in vault-layout.ts, the conflict prompt's SECTIONS (a compile error until it is added) and VAULT_SETTINGS_SECTION_LABELS ("Vault name"). It is deliberately not in PORTABLE_PREF_KEYS: it belongs to the vault, not the app.
  • VaultInfo.name is now the display name. vaultInfo(root) stays the sync folder-name constructor; describeVault(root) (main vault.ts) is what opening goes through (setVaultForWindow, setVault, the listLocalVaults fallback). syncLocalVaultName(root) in index.ts re-reads the name and updates every window on the root (WindowVaultRegistry.renameLocalVault, watchers untouched), currentVault, and the remembered list (renameLocalVault in vault.ts, in place: a rename is not a visit) whenever the list's entry differs. It runs after a local settings save, on getCurrentVault, and at the top of listLocalVaults. That last one matters: a boot opens the vault with persist: false, so the windows can carry the name from vault.json while the remembered list, which the sidebar header reads first (#153), still holds the last visit's name. Compare the list, not the windows.
  • Renderer: store.renameVault(name) normalizes, skips an unchanged name, refuses a temporary session, saves through setVaultSettings and refetches the remembered list. applyVaultNameFromSettings runs after every settings save and every external vault.json change and sets vault.name with a plain set; setVault compares roots only now, since the name is a label (a rename must not clear caches, undo stacks or closed tabs). lib/rename-vault.ts holds vaultFolderName, canRenameVault (local, not temporary, not remote) and renameVaultWithPrompt, shared by the palette command vault.rename (so :vault_rename) and the Sidebar vault menu; Settings uses a TextInputRow with commitOnBlur, settingId="vault-name", registered in the Vault category's search items and the Location sub-tab's searchIds, which the guard tests require.
  • The CLI's resolveVaultSelector (mcp/vault-ops.ts) matches the stored name (the display name once the vault was opened or renamed) first and path.basename(root) second, with the same ambiguity rule for either.
  • Tests: vault-display-name.test.ts (shared-domain), the round trip, describeVault and renameLocalVault in vault.test.ts, the registry rename in window-vaults.test.ts, rename-vault.test.ts (app-core: the store action end to end against a bridge double, the prompt helper, the palette gating), the CLI selector in cli/commands/vault.test.ts, and the conflict sections test. The live harness is tools/check-vault-display-name.mjs before|after: two launches on an acme/docs vault, both stores isolated, 25 checks; tools/demo-vault-display-name.mjs before|after is the clip's recorder.
  • The store keeps a per-note disk revision (noteDiskRevision(path) in store.ts, module-level like noteContentVersions), bumped inside the same set() that replaces noteContents[path] from a disk read: the watcher change/add branch of applyChange and the resync re-read. applyChange takes { source: 'app' } for a re-read of a file the app rewrote itself (the asset rename and move link rewrites pass it), which leaves the revision alone so the editor keeps history through it. The app's own save echo (existing.body === content.body) and a read refused over a dirty buffer never reach the bump. An entry outliving a vault switch is harmless: an editor re-baselines on every path it starts showing.
  • lib/editor-disk-sync.ts: isDiskChange({ pathChanged, bodyChanged, diskRevision, seenDiskRevision }) is the decision (a body change under an unchanged path with a moved revision), and resetUndoHistory(view, compartment) the reset idiom (remove the history field, add it back, #247's trick). EditorPane holds seenDiskRevisionRef beside seenPathRewriteSeqRef, sets it at view creation and on every effect run, and calls the reset right after the in-place dispatch when fromDisk holds; the switched block is unchanged, and so is the minimal change (the caret still rides through). PinnedReferencePane has an older copy of the sync effect (whole-document replace, no addToHistory: false, no reset) that was left alone.
  • Tests: editor-disk-sync.test.ts (the decision table; the issue's steps on a real EditorView with a control that shows , changed outside; a peer-pane change keeping history; later edits undoing to the disk text) and a #852 suite in store-note-integrity.test.ts (the revision moves for an external change to an open clean note only: not for the user's typing, the save echo, an app rewrite, or a change refused over a dirty buffer). The live harness is tools/check-undo-after-disk-change.mjs before|after (two launches, Vim on and off, 18 checks, the file on disk read after each undo); tools/demo-undo-disk-change.mjs before|after is the clip's recorder.
  • The Help view (HelpView.tsx) is built from four pieces: SectionShell (a section set apart by space, its scroll margin clearing the pinned section links even when they wrap), ArticleList (guides and concepts at a max-w-prose measure), ReferenceList/ReferenceRow (title first, keys at the end of the line, detail underneath; the keys are optional, so Settings rows and the Good to know notes use it too) and KeyList. KeyList draws a row's keys through parseHelpKeys(raw, mac) in lib/help-keys.ts: a trailing (…) becomes a plain-text aside, A / B / C (and ·) become one keycap per alternative only when every alternative has the same number of space-separated tokens (so Ctrl-w h / j / k / l stays one keycap), and a combo containing Mod goes through formatKeyToken with modifiers in the recorder's order; combos without Mod are left as written. For help.ts authors: write a key column as keys plus an optional trailing aside, spell the platform's primary modifier Mod, and keep alternatives the same shape if they should split. help-keys.test.ts runs every shortcut and Vim row through the parser and fails if any text is lost.

Demos

  • 847-template-shortcuts.mp4 (captions also in media/847-template-shortcuts.vtt): 42 s, 1920 × 1080 at 30 fps, recorded over CDP with both stores isolated and Vim mode off; real mouse and key events, a pointer dot drawn into the page, captions and key badges burned in. The first 12 s run on the packaged 2.55.0: ⌘⌥T in a note does nothing, and Settings → Keymap has only the Vim-only leader entries for templates. The rest runs on the 7cb62b0e build: ⌘⌥T opens the template picker and Meeting Notes makes a note, Settings → Keymap shows New note from template on ⌥⌘T and Insert template into current note Unbound, Change… records ⌘⌥Y for it, and ⌘⌥Y in the Plan note inserts the To-do template at the cursor.
  • 832-table-to-database.mp4 (captions also in media/832-table-to-database.vtt): 41 s, 1920 × 1080 at 30 fps, recorded over CDP with both stores isolated and Vim mode on (sticky leader hints), on a scratch vault holding a Q3 planning note with a Roadmap heading and a four-column table; real mouse and key events, a pointer dot drawn into the page, captions and key badges burned in. The first 14 s run on the packaged 2.55.0: m in a cell opens the table menu and its last entry is Sort column, and ⇧⌘P with "convert table" finds no command. The rest runs on the 7d8b9507 build: m, the filter typed to Convert to database…, Enter, the prompt prefilled with Roadmap, Enter, the table replaced by [[Roadmap]] with the toast and its Open button, gd opening the grid with Done as a checkbox, Due as dates and Estimate as numbers, and back in the note Space l showing Table to database on d beside Format note, Copy as Markdown and Toggle favorite.
  • 692-vault-display-name.mp4 (captions also in media/692-vault-display-name.vtt): 49 s, 1920 × 1080 at 30 fps, recorded over CDP with both stores isolated and Vim mode on, on three scratch vaults under /tmp/zn-demo-…/acme|beacon|cobalt/docs, all remembered; real mouse and key events, a pointer dot drawn into the page, captions and key badges burned in. The first 13 s run on the packaged 2.55.0: Space v lists docs, docs, docs (only the paths differ), and ⇧⌘P with "rename vault" finds no command. The rest runs on the 9d3d2b15 build: Rename Vault… from the palette, the prompt with docs as its placeholder, "Acme API docs" typed, the sidebar header and the title bar following, Space v telling the vaults apart, the second repo's vault opened from the switcher and renamed "Beacon handbook" from the sidebar's vault menu, the switcher reading Beacon handbook, Acme API docs, docs, and the Vault name field found through Settings search.
  • tasks-location-setting.mp4 (captions also in media/tasks-location-setting.vtt): 54 s, 1920 × 1080 at 30 fps, recorded over CDP with both stores isolated and Vim mode on, on a scratch Inbox vault with three notes; real mouse and key events, a pointer dot drawn into the page, captions and key badges burned in. The first 24 s run on the packaged 2.55.0 app: Settings search for "tasks folder" finds nothing, the setting has to be found by hand under Vault → Notes, and its folder field calls Tasks a "Vault-relative" folder. The rest runs on the ef45691b build: the same search jumps to the highlighted row, the folder field says new task files go to inbox/ and, once Tasks is typed, to inbox/Tasks/, and New Task from the command palette writes Call dentist there, as its breadcrumb (Inbox › Tasks › Call dentist) shows.
  • settings-search-every-setting.mp4 (captions also in media/settings-search-every-setting.vtt): 74 s, 1920 × 1080 at 30 fps, recorded over CDP with both stores isolated, Vim mode and daily notes on; real mouse and key events, a pointer dot drawn into the page, captions and key badges burned in. The first 27.5 s run on the packaged 2.55.0: "clipboard", "rollover" and "trash folder" each answer "No settings match your search.", and "math renderer" is found but its hit opens Editor on the Vim tab, not Writing where the row lives. The rest runs on the e28c5aec build: the same four searches land highlighted on Sync clipboard with Vim registers (Editor → Vim), Roll over unfinished tasks to today (Vault → Periodic notes), Trash path (Vault → Folders) and Math renderer (Editor → Writing, found from Editor → Search), and with Vim mode switched off the clipboard hit highlights the Vim mode switch instead of an empty tab.
  • 851-calendar-task-placement.mp4 (captions also in media/851-calendar-task-placement.vtt): 34 s, 1920 × 1080 at 30 fps, recorded over CDP with both stores isolated and Vim mode on, on a scratch vault with daily notes for today and tomorrow, each a ## Tasks section above a --- and ## Notes; real mouse and key events, a pointer dot drawn into the page, captions and key badges burned in. The first 10.5 s run on the packaged 2.55.0: "Call the bank" added from the calendar panel lands at the bottom, under Notes. The rest runs on the 73691f5c build: the same task joins the Tasks section after "Pay rent", then "Email Sam" moves into tomorrow's note with m, l, Enter and Enter on the drop menu, and lands under tomorrow's Tasks heading, above the divider.
  • 849-task-code-block.mp4 (captions also in media/849-task-code-block.vtt): 21 s, 1920 × 1080 at 30 fps, recorded over CDP with both stores isolated, Vim mode on, split view and completed tasks gray and struck, on a scratch vault holding the issue's note; real mouse and key events, a pointer dot drawn into the page, captions burned in. The first 10.6 s run on the packaged 2.55.0: in the preview the code block sits above "1. Step one.", and checking the step off strikes and grays the code block along with the text. The rest runs on the e28c5aec build: the step text, then its code block, then the line after it, and checking it off strikes the text while the code block stays plain.
  • 852-undo-after-disk-change.mp4 (captions also in media/852-undo-after-disk-change.vtt): 32 s, 1920 × 1080 at 30 fps, recorded over CDP with both stores isolated, Vim mode on and live preview off, on a scratch vault with one empty note; real key events, a pointer dot drawn into the page, captions and key badges burned in. The first 14.5 s run on the packaged 2.55.0 and follow the issue's steps: i, Hello, Esc, the save, the file rewritten from outside as Hello, changed outside appearing in place, then u leaving , changed outside, which is saved. The rest runs on the a80a45a5 build: the same steps, u doing nothing, then A and mine, Esc and u undoing back to the text on disk.
  • help-redesign.mp4 (captions also in media/help-redesign.vtt): 49 s, 1920 × 1080 at 30 fps, recorded over CDP with both stores isolated and Vim mode on; real mouse and key events, a pointer dot drawn into the page, captions and key badges burned in. The first 16.5 s run on the packaged 2.55.0: ⇧⌘P, Open Help lands on the banner of buttons, stats and chips, How-To shows every guide in its own full-width card, and the shortcut rows lead with keycaps holding their asides (Ctrl+D (in Search notes), Mod+F (in the editor)). The rest runs on the 9184b48a build: the same Open Help lands on the title, search field and section links, How-To reads at a comfortable width, the shortcut rows lead with the action (Ctrl+D with "(in Search notes)" beside it), the section links stay pinned past ⌘4 / ⌘5 / ⌘6, and typing "template" into the search narrows the section links to six and the shortcuts to the template rows (⌥⌘T, and Unbound for Insert template into current note).

Verification

  • Tasks location, unit: npm run typecheck clean, 7 of 7 tasks; the app-core suite green, 235 files and 2719 tests (1 skipped). shared-domain and desktop are unchanged by this fix and were not rerun for it. New: 6 tests (4 in vault-layout.test.ts, 2 in SettingsModal.test.ts); the two SettingsModal tests were run against the pre-fix SettingsModal.tsx and fail there, on the missing search entry and the missing destination text.
  • Tasks location, live (tools/check-tasks-location.mjs), both stores isolated: the packaged 2.55.0 app passes 2 of 13, the two "where New Task writes" checks (inbox/Tasks/Call dentist.md, and Tasks/Water plants.md in the Vault root vault), which is the point: where files go did not change. Every search came back empty, and the folder field said "Vault-relative subfolder for new task files" in both vaults, the root one included. The ef45691b build passes 13 of 13: each search lists its row, the hit opens Vault → Notes, the field says inbox/ while empty, inbox/Tasks/ with Tasks, and Tasks/ in the Vault root vault, and New Task writes exactly there.
  • Tasks location, how to test locally: npm run build --workspace @zennotes/desktop, then node docs/releases/v2.56.0/tools/check-tasks-location.mjs after (and before for the packaged 2.55.0 in dist/mac-arm64). By hand: npm run dev, press Cmd+, and type tasks folder in the Settings search. Before: "No settings match your search." After: "Default tasks location" (Vault); click it and Vault → Notes opens with the row highlighted. Pick Specific folder, type Tasks, press Enter. Before: the field says "Vault-relative subfolder for new task files". After: "New task files go to inbox/Tasks/." (Tasks/ in a Vault root vault). Then Cmd+Shift+P, New Task, a title, Enter: the file lands in the folder the field named. This writes the setting into the open vault's vault.json, so set it back to Primary location afterwards.
  • Tasks location, not exercised live: the web client (same core, same SettingsModal) and the phone shells, which pick the change up when they adopt core 2.56.0.
  • Settings search, unit: npm run typecheck clean, 7 of 7 tasks (app-core's tsc --noEmit also run directly, since turbo served 3 from cache); the app-core suite green, 235 files and 2723 tests (1 skipped). New: 4 tests in SettingsModal.test.ts, all run against the pre-fix SettingsModal.tsx and failing there, with 10 rows without a search item and 31 sub-tab problems between them.
  • Settings search, live (tools/check-settings-search.mjs), both stores isolated: the packaged 2.55.0 app passes 0 of 20. Every new query came back empty, Math renderer and Relaxed $$ math delimiters left Editor on its Search tab with nothing highlighted, and Quick Note prefix opened Quick capture. The build passes 20 of 20: each of 16 hits opened its category and tab from somewhere else and highlighted its row in view, and with Vim mode and daily notes off the four gated hits highlighted Vim mode and Enable daily notes.
  • Settings search, how to test locally: npm run build --workspace @zennotes/desktop, then node docs/releases/v2.56.0/tools/check-settings-search.mjs after (and before for the packaged 2.55.0 in dist/mac-arm64). By hand: npm run dev, press Cmd+, and type rollover in the Settings search. Before: "No settings match your search." After: "Roll over unfinished tasks to today" (Vault); click it and Vault → Periodic notes opens with the row highlighted, or with Enable daily notes highlighted if daily notes are off. Then search math renderer and click the hit. Before: Editor opens on the Vim tab and nothing is highlighted. After: Editor → Writing, with Math renderer highlighted. Same for trash folder (Vault → Folders → Trash path) and, with Vim mode on, clipboard (Editor → Vim). Nothing is written to the vault.
  • Settings search, not exercised live: the web client, whose Settings is the same SettingsModal but hides the desktop-only rows, and the phone shells.
  • Calendar placement, unit: npm run typecheck clean, 7 of 7 tasks, on the merge; the app-core suite green on the merge (235 files, 2723 tests, 1 skipped) and at 73691f5c (236 files, 2725 tests, 1 skipped). New: 2 tests in ContextMenu.test.ts, run against the code before #850 (the mount-focus test fails there), #850 as submitted and the follow-up (both pass). The PR brought no tests of its own; insertTasksUnderTasksHeading is covered in shared-domain (#452, #483).
  • Calendar placement, live (tools/check-calendar-placement.mjs), both stores isolated: the packaged 2.55.0 app passes 7 of 9, failing only the two placement checks (the moved task and the added task both appended at the bottom, under ## Notes); its menus already held focus. The merge passes 9 of 9, and so does the 73691f5c build.
  • Calendar placement, how to test locally: npm run build --workspace @zennotes/desktop, then node docs/releases/v2.56.0/tools/check-calendar-placement.mjs after (and before for the packaged 2.55.0). By hand: npm run dev with daily notes on, and a daily note holding ## Tasks, a task, ---, ## Notes. Open it; the calendar panel opens beside it. Type a task into "Add a task for …" and press Enter. Before: it lands at the end, under Notes. After: it joins the Tasks list. Then click the day, press m on a task, l, Enter, and Enter again on "Move into …'s note": before, it lands at the bottom of the next day's note; after, under its Tasks heading.
  • Calendar placement, not exercised live: the web client and the phone shells (same core), a mouse drag onto a day (it opens the same drop menu the keyboard does), and Linux, where the contributor (Niri on Wayland) may have needed the immediate focus.
  • #849, unit: npm run typecheck clean, 7 of 7 tasks; the app-core suite green, 237 files and 2730 tests (1 skipped). New: 5 tests in lib/preview-task-body.test.ts, run against the old algorithm pasted into the helper (the loose-order and tight code-block tests fail there, the #512 sub-list and state-marker tests pass on both).
  • #849, live (tools/check-task-code-block.mjs), both stores isolated: the packaged 2.55.0 passes 7 of 12. Its loose item reads wrapper[code] | p:Step one | p:Expected (the code block first), the checked loose and tight items put the code block inside the struck wrapper, and the #512 checks pass. The e28c5aec build passes 12 of 12: p:Step one | code | p:Expected loose, checkbox | wrapper[step text] | code | wrapper[Expected] tight, the code block never inside a wrapper, and a checked parent still struck with its open sub-task outside.
  • #849, how to test locally: npm run build --workspace @zennotes/desktop, then node docs/releases/v2.56.0/tools/check-task-code-block.mjs after. By hand: npm run dev, paste the issue's markdown into a note (a task holding a fenced code block, a blank line, then a second task) and open Preview. Before: the code block sits above the task's text. After: text, then code block, then the line after it. Set Settings → Editor → Writing → Completed task style to Both and check the task off. Before: the code block is struck through too. After: only the text is.
  • #849, not exercised live: the web client (same core, same Preview) and the phone shells; Windows, the reporter's platform (the fix is platform-independent DOM work).
  • #847, unit: npm run test:run green (app-core 237 files and 2734 tests with 1 skipped, desktop 60 files and 956 tests with 4 skipped, shared-domain), npm run typecheck 7 of 7. New: 3 tests in keymaps.test.ts (the Mac default and the unbound Linux and Windows ones, the event a Mac sends for ⌘⌥T with the T turned into † by Option, no clash between unbound actions) and 1 in commands.test.ts (the palette shows the chord once set, the leader key only with Vim on).
  • #847, live (tools/check-template-shortcuts.mjs), both stores isolated: the packaged 2.55.0 passes 1 of 5 (⌘⌥T opens nothing with Vim off or on, neither Keymap row exists; the one pass is the vacuous "nothing typed"). The 7cb62b0e build passes 8 of 8: with Vim off, ⌘⌥T from the editor opens the picker and Meeting Notes makes a note, Settings → Keymap lists New note from template on ⌥⌘T and Insert template into current note as Unbound, the recorder binds ⌘⌥Y, and ⌘⌥Y then opens the insert picker; with Vim on, ⌘⌥T from normal mode opens the picker without typing into the note.
  • #847, how to test locally: npm run build --workspace @zennotes/desktop, then node docs/releases/v2.56.0/tools/check-template-shortcuts.mjs after. By hand on macOS: npm run dev, turn Vim mode off, open a note, press ⌘⌥T. Before: nothing happens. After: the template picker opens; pick one and a note is made from it. Then Settings → Keymap, Insert template into current note, Change…, press a chord, Save, and press it in a note: the insert picker opens. On Linux or Windows, give New note from template a key there first.
  • #847, not exercised live: Linux and Windows, where the default is unbound (covered by the platform tests), the web client (same core; a browser may claim some chords before the page sees them) and the phone shells.
  • #832, unit: npm run typecheck clean, 7 of 7 tasks; npm run test:run green across the monorepo: shared-domain 59 files and 1691 tests, app-core 239 files and 2759 tests (1 skipped), desktop 60 files and 963 tests (4 skipped). New: 26 tests (6 for initialDatabaseContents in database-domain.test.ts, 1 each in databases.test.ts and database-ops.test.ts for the seeded creation on disk and in the memory vault, 3 for parseTableBlock, 15 in table-to-database.test.ts). The KEYMAP_CATALOG mirror test caught the missing catalog row for vim.leaderTableToDatabase on the first full run.
  • #832, live (tools/check-table-to-database.mjs), both stores isolated, three launches: the packaged 2.55.0 passes 9 of 53 (the widget, the m and right-click menus and the palette open; the menu ends with Sort column, :table_to_database is "Not an editor command", the palette lists nothing for "convert table", and no .base folder appears), and "u brings the table back" passes vacuously there, since the table never left. The 7d8b9507 build passes 53 of 53: the m menu, Space l d with a colliding name landing as [[Roadmap 2]], :table_to_database on a raw table, the palette entry with a typed name, the caret outside a table answering with a toast and no prompt, and the right-click menu with Vim mode off; each time the note is autosaved with the link, data.csv reads id,Item,Done,Estimate with "Write ""spec"", v1" and "Ship, then rest" quoted, and schema.json types Done as checkbox and Estimate as number with the id field hidden.
  • #832, how to test locally: npm run build --workspace @zennotes/desktop, then node docs/releases/v2.56.0/tools/check-table-to-database.mjs after (and before for the packaged 2.55.0 in dist/mac-arm64). By hand: npm run dev, open a note with a heading and a pipe table under it, click into a cell and press m (or right-click). Before: the menu ends with Sort column (Z → A). After: Convert to database… is the last entry; pick it, the prompt shows the heading as the name, press Enter. The table becomes [[Heading]], the sidebar gains a Heading database, and gd (or a click) on the link opens the grid with the columns typed. Press u in the note: the table is back and the database is still in the sidebar. With Vim mode on, Space l d in a cell does the same; with tables as plain text (Settings → Editor → Render tables in live preview off), :table_to_database with the caret in the table does too. This writes a .base folder into the open vault, so use a scratch note, and delete the database afterwards from the sidebar.
  • #832, not exercised live: the web client (same core; its createDatabase is the shared database-ops composition, covered by the memory-vault test), remote workspaces from the desktop (same composition), the phone shells, and Linux and Windows.
  • #692, unit: npm run typecheck clean, 7 of 7 tasks; npm run test:run green across the monorepo: shared-domain 60 files and 1698 tests, app-core 240 files and 2769 tests (1 skipped), desktop 60 files and 968 tests (4 skipped). New: 22 tests (7 in vault-display-name.test.ts, 3 in vault.test.ts, 1 in window-vaults.test.ts, 10 in rename-vault.test.ts, 1 in cli/commands/vault.test.ts), plus the conflict sections test updated. znserver: go vet ./... and go test ./... clean with 3 new tests, the normalizer's cases the same strings as the TypeScript ones.
  • #692, live (tools/check-vault-display-name.mjs), both stores isolated, two launches: the packaged 2.55.0 passes 5 of 25 (the header and title bar read docs, Settings and the vault menu open, and "an empty field goes back to the folder name" holds vacuously); it lists no Rename Vault…, has no Vault name row, ignores a displayName written to vault.json, and answers :vault_rename with nothing. The 9d3d2b15 build passes 25 of 25: the palette rename lands in the header, the title bar, vault.json and the remembered list; Space v lists the vault by its new name with its path beside it; Settings search finds Vault name, typing into the field and leaving it renames the vault, an empty field goes back to docs and vault.json loses the key; the vault menu's Rename Vault… works; :vault_rename opens the prompt; a displayName written from outside renames the open vault and reaches the remembered list; and after a restart the header still shows it. The restart check failed on the first run and found a real gap (the remembered list, which the header reads, was only rewritten when the windows changed; a boot opens without persisting), fixed before the commit.
  • #692, how to test locally: npm run build --workspace @zennotes/desktop, then node docs/releases/v2.56.0/tools/check-vault-display-name.mjs after (and before for the packaged 2.55.0 in dist/mac-arm64). By hand: npm run dev, press ⇧⌘P and type rename vault. Before: nothing matches. After: Rename Vault…; Enter, type a name, Enter. The sidebar header, the title bar and Space v show the name; the folder in Finder is unchanged, and .zennotes/vault.json holds "displayName". Click the vault name at the top of the sidebar: Rename Vault… is in the menu. Cmd+, and search vault name: the field holds the name; clear it and click elsewhere, and the header goes back to the folder name. This writes into the open vault's vault.json, so use a scratch vault or clear the name afterwards.
  • #692, not exercised live: the web client and a desktop remote workspace against an updated server (the server change is committed, not released), a temporary folder session (the command and the field are gated off it by canRenameVault, covered by unit tests), the phone shells, and Linux and Windows (the folder-name split handles both separators, covered by the store test's POSIX root only).
  • #852, unit: npm run typecheck clean, 7 of 7 tasks; the app-core suite green, 241 files and 2781 tests (1 skipped). New: 12 tests (8 in lib/editor-disk-sync.test.ts, 4 in store-note-integrity.test.ts); the control test reproduces the old , changed outside on the pre-fix sequence, and the #247 and #793 history suites are unchanged and green.
  • #852, live (tools/check-undo-after-disk-change.mjs), both stores isolated, the issue's steps with Vim mode on (u) and off (⌘Z): the packaged 2.55.0 reproduces it, 8 of 18, with , changed outside in the editor and, 900 ms later, in the file; the a80a45a5 build passes 18 of 18: the editor shows the disk text after undo, the file is untouched, typing afterwards lands and undoes back to the disk text, and a second undo stays there.
  • #852, how to test locally: npm run build --workspace @zennotes/desktop, then node docs/releases/v2.56.0/tools/check-undo-after-disk-change.mjs after (and before for the packaged 2.55.0 in dist/mac-arm64). By hand: npm run dev, open an empty note, type Hello, wait a second for the save, then in a terminal printf 'Hello, changed outside' > <vault>/inbox/<note>.md. The editor shows the new text; press u (or ⌘Z with Vim mode off). Before: , changed outside, and cat shows the file changed to it. After: nothing changes, and the file still reads Hello, changed outside. Type something and undo: it goes back to the disk text.
  • #852, not exercised live: the web client and a desktop remote workspace (same applyChange and resync paths, covered by the store tests), a second pane on the same note (each pane runs the effect on its own; the unit test covers the decision), Windows, the reporter's platform (the fix is renderer-side), and the reference pane's own sync effect, which was left as it was.
  • Help, unit: npm run typecheck clean, 7 of 7 tasks; the app-core suite green, 238 files and 2741 tests (1 skipped). New: 7 tests in lib/help-keys.test.ts (Mod formatting and modifier order, asides, alternatives split and kept together, and every shortcut and Vim row parsed with no text lost). No arbitrary Tailwind values in the view.
  • Help, live, both stores isolated, one instance at a time: screenshots of every section in the dark and Light themes at 1440 × 900 and in an 820 px window. Prose measures 655 px (was 996) at 16 px with 28 px leading; at 820 px the section links wrap onto two rows with no scrollbar. A behaviour pass with Vim mode off, 8 of 8: all 8 section links unfiltered, "template" narrows the links to 6 and the shortcuts to the template rows (New note from template on ⌥⌘T, Insert template into current note as plain-text Unbound, not a keycap), "zzzqqq" shows "Nothing in the manual matches “zzzqqq”." and hides the links, and a real mouse click on Settings lands its heading below the pinned links (headings land 47 px below them at 1440 px, 11 px below the wrapped two-row links at 820 px).
  • Help, how to test locally: npm run build --workspace @zennotes/desktop, then npm run dev (or launch the build) and press ⇧⌘P, type Open Help, Enter. Before: a gradient banner of buttons, stats and chips, and guides in full-width cards. After: a title, a search field and section links; click Shortcuts and the links stay pinned while you scroll, rows read "Move the highlighted note to Trash … Ctrl+D (in Search notes)", and "Edit / Split / Preview mode" shows ⌘4 / ⌘5 / ⌘6 as three keycaps. Type template into the search: the links narrow and the shortcut rows show ⌥⌘T and Unbound. Narrow the window: the links wrap and the keys drop under their titles. Nothing is written to the vault.
  • Help, not exercised live: the web client (same core, same HelpView) and the phone shells, and Linux and Windows key spellings in the view (the Mod formatting for them is covered by the unit tests).

Distribution channels

  • Release workflow: the tag's run 36064942434 built every platform and finished green on the first attempt, 43 minutes from tag to the last upload (22:00:33 to 22:43:23 UTC on September 24). 34 assets: the same 25 installers and manifests as 2.55.0, plus the nine demo clips uploaded by hand right after the tag. macOS is Developer ID signed and notarized. Both macOS apps carry ZenNotes CLI 0.4.0 as the bundled zn (read from Contents/Resources/terminal/manifest.json inside the arm64 DMG).
  • AUR: 815f78f in the separate zennotes-bin clone (zennotes-bin 2.56.0-1), mirrored on desktop main and v2.56.0 as 55cd419c; aur-check.yml passed (runs 36066446400 on main and 36066444033 on v2.56.0). The tarball downloaded from the release has SHA-256 ef671bfa6802dfa6db0ce80e56ad2f60c74f9bb4df8e8bcc5a5351356cd9ae45, equal to GitHub's asset digest and to the pinned value.
  • Nix: nix-update.yml run 36066459248 opened bot PR #856, lifted byte-for-byte onto main and v2.56.0 as 2e2ef327 and closed with a one-line comment pointing at that commit; nix-build.yml run 36066679419 passed on main first try. desktopHash (sha256-72cb+mgC36bbDOgOVq0vYMdPm7TfjovMWlNRNWzZrkU=) is the same tarball digest as the AUR pin, in SRI form. The bot PR's own nix-build event (36066607798) shows a zero-job failure, the same noise as on 2.55.0.
  • nixpkgs: PR #561418 amended in place to zennotes-desktop: 2.45.0 -> 2.56.0 (fork commit 37cb26d6, force-pushed to zennotes-desktop-2.46.0, body in nixpkgs-pr-body.md), after checking that nixpkgs master still carries 2.45.0. Only version, npmDepsHash (sha256-K7B4kx09VDsB3Pj61HRCrLbY1PDfS28icgvGMQnr5Nk=) and the source hash (sha256-V1KE1fFt/QO8Zi1joKYkQ2MNzYX3m4LJRADt3GeUilE=, both lifted from packaging/nix/release-data.json) change; the refetch of upstream master was skipped again, as the earlier handoffs advise.
  • Homebrew: dfae031 in the tap (zennotes 2.56.0), mirrored on desktop main and v2.56.0 as 7d2ce23a. The cask pins both DMG digests from the GitHub API (arm64 ba7018be5357b0a7cd4839b8cf1a5707f2501b2912fe7cce8c73cad70dfe0b3d, x64 4e6d6db9e111f2149d370d2118078285ddbc51f2ec990a401134cdf58698ba23), and both equal a local shasum of the downloaded DMGs.
  • Website: the task-file docs and a settings-reference entry for the three location settings went live ahead of the release as fe13bcd on the website's main (Adib's call, no PR), written to be true of 2.55.0. The 2.56.0 docs and the releases entry went live together through PR #45 (fdf9dc12, a merge commit) at 22:47 UTC, after releases/latest had flipped and every installer was verified: the #832 docs (a feature card, a databases-section row and the Space l d leader row), the #692 docs (a feature card, a Vault name settings row and the Space v detail) and the #852 docs (the Tabs and splits card and the undofile settings detail), written during the cycle on a branch that had since been merged for unrelated work and carried over onto website/docs-2.56 from main as three clean cherry-picks, plus the releases entry (three features, seven fixes, nine clips, ce5f537). The full php artisan test passed before the push (915 tests). Laravel Cloud deployed it from main; the release page led with 2.56.0 at 22:54 UTC (49 documented releases), with the #852 clip and a caption file answering 200 and the docs page carrying the new sections.
  • Docker and the server: the Go server is released separately from ZenNotes/znserver. Its 2.56.0 (the vault display name mirror for #692, f6374ce, so a web client's settings save keeps a desktop-written displayName and the web app shows it; a 2.55.0 server drops the field, as a side-by-side run on one scratch vault confirmed) pins the 2.56.0 web boundary artifact web-2.56.0-web.h377717cfb31ad477, built by run 36067133114 from the tag commit with a clean tree and published as a pre-release at 22:29 UTC on Adib's yes. The server release followed the desktop one the same evening: znserver PR #4 (f6374ce and the pin 62100d8, both check runs green) fast-forwarded into its main; Prepare server release run 36070524496 built five binaries and the draft (the darwin-arm64 binary matches SHA256SUMS, the manifest.json asset equals the committed pin byte for byte, and the released binary passed the same standalone checks as the candidate); published as v2.56.0 (latest) at 23:03 UTC; Publish Docker image run 36070881615 from the tag pushed adibhanna/zennotes:2.56.0, 2.56 and latest for linux/amd64 and linux/arm64 (one digest, sha256:b64a858c91b8…) at 23:06 UTC; the README's install commands moved to 2.56.0 through PR #5 (3ae1229).
  • npm run verify:channels -- 2.56.0: passed at 22:46 UTC: Homebrew tap, AUR, Nix, and all seven website download redirects serve 2.56.0. releases/latest on GitHub answers v2.56.0. The live release page leads with 2.56.0 (49 documented releases).

Release validation

  • Release PR #855: every check passed on the first run (CI 36064110041 on macOS, Ubuntu, Ubuntu arm64 and Windows, browser 36064110010, candidate 36064110023, CodeQL 36064110035, the dependency audit), and main was fast-forwarded onto the head without a merge commit.
  • Before the cut, on the bumped branch: npx turbo run typecheck --force 7 of 7 (21:21 UTC). The forced test:run failed once, a real finding in a test from this cycle: the #852 test that checks a watcher change refused over a dirty buffer left the store's 350 ms debounced save armed, and under turbo's parallel load the timer fired into a later #585 test that was collecting writes (unsaved typing landing in Vault CLI Cheatsheet.md); alone, the file was green every time. Adib approved the test-only 2c3dd065 (the test settles its own save with persistNote and asserts the user's write does not move the disk revision either), and the forced run then passed everywhere: shared-domain 60 files and 1698 tests, app-core 241 files and 2781 tests with 1 skipped, desktop 60 files and 968 tests with 4 skipped (21:30 UTC). apps/desktop npm run build:prod green (21:31).
  • The local pack hit a machine fault, not a build fault: codesign --timestamp failed on every file with "A timestamp was expected but was not found". Apple's timestamp server answered a hand-made request correctly, curl reached it over IPv4 and IPv6, and codesign itself signed through a local proxy and against the IPv4 literal, but failed instantly against the IPv6 literal; with the hostname it picks the AAAA record and does not fall back. The one thing that changed on the Mac since the 2.55.0 pack was a Tailscale network-extension update whose old build is still "waiting to uninstall on reboot". The gate pack was therefore run as electron-builder --dir -c.mac.timestamp=http://17.32.213.161/ts01 in apps/desktop (the pack script's second half; its first half, build:prod, had just run green twice on the same head), giving a Developer ID signature with a real Apple timestamp that codesign --verify --deep --strict accepts. The CI release build is on a GitHub runner and was untouched by this.
  • On that packaged app: the skill's packaged-launch-check.mjs found a CDP page target in 1.5 s, reporting 2.56.0, both stores isolated, no instance left behind; test:sidebar-vim and test:editor-improvements passed; test:vim-editor lost editor focus after its third check on the first run (the four remaining checks reported focused: false with an unmoved cursor, keys no longer landing) and passed 12 of 12 on the second run.
  • The release workflow's tag-push path behaved as it did for 2.55.0: each platform's installers went up one at a time and its update manifest last (Linux arm64 done 22:12:36, Linux x64 22:14:48 with the x64 tarball at 22:14:25 and latest-linux.yml at 22:14:45; Windows: exe 22:35:49, blockmap 22:35:50, latest.yml 22:36:04; macOS: arm64 DMG 22:42:35, x64 DMG 22:43:02, latest-mac.yml 22:43:23). macOS was the slowest job at 43 minutes. No asset was uploaded twice and nothing had to be rebuilt.
  • Verified by hand after the uploads: verify-installers.py 2.56.0 --download downloaded the 13 installers named by the four manifests in full and every one matches its manifest's size and SHA-512 ("ALL GOOD"); the arm64 macOS app inside the DMG is accepted by Gatekeeper as Notarized Developer ID (Lumary Labs LLC) with a valid stapled ticket (spctl -a -vv accepted, stapler validate worked), reads version 2.56.0 and bundles zn 0.4.0. The DMG was attached read-only with hdiutil attach -readonly and ejected afterwards.
  • Expected red marks, none of them about the release: CI run 36066446417 on main (the AUR packaging commit 55cd419c) was cancelled by the next push, the Nix packaging commit 2e2ef327, which CI then ran on and passed (36066679626), as did the Homebrew mirror 7d2ce23a (36069086163). The same pattern appeared on 2.55.0.

Local-first and keyboard-first, as always.

Don't miss a new zennotes release

NewReleases is sending notifications on new releases.