Repository navigation
Conversation
…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.
…asks out of open (by @vishalhabib99)
… 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.
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.69.0.
New
dd0f929b): with the app on a token-protected ZenNotes Server, "Let assistants use it" writes the app's token into zn'scredentials.toml(zn's key and file rules,0600, receipts so the shared copy follows the app's token and is removed when the server is forgotten); both MCP engines read it. The renderer only ever sees a state. Bridge methods optional and desktop only (faa8f873).requestCreateBrowseNotein the core (f9e219b2); the shells add the row on their release branches.Fixes
zn connect, the bundled MCP server reaches the server too ([Feature] Guide remote-token setup when installing MCP integrations #830,ef122a4b).ed6fb808); the Claude MCP labels name the Claude app's tabs (504c1551).bcc52592, follow-upfb9c6fcc): the New note form's footer names the focused field's keys.c0d25dd2): cancelled tasks are not open forlist_tasksandzn task list.00c592bb); the manual renders bold and paragraphs (a72e97c9) and names Save archive (5ce41c4c).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).69342fa3, manifest verified againstzn-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, bundledzn v0.7.1in a drwxr-xr-xResources/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=highclean (the same 15 low or moderate as 2.68.0).