Skip to content

Release 2.49.0 - #771

Merged
adibhanna merged 6 commits into
mainfrom
v2.49.0
Sep 13, 2026
Merged

adibhanna merged 6 commits into
mainfrom
v2.49.0

Conversation

@adibhanna

Copy link
Copy Markdown
Contributor

ZenNotes 2.49.0: four community reports from the days after 2.48.0.

Gates at the cut: apps/desktop npm run pack (typecheck, 750 tests, build, packaged CLI isolation, signed app), Go vet and tests, turbo typecheck 7/7, shared-domain 1,593 and app-core 2,024 tests, the packaged app reaching a CDP page target from an isolated profile in 3.3 s, and the website suite (748) with the release page entry.

Release notes: docs/releases/v2.49.0/RELEASE_NOTES.md.

https://claude.ai/code/session_01FQEKNPYe43aX3SzH5AmeU8

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 1400 px never started loading, and
printToPDF captured an empty frame with the caption underneath. The
images near the top survived only because they happened to sit 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 reporting ready. The wait is capped at
8 s so one dead remote URL cannot hang the export; the main process
keeps its own 15 s ceiling. The preview's on-screen lazy loading is
unchanged.

Verified over CDP against the built app with an isolated profile: a
note with two images at the top and two after sixty paragraphs now
exports all four; before, the bottom two printed as captions only.

Claude-Session: https://claude.ai/code/session_01FQEKNPYe43aX3SzH5AmeU8
…e marker (#770)

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. Delimiters pair off left
to right, so for a symmetric marker an odd number of closers ahead means
one of them already belongs to this opener, and for `[[` the first `]]`
ahead has to come before the next `[[`. In both cases the snippet stays
out of the way and Space inserts a space. An opener whose only closers
ahead belong to a later, complete pair still expands. Backspace is
unchanged: it only ever removed an empty pair, and there is no longer
one to remove.

Verified over CDP in the built app, Vim insert mode, the reporter's
keystrokes: `0ll` to `**|word**`, `i`, Space gives `** word**` with the
cursor after the space, Backspace gives `**word**` back.

Claude-Session: https://claude.ai/code/session_01FQEKNPYe43aX3SzH5AmeU8
…ow which links are missing (#768)

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, which is the common case when you are
working through a note and stubbing out its neighbours one link at a
time.

Two changes, both surfaces.

A modifier click creates the note at once. Cmd (macOS) or Ctrl held
while clicking an unresolved wikilink, in the live preview, the Cmd/Ctrl
click path in edit mode, or the reading view, creates the note 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.

Unresolved links look different. The reading view already drew them
muted with a dashed underline; the editor now does the same, with the
same definition of "resolved" on both: a note, a spot 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 live the moment its note
is created, and a link at an asset no longer reads as a note waiting to
be made. Resolution scans the notes list, so answers are memoized until
any input changes, and a partial store degrades to the live look
instead of crashing the plugin.

The keymap registry, the shared catalog, the in-app manual and the
Vim command table all carry `gD`; the website docs follow separately.

Verified over CDP in the built app: the two dead links render muted
and dashed while the live one keeps the accent, a Cmd-click creates
the note with no prompt and the link reads live on return, `gD` on the
third link creates its note the same way, and the reading view's dead
link creates on Cmd-click too.

Claude-Session: https://claude.ai/code/session_01FQEKNPYe43aX3SzH5AmeU8
…gned PUT)

Sync failed with "ZenNotes Cloud object upload failed (411)" on any
vault holding an attachment above the 5 MB inline limit (Sasori and
Unyanda on Discord, 2026-09-13). Such a file streams 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 fetch sent the body chunked. S3-style storage refuses a
chunked PUT that carries no Content-Length before reading a byte: 411
Length Required. Before 2.48.0 the same rejection surfaced as
"Controller is already closed", because the storage answered before
reading the body and the old stream adapter crashed on that; 55cafa0
fixed the adapter, which is why the real status appeared "on the
latest".

The direct upload now sends the mutation's byte count as Content-Length
and its media type as Content-Type, unless the service already set
them. undici then frames the stream with a length, and a file that
changes size mid-upload fails the request instead of storing a
truncated or padded object. Only the host is in the signature, so the
extra headers cannot break it.

The tests were green because both upload fixtures handed the client a
Content-Length the real service never sends. They now mirror
production: the presign returns only Host, and the real-HTTP fixture
answers 411 to any PUT that is chunked or has no length. With the
client change stashed, all six network tests fail with the reporters'
exact message; with it, the desktop suite passes. A wire probe under
Electron 41's bundled Node 24 (undici 7.28) confirmed the chunked and
framed shapes. A round trip of a file over 5 MB against the real Cloud
is still to be run on a linked account.

Claude-Session: https://claude.ai/code/session_01FQEKNPYe43aX3SzH5AmeU8
Notes and post for the four fixes, plus the captioned demo (one clip
for all three issues and one per issue) and the two verification
stills, recorded from the built app.

Claude-Session: https://claude.ai/code/session_01FQEKNPYe43aX3SzH5AmeU8
@adibhanna
adibhanna merged commit 5a94b32 into main Sep 13, 2026
@adibhanna
adibhanna deleted the v2.49.0 branch September 13, 2026 23:32
}
const cached = resolverCache.memo.get(target)
if (cached != null) return cached
let resolves = true
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