Skip to content

ZenNotes 2.69.0 - #934

Merged
adibhanna merged 21 commits into
mainfrom
v2.69.0
Oct 13, 2026
Merged

adibhanna merged 21 commits into
mainfrom
v2.69.0

Conversation

@adibhanna

Copy link
Copy Markdown
Contributor

ZenNotes 2.69.0.

New

Fixes

  • After zn connect, the bundled MCP server reaches the server too ([Feature] Guide remote-token setup when installing MCP integrations #830, ef122a4b).
  • Setup skips the layout step for a server vault (ed6fb808); the Claude MCP labels name the Claude app's tabs (504c1551).
  • New Note Form Footer Hints Reflect The Focused Field #933 by @uNyanda (bcc52592, follow-up fb9c6fcc): the New note form's footer names the focused field's keys.
  • Fix(tasks): list_tasks and zn task list leave cancelled tasks out of open #929 by @vishalhabib99 (c0d25dd2): cancelled tasks are not open for list_tasks and zn task list.
  • A vault another device encrypted reads as encrypted, on phones too (00c592bb); the manual renders bold and paragraphs (a72e97c9) and names Save archive (5ce41c4c).
  • Found during this release: app shortcuts acted behind What's New, and note-switching or note-creating shortcuts behind Settings and palettes (12908407); the Tasks calendar's keys changed tasks behind dialogs (6da71c43); Settings search results carry their section and Keymap lists only the keys the host has (803ae295); an encrypted vault's name appears on unlock, and phones get a note instead of a Save archive that could only fail (d1344076).
  • Bundled CLI raised to zn 0.7.1 (69342fa3, manifest verified against zn-release-1).

Local gates on d1344076: typecheck 8/8 and test:run 6/6 with no turbo cache (shared-domain 1,994, app-core 3,220, quicklook 15, desktop 1,139). Pack: Developer ID signed (deep strict codesign OK, timestamped), notarization skipped by design, bundled zn v0.7.1 in a drwxr-xr-x Resources/zn-cli, the Quick Look extension; packaged launch check: page target and drawn app in 1.5 s, version 2.69.0. Smoke suites: sidebar-vim (12) and editor-improvements (19) pass; vim-editor failed one check on its first run (repeated L read the same scroll position twice) and passed all 12 on the re-run. npm audit --omit=dev --audit-level=high clean (the same 15 low or moderate as 2.68.0).

vishalhabib99 and others added 21 commits October 12, 2026 20:02
…open

A [-] cancelled task is abandoned, and isTaskOpen already keeps it off
the calendar and Kanban surfaces (#450, #476). The MCP list_tasks
"open" filter and the zn task list default/--unchecked filters still
returned it next to actionable tasks. Exclude t.cancelled in both;
status "all" and --all still list it.
The manual's end-to-end encryption entry said that **Download** under
Backups saves a readable archive of an encrypted vault. No button has
that name: the backup row's button reads Save archive. And only the
desktop app can do it. Its main process decrypts the service's archive
item by item, while both phone apps refuse an encrypted vault's backup
and point to the desktop, since that archive holds ciphertext under
path ids. help.ts reaches the phones through app-core, so the sentence
has to hold there too: it now names the desktop app and says the phones
send you to it.

The same entry promised that other devices linked to a vault that was
just encrypted "say it is now end-to-end encrypted". That is the
desktop's message for the service's 410 VAULT_SUPERSEDED answer; the
phones do not special-case it and show the service's own text. The
entry now says what every device does: it stops syncing the old vault,
and you unlock encryption on each one, then open the encrypted vault
from Settings → Cloud.

Both sentences now match the website's encryption docs, corrected the
same way before the launch (ZenNotes/website#74). Checked in the built
app over CDP with a throwaway profile: Open Help, search "Encrypt a
Cloud vault", and the entry reads the new text. Left alone: the Help
view prints **bold** markers literally and runs the entry's two
paragraphs together, which 13 entries share; that is its own change.
When one device moves a readable Cloud vault into an end-to-end
encrypted one, the service answers every other device still linked to
the old vault with 410 VAULT_SUPERSEDED. Two things went wrong after
that.

The phones never let go. Both shells sync through shared-domain's
CloudSyncHostService, which retired a link only when Cloud said the
vault was gone (404). On a 410 it kept the link, so every sync failed
again with the service's own text, "Update ZenNotes on this device to
keep syncing it", even on a phone that was already up to date and able
to open the encrypted vault.

The desktop let go but told the wrong story. Its main process retired
the link and said the vault is now end-to-end encrypted, but app-core's
notice classified any dropped link it could not name as deleted, so the
status bar read "Cloud vault deleted" and Settings said the vault had
been deleted from ZenNotes Cloud.

Now cloud-vault-availability owns the check (a CloudServiceRequestError
with 410 and VAULT_SUPERSEDED, which, unlike a 404, always concerns the
vault itself and needs no second request) and the message the hosts
throw. The host service retires the link on it during a sync and during
the background check for remote changes, exactly as it does for a
deleted vault, so the phones get the fix from the next core without a
line of shell code. The desktop uses the same two pieces instead of its
own string, and its probe now handles the 410 as well.

app-core gains a third removal reason, "encrypted", read from the 410
when an in-process host keeps it and from the hosts' first sentence when
the error crossed IPC as text. The status bar says "Cloud vault now
encrypted"; Settings says the vault "was replaced by an end-to-end
encrypted Cloud vault" and points the way on: an unlocked device chooses
the encrypted vault, a locked one first unlocks under End-to-end
encryption, the section right above. A stored notice with the new
reason survives a reload like the other two, and the manual's Space r
entry says what the status bar can now show; its website mirror waits
on website/docs-2.69.

Checked: typecheck and the whole test run; the new host tests fail
against the old service; the desktop live test against a local Cloud
with CLOUD_E2EE=beta passes, its second device getting the real 410;
and in the built app over CDP, the status bar and both Settings states
read as above. Not done here: a phone run, which needs a core artifact
built from this commit. The service's 410 text stays as it is: only
apps without this fix show it, and for them updating is the right
advice once the fixed versions are in the stores.
…r markers

The manual's strings mark the names of buttons and settings in **bold**
and break longer guides with a blank line, but the Help view only ever
understood backtick code spans. Thirteen entries printed their asterisks
as typed, the encryption guide among them ("**Save archive** under
Backups"), and its two paragraphs, like the Typst guide's, ran together
as one.

The renderer moves out of HelpView into lib/help-rich-text, where it can
be tested on its own, and learns the two missing pieces. Bold is read
only outside code spans, so `inbox/**/*.md` stays a glob, and only when
the bold text does not start or end with a space. A blank line starts a
paragraph; paragraphs are block spans, not <p>, because a shortcut
section's description already sits inside a <p>, and a string with one
paragraph renders inline exactly as before. Code spans keep their old
behavior to the class name: short ones inline, long or multi-line ones
as a block.

It deliberately does not reuse the task cards' inline-markdown renderer.
The manual's prose mentions [[ 43 times, #tags 13 times, and shows
[Paper](zotero://...) as an example to type; that renderer would turn
all of them into live wikilinks, tag chips and links.

The search now matches what the reader sees: markers drop out and
whitespace collapses before the comparison, so a phrase that runs
through a bold name or a code span finds its entry ("choose encrypt
this vault in settings" did not before).

Checked: typecheck; the app-core suite, with eight new tests, one of
which renders every string in the manual and fails on any ** left
outside code; and the built app over CDP with a throwaway profile, where
Open Help and "Encrypt a Cloud vault" show two paragraphs with four
bold names, and the Typst guide two paragraphs. Phones get it with the
next core artifact.
…t have

The New note form's footer was unconditional while the list of choices it
describes is gated, so on arrival — focused in Name, where no list is
rendered — it already claimed `↑↓ pick`. `↑↓` never moved between Name,
Folder and Tags; it only ever moved through the choice rows under Folder
or Tags, and Tab is the only field-level navigation. Neither was stated.

`↵ create` was wrong in three of the states it appeared in. With a row
highlighted, Enter picks it. In Tags, with a valid tag typed and no
highlight, Enter commits the typed tag as a chip. In Tags, with text that
is not a valid tag, Enter does nothing at all.

Derive both hints from state the component already holds:

  const hasList =
    focused === 'folder' ? folderRows.length > 0
    : focused === 'tags' ? tagRows.length > 0
    : false

  const rowPicked = hasList && activeRow >= 0
  const inTags = focused === 'tags'
  const enterLabel =
    rowPicked ? (inTags ? 'add tag' : 'choose folder')
    : inTags && typedTag.trim() ? (normalizeTag(typedTag) ? 'add tag' : null)
    : 'create note'

`hasList` is load-bearing in `rowPicked`. Typing in Folder preselects row
0 so Enter picks without an arrow key first (#467), and that index outlives
the row once the text filters the list away — folderRows[0] is then
undefined and Enter falls through to create(). A highlight pointing at
nothing is not a highlight.

The null branch is a fourth state only Tags can reach. Offering it any
label would promise an action Enter cannot take, so the hint is omitted
and the status line carries the reason.

No key handling changes, no new imports, and Tab order is untouched — the
chips and rows were already tabIndex={-1}. data-keyboard-hints stays on
the container so a touch shell still drops the hints while keeping Back
and Create.

Adds SearchCreateForm.test.ts, the first test for this component. The
extension is .ts rather than .tsx because vitest.config.ts includes only
src/**/*.test.ts, so a .tsx file would silently never run. 11 cases assert
the full hint list per state rather than one substring, so a hint that
should be absent is proven absent.
Kevin set up a fresh Mac against his token-protected ZenNotes server,
installed the Claude Code integration from Settings → MCP, and ran
`zn connect https://<server>` as the manual says. `zn vault info`
reached the server; the assistant still got "The MCP server has no
valid token for this ZenNotes server", restarted or not.

Install has two shapes. When zn was installed from Settings → CLI, it
registers `zn mcp`, the Go server, which since 2.57 falls back to the
token `zn connect` saved for the app's server URL. Otherwise it
registers the server bundled with the app, the Electron binary in node
mode running out/main/mcp.js, and "otherwise" includes a zn from
Homebrew, like Kevin's, and every Windows install. That server followed
the app to its server but looked for a token only in --token,
ZENNOTES_REMOTE_TOKEN and a legacy plaintext profile token. The app's
real copy lives in the OS secret store, which a node-mode process
cannot read, so it had none, and running `zn connect` changed nothing
for it.

Now it reads zn's credentials.toml, the private file `zn connect`
writes beside config.toml, by exactly zn's key (the URL trimmed,
without trailing slashes, lowercased). The order is the Go server's
when it follows the app: --token, the environment, a legacy profile
token, then the saved one. The lookup uses the URL the app already
chose, so a saved token can only authenticate that server, never pick
another. The session resolves its target before every call, so the
call after `zn connect` works without restarting the assistant.
--server and --vault get the same fallback when they name a server.

The 401 text now matches the Go server's: it names the exact
`zn connect <url> --no-default` to run, and blames a stale
ZENNOTES_REMOTE_TOKEN when that is the rejected token, since the
environment outranks a saved one. vault_info's note for a server with
no token says the same. The manual's MCP card says which server Install
registers when, and that the saved token reaches both.

It deliberately writes the app's token nowhere. Handing it to agents
with no terminal step, Kevin's "Allow CLI and agents to use this
connection", would move it out of the secret store into a plaintext
file, and stays a separate decision.

Checked: typecheck and every package's tests; the new resolver and MCP
tests fail with the fallback removed (five of them); and end to end
with real binaries: a token-protected znserver, `zn connect` from Go zn
built at 0.7.0, and mcp.js run exactly as Install registers it. Before,
vault_info still failed after `zn connect`, in the same session and in
a fresh one. After, the same running session read the server's vault
and notes on its next call, and a fresh one did too.
The setup wizard's Vault step can connect to a ZenNotes Server, and the
next step then asked how the vault should be organized: an inbox folder
or the vault root, daily notes on or off. For a server vault that is
settled. It was set up where it lives, its vault.json is the server's,
and every app connected to it shares that layout, so the step could
only repeat what the server says or, clicked through, change it for the
phones and the web app as well. Kevin, connecting from a fresh Mac,
asked why it was there at all.

The wizard now takes its steps from the workspace mode. A vault on a
server goes Welcome, Vim mode, Theme, Vault, Done; a folder on this
machine keeps Layout. Connecting sets the vault and the mode in one
store update, so the auto-advance past the Vault step already knows
where it goes, and Back from Done returns to Vault. Going back and
picking a local folder brings Layout back, and the reverse drops it. A
returning user whose server vault is already open starts on Done. The
web app reports no remote workspace and keeps the step, which is right
there: picking a server folder is how a new server vault is made.

Advancing through goToStep also fixes a small rail glitch: the
auto-advance never recorded the step it landed on, so after Back the
rail would not let you click forward to it again.

Checked: typecheck; and the built app over CDP with throwaway profiles
and a loopback Go server. Before, connecting landed on "Vault layout";
after, on "You're ready to write", with Back and the arrow keys moving
between Vault and Done, and the server's layout (root, daily notes on)
untouched. Choosing a local folder, with the folder dialog answered
from the main process, still lands on "Vault layout".
…ey reach

Settings → MCP offered "Claude Code" and "Claude Desktop", which reads
as one entry per app. But the Claude desktop app's Code tab runs Claude
Code and loads the user-scope servers in ~/.claude.json, the file the
Claude Code entry writes, while claude_desktop_config.json, the Claude
Desktop entry's file, reaches only its Chat tab. Someone who works in
the Code tab would pick Claude Desktop and find nothing there. Kevin
suggested the fix, on Discord.

The labels are now "Claude Code (CLI + Desktop Code tab)" and "Claude
Desktop (Chat)", the descriptions say the same, Settings search finds
"code tab", and the manual's MCP card names both entries.

Checked: typecheck and the app-core and shared-domain suites; a Code
tab session in the Claude desktop app has the zennotes server from
~/.claude.json, which claude_desktop_config.json does not list; and the
built app over CDP shows both labels in Settings → MCP.
…al (#830)

With the app connected to a ZenNotes Server that needs a token, an
assistant's MCP server could get one only from a terminal: `zn connect
<url> --no-default`, typing the token a second time on every machine,
because the app keeps its copy in the OS secret store and no MCP
process can read it. Kevin asked for the app to hand it over.

Settings → MCP → Server token now does, on request. Let assistants use
it writes the token the app connects with into zn's credentials.toml,
the file `zn connect` writes and both MCP servers read since ef122a4,
so `zn mcp` and the bundled server reach the server on the assistant's
next call. It is opt-in, and the section says why: the token moves out
of the secret store into a file any program running as the user can
read. Stop sharing takes it out again.

The main process does all of it from its own config. The server is the
persisted workspace both MCP servers follow, picked the way they pick
it, and the token comes from the secret store. The renderer names no
server and never sees the token, only one of five states: nothing to
share (a local vault, or a server without a token), missing, shared, a
different token in the file (`zn connect`, or the server's token
changed since), or a file that cannot be read.

Writing keeps zn's rules, in a module the bundled server now shares for
reading: zn's key for the URL, every other token kept, the file private
(0600) and replaced atomically, and a file that is not zn's format never
overwritten, since the tokens in it would be lost; the error never
quotes the file, whose lines may be tokens. Stop sharing removes only
the app's own token, never a different one the user saved.

The app keeps receipts of the servers whose token it shared, so the
shared copy follows the app's token and nothing else: a new token saved
for the server replaces it, forgetting the server in Settings → Vault
removes it (unless another saved vault uses the same server), and a
token the user saved with `zn connect` is never touched, even one equal
to the app's. Disconnecting keeps it, so reconnecting needs no second
share. The bundled server's 401 hint now names the button first.

Checked: typecheck and every package's tests, with new tests for the
file rules and for each state and receipt rule (breaking the remove
guard or the receipt check fails them); and end to end over CDP against
a token-protected Go server, with the dev app's safeStorage swapped for
an in-memory fake so no Keychain was touched. Connected from the
wizard, Let assistants use it, and the real Go zn (--server) and mcp.js
run as Install registers it both reached the server; Stop sharing
turned both away; sharing again and then saving a new token for the
server updated the file; forgetting the server emptied it. Settings
search finds the section, and a local vault sees a one-line note
instead of a button.
…er picks

Review follow-up to @uNyanda's footer hints (#933), kept apart so their
commit stands as theirs.

- The Enter hint counted a highlight whenever the focused field's list
  had any row, while the Enter handlers pick the row at the highlight.
  The two only part ways when a list shrinks under the highlight with no
  typing, which the form allows: it follows the vault's notes, so a sync
  or another window can drop a tag while it is open. The footer then
  said "add tag" while Enter created the note. The hint now reads the
  row exactly the way the handlers do, as the PR's own comment
  describes, and a new test shrinks the tag list under a highlight.
  typedTag is already trimmed, so its extra trim goes too.
- The new test file seeded the store with vaultSettings: null, which the
  store's type does not allow. Vitest does not typecheck, so the tests
  passed while every CI build failed at the app-core typecheck; it seeds
  DEFAULT_VAULT_SETTINGS now.
- Two comments lose their long dashes, per the house style.

Checked: app-core typecheck; the form, search-create and palette-nav
suites (42 tests, the new one failing on the merged code); and the built
app over CDP, reading the footer in Name, Folder (with and without a
highlight, and filtered to nothing) and Tags (with a highlight and with
invalid text): every state matches the PR.
… still compile

dd0f929 added mcpGetServerTokenState, mcpShareServerToken and
mcpRemoveServerToken to ZenBridge as required methods, and gave the web
bridge stubs for them. The two phone shells implement ZenBridge as well
(src/bridge/mobile-bridge.ts in each), so adopting core 2.69 would have
failed their typecheck. It surfaced while adopting a rehearsal core
into the Android shell for zennotesandroid#108.

The three methods are optional now and marked desktop only, the way
cliUpdateGetState and cliUpdateCheck already are, so a bridge with no
assistants to hand a token to needs no stub, and the web stubs go.
Settings → MCP → Server token reads a missing method as nothing to
share and shows its one-line note.

Checked: typecheck across every package, and the Android shell's
typecheck against a rehearsal core built from this tree; the app-core
and desktop suites.
The phones' Browse list offers New folder in every folder but no New
note, so a note could only be created at the top and then moved, and
New Note in Current Folder means the open note's folder, not the one
being browsed (zennotesandroid#108, also filed as #923).

The core half is requestCreateBrowseNote(host, directory) in the browse
boundary, beside requestCreateBrowseFolder and with the same guards: an
existing primary-notes folder ('' is the root), no database folder, no
dialog already open, and nothing once the vault or the host has moved
on. It then creates the note the way the phones' Note button does,
through createAndOpen with the title focused, so there is no prompt:
naming the note is the next thing typed either way. The shells add the
row and call it once they adopt this core.

Checked: typecheck; browse-actions tests for a folder and the root and
for every refusal; the app-core package test renders the new boundary
call; and end to end on an Android emulator with a rehearsal core:
Browse, New folder "Projects", into it, New note, which opened
"Untitled" with its title selected and wrote inbox/Projects/Untitled.md.
What's New opens on the first launch after the update with five
highlights: Settings → MCP → Server token for a ZenNotes Server that
needs a token (#830), the Claude integrations naming the tab they
reach, setup skipping the layout step for a server vault, the New note
form's footer naming the keys of the focused field (#933), and a Cloud
vault another device encrypted reading as encrypted, not deleted.
The bundled CLI is the floor a desktop build guarantees, and 0.7.1 is the
newest release. It carries this release's two zn changes into a
desktop-managed install from the first launch instead of the first daily
update check: `zn task list` and `zn mcp`'s `list_tasks` leave cancelled
tasks out of the open ones, matching the bundled MCP server since #929,
and when a server needs a token, zn's message points to the new
Settings → MCP → Server token as well as `zn connect`. 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 91bb33b, integration protocol 1, all four archives matching
checksums.txt). Staged through `npm run terminal:stage`; the native
darwin-arm64 binary reports zn v0.7.1. The in-app manual's CLI card says
0.7.1 and what it changes.
… a palette

What's New opens by itself on the first launch after every update, so
the keys people press out of habit land on it. None of the app's
shortcut handlers knew about it. Mod+P opened a note search underneath
it that nobody could see, took the keyboard out of the window, and
whatever was typed next went into that hidden search; Escape then
closed What's New and revealed it. Mod+N and Shift+Mod+N made a note
behind it and moved the keyboard into the new note's title, Shift+Mod+P
opened the command palette, and Mod+, opened Settings under it. Found
while recording this release's demos, where a whole scripted take ran
invisibly behind the window.

The capture-phase handler (Mod+P, Mod+N, pane focus and pane modes)
now bails for What's New as it already did for Settings and the
palettes. The bubble-phase handler returns at once while What's New is
up, so no app shortcut acts behind it; its own buttons, Tab and Escape
are its own. Asking for Settings is the exception: Mod+, replaces What's
New with Settings, and so does Settings… in the macOS menu, which takes
the key before the page does.

The same check found four shortcuts that acted behind Settings, any
palette, a menu or a prompt: Ctrl+Tab (previous note), Alt+Left/Right
(note history) and Shift+Mod+T (reopen closed tab) switched the note
under the overlay, and Shift+Mod+N made a Quick Note there and pulled
the keyboard into its hidden title. The handler's own comment on tab
selection names the rule they broke: switching the note under an
overlay strands the user on a different note than they left. The
overlay check that guarded tab selection and the template pickers now
sits at the top of the handler and guards these four as well. Shortcuts
that only change the view (sidebar, panels, Zen mode, word wrap, zoom)
and the command palette's own toggle are unchanged.

Checked in the built app over CDP with throwaway profiles
(docs/releases/v2.69.0/whats-new-overlay/). Before: with What's New up,
Mod+P opened a hidden search that took the typing, Mod+N wrote
inbox/Untitled.md, Shift+Mod+N a Quick Note, Shift+Mod+P and Mod+,
opened their dialogs under it; behind Settings, Ctrl+Tab, Alt+Left and
Shift+Mod+T changed the active note and Shift+Mod+N wrote a Quick Note.
After: none of them acts behind the window or Settings, focus stays
where it was, Mod+, replaces What's New with Settings, and with nothing
open all four note shortcuts still work. app-core typecheck and suite
(3,214) pass.
The Tasks calendar listens for keys on the whole window in the capture
phase, so it runs before anything else, and unlike the list and the
board it never asked whether a dialog was open. With Settings, What's
New or a palette over it and the keyboard on one of the dialog's
buttons, Space (or x in Vim mode) marked the selected day's task done
on disk, Tab cycled the calendar's tasks instead of the dialog's
buttons, and in Vim mode dd deleted a task, < and > rescheduled it.

It now bails while a modal, menu or Settings is open, with the same
isOverlayOrDialogOpen check TasksView and TasksKanban use. The
calendar's own right-click and drop menus are menus too, so the
calendar already waits for them to close, as the list does.

Checked in the built app over CDP: the calendar showing a task due
today, Settings open with focus on its Appearance button. Before: Space
(Vim off) and x (Vim on) each wrote "- [x] Pay rent" to the note. After:
the file is unchanged, and with nothing open both keys still mark the
task done. app-core typecheck and suite pass.
…hey have

Two places in Settings listed things a phone or an iPad cannot use.

Search. The phone shells hide the sections they have no use for (MCP and
CLI everywhere, Keymap on a phone) by their labels in the section list,
but Settings search builds its results from app-core's index, which knows
nothing of that. Searching "review" on a phone offered "Review agent
edits", and a tap opened a section the list hides. Each result now
carries its section's id (`data-settings-result-category`), so a shell
can hide the results of the sections it hides; the Android and iPhone
shells do that with this core. The web client keeps its MCP and CLI
pages, which explain that they need the desktop app, so app-core does
not hide them itself.

Keymap. Settings → Keymap listed every shortcut on every host, so an iPad
offered the four keys and four Vim keys of agent edit review, which only
the desktop app has, and the Harper keys where Harper is not available.
`keymapAvailableOnHost` leaves a shortcut out when the host lacks its
feature (`supportsAgentEditReview`, `supportsHarper`); the desktop has
both, so its list is unchanged, and the web client loses only the review
keys.

Checked: app-core typecheck, `keymaps.test.ts` (three new tests: the
eight review keys and the Harper keys leave the list where their feature
is missing, stay where it is present, and nothing else is filtered). The
phone half is verified with the shells against this core.
…ones say where to save its archive

Two rough edges of end-to-end encryption in Settings → Cloud, both on the
list of follow-ups from the launch.

An encrypted vault's real name is sealed; the host opens it only while
this device's keys are unlocked and lists the placeholder "Encrypted
vault" otherwise. Settings read the vault list once, so unlocking left
the placeholder on screen until Settings was opened again. It now reads
the list again whenever the device locks or unlocks (not for the first
status, which arrives with the first load).

The phones cannot open an encrypted vault's backup archive (it holds
ciphertext under path ids; the desktop app opens it item by item), and
their host refuses the download with "Download backups of encrypted
vaults from the ZenNotes desktop app." Settings still offered Save
archive there, a button that could only fail. A new capability,
`supportsEncryptedBackupArchive`, which the desktop app sets, decides it:
without it, an encrypted vault's backups show "To save an archive of this
encrypted vault, use Save archive in the ZenNotes desktop app." instead
of the button. Browse notes and Restore work on every host and stay.

Checked: typecheck across every package; `CloudSettings.test.ts` (74,
three new): unlocking through the real dialog reads the list a second
time and shows "Work notes" where it said "Encrypted vault", and Save
archive is offered for an encrypted vault on the desktop and replaced by
the note on a phone. Both new tests fail with their fix removed.
@adibhanna
adibhanna merged commit d134407 into main Oct 13, 2026
11 checks passed
@adibhanna
adibhanna deleted the v2.69.0 branch October 13, 2026 16:14
@adibhanna
adibhanna restored the v2.69.0 branch October 13, 2026 16:14
@adibhanna
adibhanna deployed to boundary-artifacts October 13, 2026 16:20 — with GitHub Actions Active
@adibhanna
adibhanna deployed to boundary-artifacts October 13, 2026 16:20 — with GitHub Actions Active
@adibhanna
adibhanna deployed to boundary-artifacts October 13, 2026 16:24 — with GitHub Actions Active

This branch was successfully deployed

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

3 participants