Repository navigation
Conversation
…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.
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ZenNotes 2.68.0.
New
57f64bc1, design indocs/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'szn 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).a8c9d8f1themes the folded-lines bar. MCP also stops writing into.zennotes(cb98803f).d7fc9cf7): awindows-11-armrelease job (npm run dist:win:arm64) whose feed is renamedlatest-arm64.yml, read through electron-updater's channel on ARM64 and on x64 under ARM64 emulation; CI's matrix addswindows-11-arm, which packages the ARM64 app and runstooling/scripts/packaged-launch-check.mjs --expect-arch arm64(no maintainer has ARM64 hardware, so this is the proof it starts).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
zennotes://registered on Windows at every launch, and Cloud sign-in returns through a loopback page on Windows and Linux (Allow the ability to click on links to documents via MCP output. #509,238d6504).b73fc068).2e92989e).aa24d748): 325 strings rewritten by hand; the demo tour's notes are renamed "NN Title", with older tours replaced on Regenerate and removed by Remove (exact match on the old separator,currentDemoTourPath).89798b6c, manifest verified againstzn-release-1).Local gates on
428b8462(the app;81f45ceeadds 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=highclean (the same 15 low or moderate as 2.67.0). Pack: Developer ID signed, deep strict codesign OK, notarization skipped by design, bundledzn v0.7.0inResources/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); with81f45ceeall three pass on the same build. CI adds the firstwindows-11-armrun, which packages the ARM64 app and launch-checks it.