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 inconfig.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_databaseon the ex line, orSpace l din Vim mode; in a rendered table the same entry sits at the bottom of the cell menu (right-click, ormin a cell). A prompt takes the name, prefilled from the heading above the table or the note's title. ZenNotes creates a<Name>.basedatabase 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 agdor a click away; a name the vault already has becomesName 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 oneuaway, 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'sdocs/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_renameon 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 andzn vault list, andzn --vault docsstill finds the vault after a rename, since the folder name is matched second. It is saved in the vault's own.zennotes/vault.jsonasdisplayNamerather 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 tovault.jsonfrom 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
Tasksisinbox/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/landEnter), was appended to the bottom of the note even when the note had a## Taskssection, so it landed under whatever came last, often a## Notessection. 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-up73691f5c) - 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
Hellointo a note, let it save, and have another program rewrite the file asHello, changed outside: the editor showed the new text, which is right, but undo then removedHelloand 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_alland 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 / ⌘6is three), with an aside like "(in Search notes)" beside them in plain text and Unbound as a word. Keys the manual spelled with the internalModnow 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)inpackages/app-core/src/lib/vault-layout.tsreturns the vault-relative directory a "Specific folder" location creates files in (''is the vault root). It isresolveCreateLocationplusvaultRelativeFolderPath, the same paircreateNoteresolves 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
searchItemshas an entry for itssettingId, and it only opens the right sub-tab if that sub-tab'ssearchIdslists it. The three location rows now have both (drawings-location,databases-location,tasks-locationon 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.tsnow enforce both halves for every row. The first scansSettingsModal.tsxfor every row it registers (settingId="…",settingsSearchTargetProps("…"),data-settings-search-id="…") and fails for any without a search item whoseidortargetIdnames 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 handsgetSettingsSearchResultsthroughvi.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'ssearchIds, andsearchIdsmay not list a row the sub-tab does not render. It sees rows named by an expression, such as the${key}-pathfolder rows, which a text scan cannot. A row behind a new kind of gate needs that gate switched on in the test'srenderEveryRow. - A search item for a row that only renders behind a switch takes a
targetIdthat falls back to the switch while it is off (vimYankToClipboardTargetId,dailyNotesRolloverTargetIdand the other two), the waywrappedLineMotionsTargetIdalready 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:
specificFolderDestinationinvault-layout.test.ts(Inbox vault, root vault, remapped inbox, no folder named yet), and two inSettingsModal.test.ts: every location row is found and opened from search, and the tasks folder field namesinbox/Tasks/in an Inbox vault andTasks/in a root vault. Both SettingsModal tests fail on the code beforeef45691b. - The live harness is
tools/check-tasks-location.mjs before|after:beforelaunches the packaged 2.55.0 app indist/mac-arm64,afterthe build inapps/desktop/out, both stores isolated, one instance at a time. Two vaults: an Inbox vault (six searches, the jump, the folder field empty and withTasks, and where New Task from the command palette writes) and a Vault root vault withtasksLocationalready 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⌘Pas a box.- Contributor PR #850 (@uNyanda) merged as
f3226405ontov2.56.0, its base retargeted frommain; their commit,26dbed0f, stands as they wrote it. Calendar add (addTaskForDate) and calendar move (moveTaskToDate) now place tasks throughinsertTasksUnderTasksHeading, like forwarding and rollover. The follow-up73691f5cfolds the PR's second focus effect inContextMenuinto 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, andContextMenu.test.tspins 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.tsxintolib/preview-task-body.ts(wrapTaskItemOwnText(li, checkbox)): it wraps each run of the item's inline content in its ownspan.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-bodyin tight items,> pin loose ones). Its tests run onrenderMarkdown'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") andglobal.insertTemplate(""), in bothKEYMAP_DEFINITIONSand the shared-domainKEYMAP_CATALOG, dispatched inApp.tsxright 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, theconfig.tomlwriter) 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|afterdrives 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 optionalDatabaseSeed(headers,rows, optionalcolumnWidths;bridge-contract/databases.ts). What a new database is written with is decided in one place now,initialDatabaseContents(seed, genId)inshared-domain/database-csv.ts: without a seed theid+Namegrid, with one the seed run throughinferFields, the same inference that adopts a plain CSV, rows keyed by the resulting field ids. Both IO twins build from it (desktop main'sdatabases.tsand the IO-abstracteddatabase-ops.tsbehind the web bridge, the CLI and remote workspaces), so the files stay byte-compatible across transports; the two hand-rolledid+Nameblocks 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
createDatabasegained a fifth argument,{ seed?, open? }, and now resolves to the createdDatabaseDoc(orundefinedwhen nothing was created or the workspace moved on), so a caller can read the title the vault actually gave it.open: falseleaves the caller in place; every existing caller is unchanged. - Finding the table:
tableRangeAt(view, pos)incm-table.tsis exported (syntax tree first, the #485 line scan as fallback, both extended over azen:colsmarker),parseTableBlockinmarkdown-table.tslifts that marker intocolWidthsinstead of reading it as a body row, andfocusedTableAnchor(view)is the new piece: each table widget records itself onfocusin(per editor, aWeakMap), 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)innote-editor-context.tsgives 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# commentin a code fence is not a heading),defaultDatabaseTitle,validateDatabaseTitle,tableToSeed, andconvertTableToDatabase(view, anchor?), which the palette (table.to-database, so:table_to_database), VimNav'sSpace l d(vim.leaderTableToDatabase, inKEYMAP_DEFINITIONSand 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. registerCommandPaletteExinEditor.tsxnow builds frombuildCommands({ includeUnavailable: true });runCommandalready re-checkedwhenat invocation. Any new palette command with an editor-scopedwhenis an ex command from now on without further work.- Tests:
initialDatabaseContentsindatabase-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 indatabases.test.tsand in the memory vault indatabase-ops.test.ts,parseTableBlockinmarkdown-table.test.ts, andtable-to-database.test.tsin a realEditorView(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|afteris the clip's recorder. VaultSettings.displayName(bridge-contract) is the first top-level scalar invault.json.normalizeVaultDisplayNameandresolveVaultNameinshared-domain/vault-display-name.tsown 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,undefinedwhen nothing is left so the key is omitted); znserver'sinternal/vault/display_name.gois the byte-for-byte mirror and its test runs the same inputs. Every enumeration of the sections gained the key: main'snormalizeVaultSettingscandidate and return,cloneVaultSettings, the renderer'snormalizeVaultSettingsinvault-layout.ts, the conflict prompt'sSECTIONS(a compile error until it is added) andVAULT_SETTINGS_SECTION_LABELS("Vault name"). It is deliberately not inPORTABLE_PREF_KEYS: it belongs to the vault, not the app.VaultInfo.nameis now the display name.vaultInfo(root)stays the sync folder-name constructor;describeVault(root)(mainvault.ts) is what opening goes through (setVaultForWindow,setVault, thelistLocalVaultsfallback).syncLocalVaultName(root)inindex.tsre-reads the name and updates every window on the root (WindowVaultRegistry.renameLocalVault, watchers untouched),currentVault, and the remembered list (renameLocalVaultinvault.ts, in place: a rename is not a visit) whenever the list's entry differs. It runs after a local settings save, ongetCurrentVault, and at the top oflistLocalVaults. That last one matters: a boot opens the vault withpersist: false, so the windows can carry the name fromvault.jsonwhile 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 throughsetVaultSettingsand refetches the remembered list.applyVaultNameFromSettingsruns after every settings save and every externalvault.jsonchange and setsvault.namewith a plainset;setVaultcompares roots only now, since the name is a label (a rename must not clear caches, undo stacks or closed tabs).lib/rename-vault.tsholdsvaultFolderName,canRenameVault(local, not temporary, not remote) andrenameVaultWithPrompt, shared by the palette commandvault.rename(so:vault_rename) and the Sidebar vault menu; Settings uses aTextInputRowwithcommitOnBlur,settingId="vault-name", registered in the Vault category's search items and the Location sub-tab'ssearchIds, 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 andpath.basename(root)second, with the same ambiguity rule for either. - Tests:
vault-display-name.test.ts(shared-domain), the round trip,describeVaultandrenameLocalVaultinvault.test.ts, the registry rename inwindow-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 incli/commands/vault.test.ts, and the conflict sections test. The live harness istools/check-vault-display-name.mjs before|after: two launches on anacme/docsvault, both stores isolated, 25 checks;tools/demo-vault-display-name.mjs before|afteris the clip's recorder. - The store keeps a per-note disk revision (
noteDiskRevision(path)instore.ts, module-level likenoteContentVersions), bumped inside the sameset()that replacesnoteContents[path]from a disk read: the watcherchange/addbranch ofapplyChangeand theresyncre-read.applyChangetakes{ 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), andresetUndoHistory(view, compartment)the reset idiom (remove the history field, add it back, #247's trick).EditorPaneholdsseenDiskRevisionRefbesideseenPathRewriteSeqRef, sets it at view creation and on every effect run, and calls the reset right after the in-place dispatch whenfromDiskholds; theswitchedblock is unchanged, and so is the minimal change (the caret still rides through).PinnedReferencePanehas an older copy of the sync effect (whole-document replace, noaddToHistory: false, no reset) that was left alone.- Tests:
editor-disk-sync.test.ts(the decision table; the issue's steps on a realEditorViewwith a control that shows, changed outside; a peer-pane change keeping history; later edits undoing to the disk text) and a#852suite instore-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 istools/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|afteris 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 amax-w-prosemeasure),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) andKeyList.KeyListdraws a row's keys throughparseHelpKeys(raw, mac)inlib/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 (soCtrl-w h / j / k / lstays one keycap), and a combo containingModgoes throughformatKeyTokenwith modifiers in the recorder's order; combos withoutModare left as written. Forhelp.tsauthors: write a key column as keys plus an optional trailing aside, spell the platform's primary modifierMod, and keep alternatives the same shape if they should split.help-keys.test.tsruns 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 the7cb62b0ebuild: ⌘⌥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:min 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 the7d8b9507build: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,gdopening the grid with Done as a checkbox, Due as dates and Estimate as numbers, and back in the noteSpace lshowing Table to database ondbeside 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 vlists docs, docs, docs (only the paths differ), and ⇧⌘P with "rename vault" finds no command. The rest runs on the9d3d2b15build: Rename Vault… from the palette, the prompt withdocsas its placeholder, "Acme API docs" typed, the sidebar header and the title bar following,Space vtelling 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 callsTasksa "Vault-relative" folder. The rest runs on theef45691bbuild: the same search jumps to the highlighted row, the folder field says new task files go toinbox/and, onceTasksis typed, toinbox/Tasks/, and New Task from the command palette writesCall dentistthere, 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 thee28c5aecbuild: 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## Taskssection 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 the73691f5cbuild: the same task joins the Tasks section after "Pay rent", then "Email Sam" moves into tomorrow's note withm,l,EnterandEnteron 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 thee28c5aecbuild: 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 asHello, changed outsideappearing in place, thenuleaving, changed outside, which is saved. The rest runs on thea80a45a5build: the same steps,udoing nothing, thenAand mine, Esc anduundoing 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 the9184b48abuild: 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+Dwith "(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 typecheckclean, 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 invault-layout.test.ts, 2 inSettingsModal.test.ts); the two SettingsModal tests were run against the pre-fixSettingsModal.tsxand 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, andTasks/Water plants.mdin 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. Theef45691bbuild passes 13 of 13: each search lists its row, the hit opens Vault → Notes, the field saysinbox/while empty,inbox/Tasks/withTasks, andTasks/in the Vault root vault, and New Task writes exactly there. - Tasks location, how to test locally:
npm run build --workspace @zennotes/desktop, thennode docs/releases/v2.56.0/tools/check-tasks-location.mjs after(andbeforefor the packaged 2.55.0 indist/mac-arm64). By hand:npm run dev, press Cmd+, and typetasks folderin 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, typeTasks, press Enter. Before: the field says "Vault-relative subfolder for new task files". After: "New task files go toinbox/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'svault.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 typecheckclean, 7 of 7 tasks (app-core'stsc --noEmitalso run directly, since turbo served 3 from cache); the app-core suite green, 235 files and 2723 tests (1 skipped). New: 4 tests inSettingsModal.test.ts, all run against the pre-fixSettingsModal.tsxand 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, thennode docs/releases/v2.56.0/tools/check-settings-search.mjs after(andbeforefor the packaged 2.55.0 indist/mac-arm64). By hand:npm run dev, press Cmd+, and typerolloverin 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 searchmath rendererand click the hit. Before: Editor opens on the Vim tab and nothing is highlighted. After: Editor → Writing, with Math renderer highlighted. Same fortrash 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
SettingsModalbut hides the desktop-only rows, and the phone shells. - Calendar placement, unit:
npm run typecheckclean, 7 of 7 tasks, on the merge; the app-core suite green on the merge (235 files, 2723 tests, 1 skipped) and at73691f5c(236 files, 2725 tests, 1 skipped). New: 2 tests inContextMenu.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;insertTasksUnderTasksHeadingis 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 the73691f5cbuild. - Calendar placement, how to test locally:
npm run build --workspace @zennotes/desktop, thennode docs/releases/v2.56.0/tools/check-calendar-placement.mjs after(andbeforefor the packaged 2.55.0). By hand:npm run devwith 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, pressmon a task,l,Enter, andEnteragain 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 typecheckclean, 7 of 7 tasks; the app-core suite green, 237 files and 2730 tests (1 skipped). New: 5 tests inlib/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 readswrapper[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. Thee28c5aecbuild passes 12 of 12:p:Step one | code | p:Expectedloose,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, thennode 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:rungreen (app-core 237 files and 2734 tests with 1 skipped, desktop 60 files and 956 tests with 4 skipped, shared-domain),npm run typecheck7 of 7. New: 3 tests inkeymaps.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 incommands.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"). The7cb62b0ebuild 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, thennode 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 typecheckclean, 7 of 7 tasks;npm run test:rungreen 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 forinitialDatabaseContentsindatabase-domain.test.ts, 1 each indatabases.test.tsanddatabase-ops.test.tsfor the seeded creation on disk and in the memory vault, 3 forparseTableBlock, 15 intable-to-database.test.ts). TheKEYMAP_CATALOGmirror test caught the missing catalog row forvim.leaderTableToDatabaseon 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, themand right-click menus and the palette open; the menu ends with Sort column,:table_to_databaseis "Not an editor command", the palette lists nothing for "convert table", and no.basefolder appears), and "u brings the table back" passes vacuously there, since the table never left. The7d8b9507build passes 53 of 53: themmenu,Space l dwith a colliding name landing as[[Roadmap 2]],:table_to_databaseon 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.csvreadsid,Item,Done,Estimatewith"Write ""spec"", v1"and"Ship, then rest"quoted, andschema.jsontypes Done as checkbox and Estimate as number with the id field hidden. - #832, how to test locally:
npm run build --workspace @zennotes/desktop, thennode docs/releases/v2.56.0/tools/check-table-to-database.mjs after(andbeforefor the packaged 2.55.0 indist/mac-arm64). By hand:npm run dev, open a note with a heading and a pipe table under it, click into a cell and pressm(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 aHeadingdatabase, andgd(or a click) on the link opens the grid with the columns typed. Pressuin the note: the table is back and the database is still in the sidebar. With Vim mode on,Space l din a cell does the same; with tables as plain text (Settings → Editor → Render tables in live preview off),:table_to_databasewith the caret in the table does too. This writes a.basefolder 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
createDatabaseis the shareddatabase-opscomposition, covered by the memory-vault test), remote workspaces from the desktop (same composition), the phone shells, and Linux and Windows. - #692, unit:
npm run typecheckclean, 7 of 7 tasks;npm run test:rungreen 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 invault-display-name.test.ts, 3 invault.test.ts, 1 inwindow-vaults.test.ts, 10 inrename-vault.test.ts, 1 incli/commands/vault.test.ts), plus the conflict sections test updated. znserver:go vet ./...andgo 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 readdocs, 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 adisplayNamewritten tovault.json, and answers:vault_renamewith nothing. The9d3d2b15build passes 25 of 25: the palette rename lands in the header, the title bar,vault.jsonand the remembered list;Space vlists 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 todocsandvault.jsonloses the key; the vault menu's Rename Vault… works;:vault_renameopens the prompt; adisplayNamewritten 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, thennode docs/releases/v2.56.0/tools/check-vault-display-name.mjs after(andbeforefor the packaged 2.55.0 indist/mac-arm64). By hand:npm run dev, press ⇧⌘P and typerename vault. Before: nothing matches. After: Rename Vault…; Enter, type a name, Enter. The sidebar header, the title bar andSpace vshow the name; the folder in Finder is unchanged, and.zennotes/vault.jsonholds"displayName". Click the vault name at the top of the sidebar: Rename Vault… is in the menu. Cmd+, and searchvault 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'svault.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 typecheckclean, 7 of 7 tasks; the app-core suite green, 241 files and 2781 tests (1 skipped). New: 12 tests (8 inlib/editor-disk-sync.test.ts, 4 instore-note-integrity.test.ts); the control test reproduces the old, changed outsideon 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 outsidein the editor and, 900 ms later, in the file; thea80a45a5build 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, thennode docs/releases/v2.56.0/tools/check-undo-after-disk-change.mjs after(andbeforefor the packaged 2.55.0 indist/mac-arm64). By hand:npm run dev, open an empty note, typeHello, wait a second for the save, then in a terminalprintf 'Hello, changed outside' > <vault>/inbox/<note>.md. The editor shows the new text; pressu(or ⌘Z with Vim mode off). Before:, changed outside, andcatshows the file changed to it. After: nothing changes, and the file still readsHello, 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
applyChangeandresyncpaths, 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 typecheckclean, 7 of 7 tasks; the app-core suite green, 238 files and 2741 tests (1 skipped). New: 7 tests inlib/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, thennpm run dev(or launch the build) and press ⇧⌘P, typeOpen 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 / ⌘6as three keycaps. Typetemplateinto 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 (theModformatting 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 fromContents/Resources/terminal/manifest.jsoninside the arm64 DMG). - AUR:
815f78fin the separatezennotes-binclone (zennotes-bin 2.56.0-1), mirrored on desktopmainandv2.56.0as55cd419c;aur-check.ymlpassed (runs 36066446400 onmainand 36066444033 onv2.56.0). The tarball downloaded from the release has SHA-256ef671bfa6802dfa6db0ce80e56ad2f60c74f9bb4df8e8bcc5a5351356cd9ae45, equal to GitHub's asset digest and to the pinned value. - Nix:
nix-update.ymlrun 36066459248 opened bot PR #856, lifted byte-for-byte ontomainandv2.56.0as2e2ef327and closed with a one-line comment pointing at that commit;nix-build.ymlrun 36066679419 passed onmainfirst try.desktopHash(sha256-72cb+mgC36bbDOgOVq0vYMdPm7TfjovMWlNRNWzZrkU=) is the same tarball digest as the AUR pin, in SRI form. The bot PR's ownnix-buildevent (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 commit37cb26d6, force-pushed tozennotes-desktop-2.46.0, body innixpkgs-pr-body.md), after checking that nixpkgs master still carries 2.45.0. Onlyversion,npmDepsHash(sha256-K7B4kx09VDsB3Pj61HRCrLbY1PDfS28icgvGMQnr5Nk=) and the sourcehash(sha256-V1KE1fFt/QO8Zi1joKYkQ2MNzYX3m4LJRADt3GeUilE=, both lifted frompackaging/nix/release-data.json) change; the refetch of upstream master was skipped again, as the earlier handoffs advise. - Homebrew:
dfae031in the tap (zennotes 2.56.0), mirrored on desktopmainandv2.56.0as7d2ce23a. The cask pins both DMG digests from the GitHub API (arm64ba7018be5357b0a7cd4839b8cf1a5707f2501b2912fe7cce8c73cad70dfe0b3d, x644e6d6db9e111f2149d370d2118078285ddbc51f2ec990a401134cdf58698ba23), and both equal a localshasumof 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
fe13bcdon the website'smain(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, afterreleases/latesthad flipped and every installer was verified: the #832 docs (a feature card, a databases-section row and theSpace l dleader row), the #692 docs (a feature card, a Vault name settings row and theSpace vdetail) 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 ontowebsite/docs-2.56frommainas three clean cherry-picks, plus the releases entry (three features, seven fixes, nine clips,ce5f537). The fullphp artisan testpassed before the push (915 tests). Laravel Cloud deployed it frommain; 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-writtendisplayNameand 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 artifactweb-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 (f6374ceand the pin62100d8, both check runs green) fast-forwarded into itsmain; Prepare server release run 36070524496 built five binaries and the draft (the darwin-arm64 binary matchesSHA256SUMS, themanifest.jsonasset 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 pushedadibhanna/zennotes:2.56.0,2.56andlatestfor 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/lateston GitHub answersv2.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
mainwas fast-forwarded onto the head without a merge commit. - Before the cut, on the bumped branch:
npx turbo run typecheck --force7 of 7 (21:21 UTC). The forcedtest:runfailed 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 typinglanding inVault CLI Cheatsheet.md); alone, the file was green every time. Adib approved the test-only2c3dd065(the test settles its own save withpersistNoteand 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/desktopnpm run build:prodgreen (21:31). - The local pack hit a machine fault, not a build fault:
codesign --timestampfailed 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 aselectron-builder --dir -c.mac.timestamp=http://17.32.213.161/ts01inapps/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 thatcodesign --verify --deep --strictaccepts. The CI release build is on a GitHub runner and was untouched by this. - On that packaged app: the skill's
packaged-launch-check.mjsfound a CDP page target in 1.5 s, reporting 2.56.0, both stores isolated, no instance left behind;test:sidebar-vimandtest:editor-improvementspassed;test:vim-editorlost editor focus after its third check on the first run (the four remaining checks reportedfocused: falsewith 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.ymlat 22:14:45; Windows: exe 22:35:49, blockmap 22:35:50,latest.yml22:36:04; macOS: arm64 DMG 22:42:35, x64 DMG 22:43:02,latest-mac.yml22: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 --downloaddownloaded 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 asNotarized Developer ID(Lumary Labs LLC) with a valid stapled ticket (spctl -a -vvaccepted,stapler validateworked), reads version 2.56.0 and bundleszn0.4.0. The DMG was attached read-only withhdiutil attach -readonlyand ejected afterwards. - Expected red marks, none of them about the release: CI run 36066446417 on
main(the AUR packaging commit55cd419c) was cancelled by the next push, the Nix packaging commit2e2ef327, which CI then ran on and passed (36066679626), as did the Homebrew mirror7d2ce23a(36069086163). The same pattern appeared on 2.55.0.
Local-first and keyboard-first, as always.