github ZenNotes/zennotes v2.49.0
ZenNotes 2.49.0: Cloud sync uploads files over 5 MB again, every image in a note makes it into the PDF, Space at the front of existing bold no longer doubles the marker, and a missing note can be created straight from its wikilink

4 hours ago

Four community reports from the days after 2.48.0. Cloud sync no longer fails with "object upload failed (411)" on vaults with attachments above 5 MB (Sasori and Unyanda on Discord). A PDF export now waits for every image in the note, including the ones far below the first screen, instead of printing empty frames (#769). Pressing Space with the cursor just inside an existing **bold** span inserts a space instead of a second marker pair, and Backspace no longer takes the original marker with it (#770). And wikilinks at notes that do not exist yet are drawn apart from live ones, in the editor as well as the reading view, while a Cmd/Ctrl-click, or gD in normal mode, creates the note at once without the confirmation (#768).

✨ New

  • Create a missing note straight from its wikilink (#768, requested by SomeoneInTheRoom). Following a wikilink at a note that does not exist yet asked, every time, whether to create it and where. The question is right when the note belongs somewhere specific, and pure friction when it belongs exactly where the link says. Hold Cmd (macOS) or Ctrl (Windows/Linux) while clicking an unresolved wikilink, in the live preview, in edit mode, or in the reading view, and the note is created immediately at the path the prompt would have suggested: Inbox, named after the link text, with a path-style link keeping its subfolder and an explicit top folder honoured. A plain click still asks. The keyboard twin is gD in normal mode: gd without the question. A note that already exists at the suggested path is opened, never overwritten. The reading view used to do nothing at all on a dead link; it now offers to create the note like the editor does. "Inbox" here means the vault's primary notes location: a vault whose notes live at the root creates the note at the root, where New note puts it.

    How to test locally: in a note, type [[Brand new idea]] and move the cursor off the link. Cmd/Ctrl-click it. Before: the "Create note for" prompt opens. After: the note opens at once and appears under Inbox. Type another dead link, put the cursor on it in normal mode and press gD: same result. A plain click on a dead link still opens the prompt.

  • Unresolved wikilinks look different in the editor (#768). The reading view already drew a link at a missing note muted with a dashed underline; the live preview drew every wikilink in the accent colour, so there was no telling which links were live. The editor now uses the same look, with the same definition of "resolved" on both surfaces: a note, a heading or block in this note, a .base database, or a file in the vault. The editor recomputes when the vault moves under it, so a link flips to the live look the moment its note is created, and a wikilink at an image or PDF no longer reads as a note waiting to be made.

    How to test locally: open a note with [[Existing note]] and [[Nothing here]] on one line, in Edit mode with live preview on. Before: both links look the same. After: the second is muted with a dashed underline; create it (Cmd/Ctrl-click) and come back, and it is drawn like the first.

🐛 Fixes

  • Cloud sync uploads files over 5 MB again (Discord, reported by Sasori and Unyanda on Linux; not Linux-specific). A file above the 5 MB inline limit is streamed straight to object storage through a presigned PUT. The Cloud service signs only the host of that PUT and hands back no length, and a streamed file has no length of its own, so the request went out chunked, and S3-style storage refuses a chunked PUT without a Content-Length: 411 Length Required, surfaced as "ZenNotes Cloud object upload failed (411)". Before 2.48.0 the same rejection showed as "Controller is already closed", because the storage answered before reading the body and the old stream adapter crashed on that; 2.48.0 fixed the adapter, which is why the real status appeared "on the latest". The desktop now sends the file's byte count as Content-Length, and its media type as Content-Type, unless the service already set them. A file that changes size mid-upload fails the request instead of storing a truncated object. The upload tests used to hand the client a Content-Length the real service never sends; their fixtures now mirror production and refuse a chunked PUT with 411, so the framing cannot regress silently.

    How to test locally: link a vault to ZenNotes Cloud and add an attachment larger than 5 MB, then press Sync now in Settings > Cloud. Before: "ZenNotes Cloud object upload failed (411)". After: the sync completes and the file appears on the other device.

  • Every image in the note makes it into the PDF (#769, reported by Unyanda). The reading preview lazy-loads local images, which is right on screen: a long note only fetches what scrolls into view. The PDF export renders through that same preview inside a hidden window that never scrolls, so any image below the window's first viewport never started loading, and the PDF showed an empty frame with the caption underneath. Images near the top survived only because they sat inside that viewport; on Linux, where a hidden window may never observe intersections at all, even those could go missing. Both export windows (desktop and web) now flip every deferred image to eager, which starts its load at once, and wait for each load to end, success or failure alike, before printing. The wait is capped at 8 s so one dead remote URL cannot hang the export. On-screen lazy loading is unchanged.

    How to test locally: open a note with an embedded image after a couple of screens of text and press Ctrl+Shift+E (Cmd+Shift+E on macOS). Before: the PDF shows an empty frame with the file name under it. After: the image is in the PDF, at the same size as the ones near the top.

  • Space at the front of existing bold no longer doubles the marker (#770, reported by Unyanda). The Space snippet turns a freshly typed ** into an empty **|** pair. It decided whether the ** before the cursor was a fresh opener by looking only at the text before it, so with the cursor just inside an existing span, **|word**, the opener looked unmatched and Space expanded it into **|**word**. The Backspace that followed then found an empty pair around the cursor and removed it as one, four characters for the two that had been inserted, taking the original opener with it. The rule now also reads the rest of the line: markers pair off left to right, so an odd number of closers ahead means one already belongs to this opener, and the snippet stays out of the way. The same applies to __, ~~, ==, %%, backticks and [[ ]]. An opener whose only closers ahead belong to a later, complete pair still expands.

    How to test locally: type **word**, put the cursor right after the opening ** (in Vim: 0ll then i), and press Space. Before: ****word**, and Backspace leaves word**. After: ** word**, and Backspace gives **word** back.

🧰 For contributors

  • Sources: 62eaf82b (PDF export images), 3a58b386 (bold marker snippet), 094c9401 (wikilink creation and unresolved styling), and the Cloud upload Content-Length fix in cloud-sync-client.ts. New tests: export-images.test.ts, the #770 block in cm-markdown-snippets.test.ts, create-note-from-link.test.ts, the #768 blocks in follow-link.test.ts and cm-wikilink-render.test.ts, the "keeps a Content-Length the service already set" case in cloud-sync-client.test.ts, and the 411-refusing fixture plus framing assertions in cloud-sync-upload-network.test.ts.
  • Verified for the upload fix: a local wire probe under Electron 41's bundled Node 24 (undici 7.28) showed the stream body going out chunked and the same body with a Content-Length header going out framed; the real-HTTP network test drives the production coordinator and client through the 411-refusing fixture and fails without the client change. The reporters are asked to confirm on their own vaults, since the round trip against the production bucket needs a linked account with a file over 5 MB.
  • Keymaps: vim.createNoteFromLink (default g D) joins the registry, the shared catalog and the manual; rebind it under Settings > Keymaps or in config.toml.
  • Verified: all three were driven in the built desktop app on macOS over CDP against an isolated profile. The export check stubs the native save dialog through the main-process inspector (--inspect) and probes every <img> right before printToPDF; the recipe lives in the session scratchpad and is worth lifting into tooling/scripts if it is needed again.
  • Build with npm run build --workspace @zennotes/desktop. Create a scratch root with mktemp -d /tmp/zennotes-249.XXXXXX, then launch ZEN_PERF=1 ZENNOTES_USER_DATA_PATH=<scratch>/userdata ZENNOTES_CONFIG_DIR=<scratch>/config apps/desktop/node_modules/.bin/electron apps/desktop/out/main/index.js --remote-debugging-port=9326. Use a scratch vault only.
  • Gates at the cut: apps/desktop build:prod through npm run pack (typecheck, tests, build, electron-builder --dir), Go vet and go test -count=1, turbo typecheck 7/7, shared-domain 1,593 tests, app-core 2,024, desktop 750, the packaged app launched in an isolated profile and reaching a CDP page target, and the website suite with the release page entry in place.

Local-first and keyboard-first, as always.

Don't miss a new zennotes release

NewReleases is sending notifications on new releases.