Skip to content

ZenNotes 2.31.0: large files sync for real - #640

Merged
adibhanna merged 6 commits into
mainfrom
v2.31.0
Aug 19, 2026
Merged

ZenNotes 2.31.0: large files sync for real#640
adibhanna merged 6 commits into
mainfrom
v2.31.0

Conversation

@adibhanna

@adibhanna adibhanna commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

The 2.31.0 release branch. The headline is direct uploads for ZenNotes Cloud: large files stream from disk to storage instead of traveling inline through the sync request. Around it, a four-issue community sweep, three of them reported by @uNyanda.

Large files sync for real

Sync used to inline every file into the mutation JSON, so a big PDF or video buffered whole in memory and pushed against server limits. Files over 5 MB now travel through an upload session: the client asks the cloud for a signed destination, streams the bytes straight from disk, and completes the session, which folds the result back into the ordinary mutation response, conflicts included. Batch order is preserved around direct uploads, failed uploads abort their session server-side, completion retries transient errors, and revision/path/quota conflicts surface as normal sync conflicts rather than transport failures. Local scans hash oversized files in a stream, and the signed URL must be https.

The issue sweep

  • #636 the leader hints card stays inside short windows: the bottom-anchored which-key card had no height cap, so tiled WMs (Niri) and high zoom clipped the header, the only part naming the menu, off the top. It now caps at the viewport, pins the header, and scrolls the shortcut list, wheel included.
  • #634 the Raycast installer finds nvm-managed node: zsh reads ~/.zshrc only when interactive, which is exactly where nvm initializes, so the non-interactive login-shell lookup never saw it. The shared resolver now reads the interactive shell's PATH and scans it directly, with a ~/.nvm/versions/node probe for lazy-loaded setups; ripgrep/fzf detection and the CLI installer's PATH check inherit the same fidelity.
  • #633 Ctrl+O and Ctrl+I walk through the Tasks and Tags tabs: the jumplist shut the panels out twice over (never recorded as destinations, and opening one recorded no origin), so a panel mid-path could kill the trail entirely. Both panels now ride the jumplist like any note; help/trash/asset tabs stay excluded on purpose, and tab-strip clicks remain non-jumps.
  • #635 the buffer switcher leads with the title: rows used to sacrifice the note title to the full file path. They now show the full title with the note's folder dimmed beside it, and a folder that runs out of room truncates from the left so the distinguishing tail stays readable.

Every fix was verified in the built app over CDP against the reporter's repro, alongside the unit suites (new store tests cover the #633 round-trips; the direct-upload work lands with its own client, filesystem, and API tests) and repo typecheck.

Full release notes land on the GitHub release when CI finishes the installers.

https://claude.ai/code/session_011wnqvUakYs9Me5kyRgsUw2

The which-key card is bottom-anchored with no height cap, so on a
short window (tiled WMs like Niri, high zoom factors) it grew past
the top edge of the viewport, and the header naming the menu was
exactly what clipped first. Below the sm breakpoint the grid also
collapses to one column, doubling the card's height, which is why
narrow tiles hit this hardest.

Cap the card at the viewport height, pin the header, and let the
item grid scroll under it. The card takes pointer events now so the
wheel can reach the list (the wrapper stays click-through), and
overscroll containment keeps the page behind from scrolling along.

Claude-Session: https://claude.ai/code/session_011wnqvUakYs9Me5kyRgsUw2
The installer already asked a login shell for node and npm, but zsh,
the macOS default, reads ~/.zshrc only when interactive, and that is
exactly where nvm initializes. A plain `-lc` shell never saw an
nvm-managed node, so the settings page claimed node was missing on
machines where the terminal finds it instantly.

The shared resolver now asks the interactive login shell for its PATH
first and scans those directories itself instead of parsing
`command -v`, which in an interactive shell is free to answer with an
alias definition or a lazy-loader function instead of a path. The
answer travels on a marked line so rc-file banners cannot corrupt it,
and a sanity gate rejects fish's space-joined PATH expansion. When the
shells come up empty for the node toolchain, ~/.nvm/versions/node is
probed directly (default alias first, newest install otherwise), which
also covers setups that lazy-load nvm and have no node on any shell's
PATH at all.

The Raycast module's private login-shell duplicate is deleted in favor
of the shared helper, so ripgrep/fzf detection and the CLI installer's
PATH check inherit the same fidelity.

Claude-Session: https://claude.ai/code/session_011wnqvUakYs9Me5kyRgsUw2
The jumplist admitted only notes and database surfaces, and the panel
views were shut out twice over: the gate refused to record them, and
openTasksView/openTagView open their tab through the raw pane
primitive, which keeps no history. So the panel was not just skipped;
putting it in your path could kill the whole trail, because the
note-to-panel hop recorded nothing and the panel-to-note hop was
gate-blocked. Ctrl+O sometimes simply did nothing.

The Tasks and Tags views are destinations the user navigates to on
purpose, so they now ride the jumplist like everything else: the gate
admits them, opening a panel records the origin note (and clears the
forward stack, as any jump does), and jumping back reopens the tab
through focusTabInPane, which already carries each panel's reopen
semantics. Panel entries are captured with zeroed scroll numbers, a
lingering editorViewRef belongs to some other note. Deliberately
unchanged: tab-strip clicks stay non-jumps for every tab kind, and
help/trash/plain-asset tabs stay out of the history.

Claude-Session: https://claude.ai/code/session_011wnqvUakYs9Me5kyRgsUw2
The picker's row gave the path column shrink-0 and the title flex-1
with a zero basis, so a long path claimed its full width and the title
absorbed the entire squeeze: "Calen…" beside a fully spelled-out
sixty-character path, when the title is the thing you are switching by.

Three changes, one row. The subtitle is now the note's folder rather
than the full path (the filename part just repeated the title; the full
path stays in the search keywords). The flex priorities are inverted:
the title takes its natural width and truncates only when it alone
overflows, while the folder column takes the leftover and collapses
first. And when the folder does truncate, it truncates from the left,
keeping the distinguishing tail visible, via a dir="rtl" span with an
LRE/PDF bidi embedding so the path itself still reads left-to-right.

Verified in the built app against the report's exact vault shape:
titles measure untruncated where all three truncated before, and a
deep folder at a narrow window collapses to "…Q3/Meeting Notes And
Decisions" with the title untouched.

Claude-Session: https://claude.ai/code/session_011wnqvUakYs9Me5kyRgsUw2
Sync used to inline every file into the mutation JSON, so a big PDF or
video buffered whole in memory and pushed the request toward server
limits. Files over 5 MB now travel through an upload session instead:
the client asks the cloud for a signed destination, PUTs the bytes
directly to object storage (streaming from the file on disk, never
holding it in memory), and completes the session, which folds the
result back into the ordinary mutation response, conflicts included.

The seams are where the care went. Mutation batches keep their order
by flushing inline runs around each direct upload. A failed or
rejected upload aborts its session server-side; completion retries
transient errors a few times before giving up. Conflict responses
(revision, path, deleted, quota) map back into normal sync conflicts
rather than surfacing as transport errors. Local scans hash oversized
files in a stream and remember each file's source path so the upload
can read from disk, and the signed URL is required to be https.

Claude-Session: https://claude.ai/code/session_011wnqvUakYs9Me5kyRgsUw2
@adibhanna adibhanna changed the title ZenNotes 2.31.0: the community fixes so far (cycle in progress) ZenNotes 2.31.0: large files sync for real Aug 19, 2026
async function encodeFileContent(relPath: string, absolutePath: string): Promise<CloudSyncContent> {
const stats = await fs.stat(absolutePath)
if (stats.size <= CLOUD_SYNC_INLINE_UPLOAD_LIMIT_BYTES) {
const bytes = await fs.readFile(absolutePath)
}

if (byteLength <= CLOUD_SYNC_INLINE_UPLOAD_LIMIT_BYTES) {
const bytes = await fs.readFile(absolutePath)
@adibhanna
adibhanna merged commit db2be69 into main Aug 19, 2026
12 of 13 checks passed
@adibhanna
adibhanna deleted the v2.31.0 branch August 19, 2026 23:06
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