Skip to content

ZenNotes 2.71.0 - #955

Merged
adibhanna merged 12 commits into
mainfrom
v2.71.0
Oct 18, 2026
Merged

adibhanna merged 12 commits into
mainfrom
v2.71.0

Conversation

@adibhanna

Copy link
Copy Markdown
Contributor

ZenNotes 2.71.0: desktop. The website release page and docs follow from the tag; the server, the zn CLI, the boundary packages and the phones are not part of this release.

New

  • Mermaid fences draw with Pele (23edf8ee): pele 1.6.0 pinned, reached only through pele-runtime.ts; colours from the --z-* tokens, one cache key per diagram, live in the editor, errors as text. mermaid.js stays only for Excalidraw's Mermaid dialog.
  • D2 diagrams (c4a38631): @terrastruct/d2 0.1.33 behind d2-runtime.ts, a serialized queue, DOMPurify with foreignObject labels kept, per-insertion ids, the theme mapped onto D2's classes; Preview, live preview, the manual, the MCP instructions and the tour note 05c.
  • Keep running in the tray (A System Tray Icon Keeps The Desktop Client Running In The Background #947, 6329c89a, e902b3a1, 204ae7b0): an opt-in setting, the tray icon and menu, Space b, Quit ZenNotes, a six-hour update recheck; turning it off brings back every held window; a cancelled Windows sign-out lapses after 10 s.
  • The demo tour covers today's app (776112f8): 22 notes, four new, a sample drawing and database; removal empties the database folder.

Fixes

Also

  • What's New: the 2.71.0 highlights (603fc6dc); version bump (43bd8228).

Gates on this head (603fc6dc), in a clean checkout with its own npm ci: typecheck 8/8 uncached; app-core 3,266, desktop 1,175, quicklook 15 and the other suites green; fresh build:prod green; npm run pack and the packaged launch check pass (page target, rendered, 1.4 s, 2.71.0); the packaged app draws the tour's Mermaid and D2 notes and shows the 2.71.0 What's New; perf:desktop-runtime on 5,000 notes is within budget with no diagram chunk loaded; vim-editor, sidebar-vim and editor-improvements pass. ci-sweep.py v2.70.0: no failed jobs. hazard-scan.py --since v2.70.0: no errors; the two manualChunks warnings are comment and deferred-preload changes, and the boot chunk imports no diagram stack. dash-lint clean on the notes, commits and diff.

ZenNotes drew ```mermaid fences with mermaid.js. It worked, but it was the
heaviest stack in the renderer (mermaid.core alone is 140 kB gzipped before
any diagram chunk, about 800 kB across all of its chunks), it laid diagrams
out asynchronously through temporary DOM, and its labels were HTML inside
foreignObject, the layer behind every clipped-label bug here (#529, and the
getBoundingClientRect patch #911 needed). Theming was a 160-line
themeVariables map, rebuilt and re-rendered on every theme change.

Pele (pele.run, obsidianmd/pele, MIT) reads the same Mermaid syntax with its
own parsers and lays diagrams out itself, returning one SVG string
synchronously. Before switching, every example in Mermaid 11.17's docs that
mermaid.js accepts (476, across all 32 doc pages) rendered in Pele, as did
every fence in the repo and the demo tour; only ZenUML, which ZenNotes never
registered, is unsupported. In Chromium those 476 diagrams take 83 ms
instead of 4.2 s.

What changes:
- lib/mermaid-render.ts loads Pele through a new pele-runtime module, so the
  async chunk has a name the perf harness and the packaged-web browser test
  can hold off the boot path, and keeps the shared (fonts, source) cache.
  Once the runtime and a diagram type are loaded, peekMermaidSvg draws a new
  diagram on the spot, so typing one in live preview no longer shows a
  pending block. Fonts are the only theme input: labels are measured on a
  canvas in the note's text font, after any loading font settles.
- The cache trims the source first. Preview keeps a fence's trailing newline
  and the editor trims its body, so split view used to draw every diagram
  twice under two keys, the double work the shared cache exists to remove.
- Colour moved to CSS. A .pele rule in index.css maps --pele-* onto the
  --z-* tokens with the roles the themeVariables had (nodes on bg-2 with a
  bg-3 border, subgraphs on bg-softer, edges grey-1, series in the old pie
  and git order). One drawing is right in every theme, custom CSS themes
  included, and a theme switch repaints without a relayout.
- The #911 getBoundingClientRect patch and its test are gone: labels are SVG
  text and wrap inside their box.
- Images and links in diagrams stay off, as mermaid's strict mode had them,
  so drawing a diagram never fetches anything.
- Preview shows a render error as text in .zen-diagram-error. The old branch
  wrote the message into innerHTML, and the message quotes the diagram
  source, so a crafted fence could inject markup.
- pele 1.6.0 replaces mermaid in the five workspaces that declared it,
  pinned exactly while Pele moves fast. mermaid stays installed for
  Excalidraw's Mermaid-to-Excalidraw dialog, which still lazy-loads it; the
  chunk comments, preload deferral lists and perf patterns name both.

What it deliberately does not do: no maxWidth is passed, so Pele never flips
a wide left-to-right flowchart to top-down; the author's direction stands
and the existing pan/zoom handles width. Pele's own zoom is not used.

What users will notice: layouts are Pele's (node placement differs from
mermaid.live, mindmaps draw as an outline tree), $$ math inside a label and
raw HTML other than <br> and bold or italic show as written, and %%{init}%%
theme settings give way to the app theme. The in-app manual, the website
docs and the MCP instructions say so.
On the board grouped by Custom status, a task note with `status: done`
vanished, even with `done` listed in kanban_statuses. fieldColumns skipped
every task isBoardCard rejects before it read the field, and a task note's
checked state comes from that same `status:`: the value that names its
column was the value that took it off the board. Grouped by Status, the
note sat in Done, so switching boards made it disappear and come back.

A task note whose value is one of the configured columns now stays in that
column, checked. Only configured values count: a discovered column would
turn every finished task note in the vault into a column of its own. A
configured `cancelled` column holds cancelled task notes the same way.

Checkbox tasks keep the live-work rule on purpose. Their box and their
`@status:` are separate, and a status they inherit from the note's
frontmatter would pull every checked subtask of an in-progress note into
In progress.

The manual's Kanban card says so. Four tests, two of which fail without
the change. Reported by @mateusmatiazzi.
Closing the last window ended the session on Linux and Windows, and on
macOS it destroyed the window, which is where Cloud sync runs. Either way
a vault stopped syncing until ZenNotes was opened again, and on a tiling
desktop, where minimise has nowhere to go, closing was the only way to get
the window off screen.

Settings > Appearance > Window > Keep running in the tray (off by default)
now hides the last workspace window instead. A tray icon (a template ensō
in the macOS menu bar, the app tile elsewhere) brings it back on click; its
menu opens Quick capture, shows a linked vault's sync state, and quits for
real. Space b and Hide Window to Tray hide the window on purpose; Quit
ZenNotes in the palette quits on desktops with no app menu or no tray host,
and opening ZenNotes again, or the macOS Dock icon, brings held windows
back, including when the relaunch carries a path the opener ignores.

Quitting always wins over the hide: before-quit, the tray's Quit, app:quit,
Squirrel.Mac's before-quit-for-update and Windows' query-session-end all
mark the app as quitting, or an update could never install and a logoff
would hang. The setting lives in zennotes.config.json, not config.toml:
main needs it before any window exists, and whether a tray exists depends
on the machine.

The update check, which ran once at launch, now repeats every six hours
from a settled state, so a copy kept in the tray for days still hears
about a release; notices stay one per version.

Not in this change: an icon that changes with the sync state, an Install
update item in the tray, and an update restart that stays in the tray.

Thanks to @uNyanda for the proposal.
With two windows open, hiding one with Space b and then turning Keep
running in the tray off from the other left the first one hidden:
applyKeepRunningInTray(false) brought held windows back only when no
window at all was visible. The icon was gone by then, so nothing could
reach the hidden window, and closing the other one left ZenNotes running
with no window and no icon until it was launched again.

Turning the setting off now shows every window the tray holds, whatever
else is open. Found on Linux by the beelink tray check.
A ```d2 fence now draws as a D2 diagram (d2lang.com), the text-to-diagram
language built for architecture: nested containers, labelled connections,
sql_table data models, sequence diagrams and sketch mode. Preview gives it
zoom and Expand, live preview draws it in the editor until the cursor
enters it, and a broken diagram shows D2's message with its line and
column, as text.

D2 runs as WebAssembly from @terrastruct/d2 0.1.33 (MPL-2.0, pinned exactly
in the five workspaces that declare pele). The browser build is one 8 MB
module, so d2-runtime.ts is its only import, reached through import() in
d2-render.ts: the chunk is named d2-runtime, both Vite configs keep it out
of the boot preloads, and the perf harnesses and the packaged-web browser
test watch it. A note without a d2 fence never loads it.

Three things in d2-render are not optional. The wrapper keeps one pending
callback per worker, so a second call made before the first settles
strands the first; every call goes through a queue. D2 passes a Markdown
label's HTML straight into the SVG (an <img onerror> in a label arrived
intact), draws icon: as an <image> that fetches its URL and wraps link: in
an <a>; every SVG goes through a DOMPurify instance of its own with
images, links, href/src and handlers removed, and with foreignObject as
an HTML integration point, without which Chromium empties every label
(jsdom keeps them, so only the built app showed it). And Preview and the
hidden editor showed the same cached string, so every arrowhead's
url(#mk-...) resolved into the hidden copy; withUniqueD2Ids renames the
ids per insertion.

Colour comes from the theme: a .zen-d2-themed block maps D2's fill-,
stroke-, color- and background-color- classes (N1 to N7, B1 to B6, AA,
AB) onto the --z-* tokens in the roles Pele uses and makes D2's canvas
transparent, so a theme switch repaints without a redraw. A colour set
with style is an attribute and stays as written; a diagram that sets
theme-id or theme-overrides keeps D2's theme. The link and tooltip badge
is hidden, since neither works here.

Live preview: cm-mermaid-render now draws mermaid and d2 fences through one
widget, so the reveal-on-cursor, last-good-drawing and j/k entry rules are
shared. The built-in tour gains 05c D2 Diagrams, linked from 00, 05, 05b
and 07; the manual and the MCP instructions describe D2.

Not in this change: the self-hosted server's copy of the tour
(znserver internal/vault/demo-tour.json) and zn mcp's copy of the
instructions in ZenNotes/tui, which had drifted before this.
…953)

Preview stamped a source line only on top-level blocks, and a list is one
top-level block. In a note that is mostly one long list, double-clicking
any item opened the editor on the list's first line, and leaving Preview
with the cursor off screen (the Edit button, Mod+4) did the same, often a
screen or more above what the reader was looking at.

Every list item, nested ones included, now carries its own line. The
double-click reader already took the closest stamp, which is now the item.
The visible-range reader walks stamps in document order and lets the
innermost one still covering the top edge win, so a long list reads as the
item at the top of the view instead of as its start.

Split scroll sync is untouched on purpose: it selects only the content
root's direct children, so it still pairs block for block with the editor.

Three tests; the reader's fails without the change. Reported by @Sullti.
The hanging indent and the guide lines are measured in ch, and the source
spaces that indent a nested item render in the editor font. With a
monospace font the two agree. With a proportional one a space is about half
a ch, so a nested bullet sat left of its guide and of its parent's wrapped
text, and each level drifted further.

Each character of a list line's prefix (the indent, then a bullet or
ordered marker and its spaces) is now an inline-block box at least as wide
as the columns the hanging indent counts for it, a tab included, and the
bullet drawn over a `-` takes one ch with it. A minimum width, so with a
monospace font nothing moves: the editor is pixel-identical before and
after.

One box per character, not one for the run: the list fold arrow is a
widget inside the indent, and a mark over the whole run split around it
into pieces that each took the full width. The bullet widget is not
wrapped by a mark at all, hence the line class that sizes it.

Left alone: a list inside a quote or callout, where `>` renders its own
way, and a task's marker. Live preview draws a checkbox narrower than the
six columns a task's hanging indent counts, a gap that predates this and
shows in every font.

Five tests. Reported by @Sullti.
Windows asks every window before it signs out, restarts or shuts down,
and query-session-end set appQuitting so a close we would turn into a
hide could not hold the session up. Nothing ever cleared it: when the
user cancelled the sign-out, or another app refused it, closing the
window quit ZenNotes instead of hiding it for the rest of that run.

The question now holds close-to-tray off for a 10 s grace and then
lapses, since Electron sends no event for a cancelled session end. Only
session-end, which comes when the session really ends, makes it final.
appQuitting is left to the app's own quits (before-quit, the tray's
Quit, app:quit, an update's restart), which stay permanent.

Checked on Windows 11 with synthetic session messages. After a query
with no end, a close 12 s later hides. A close inside the grace, or
after session-end, still closes and the app exits. A real restart with
the window hidden went straight through.
On Linux and Windows, closing the last window quits ZenNotes. It did not
once Quick capture had been opened in that run. The panel hides instead
of closing, so the hotkey's next press is instant, and Electron counts a
hidden window as open: `window-all-closed` never fired, and every process
kept running with nothing on screen, no taskbar entry and no tray icon.
Only launching ZenNotes again brought a window back. It dates from Quick
capture itself in 1.2.0, and it made turning the tray off (#947) look
broken.

A hidden panel no longer keeps the app alive. After any window closes,
and whenever the panel hides, ZenNotes quits if that panel is all that is
left.

Unchanged: macOS, which keeps an app running with no window; the tray
on, where the window hides and the app and its hotkey keep running; and
every other window, hidden or not, which still keeps the app open the way
Electron counts it, so a floating note or a standalone file can outlive
the last workspace window. A panel pinned on screen holds the app until
it hides, so nothing visible closes under the user. A
`zennotes://quick-capture` launch, which opens the panel alone as the
hotkey's stand-in on Wayland, stays resident between captures until a
window it opened is closed.

Eight tests for the decision. On Linux (X11 under Xvfb and openbox) with
tarballs built before and after from one tree: with Quick capture opened
and dismissed, closing the main window left six processes running after
45 s before, and exited within 6 s after. The tray-on, pinned-panel and
deep-link launches behave as described.
The tour was written in the spring and had fallen behind the app. It said
nothing about the Tasks calendar or boards, templates, drawings, databases,
workflows, Cloud, the CLI or MCP, and some of its lines had stopped being
true: hint mode on `f` (it is `Space h`), a dropped file landing at the
vault root (it goes to `assets/`), archived notes' tasks still showing in
Tasks, a broken diagram falling back to its source, "Open Trash" as a
palette command.

It now has 22 notes. Four are new: 16 Tasks Calendar and Kanban, 17
Templates, Drawings, and Databases, 18 Workflows, and 19 Cloud, CLI, and AI
Agents. 00 to 15 were checked against the manual and the code and picked up
what shipped since: task states, fields and subtask counts, editable
tables, block links and embeds, image sizes, numbered equations, Mermaid
with Pele, the leader keys added since spring, themes and overrides, and
config.toml. Every palette name the tour quotes is the real title.

17 ships two files to try things on, inside the tour folder: a Tour Sketch
drawing and a Reading List database. The database's data.csv and
schema.json come from initialDatabaseContents, the code New Database runs,
so opening it rewrites nothing. A database is a folder, and removing the
tour has to take that folder down before inbox/demo can go:
demoTourNestedDirs lists the tour's own folders deepest first, and the
desktop's removeDemoTour empties them.

The tour test now resolves a link to a shipped file by its name and a link
to a database by its title. The server writes the same tour from its own
copy, synced in ZenNotes/znserver. The phone apps write it from this file
but remove it with their own code, which needs the same nested-folder step
when they adopt this core.
What's New opens on the first launch after the update with four
highlights. First, Mermaid diagrams drawn by Pele: faster, in the theme's
colours, live in the editor, and repainted on a theme switch. Second, D2
diagrams from d2 fences, pointing at the tour note that has examples.
Third, Keep running in the tray, with where the setting lives and what
keeps working while the window is hidden. Fourth, the refreshed demo tour,
with how to regenerate it.
@adibhanna
adibhanna merged commit 603fc6d into main Oct 18, 2026
10 checks passed
@adibhanna
adibhanna deleted the v2.71.0 branch October 18, 2026 22:59
@adibhanna
adibhanna restored the v2.71.0 branch October 18, 2026 22:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant