Skip to content

ZenNotes 2.68.0 - #930

Merged
adibhanna merged 14 commits into
mainfrom
v2.68.0
Oct 13, 2026
Merged

adibhanna merged 14 commits into
mainfrom
v2.68.0

Conversation

@adibhanna

Copy link
Copy Markdown
Contributor

ZenNotes 2.68.0.

New

  • Review an assistant's edits (57f64bc1, design in docs/ideas/agent-edit-review.md): MCP writes keep a record of the note from before the edit in .zennotes/agent-edits/ (both engines: the TypeScript MCP server and ZenNotes/tui's zn mcp, bundled here as zn 0.7.0), and the note offers an inline review on @codemirror/merge (new dependency, loaded on demand) with Accept/Reject per change, keyboard and Vim diff keys (]c, [c, dp, do). a8c9d8f1 themes the folded-lines bar. MCP also stops writing into .zennotes (cb98803f).
  • Windows on ARM ([Feature Request] Add Windows ARM64 (win-arm64) builds #928, d7fc9cf7): a windows-11-arm release job (npm run dist:win:arm64) whose feed is renamed latest-arm64.yml, read through electron-updater's channel on ARM64 and on x64 under ARM64 emulation; CI's matrix adds windows-11-arm, which packages the ARM64 app and runs tooling/scripts/packaged-launch-check.mjs --expect-arch arm64 (no maintainer has ARM64 hardware, so this is the proof it starts).
  • What's New after every update (12ec2d38), with the permanent ZenNotes Cloud card, and an anonymous install/update count (d21f2edf, POST /api/v1/app/usage, live on zennotes.org via ZenNotes/website#71; switch in Settings → About and [privacy] usage_count).

Fixes

Local gates on 428b8462 (the app; 81f45cee adds only the smoke-suite fix): typecheck 8/8 and test:run 6/6 with no turbo cache (shared-domain 1,991, app-core 3,187, quicklook 15, desktop 1,102), build:prod green, npm audit --omit=dev --audit-level=high clean (the same 15 low or moderate as 2.67.0). Pack: Developer ID signed, deep strict codesign OK, notarization skipped by design, bundled zn v0.7.0 in Resources/zn-cli (drwxr-xr-x) and the Quick Look extension; packaged launch check: page target and drawn app in 1.6 s, version 2.68.0. Smoke suites: sidebar-vim passed; vim-editor and editor-improvements failed five typing checks because 2.68.0's What's New opened over the editor for their returning-user profiles (the suites write a config.toml); with 81f45cee all three pass on the same build. CI adds the first windows-11-arm run, which packages the ARM64 app and launch-checks it.

…n-in finishes there

No Windows install could open a zennotes:// link (#509). electron-builder
writes the `protocols` scheme into the macOS bundle only, so neither the
NSIS installer nor the zip registers it, and registerAppDeepLinkProtocol
returned early on everything but macOS, as it has since v1.3.4. Links in
MCP output and browsers did nothing (Windows: "Get an app to open this
'zennotes' link"; Chromium: "the scheme does not have a registered
handler"), and a packaged app's Cloud connect, which hands the sign-in
back through zennotes://auth, could never finish on Windows.

The app now registers zennotes:// for the current user at every launch
on Windows as well as macOS (HKCU\Software\Classes\zennotes, command
"<exe>" "%1"). That also covers the zip wherever it was unpacked, and
the entry follows the app when it moves. Linux keeps the package's
.desktop file: setting the default there would override the user's own
xdg-mime choice.

Cloud sign-in in packaged Windows and Linux builds now returns through
the one-time 127.0.0.1 page (RFC 8252's loopback redirect) that
development builds already used, so it needs no link handler and no
browser prompt; an AppImage without desktop integration hit the same
dead end. Packaged macOS keeps the zennotes:// callback its signed bundle
always registers. The NSIS uninstaller still leaves the per-user key
behind.

Checked on a clean Windows 11 VM (build 26200). The official 2.67.0
installer and app left no handler and reproduced the report. With this
change the installed app registered itself on first launch; links opened
the note from Edge and through ShellExecute, with the app running and
with it closed; the zip registered its own path; and Cloud connect
completed against zennotes.org through 127.0.0.1.
… into code

Tab indented a heading line like any other line (#925). Markdown lets a
heading start at most three spaces in, and at four it is an indented code
block, so one press at the default tab size of 4 (two at 2) turned a
heading into code without a word: it lost its styling, its place in the
outline and its fold arrow, in Edit and in Preview. Vim's >>, > and
insert-mode Ctrl-T and the phones' indent button did the same.

All of those indent through @codemirror/commands' indentMore, whose
transactions carry the input.indent user event, so headingIndentGuard is
one transaction filter that drops the insertions landing on heading lines
and keeps the rest: a selection's other lines still indent, a `#` line in
fenced code or frontmatter still indents, and a list item that opens with
a heading still nests, since indenting it moves the item's marker. Lines
are read from the syntax tree: an ATX heading's `#`, or any line of a
setext heading, at the margin or inside a list item. When a press moved
nothing, an info toast says why, once while it shows. Outdenting is
untouched.

The guard ships inside noteMarkdown(), so every note editor and the
phones have it; the template editor builds its own markdown() and adds it
by hand. ZenNotes' outline, folding, level chips and heading motions
still read only headings at the margin, though CommonMark accepts up to
three spaces; that gap is unchanged. The in-app manual's indentation
section says headings stay at the margin.

Checked in the built app over CDP at tab sizes 2 and 4 and with Vim's
>>: an ATX heading, both lines of a setext heading and a heading inside a
list item stayed put on disk and in Preview, the notice showed, and a
paragraph still indented. Before the fix the same script turned all of
them into code blocks or paragraphs.
An MCP edit wrote its file and only then checked that the file was a
note. write_note, append_to_note, prepend_to_note, insert_at_line and
replace_in_note on a path under .zennotes created or changed that file,
then reported "Note not in a known folder" as though nothing had
happened. An assistant could plant or alter a note's comments, its
creation date, or anything else the app keeps there.

noteForWrite resolves the path and requires a note folder before a
byte is written, the order readNote already used. Notes, new or
existing, are written as before.
When an assistant changed a note through MCP, ZenNotes showed the new
text and nothing else: seeing what changed meant leaving the app for a
diff, and there was no taking back one change while keeping the rest
(Kevin, on Discord). Now the MCP server keeps what the note said
before, beside it in .zennotes/agent-edits/<note>.json, and the app
offers the review in the note itself.

Recording. Before write_note, append_to_note, prepend_to_note,
insert_at_line, replace_in_note or toggle_task changes a note of a
local vault, the server writes the note's text to the record, makes
the edit, then stores the note as it now reads, naming the assistant
from its MCP client name. The record reaches disk before the edit, the
workflow journal's rule. Two engines write records, this one and the Go
zn a managed install runs, so the writing side is string comparisons
only: a change someone else made between two edits is noted as
outside, never merged. Edits to one note take turns. Settings > MCP >
Review agent edits ([mcp] review_agent_edits in config.toml) turns
recording off; both engines read it without the app.

Reading. @shared/agent-edits folds the outside changes into the
baseline with the three-way merge Cloud conflicts use, then merges the
note as it is now against the assistant's last text, so the review
shows only the assistant's changes. Where the user rewrote a changed
line, the user's version wins.

Review. A banner names the assistant and when it edited; Review
changes opens CodeMirror's merge view in the note (@codemirror/merge,
loaded on demand; the renderer chunk rule keeps it out of
vendor-editor, so launch carries none of it). Each change shows the
removed lines above the new ones with Accept and Reject, the bands
running the full width of the editor; the note is read-only and Live
Preview off while reviewing; Accept all, Reject all and Done sit on a
bar above. Reject is an ordinary edit, saved before the record moves
on; each decision saves progress, and the last one deletes the record.
Keys: Mod+Alt+Down and Up, Mod+Shift+Enter, Mod+Shift+Backspace, and in
Vim mode ]c [c dp do, all rebindable. dp and do cannot be codemirror-vim
mappings, which run the d operator as soon as it fully matches, so
review mode reads normal-mode sequences ahead of Vim; Escape after a
lone d cancels it rather than leaving the review. The palette lists
every note waiting.

Records follow their notes through relocateNote, folder renames,
deletes, both trashes, creation and workflow runs (journalled for undo
as a third sidecar kind, which older ledger readers skip). Cloud sync
carries them, the watcher reports them as an agent-edits change scope,
and the bridge gains supportsAgentEditReview with three optional
methods, desktop only for now. Not covered: vaults on a self-hosted
server, notes an assistant creates, and moves and deletions. Design:
docs/ideas/agent-edit-review.md.

Checked in the built app over CDP with a real MCP client named
claude-code driving the MCP server process: the banner, both changes
inline, Reject by button restoring and saving the old line, Accept by
keyboard, and the record and its folder deleted; then the same in Vim
mode with do, ]c and dp.
…stripe

Reviewing an assistant's edits folds a long unchanged stretch of the
note into an "N unchanged lines" bar. @codemirror/merge paints that bar
light or dark from CodeMirror's dark-theme flag, which ZenNotes never
sets (its themes are CSS variables), so every dark theme drew a
near-white stripe across the note. The bar now takes the theme's own
raised background and muted text, and runs the full width of the
editor like the change bands around it.

Found while recording the release demo: reopening a review with one
change left at the end of the note folded the ten lines above it.
Every release shipped Windows as x64 only: win.target lists nsis and
zip with no architecture, and the release's one Windows job ran on an
x64 runner, so ARM PCs (Snapdragon X and the like) ran ZenNotes through
Windows' emulation. Releases now carry ZenNotes-<version>-win-arm64.exe
and .zip, built natively on GitHub's windows-11-arm runner by a new
"Windows arm64" job running dist:win:arm64 (a named script, because
PowerShell's npm.ps1 shim can swallow the `--` before --arm64).

Updates. electron-builder names every Windows feed latest.yml whatever
the architecture, and both jobs upload to one release, so the ARM64 job
publishes its feed as latest-arm64.yml, and either job fails if its
output carries the other architecture's file names.
windowsUpdateChannel() points electron-updater at that feed on ARM64
Windows: for the native build, and for an x64 copy running under
emulation (app.runningUnderARM64Translation), which moves to the native
build with its next update, as electron-updater already does for an
Intel copy under Rosetta on a Mac. Setting a channel also turns
allowDowngrade on; it goes back off. The app ships nothing native and
Windows bundles no zn, so the package itself needed no other change.

Proof. Nobody here has an ARM64 Windows machine, so CI's build matrix
gains windows-11-arm, which also packages the ARM64 app and runs the
new tooling/scripts/packaged-launch-check.mjs on it: a CDP page target,
the app drawn, and AArch64 in the executable's PE header
(--expect-arch). A cross-build from macOS gave an all-AArch64 package
(its NSIS step needs Wine there; a Windows runner does not). Not
tested: an x64 install upgraded by the ARM64 installer on real
hardware, which is electron-builder's ordinary NSIS upgrade path.

The README lists both installers; zennotes.org's download section
(the website repo) gets the ARM64 row.
)

Dragging a column border in the editor saves the widths under the table
(<!-- zen:cols=... -->), and Preview did read them, but its table
carried max-width: 100%: whenever the widths added up to more than the
reading column, every column shrank to fit and the cells wrapped again
(the reporter's "hypervisor" broke over two lines). Reading width
stopped below Editor width, so a table that fit the editor could
overflow Preview. The editor did the same once a table outgrew it: its
wrapper's max-width: 100%, from 2.4.0, predates the widths and kept
#294's "scrolls if wide" from ever happening, so a drag past the edge
squeezed the other columns instead of growing the table.

Now a table with set widths is exactly as wide as its columns in both
places, and scrolls sideways when that is wider than the column. In the
editor the wrapper grows with the table (the add-column rail stays at
its edge) and the widget scrolls. In Preview the table goes into a
.zen-table-scroll wrapper, which takes over its data-source-line,
because split scroll sync reads that stamp from direct children only.
A page cannot scroll, so print media keeps the fit to the page for PDF.
Reading width now goes to 1600 like Editor width (the store already
clamped at 1600), and a new help topic, mirrored on the website, says
where the widths live.

Checked in the built app over CDP: a 300/900/300 table draws
300/900/300 in the editor, Preview and split view, each scrolling, and
177/501/177 within the page under print media; tables that fit and
tables without widths are unchanged; dragging a column 500 px to the
right grows the table and writes the new width to the note.
ZenNotes had no way to tell people what an update brought, or that
ZenNotes Cloud exists, beyond release notes few read. Now the first
launch after an update opens a short window: the version's highlights,
a ZenNotes Cloud card (Set up ZenNotes Cloud opens Settings at Cloud,
Plans and pricing goes to the site), and Full release notes on the
releases page of zennotes.org. Escape, Enter on Close or the backdrop
dismisses it; What's New in ZenNotes in the palette and :whatsnew bring
it back.

The highlights are bundled (lib/whats-new.ts), one entry per release,
written in the release PR: the window works offline, and its copy is
reviewed with the code it describes. An update across several versions
shows each entry in between; a version without one still opens the
window, with the Cloud card and the link to its notes. The Cloud card
is part of every update's window, left out for someone already on Cloud
(asked before the window opens, so it never flashes and vanishes).

Once per version: the machine-local lastSeenWhatsNewVersion is recorded
before the window opens, so a second window or a crash cannot show it
twice. A fresh install records its version and opens nothing, the
onboarding being its welcome; whether a launch is a returning user's is
read with the first prefs load, so skipping the onboarding within the
window's 1.2 s delay does not count. A downgrade records and opens
nothing. Desktop only: the phones announce through their stores, and a
self-hosted web app updates with its server.

whatsNewOpen joins VimNav's skip list and the command palette's overlay
flags. Without that, j moved the sidebar under the open window, and the
palette could hand focus back to the editor behind it.

Checked in the built app over CDP: the palette path (7/7), the first
launch after an update on a build made as 2.68.0 (8/8: a true fresh
install opens nothing, a returning user gets the window once, j scrolls
it in Vim mode, Enter closes it), and a build made as 2.68.1, which has
no entry (the Cloud card and the pointer to the notes).
…in Settings

Nothing told us how many copies of ZenNotes are in use: GitHub's
download counts mix in package builders and bots. Now the desktop app
tells zennotes.org once when it is installed and once per update, at
the same first launch of a version that opens What's New:

  POST https://zennotes.org/api/v1/app/usage
  {"event":"update","version":"2.68.0","platform":"macos","arch":"arm64","install":"dmg"}

That is the whole message. event is install or update; platform is
macos, windows or linux; arch is x64 or arm64; install is the format
(dmg, mas, nsis, portable, store, appimage, deb, rpm, pacman, managed or
unknown), read through the updater's own detection so the two never
disagree. No identifier, no cookies (credentials omit), a bare user
agent, and an Accept-Language of *: a capture of Electron's net.fetch
on the wire showed Chromium adding the system language otherwise. The
site keeps daily totals only (the website change is separate).

It is on by default. Settings > About > Count this copy anonymously
turns it off, and the portable pref travels as [privacy] usage_count in
config.toml. The main process refuses development builds, ZEN_PERF and
CI runs, other architectures and prerelease versions, and sends each
event at most once per run whatever the renderer asks; a send that
fails is dropped, not retried. The phones and the web app send nothing.

Tests: usage-count.test.ts (the body and every refusal, once per event,
the request's headers) and the store's launch tests (one count per
install and per update; none with the switch off, on a relaunch, after
a downgrade, or on the phones).
The long dash (U+2014) was everywhere a user reads: Settings, the palette,
the in-app manual, toasts, onboarding, the welcome note, the comments
ZenNotes writes into config.toml, the zn CLI's help, the MCP server's
instructions and tool descriptions (which assistants show), and all
through the demo tour. It reads as machine-written. Each of the 325 was
rewritten by hand in its sentence: a colon before a definition or a
list, parentheses for an aside, a comma, or a new sentence. Label pairs
take the middle dot the app already uses ("Theme preview · ↵ to keep").
A cell or title that was only the dash now says what it means: None,
Untitled, Checking… or Loading…, and the slash menu's Divider icon is a
drawn line.

Names change in two places. The demo tour's notes are "00 Start Here"
and so on instead of "00 <long dash> Start Here", with every wikilink,
alias and the tour graph updated (a new test resolves each link against
the notes the tour ships). A vault seeded by an older version, or by a
self-hosted server that still writes the old names, keeps working:
Regenerate replaces the old files instead of writing a second tour,
Remove deletes them, and the palette still finds the tour. That match is
exact on the old separator because both paths delete files. And the
Meeting Notes and 1:1 templates title new notes "Meeting 2026-10-12"
rather than with the dash; existing notes keep their names.

Not touched: code comments, test names, and text inside third-party
libraries (Excalidraw's tooltips and translations, Mermaid's errors,
KaTeX). The self-hosted server's own demo tour copy changes with its
next release.
The bundled CLI is the floor a desktop build guarantees, and 0.7.0 is
the newest release. Its `zn mcp`, which a desktop-managed install runs
for assistants, records an assistant's edits for the review this release
adds (until now a managed install recorded nothing), refuses writes into
`.zennotes`, and words its MCP instructions and templates the way this
release does. The pin is the release's terminal-release.json copied byte
for byte after it verified against packaging/release-signing/zn-release-1.pub
in ZenNotes/tui (source commit 9650f8d, integration protocol 1, all four
archives matching checksums.txt). Staged through `npm run terminal:stage`;
the native darwin-arm64 binary reports zn v0.7.0. The in-app manual's
CLI card says 0.7.0 and what it changes.
2.68.0 opens What's New about a second after launch for a returning
user, and the smoke profiles count as one because they write a
config.toml. The window takes focus and the keys: against the 2.68.0
build, five checks that type into the editor failed (I at a display-row
start, `->` becoming an arrow, the cursor across mode switches, a second
note opening, Ctrl+Tab), while nothing in the editor had changed.

The suites now wait up to four seconds for the window once the app is
ready and close it with its own button; with that, vim-editor,
editor-improvements and sidebar-vim pass every check on the same build.
The window stays as designed: someone upgrading should see it.
CodeQL flagged the packaged launch check (#928) for calling a handler
looked up by an id read off the DevTools socket
(js/unvalidated-dynamic-method-call, high). The map only ever held
functions the script stored itself, so nothing could dispatch anywhere
else, but the check now takes a reply only for a numeric id it handed
out and calls the handler only when it is a function. Rerun against the
packaged 2.68.0 app: page target, app drawn, version 2.68.0.
@adibhanna
adibhanna merged commit 881baa6 into main Oct 13, 2026
10 checks passed
@adibhanna
adibhanna deleted the v2.68.0 branch October 13, 2026 00:28
@adibhanna
adibhanna restored the v2.68.0 branch October 13, 2026 00:28
@adibhanna
adibhanna deployed to boundary-artifacts October 13, 2026 00:33 — with GitHub Actions Active
@adibhanna
adibhanna deployed to boundary-artifacts October 13, 2026 00:35 — with GitHub Actions Active
@adibhanna
adibhanna deployed to boundary-artifacts October 13, 2026 00:38 — with GitHub Actions Active

This branch was successfully deployed

1 active deployment
boundary-artifacts — 881baa6a Deployed Oct 13, 2026 by adibhanna via draft #48
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.

2 participants