ZenNotes 2.21.0: tasks that say how far along they are
A task can be in progress, a parent shows how far its subtasks are, every task surface has a right-click menu, Mermaid draws while you write, and the
znCLI works again on an installed app. Installers for macOS, Windows, and Linux (x64 + arm64) are attached below.
✨ New
-
Headings are easier to scan, fold, and navigate. Settings can now show H1 through H6 labels before headings. Every heading keeps its fold arrow, and sections can be folded by clicking it, with
Ctrl+Alt+F/Ctrl+Alt+U(Cmd+Option+F/Cmd+Option+Uon macOS), or withzc/zoin Vim mode.Ctrl+Tabswitches to the most recently used note and alternates between the same two notes when pressed again. The editor also remembers the cursor when moving through Edit, Split, and Preview.A new Text replacements settings tab expands typed snippets, starting with
->to→, and accepts custom symbols, words, and phrases. Tab width is configurable from 1 through 8 spaces. KaTeX display blocks now number unstarred\\begin{equation}…\\end{equation}environments in document order, consistently in the editor and Preview. (Requested in discussion #516.)How to test locally: enable heading level labels, fold a heading by click and shortcut, add a text replacement, change the tab size, press
Ctrl+Tabbetween two notes, switch Preview and back after moving the cursor, and render two equation environments. The real Electron regression uses isolated user-data and config directories. -
Mermaid diagrams draw while you write, not only in preview. A ```mermaid block in the editor now shows the diagram itself. Put the cursor anywhere in the block and the code comes back so you can edit it; move away and it redraws. Live preview has to be on, since this is part of that bargain rather than a mode of its own.
A diagram you are halfway through typing keeps its last good picture rather than flashing an error at every keystroke, and the error only appears once you leave the block alone and it still does not make sense. Arrow keys and Vim's
j/kstep into a drawn diagram the same way they step into rendered math, so you never need the mouse to get at the source.Mermaid only for now. The other diagram languages either run a whole typesetting engine or are interactive, and they need their own answer rather than a shortcut. (Asked for by @junereycasuga, seconded by @naingyeminn. #530)
How to test locally: open a note with a
mermaidfence and pressMod+4for editor mode. The diagram is drawn in place; press down-arrow until the cursor reaches it and the code returns. -
A task can say it is in progress.
- [/]joins open, done, forwarded and cancelled as a task state, following the Obsidian Tasks convention. Type it, run "Mark Task In Progress" from the palette with the cursor on a task, or pression a task in the Tasks list, on the Kanban board, or on the calendar. It draws as a half-filled box in the editor and the reading view. A whole-note task file usesstatus: in-progress, anddoing,startedandwipread the same way.The point of it is what separates it from forwarded and cancelled: in progress is still open work. It keeps its place in Today instead of collecting under a group of its own, it stays on the calendar and the board, and it rolls forward with your unfinished tasks when a daily note rolls over,
/and all. A half-done task is the one you most want in front of you tomorrow.It round-trips as plain markdown everywhere: the editor, the
znCLI, the MCP server and the self-hosted Go server all read it, so a state one surface understands is never dropped by another. (Asked for by @NunonuN in #512. The second half of that request, what a parent with half-done children should say, ships right below.)How to test locally: write
- [ ] rewrite the pricing page, put the cursor on it, run "Mark Task In Progress", then move off the line. Open Tasks and it is in Today with the rest of your live work; pressito set it back. -
A parent task shows how far its subtasks are. A task with checkboxes nested under it now carries a chip at the end of its line,
2/5, neutral while work remains and green once every child is done. It draws in the editor and in the reading view, which also covers exported HTML and PDF and notes shared by link, and hovering it reads "2 of 5 subtasks done", which is also what a screen reader gets.The number is derived every time the note renders and is never written into the file. That is the design decision the rest of #512 was waiting on, and @NunonuN chose it deliberately: ZenNotes is not the only writer of a vault. Obsidian, vim, the phone and the sync server can all check a child off, and none of them would fire a rollup that rewrites the parent line. A written marker goes stale the moment someone else edits; a derived count is right every time the file renders, and un-checking a child needs no un-write.
The counting follows the Markdown structure rather than raw indentation, and one implementation feeds both renderers, so the editor and the preview cannot disagree about the number. Direct children only: a grandchild rolls into its own parent, which shows its own chip. Cancelled and forwarded children leave the count entirely, they are no longer that list's work, and an in-progress child counts toward the total but not toward done. The Tasks list, the board and the calendars deliberately stay flat; the chip lives where the hierarchy does, in the note. (The second half of #512.)
How to test locally: write
- [ ] parentwith children- [x] done,- [/] startedand- [-] cancelled, and the parent reads 1/2 in the editor and the preview: the cancelled child does not count, the started one is not done. Check the started child off in any editor, ZenNotes included, and the chip follows. -
Right-click a task, anywhere you can see one. The Tasks list, the Kanban board, the Tasks calendar and the calendar side panel now share one menu: open the note, mark done, mark in progress, cancel,
@waiting, forward to another note, due today / tomorrow / next week / clear, priority, and delete. Each entry shows the key that does the same thing, so the menu doubles as a way to learn the shortcuts, and typing filters it, so "prio" narrows to the priority entries.Before this the calendar had a menu and the list and the board had none, which meant the same task offered a page of actions on one tab and nothing on the next.
iandcnow work on the board and the calendar too, not just in the list.How to test locally: right-click a task in the Tasks list, then switch to Kanban and Calendar and right-click there. Same menu, same actions.
-
Select several steps on the Workflows canvas and move them together. Hold Shift (or ⌘ on macOS, Ctrl elsewhere) and click to add a step to the selection, or hold Shift and drag a box across the canvas.
Mod+Atakes the whole workflow. Dragging any one of them moves the group, and Esc clears it.The options panel still edits one step at a time, so it steps aside while a group is selected rather than picking one of them to show. Deleting a group is deliberately left out for now: removing several steps at once has to renumber the rest of the file as it goes, and that deserves its own change rather than a free ride on this one. (Asked for by @ShowhyT in #532, built as #534)
How to test locally: open a workflow with a few steps, click one, then Shift-click another. Both are outlined. Press
Mod+A, drag any step, and the whole group moves; reopen the file and every new position is on the layout line.
🐛 Fixes
-
Vim motions follow the line you can see. In a soft-wrapped paragraph,
$andAjumped to the end of the whole paragraph, whileIjumped all the way back to its beginning. They now stop at the end or start of the current display row, the same place the cursor appears to be on screen. A count still means logical lines, so3jfollows the relative line-number gutter instead of counting three wrapped rows.The rest of the reported navigation gaps close with it.
:13jumps straight to line 13 again without taking ZenNotes':movenote command away, and pressingHorLagain at the top or bottom of the viewport keeps moving and scrolling instead of becoming a dead key. Square-bracket finds such asf[andF[already worked; they now live in the same real-app regression check so that support cannot disappear unnoticed. The behavior is shared by the main editor, Quick Capture and floating note windows. (Reported by @nianiam in #536, alongside the broader Vim motion discussion in #513.)How to test locally: enable Vim mode and word wrap, open a paragraph long enough to wrap, then use
$,AandIfrom a middle display row. Turn on relative line numbers and confirm3jlands three gutter lines away, type:13, and pressHorLtwice at a viewport edge.npm run test:vim-editordrives all of these in an isolated real Electron app. -
Hint mode survives home-row modifier taps. If your keyboard remapper turns held home-row keys into modifiers, Kanata-style f/j as Shift and d/k as Control, hint labels containing those letters could do nothing at all: a label letter held a beat too long reaches the OS as a bare modifier instead of the letter. Hint mode amplified that misfire twice over. Its handler treated any key that is not a label letter as a request to leave, so the bare Shift keydown silently closed the overlay; and when the misfired Shift was still down as the letter arrived, the uppercase J failed the lowercase-only test and closed it too. From the keyboard, the label just looked dead.
Bare modifier presses are now swallowed while the labels wait, and letters match case-insensitively. A letter the remapper never sent cannot be conjured back, so the worst case becomes pressing it again with the labels still up, instead of hint mode dying underneath you. Every other key still exits, and Escape and Backspace keep their jobs. (Reported by @RonnyOtieno20 in #526, whose Kanata config answered what five reproduction attempts could not.)
How to test locally: turn on Vim mode, press
Space h, tap Shift or Control mid-label and watch the labels stay; then type a label with Shift held down and the target still activates. -
Non-standard task states are drawn in the reading view, not spelled out. A cancelled
- [-]task or a forwarded- [>]one showed up as literal[-]and[>]characters in the preview, in HTML and PDF export, and in a note shared by public link, while the editor drew a proper marker for the same line. The manual has said since 2.19 that a cancelled task renders with a muted ✕ and struck-through text; now it does, everywhere the renderer runs.How to test locally: put
- [-] scrappedand- [>] movedin a note and pressMod+2. Before this they read as brackets. -
Clicking a checkbox in the reading view checks the task you clicked. In a note whose first task was cancelled or forwarded, clicking a checkbox in the preview wrote to a different line: one row off for each stateful task above it, silently. The reading view counted only the checkboxes it drew, while the file counts every task line.
How to test locally: make a note starting with
- [-] scrappedfollowed by- [ ] real task, pressMod+2, tick the real task, and check that it, not the cancelled line, is the one that changed. -
Editing a forwarded task on the calendar keeps its metadata. Pressing
eon a- [>]task in the Tasks calendar or the side panel prefilled the edit box with the task text stripped of itsdue:,!priority,#tagsand[[Target]]backlink, and saving wrote that stripped version back over the real line. Cancelled tasks were spared only because they are kept off the calendar entirely.How to test locally: give a
- [>]task adue:token so it shows on the calendar, presseon it, press Enter, and check the line still has its tokens. -
A cancelled or forwarded task no longer looks exactly like an open one in the Tasks list and on the board. Both drew every state as the same empty checkbox, so the only thing telling them apart was which group they were filed under. Each state now draws its own mark: a check, a half-filled box, a ✕, an arrow.
-
The Tasks list stops advertising a key that cannot work. The cursor row showed "Space check", but Space is the Vim leader by default, so pressing it opens leader hints and nothing gets checked. The row now names the key that actually toggles, and names none at all with Vim mode off, where single-key list shortcuts do not exist.
The hint line at the bottom of the Tasks view had the same disease: with Vim mode off it still advertised
j/k move,x toggle,i start,c canceland:q close, none of which fire in the list there. Each view's line now names only keys that work in the current mode. -
A card flicked onto Today lands due today, not tomorrow. Flicking a Kanban card into Today could set its due date to tomorrow and drop it in Upcoming, a column you never aimed at. The drop applied to the column under the last pointermove the renderer processed, never to where the pointer was released; pointermoves are frame-aligned and coalesce while the main thread is busy, while pointerup arrives immediately. The status board orders its columns Today, Upcoming, Waiting, Done, so a drag from Waiting or Done into Today crosses Upcoming last, and a fast flick releases over Today while the trailing state still says Upcoming, whose semantics for a task without a due date are "set due tomorrow".
The drop now belongs to the column under the release point, resolved with the same hit test the hover path uses. The last hovered column remains the fallback when the release lands where no column resolves, the gap between columns or the padding above them, so a drop the board cannot place still honors the indicator you last saw. The keyboard moves (
Shift+H/L) never had the problem and are untouched. (Reported from the EU, where every timezone theory died quickly: the due writers have formatted dates from local components since the Kanban shipped, and an EU offset could only misdate in the other direction anyway.)How to test locally: open Tasks, press
3for the Kanban, and flick a@waitingcard with no due date into Today, releasing as fast as you can. Before, an unlucky release wrote tomorrow's date and dropped the card in Upcoming; now it lands in Today, due today. -
The
zncommand works again. If you installed ZenNotes and ranzn, it did not start. It printedCannot find module 'smol-toml'and quit before reading a single argument. This was every platform and every install method, and it had been true since 2.20.0, so three releases went out with a CLI that could not run. 2.19.0 was the last one where it worked.The CLI ships next to the app rather than inside it, in a folder that has no library directory of its own. Nearly everything it needs is compiled into the one file, but one library was left to be looked up at startup instead, and in that folder there is nothing to look it up in. It is now compiled in with the rest.
The reason it shipped three times is more interesting than the bug. Running the CLI from a development checkout works perfectly, because the project's own libraries sit a few folders above and quietly answer the lookup. The failure only exists in the installed app, which is the one version that never got run. So there is now a check that copies the built CLI somewhere with nothing around it and runs it, the same way you would. It takes about a second and the build fails if the CLI cannot start. (Reported by @oliviertripet with the full stack trace, and confirmed by @naingyeminn on a different distro and a different installer, which is what showed it was not Arch-specific. #524)
How to test locally:
npm run pack, then rundist/mac-arm64/ZenNotes.app/Contents/Resources/zen --version. On 2.20.2 it exits with a module error, now it prints the version.node tooling/scripts/verify-packaged-cli.mjsruns the same check against a plain build. -
A folder path the app cannot use now says so. Settings ▸ Vault ▸ Folders lets you point each system folder at a directory. Type something it cannot accept, such as a nested path like
docs/notes/quick, and the box used to go blank: nothing saved, nothing said. Written intovault.jsonby hand, the line stayed in the file while the app quietly carried on with the default, which looks exactly like a feature that does not work.The rule itself has not changed: a system folder is one folder name at the top level of your vault, because everything in the app decides which folder a note belongs to by reading the first part of its path. What changed is that a refusal explains itself, naming the actual problem: a nested path, a leading dot, characters a folder cannot hold, another system folder's name, or a directory already in use. The same sentence goes to the log when a hand-edited
vault.jsonline is ignored, so it can be found rather than guessed at.Nested paths remain unsupported. That is a real feature rather than a loosened check, since it changes what "the folder a note is in" means everywhere, and it needs its own change. (Reported by @hotondo, following #115. #533)
How to test locally: Settings ▸ Vault ▸ Folders, type
docs/notes/quickinto any folder-path box and click away. Before this it went blank with no explanation. -
Tags in any script can be typed on the Workflows canvas. Typing
#кириллица-работаетinto a step's tag field left--behind: every Cyrillic letter was deleted as it was typed, and only the hyphens survived. Greek, Japanese, and plaincaféwent the same way.The field checks each keystroke so a half-finished tag never writes something the workflow cannot read, which is right. It just had the wrong idea of what a tag character is: its rule covered the English alphabet and nothing else. Everything downstream was already fine, which is what made this confusing to hit, since the same tag works everywhere else in the app, including in the workflow file you typed by hand.
The rule for what a tag may contain now lives in one place instead of being written out twice, so the canvas cannot disagree with the file format again. (Reported by @ShowhyT, with a sample workflow that put Cyrillic in three different positions and made it quick to tell which parts were fine. #532)
How to test locally: open a workflow on the canvas, click a step with a tag, and type a Cyrillic tag into it. Before this the letters vanished as you typed; now the field and the saved file both keep them.
-
The CLI install screen stops claiming a directory is missing from your PATH when it isn't. Settings would warn that
~/.local/binwas not on your PATH, and offer you a line to paste into your shell config, while your terminal had that directory all along. The line would have added something that was already there.When you open an app from the Dock or Finder on macOS, it does not get the PATH your terminal has. It gets a bare one from the system, and your shell config is never read. So the app was asking itself a question only your shell can answer. Open the very same app from a terminal and it behaves, which is exactly why this went unnoticed for so long.
ZenNotes now asks your login shell what your PATH is, the same way it already finds tools installed by Homebrew or cargo that a Dock launch cannot see. If your shell cannot be reached, it falls back to what it knew before, so nothing gets worse in the odd setup. The warning still appears when the directory really is missing, which is the whole point of having it. (Reported by @kevindiaz314, with the
echo $PATHthat made it obvious the directory was fine and the app was wrong. #528)How to test locally: put
~/.local/binon your PATH from your shell config, launch a packaged build from Finder rather than a terminal, and open Settings ▸ CLI. Before this it warned; now it installs quietly. -
Links to notes inside a database work on a self-hosted server. A
[[wikilink]]pointing at a note that lives inside a database resolved fine on a local vault and came back "No note resolves this wikilink yet" against your own server. Same vault, same link, different answer depending on where the files were being read from. Clicking through would have created a second note beside the one already sitting there.The server was walking past database folders when it listed your notes. Half of that was right and stays: a database's spreadsheet data and its schema are not notes, and the sidebar should show you a database rather than a folder full of parts. The other half was not. The pages inside a database are notes, you open them and write in them and link to them, and a note the server does not list is a note nothing can link to.
Tasks had the same hole: a
- [ ]written inside a database page appeared in Tasks on a local vault and nowhere on a remote one. Fixed with it, since it is the same question about what counts as a note.The sidebar was the third face of it: a database on a remote vault had no expand arrow and showed none of its notes, while the table view of the same database listed them fine. The grid reads the spreadsheet file directly; the tree reads the note list, and the note list was the thing missing them. (Reported by @naingyeminn twice from different angles, with local and remote screenshots side by side both times, which is what made it a five minute diagnosis. #527 and #531)
How to test locally: point the server at a vault holding
inbox/Test.base/Reference 01.md, link[[Reference 01]]from another note, connect the app to that server and open Connections. Before this the link showed a CREATE badge; now it resolves with a HOVER badge and the full path, matching what a local vault has always done. -
A self-hosted server on Windows refuses to read a directory as a file with the same answer as everywhere else. Ask the server to read a database folder as if it were a note and it answers 400, "that is a directory, not a file". On Windows it answered 500, because the classification was inferred from the errno the read returned, and Windows reports a different one than Unix for that mistake. The server now decides from the stat it already performs before reading, so the answer no longer depends on which platform's error code comes back. Same request, same answer, every platform.
How to test locally:
cd apps/server && go test ./.... The newTestReadNoteRejectsADirectoryOnEveryPlatformfails before the change on any platform. -
Finishing the Workflows tutorial really does clean up after itself. The tutorial promises, in its last chapter and again if you leave early, that the practice folder, the practice workflow and its run history all go away and your vault ends exactly as it started. On some vaults the folder stayed put, with all four practice notes in it. Nothing was lost and you could delete it by hand, but a promise made that plainly should hold.
It held on a vault laid out the default way, which is why it survived. The tutorial referred to its practice folder in two different ways: a fixed path when writing the notes, and "the Inbox, plus this subfolder" when creating and deleting it. Those mean the same directory right up until your Inbox is not where the fixed path assumed. Two supported layouts do exactly that: a vault that keeps its notes at the top level rather than in an Inbox folder, and (since 2.20) a vault whose system folders have been pointed at directories you named yourself. On those, the tutorial made its folder in one place and wrote the notes into another, then cleaned up the first one.
Now everything the tutorial creates is placed by asking the vault where the folder actually is, so cleanup reaches all of it. On a top-level vault it also stops leaving an empty "Workflow tutorial" folder behind, which was the same mistake seen from the other side. (Reported by @oliviertripet, who also checked that leaving the tutorial early behaved the same way, which ruled out a whole class of guesses. #525)
How to test locally: on a vault whose notes live at the top level (
"primaryNotesLocation": "root"in.zennotes/vault.json), run Start Workflows Tutorial from the command palette, then either press Finish on the last chapter or leave early with the panel's X. The practice folder and its notes are gone either way. Before this,inbox/Workflow tutorialstayed behind. -
Mermaid diagram labels stay inside their shapes. A flowchart node reading "User types a sentence" could render as "User types a sent", a state box reading "Draft" as "Draf". The text was not wrong, it was measured wrong, and the shape got sized for a narrower string than the one it ended up painting.
Mermaid works out how wide a box has to be by laying the label out in a throwaway element it attaches to the page body, then discarding it. ZenNotes told mermaid to use
inheritas the label font, and the two places that word resolves are not the same place: the app chrome uses the interface font, which is a sans, while note content uses the text font, which is a mono by default. So the measuring happened in a sans, the painting happened in a mono, and mono is the wider of the two. Every label came out slightly too big for the box that had just been sized for it.Now ZenNotes passes the actual resolved font stack instead of
inherit, so the font that measures the label is the font that paints it, including when you have chosen your own text font under Settings → Typography. It also waits for fonts to finish loading before rendering, which closes the same gap from the other side: a diagram rendered mid-load was measured against a fallback face and then repainted in the real one. (Fixed by @junereycasuga in #529, with before and after screenshots that made the failure obvious on sight.)How to test locally: open a note with a mermaid fence whose nodes have multi-word labels, for example
flowchart LR A([User types a sentence]) --> B{Vim mode enabled?} B -- yes --> C[CodeMirror vim keymap handles it]
and press
Cmd+6for preview. Every label should sit fully inside its shape. On 2.20.2 the same diagram cuts them off mid-word. Then set a text font under Settings → Typography, a serif such as Georgia makes the difference easy to see, and confirm the labels still fit. -
The dock icon is the ZenNotes icon, on an installed Mac app. The app looked for its icon inside itself, in a folder that only exists in the source tree, so on every launch it failed to find it and said so in the log. Cosmetic, and it had been that way since 1.1.0, but it was one of two places reading the icon from the wrong path. The other one answers "give me the app icon" with nothing at all on an installed build, which no screen asks for yet and would have been a puzzling first bug for whichever one asks first.
Found by launching the packaged app and reading its log rather than the checkout's, which is the same lesson the CLI fix above is about, one rung further up.
How to test locally:
npm run pack, launchdist/mac-arm64/ZenNotes.appand watch stderr. Before this there is aFailed to set dock iconline on every launch, now the log is clean.
🧰 For contributors
tooling/scripts/verify-packaged-cli.mjsruns the built CLI from a directory with nonode_modulesabove it, which is the only place a missing external is visible. The desktop build gates on it. If you add a dependency that main and the CLI share, it goes inPACKAGED_CLI_RUNTIME_PACKAGES, and this check tells you so by name when you forget.vaultRelativeFolderPath(folder, subpath, settings)invault-layout.tsis the forward composer for a (folder, subpath) pair, sitting next to thenotePathWithinFolderit inverts. Reach for it instead of joininginbox/…: root-mode vaults and remapped system folders both make that join point at a directory the app never lists.database-ops.tscarries a private version of the same rule.- Anything in main that reads a file from
build/needsapp.isPackagedhandling:filesshipsout/**only, sobuild/never enters the asar and those assets travel as extraResources.windowIconPath()is the pattern; two icon call sites had open-coded the dev-only path instead. - The mermaid label font fallback is now a single named constant in
Preview.tsxrather than the same long stack written twice. It has to stay byte-identical to the--z-text-fontfallback inindex.css, because a drift between the stack that measures and the stack that paints is exactly what clips the labels. - Shortcut keymap defaults are written in the portable
Modspelling ("Mod+Alt+F"), never"Ctrl+...". The load-time normalizer canonicalizes the platform-primary modifier toMod, and that rewrite is platform-dependent, so aCtrlspelling reads back differently on Linux than on the Mac it was written on, and the shared-domain catalog can only mirror one string. A test now round-trips every shortcut default under darwin, linux and win32, so the nextCtrlspelling fails on the machine it is written on instead of on Linux CI. Sequence bindings (Ctrl+Wand friends) keep their literalCtrlon purpose. task-rollup.tsis the single source for the subtask chip: the task-line rule, the counting, and the "N of M subtasks done" label all live there and feed both the CodeMirror widget and the remark plugin. A chip rule that lands in only one renderer is exactly the editor/preview disagreement this layout exists to prevent.
Keyboard-first and local-first, as always.