Repository navigation
Conversation
… and other apps to Quick Notes charles b. asked on Discord for a native Share to ZenNotes extension, so Safari and other apps could send a URL or selected text straight to Quick Notes. ZenNotes.app now carries ZenNotesShare.appex in Contents/PlugIns (apps/share-extension), next to the Quick Look extension. The sheet previews what is shared (the page title and site, the selection quoted), takes an optional note line and a vault, and saves on Return. Escape cancels, and Cmd+1 to Cmd+9 pick a vault: with macOS's default keyboard settings Tab never reaches a pop-up button. Safari runs ShareExtensionPreprocessing.js on the page, so the sheet gets the page's own title, address and selection; other apps hand a selection over as plain text, RTF or HTML, and all three are read as text. The extension is sandboxed, as macOS requires, and reaches no vault. It leaves each share as one JSON file in a share inbox, ~/Library/Application Support/ZenNotes/Share Inbox, the one folder a temporary-exception entitlement opens to it (the kind Quick Look already ships). The app files the shares (main/share-inbox.ts): at startup, as they arrive while it runs, and when a server a share names connects. It writes the note through vault.ts's createNote, so a Quick Notes folder remapped in systemFolderPaths is honored and no Swift copy of the folder rules exists, and it deletes a share only after its note is written. The app keeps vaults.json in the inbox, following every config save, so the sheet can offer the local and Cloud vaults and the servers the app knows. What it chooses: - Not an App Group. macOS refuses the unsandboxed app's own mkdir of its group folder even with the entitlement; only containermanagerd creates it, for a Foundation caller, which Electron's main process is not. The inbox folder needs no change to the app's own entitlements. - A share made while ZenNotes is closed waits for the next launch; nothing launches behind the user's back. - The note: the page title as heading and filename (fetched through link-metadata.ts when the sharing app gives none; else the first line of the text, then the host), the selection quoted, the note line, and [title](url) last. Text alone follows Quick Capture: its first line is the title. deriveTitleFromBody and timestampTitle move to shared-domain so both surfaces name notes alike. - A share file is untrusted input. Its vault is a key looked up in the app's remembered vaults, never a path; sizes are capped; a vault whose folder is gone is not recreated by filing into it; unreadable files are set aside, not deleted. A share for a self-hosted server waits until that server is the connected workspace. What it does not do: files, images and Finder documents are not accepted; there is no toast in the app (the note appears in Quick Notes as the watcher sees it); macOS may list a newly registered share extension switched off, and the app does not switch it on for the user. The in-app manual and the website docs describe it. Verified on packaged builds through a test copy with its own bundle ids and scratch vaults: a share made with the app closed was filed on the next launch into a remapped Quick Notes folder, titled from the fetched page; with the app running a text share was filed within two seconds and showed under Recent; inside the extension, Cmd+2, typing and Return saved to the second vault, and Escape cancelled; RTF text and a link with a multi-line selection came through; a share for a self-hosted server waited, then was filed through the server into its remapped folder once it connected. The release pack signs the extension with the Developer ID, hardened runtime and a timestamp, carries no self-test code, and opens a page target in two seconds. A share from Safari itself was not exercised.
…photographers
charles b. asked on Discord for Unsplash as a source of covers. The
cover picker now has a second tab, Unsplash, next to Covers. Tab in the
search field switches between them and keeps the text, so "fog" on
Covers then Tab searches Unsplash for fog. Space l u in Vim mode, Add
Cover from Unsplash… in the palette and :cover unsplash <search> open
the tab directly; the last fills in the search with the first photo
selected, so Enter then sets it. Photos appear as you type, after a
400 ms pause and from two characters. The arrows, Ctrl+N/P and
Ctrl+J/K move a row, reaching the last row loads the next page, and the
line under the grid credits the selected photo, "Photo by <name> on
Unsplash", with both names linked.
A tab rather than another section, on purpose: a section would send
every search typed in the picker to the network, the names of vault
images included, and would spend an hourly allowance that every
ZenNotes user shares on people who never wanted a photo. The Covers
tab never goes online.
Unsplash's API terms shape the rest. They require the photo to be
hotlinked ("you must directly use or embed the related image URLs
returned by the API", and their own example is a picked background),
so unlike a gradient or a gallery photo an Unsplash pick is not copied
into assets/. It is an https cover, which covers already were, with the
photographer's credit on the lines under it:
banner: "https://images.unsplash.com/photo-...?ixid=...&w=2400&q=80&auto=format"
banner_credit: Jane Doe
banner_credit_url: "https://unsplash.com/@janedoe"
The size parameters are appended as text, so Unsplash's own query
string, ixid included, stays byte for byte. The credit keys are
generic (note-cover.ts reads and writes them for either cover key) and
belong to the picture: a new image or a removed cover takes them away
with banner_y, and the whole pick is one edit, so u takes it back. For
a photo on images.unsplash.com the cover draws "Photo by ... on
Unsplash" in its bottom-left corner, adding the referral tags Unsplash
asks for at that moment so the file keeps clean links; any other
credit draws as written. It shows with the cover's buttons on hover or
focus, all the time on a screen that cannot hover, and all the time on
a published note's page. Picking a photo pings its download_location
(Unsplash's download tracking), fire and forget, once the note has it.
A web cover that cannot load now says "Could not load the cover from
images.unsplash.com" instead of calling the image missing.
The app holds no Unsplash key: the terms keep it confidential and do
not let users bring their own. Search and the ping go through
zennotes.org/api/v1/unsplash, which signs them with ZenNotes' key and
keeps the budget (the website's feat/unsplash-covers). Two optional
bridge methods carry them. Desktop main sends them with net.fetch, no
cookies, a bare user agent and no language, after checking the
arguments, and caps one run at 40 requests a minute so a renderer bug
cannot spend everyone's hour; the renderer's CSP is unchanged. The web
client calls the proxy with fetch. A host without the methods, the
phone shells today, shows no Unsplash tab. Everything the proxy returns
is checked in shared-domain unsplash.ts before it is drawn: images only
from images.unsplash.com, links only to unsplash.com, a ping only to an
api.unsplash.com download endpoint. ZENNOTES_UNSPLASH_PROXY_URL points
a test run at another proxy and, like the other overrides, is honored
only unpackaged or with ZEN_PERF=1.
The tab says what went wrong when a search fails: Unsplash could not be
reached ("the Covers tab works offline"), the shared hourly allowance
is spent ("resting until 3:00 PM"), the service is unavailable, or
nothing matches. Answers are kept for the session, so switching tabs or
reopening the picker asks nothing again.
Not here: the phones get the tab when their shells add the two bridge
methods after a core release, and Quick Look still leaves web covers
out.
Verified in the built app over CDP, both stores isolated, against a
local stand-in for the proxy (48 checks, Vim on and off): Space l u
and its which-key row; one search for "fog" typed letter by letter,
with no cookie and a bare agent; hotlinked thumbnails; the credit's
tagged links; ↓, Ctrl+N and Ctrl+J, and page 2 at the last row; Enter
writing the banner and credit with nothing in assets/ and one ping;
the cover loading and its credit showing on hover; u and Ctrl+R; Tab
and Shift+Tab with a cached answer; :cover unsplash then Enter; the
rate-limited, unavailable, no-match and unreachable messages; an
unreachable web cover; a new image dropping the credit; a cover seen
before drawing offline, in the same run and after a restart; :cover!
restoring the note; with Vim off, the palette route, j typing into the
field and ↓ moving. Then live, through the website served locally with
a demo key: a real search, real thumbnails and credit, a banner keeping
Unsplash's ixid, a 2400 px cover, and the ping counted against the key.
Tests: the credit keys, the URL rules, response parsing, the proxy
client and its failures, the desktop client's argument checks, cap and
override, the search controller's debounce, stale answers, cache and
paging, and the read-only cover's credit. shared-domain 2,184, app-core
3,609, desktop 1,202 and quicklook 18 pass; typecheck clean.
How to test locally: serve the website's feat/unsplash-covers branch
with UNSPLASH_ACCESS_KEY set, build the desktop app, then run
ZENNOTES_UNSPLASH_PROXY_URL=http://127.0.0.1:<port>/api/v1/unsplash
electron apps/desktop/out/main/index.js. Open a note, press Space l u
(or Tab in the Space l c picker), type "fog", move with ↓ and press
Enter: the note gains banner, banner_credit and banner_credit_url, and
hovering the cover shows "Photo by ... on Unsplash". Without a proxy
the tab says Unsplash could not be reached, and the Covers tab works as
before.
…ion from Safari and other apps to Quick Notes
… as raw Markdown Found on the phones once their read-open guard shipped (iPhone 1.34.0, Android 1.1.49): a note opened from Home, a list or a zennotes:// link now opens with the keyboard down, as it should, and with its title drawn as a raw `# Garden plan`. Live preview hides the Markdown markers on every line except the ones the selection covers, and a note opened to be read still has a selection: the caret parked on line 1. Nobody was editing that line, but it read as source. Desktop showed the same wherever an editor sits unfocused: a hovered link's preview popover, the pinned reference pane, and the note a window restores at launch. Source is now revealed only on lines the user is editing. A state field (lib/cm-source-reveal.ts) says whether the editor has taken the focus since its note opened. Until it has, every line renders, the caret's too. The first focus turns revealing on, and a blur leaves it on, so a palette, a menu or a dialog over a note being edited redraws nothing (and a phone's keyboard going down is not the end of editing either). A note put into an editor in place of another starts over, unless the editor holds the focus as it arrives: a tab switch or a followed link from the keyboard keeps revealing. Every reveal asks the same question: the line markers, links, ==highlights==, template tokens, callout heads, rules, fences and bullets, and the math, Mermaid/D2 and embed blocks. The editors that share these extensions (the main panes, the reference pane, the link popover, floating and external-file windows) all carry the field; one without it (Quick Capture, the template editor) reveals as before. The focus is taken up in a microtask of the focus event, not on CodeMirror's own focus notice, which comes 10 ms later: by then a frame with the caret's line rendered would already be painted, and every note desktop opens would flicker its first line. A focus a phone shell turns straight back (the read-open guard blurs it in focusin) never reveals. What it deliberately does not change: when anything takes the focus, and the parser (the note grammar's full reparse is untouched). Desktop opens a note and focuses it exactly as before, so the caret's line shows its markers from the first painted frame. Desktop, visible only where an editor is unfocused: the link preview popover and the reference pane draw their first line rendered, a note a split's other pane switched to while unfocused renders its caret line, and a note restored at launch renders its caret line until the editor is clicked or focused. Tests: cm-source-reveal (an unfocused note renders its caret line; an editor without the field reveals; the first focus reveals before any timer runs; a blur keeps revealing; a refused focus never reveals; a note swapped in unfocused is read, swapped in focused reveals; a note left to read renders again; highlights and inline math follow it). In the built app over CDP with isolated stores, sampled once per painted frame on the 2.78.0 build and on this one: opening from Home, the sidebar, Search and Search Text in Vault, Vim gg/j/k and an A edit that reaches disk, a palette over a focused editor, Read to Edit, Split and the find panel paint the same caret-line states on both; the link popover and the launch restore show the change. On the iPhone shell (web build at phone size, rehearsal core from this tree): a note opened from Home, and the one restored at launch, show their heading rendered with no `#`, and a tap into the text focuses it and reveals that line.
…d down The phones' read-open guard keeps the keyboard down for a note opened from Home, the lists and a zennotes:// link, but a pick in Search still raised it: every one of core's searches ends with focusEditorNormalMode, the hand-off desktop wants (the next keystroke edits or moves in the note), and on a phone that focus is the soft keyboard over half the note the user came to read. Traced on the iPhone shell, that one call was the only editor focus after a pick from the note search, from Search in all notes and from the outline, and after cancelling a search too (the shell's Cancel sends the Escape that App's window handler answers the same way). The outline jump also focused the editor itself. The host now says how it wants a pick to land. EditorHostOptions (the editor host the shells already install for native typing and keyboard insets) gains `openNotesToRead`, asked at each pick. Where it returns true, a pick in the note search (notes and tagged blocks), Search in all notes, the outline palette and the buffer switcher opens the note at the pick without taking the focus, and the note is left to read (every line rendered, see the previous commit). If the closing palette gave the focus back to an editor that opened it (an iPhone keeps the focus in the text under a tap on a button), that hand-back is undone a frame later. Closing one of those searches without a pick leaves the focus where the palette returns it, to what had it before. Where it is absent, false or throws, nothing changes: desktop and the web app focus exactly as before. Why the editor host and not a bridge-contract capability: the answer is the phone layout, which a tablet changes at runtime (Split View), so it has to be a question asked at the pick, not a static capability; the shells already register this host; and the bridge is the vault and IO contract, with nothing for desktop or web to say here. The touch media query was not enough either: it is true on tablets in the desktop layout, which keep core's focus as the guard does. What it deliberately does not do: the list openers the shells' guard already covers keep focusing as before (the guard refuses it), and the command, template and drawing palettes keep their hand-off. Creating a note from Search still focuses it, as a new note should be written in. Shells: one line in src/ui-mobile/editor-host.ts, in both phones: `openNotesToRead: isPhoneViewport` in the installEditorHost options (isPhoneViewport from '../viewport'). Without it, a phone that adopts this core behaves as today for Search. Tests: editor-focus (desktop focuses; a host that answers no or throws keeps it; a reading host leaves the editor unfocused and read, undoes a hand-back to the editor, and leaves the focus alone on close); SearchPalette (a pick goes through the pick hand-off, Escape through the close one) and SearchPaletteBlocks (a block pick). On the iPhone shell (web build at phone size, rehearsal core from this tree, the one line above): picks from Search (from a note and from Home), Search in all notes and the outline open the note at the pick with the editor never focused and the caret line rendered; Cancel keeps it unfocused; a tap into the text focuses it and shows that line's `#`. Without the line, the same picks focus as before. On desktop over CDP every search still focuses the editor, painted the same as on 2.78.0.
… command, keyboard down After 0a46efb a Search pick on a phone opened its note to read, but two of core's hand-offs still raised the keyboard there. Closing the command palette, the template palette or the drawing palette without picking anything called focusEditorNormalMode, from the palette's own Escape and from App's window handler that answers the shells' Cancel button. And a command run from the command palette that only goes to a note already there (Go Back, today's daily note) was followed by the same editor focus, twice for the periodic notes: the palette refocuses after every command that lands on the editor, and openDailyNoteForDate and its weekly and monthly twins ask for the focus themselves (#353). Both now go through the editor host's `openNotesToRead`, the question 0a46efb added. The two hand-offs it introduced are renamed for what they now cover: focusEditorAfterReadOpen (a note opened to be read, from a search pick or a command) and focusEditorAfterPaletteClose (any of these palettes closed without a pick). On a host that opens notes to read, a palette closed without a pick leaves the focus where the palette returns it, to whatever had it before the palette opened, so backing out over a note being read keeps the keyboard down and backing out over a note being written keeps it up. Desktop and the web app focus the editor exactly as before, with the same retries. Commands say whether they open to read through a new `Command.opensToRead`, asked just before the command runs (afterwards a daily note exists either way). The rule: a command whose whole effect is to put an existing note on screen opens it to read; a command that creates a note, or works on the note in place, keeps the focus. Open to read (the palette hands over like a search pick): - nav.back, nav.forward, nav.toggle-recent (Go Back, Go Forward, Switch to Previous Note): history only holds notes that exist. - tab.reopen, tab.next, tab.previous (Reopen Closed Tab, Next Tab, Previous Tab): another note that is already open. - tab.close (Close Tab): what remains on screen is the neighboring tab. - note.daily.today, note.weekly.thisWeek, note.monthly.thisMonth: only when that note is there already. When the command has to create it, the note is new and is focused to be written in, as today. The command passes `readIfExisting`, so the opener's own focus request follows the same rule (a `read` detail on zen:focus-editor that App lands like a pick); other callers of those openers (the leader keys, the Calendar, Home, the phones' Daily note row) are unchanged. - note.daily.rollover (Roll Over Unfinished Tasks to Today): the same rule on today's note. Keep the focus, unchanged: every command that creates a note to write in (note.new.quick, note.new.inbox, note.new.here, task.new, task.new.folder, database.new, drawing.new, embed.drawing.new, and the template palette's create and insert); commands that act on the open note in place (format, rename, favorite, cover, archive and restore, copy, export, folds, task state, the editor toggles); nav.focus.editor and ref.focus, whose point is the focus; and split.right/split.down. The command palette has no command that follows a link: Open Link Popover peeks at one over the current note and keeps its own focus. Commands that open another palette or dialog still hand nothing over, and a theme picked in the palette still returns to the editor. What it deliberately does not do: change the commands themselves, Home or the Calendar, or anything on a host without `openNotesToRead`. Tests: commands (exactly those eleven commands answer opensToRead, history and tab commands always, the periodic ones only for an existing note, the writing commands never, and the periodic commands ask their opener for readIfExisting); store (the daily opener requests a read landing only for an existing note opened that way); editor-focus (a read open keeps its caller's retries on desktop); a palette test (the command, template and drawing palettes close through the close hand-off, a read command lands through the read hand-off, a writing one still focuses). app-core 3,624 and shared-domain 2,164 pass; typecheck clean (8/8). In the built app over CDP with isolated stores, on 2.78.0 and on this build: Escape out of the command palette (from its input and from a window Escape like the phones' Cancel), the template palette and the drawing palette, then Open Today's Daily Note creating the note, Go Back, Open Today's Daily Note again, Switch to Previous Note, Close Tab, Reopen Closed Tab and Next Tab each leave the same note on screen with the editor focused on both builds.
…xcept while Cloud needs you Piotr asked on Discord (2026-10-28), with an Android screenshot: "On android, can we hide the cloud connected / setup bar??" On the phones the bar along the bottom carries only its Cloud half, so a device that is signed in but not linked showed "ZenNotes Cloud · Cloud connected · Set up" under every note for as long as it stayed that way, and on Android the circle was lifted above that row too. There was no way to turn it off, on the phones or on desktop. Settings, Appearance, Chrome gains Status bar, on by default, next to the title bar switch, with the words people search for (cloud, cloud connected, footer, word count). Turned off, the bar is not drawn at all on any surface: desktop gets its 32 px back, and on Android the Cloud row goes and the circle drops back to the bottom, because the shell sizes that row and lifts the circle from the bar's data-cloud-sync-* hooks, which are no longer in the page. It stays off except while Cloud needs the person. cloudSyncNeedsUser (cloud-auto-sync.ts) is the same set the phones already key their Cloud row on: the attention and error phases (a stopped or failed run, including Can't reach Cloud), a Cloud vault that went away and a device Cloud signed out (both drawn as attention, now one cloudSyncShownPhase that the status row uses too), and files or the vault settings question waiting for a decision. Then the bar comes back until that is dealt with, so turning it off never hides the one place that says sync has stopped. Zen mode still clears the bar whatever it says, as before. statusBarVisible (lib/status-bar-visibility.ts) holds the rule. It is a portable preference: showStatusBar in PORTABLE_PREF_KEYS, show_status_bar under [appearance] in config.toml beside show_window_title_bar. The phones keep it with the rest of app-core's preferences (zen:prefs:v2, which both shells mirror into native storage), so it needs no shell code. Keyboard: the command palette's Toggle Status Bar (and so :view_status_bar), and `:set statusbar`, `:set nostatusbar`, `:set statusbar?` (alias `stb`) in Vim mode, the way `:set undofile` works. No single-key binding, so Vim-off gating is untouched. The option is defined without a default value: codemirror-vim applies a truthy default through the callback when the option is defined, and the first desktop relaunch over a saved show_status_bar = false came back with the bar on. Docs: the Appearance entry, the Cloud card and the ex command list in help.ts; the website follows in its own branch. What it deliberately does not do: change the iPhone, whose shell already shows the Cloud row only while Cloud needs the user, so there the switch changes nothing until the shell decides otherwise; or add a global shortcut or leader key, which none of the other chrome switches have. Tests: statusBarVisible (on by default, off until Cloud needs the user, Zen mode wins); cloudSyncNeedsUser (quiet for the everyday phases; needs the user for attention, error, a removed vault, a signed-out device, waiting files even mid-sync, and the settings question); the Settings search finds Status bar by "cloud connected" and its switch turns it off; Toggle Status Bar flips and saves it; `:set statusbar` leaves a saved choice alone when it is defined (fails with a truthy default) and sets, clears and reports it; config.toml round-trips [appearance].show_status_bar; PORTABLE_DEFAULTS still matches DEFAULT_PREFS. app-core 3,636, shared-domain 2,164 and desktop 1,198 pass; typecheck clean (8/8). In the built app over CDP with isolated stores: Toggle Status Bar hides the bar and the editor gains 32 px, config.toml says show_status_bar = false, `:set statusbar` and `:set nostatusbar` bring it back and hide it; with the main process's Cloud handlers swapped for a linked account whose sync fails, the hidden bar comes back as "Sync failed", and goes again once a sync succeeds; a relaunch with a fresh userData and the same config.toml opens with the bar hidden.
…oard down Found while verifying 42865ca: on a phone, Toggle Status Bar from the command palette raised the keyboard. Every command that lands on the editor is followed by focusEditorNormalMode, so any setting or view toggle run from the palette (Light Sidebar, Line Numbers, Word Wrap, Live Preview, a theme) put the soft keyboard over half the note, with nothing to type. Two commands also focused the editor themselves: the four fold commands (runFoldCommand) and Copy Link Under Cursor. The palette now asks where the keyboard should go (lib/command-landing.ts), with the editor host's `openNotesToRead`, the same question the Search picks and palette closes ask. The rule defaults to leaving the focus where it was before the palette opened, and names the commands that should give the editor the keyboard, so a toggle added later cannot raise a phone's keyboard by being forgotten. On desktop every answer focuses the editor, exactly as before. On a phone, after a command run from the palette: - read, the note is left to read, keyboard down (e443b86, unchanged): nav.back, nav.forward, nav.toggle-recent, tab.reopen, tab.next, tab.previous, tab.close, and note.daily.today, note.weekly.thisWeek, note.monthly.thisMonth and note.daily.rollover when that note already exists. - edit, the editor takes the keyboard, because typing or the focus is the point: note.new.quick, note.new.inbox, note.new.here, task.new, task.new.folder, database.new, drawing.new, embed.drawing.new; the four periodic-note commands when they create the note; template.create and template.insert; note.format (Format Markdown), editor.reflow-paragraph and editor.add-comment; nav.focus.editor and ref.focus; split.right, split.down and the four pane.focus commands. - stay, the focus goes back where it was: everything else. That covers every View, UI and App command (sidebar, status bar, line numbers, sort and group, panels, Tasks, Tags, Atlas, zoom, themes, Settings); the Editor toggles (word wrap, live preview, completed-task style, which-key hints, tabs, keep panels, undo history, auto reveal, quick note dates, PDF embeds); task state, folds, copies and exports; archive, trash, rename, favorite and cover; vault, Cloud, CLI and workflow commands. Enable and Disable Vim Mode stay too: the mode applies the next time the editor has the focus, and on a phone that is a tap into the text, with a hardware keyboard or without. The fold commands and Copy Link Under Cursor no longer focus the editor themselves on such a host. What it deliberately does not do: change desktop, the web app, or the commands a phone's own menu runs through runAppCommand (those never handed the focus over). Tests: commandLanding (an existing note is read even for a writing id, the edit list takes the keyboard, toggles and an unknown future command stay); every id on the edit list is a real command; the palette hands a toggle back through the close hand-off and still focuses for a writing command. app-core 3,641, shared-domain 2,164 and desktop 1,198 pass; typecheck clean (8/8). In the built app over CDP with isolated stores, on 2.78.0 and on this build, Light Sidebar, Line Numbers: Relative, Disable Word Wrap, Completed Tasks: Strikethrough, Fold All Headings and Lists, Disable Live Preview and Format Markdown each leave the editor focused on both (Toggle Status Bar too, on this build).
…promises (#1008) davi-jorge-art cleared the Quick Note prefix in Settings, on Windows and on Android, and new quick notes still came out as "Quick Note 2026-10-24 1658". The field promises "Leave blank for a bare timestamp or date". The blank value was saved all along. The store keeps a cleared field as null, config.toml writes it as quick_note_title_prefix = "" and reads it back as null, and the field is still empty after a restart. The title builder lost it: resolveQuickNoteTitle took the prefix as an optional argument that defaulted to "Quick Note", and all four callers (the sidebar +, Shift+Cmd+N, New Quick Note in the palette, n in the Quick Notes view) passed `prefix ?? undefined`. null became undefined and the default came back. The prefix argument is now required and typed string | null, null meaning no prefix, so a caller can no longer leave it out and pick up the old default. The field also looked as if the default were still set. Once it was empty its placeholder showed "Quick Note" in grey, which is what the reporter read as the field going back to "Quick Notes". It now reads "No prefix". And the field could not take a two-word prefix typed key by key, including the default "Quick Note": live Settings rows commit the trimmed text on every keystroke and drew the stored copy back into the field, so a typed space was gone at once. TextInputRow now shows what was typed while it has focus and the stored value otherwise. Rows still commit on every keystroke, so a phone that leaves Settings without a blur keeps the edit. The same fix lets the Inbox, Quick Notes, Archive and Trash label rows take a space. The phones and the web app build quick-note titles in this core code, so they pick the fix up with it. Quick Capture and the Share menu title a note from its first line and are left alone, as are the MCP server and zn, which never use the prefix. The help text already promised a bare timestamp or date and is now true. Tests: resolveQuickNoteTitle (null, empty and blank prefixes give the bare timestamp or date, date titles still number " (2)"); New Quick Note after the setter clears the prefix (fails before this change); the Settings field keeps a typed space, says "No prefix" and saves an empty field as null; config.toml round-trips a null prefix as "". In the built app over CDP with both stores isolated: before the fix, clearing the field and pressing the Quick Notes + wrote "Quick Note 2026-10-28 1005.md", and Shift+Cmd+N after a relaunch wrote "Quick Note 2026-10-28 1005 2.md". After it, the same steps wrote "2026-10-28 1011.md" and "2026-10-28 1011 2.md" with matching headings, the field stayed empty with "No prefix" across the relaunch, config.toml kept quick_note_title_prefix = "", and "My Notes" typed key by key kept its space.
…ne sentence (#1003) A Windows 11 copy of 2.71.0 (NSIS) pressed Check for Updates at 21:14:03 UTC on October 27 and got a dialog holding electron-updater's whole error: the request, twenty response headers, a stack through app.asar, and at the end "GitHub returned a temporary network or server error". Nothing was wrong with GitHub. release.yml creates the release public and "latest" first (2.78.0 at 20:42:54) and uploads each platform's update feed after its installers, so for a while "latest" named a release without the feed a copy reads: latest-linux.yml came 12 minutes later, latest-linux-arm64.yml 15, latest-mac.yml 32, latest.yml 39 and latest-arm64.yml 50. electron-updater follows releases/latest, asks that tag for <channel>.yml, gets a 404 and throws ERR_UPDATER_CHANNEL_FILE_NOT_FOUND with the HTTP error's message and stack pasted into its own. humanizeUpdateError showed that message and picked a hint by testing the whole text. Its `5\d\d` matched the `536` in GitHub's strict-transport-security header (max-age=31536000), so every GitHub 404 read as a temporary server error, and isRetryableUpdateError, the same test, retried the 404 twice more before giving up. The notify-only check (AUR, tarball) said "GitHub answered 404 for the release feed" followed by a hint about private-repo tokens. describeUpdateError (update-error-copy.ts) now turns a failed check or download into a sentence, the original text as `detail`, a kind, and whether an instant retry makes sense. The HTTP status is read only where errors keep it: `statusCode`, the HTTP_ERROR_NNN code, "HttpError: NNN" at the head of a wrapped error, or the notify-only fetch's own status, which it now throws as ReleaseFeedHttpError with the URL GitHub redirected it to. Never from header values, request ids or line numbers. - A missing feed on the newest release, on every platform's feed, is "ZenNotes 2.79.0 is still being published. Try again in a few minutes.", the version read from the feed's URL, and is not retried. - A 5xx, a 429 or a timeout keeps the temporary-error sentence (a download gets its own wording); only the 5xx and the timeout retry. - A refused code signature on macOS, and a download that lost the network ("Check the connection and try again"), keep their hints. - Everything else is one plain sentence per action. AppUpdateState carries the original text as an optional `detail`. The About page shows it under a closed Details disclosure with Copy details and Report an Issue; that disclosure moves out of CloudErrorMessage into ErrorDetails, and Cloud errors render it as before. Main writes it to the log as `[updater] check failed (<kind>)`. The alert behind Check for Updates and the menu's native dialog show the sentence only. The in-app manual's update guide says what the new sentence means. Deliberately not here: the state stays `error`, so the badge, the background notice and the retry and recheck rules are unchanged; a new phase for "publishing" would ripple through every host. The cause, release.yml marking the release latest before its feeds exist, is a workflow change that needs its own yes (proposed: create the release with --latest=false and mark it latest once all five feeds are up). The website's docs mirror is not updated here. The 2.71.0 copy in the report keeps its old wording; this changes what copies from the next release say. Verified: desktop 1,239 tests and app-core 3,609 pass, typecheck clean. In the built app over CDP, both stores isolated, the notify-only check pointed at a local stand-in for github.com (302 from releases/latest/download, then 404, 503, and a real feed; 23 checks): the background check and Check for Updates both say "ZenNotes 2.79.0 is still being published. Try again in a few minutes." with one request and no dump, Details holds the 404 with Copy details and Report an Issue, a 503 is retried three times and gets the temporary-error sentence, and a published feed clears the details. Real electron-updater 6.8.9 (MacUpdater) against the same kind of stand-in throws ERR_UPDATER_CHANNEL_FILE_NOT_FOUND, and the classifier words it as the same sentence for Windows, macOS and Linux.
… so in one sentence (#1003)
…1008) davi-jorge-art asked, in the same issue, whether it is on purpose that the Tasks filter sees a tag in a task's title but not in its body. The task they mean is a file task, the kind New task makes (+ New task in the Tasks header, :newtask, a phone's Action Button): a note with title, status and `tags: [task]` in its frontmatter and the rest of the note as its body. The title is typed in the New task prompt, which has no tag picker, and so is its `title:` line, since the editor offers no tags inside frontmatter. The body is ordinary note text where # brings up the picker, so on a phone that is where a tag is easy to add. parseTaskFile takes a file task's tags from the frontmatter list alone, and the filter matches the task text (the title), the note title, those tags, priority and fields. A #tag typed in the title was found as text; one in the body was found by nothing. The Tags view meanwhile listed the same note under that tag, since a note's tags include its body. filterTasks now takes each note's tags by path (NoteMeta.tags, the list the Tags view and the sidebar read) and lets a file task match the tags anywhere in its note. The `task` marker is left out, as parseTaskFile leaves it out, so #task does not narrow to every task note. A checkbox task keeps to its own line: a tag elsewhere in its note says nothing about that one task. TasksView builds the map once and hands it to the list, to the calendar through the filtered list, and to the board, so the three sub-views keep agreeing. What it deliberately leaves alone: a task's `tags` still hold only the frontmatter list, so parseTaskFile and its copies (MCP list_tasks, zn, the Go server) report what they reported, the row's tag chips are unchanged, and none of the four task-line parser copies moves. The indented lines under a `- [ ]` are not treated as its body here; that would need the parser copies to agree on where a task's block ends. The Tasks filter paragraph in the in-app manual says so. The website's Tasks Filter entry (docs.blade.php) mirrors that paragraph and still needs the sentence; this branch does not touch the website repo. Tests: filterTasks with a task parsed by parseTaskFile whose body carries #errands and #Admin/Paperwork: found by #errands, errands, #admin and the full nested tag; a checkbox in a note tagged #errands on another line is not; #task finds nothing; without the map nothing changes; computeTasksRender passes the map through. In the built app over CDP with both stores isolated, a vault with that task note, a second task note with no tag, "- [ ] Buy milk #errands" and "- [ ] Water the plants" in a note that says #errands on another line: before, "#errands" listed Buy milk alone (1 of 4), the calendar showed 1 task today and the board 1 card. After, it lists Renew the passport and Buy milk (2 of 4), the calendar shows 2 and the board 2 cards, Water the plants and Call the bank stay out, #task lists nothing, and the Tags view for #errands lists the same two notes.
…one (#1006) Ctrl+A in a note selected the title along with the text. The title here is not the header or a separate field: a new note is written as `# <title>`, a rename keeps that heading in step with the file name (#455), and Live Preview shows it as the large heading at the top of the page. CodeMirror's select all takes the whole document, so it took the title too, and typing or pasting over a select all replaced it. Measured on the built app over CDP with a `# Shopping list` note: every mode that selects (Vim off, Vim insert, Vim normal and visual on macOS, Live Preview and source) selected 0 to 82 of 82 characters, and the DOM selection never left the editor. Nothing outside the editor could be the title on screen anyway, since the app chrome is user-select: none. Select all from the note's text now selects that text: from the first non-blank line under the title to the end of the note. A second press selects the whole note, frontmatter and title included, as before. The first press does that too when the cursor is on the title or above it, where the user is editing the title rather than the text, and in a note that does not start with a `# ` heading. The title line is the same one a rename retitles: `leadingTitleHeading` in shared-domain now locates it for both, and `retitleLeadingHeading` is rebuilt on it unchanged. Every editor keymap gets the new select all from one place, Vim on or off and on every platform, and the editor menu's Select All runs the same command. #993 stays as it was. In Vim normal and visual mode off macOS, Ctrl+A is still Vim's increment. With no number on the rest of the line it does nothing, as in Vim, and Vim still consumes the key; it does not fall back to select all. That would make Ctrl+A increment or select depending on where the cursor sits, and a missed increment would leave a visual selection for the next `x` or `c` to delete. `ggVG` selects in Vim, and insert mode selects with Ctrl+A. On the built macOS app over CDP, Cmd+A from the text now selects 17 to 82 (the text) in every mode that selects, a second press 0 to 82, and from the title line 0 to 82. The non-mac keymap was checked in the same build: the preload reported linux and navigator.platform was Linux, so both the app's platform check and CodeMirror's Mod read Ctrl, and Ctrl+A carried Chromium's Linux and Windows default action (selectAll), which runs only when nothing handles the key. Before, Vim off and insert mode selected 0 to 82; after, 17 to 82, then 0 to 82. Normal mode increments `Buy 3` to `Buy 4` and does nothing on a line without a number, before and after. The editor menu's Select All selects the text, and typing over Cmd+A leaves `# Shopping list` in the file on disk. Tests: select all dispatched through the keymap and Vim in the editors' key order (fails on the old keymap), the title locator, and the guards for #993. app-core 3,619 tests, shared-domain 2,184 and desktop 1,221 pass; typecheck clean. The website docs mirror of the new help entry is not in this change.
…amp, and the Tasks filter reads a task note's body tags (#1008)
…dow that dims Safari The first real Safari share opened the ZenNotes sheet as an empty window: Safari dimmed behind it, typing reached the note line, and nothing showed. Safari had done its part (the page script handed over the title, address and selection). The view was built in code with a zero frame, and the share sheet takes the view's size when it presents, before Auto Layout has run. The root view now starts with a real frame, and the controller sizes itself to its fitted layout and reports it as preferredContentSize whenever what it shows changes (the shared text, the vault row, the notice). The second line under the title shows the page's address without its scheme, so a page shown by its host alone still says which page it is. Verified: the NSSharingService harness, which had shown the same empty window, draws the full sheet; then a real Safari share of the Zen article with a selection saved on Return and was filed with the page's own title, the quoted selection, the note line and the link.
Shares always landed in Quick Notes. Settings > Vault > Notes now has a
Default shares location beside the drawings, databases and tasks location
rows: Quick Notes (the default), the primary notes location, or a specific
folder inside it such as Clippings. Search finds it by "safari", "share" or
"clippings".
It is a vault setting, `sharesLocation` in vault.json, like the other
location settings, so each vault keeps its own choice and it follows the
vault to every machine. shared-domain's share-location module reads it the
same way for every caller: the main process resolves it per vault when it
files a share (createNote still resolves remapped system folders), and the
vault list it keeps for the sheet now names each vault's destination, so the
sheet's heading says where a share will go ("Save to Clippings"). The list
is rewritten when a vault's settings are saved or change on disk, not only
when the remembered vaults change. The normalizers in main and app-core and
the settings-conflict sections carry the new key, so no round trip drops it.
What it does not do: the row shows only in the macOS desktop app with a
local vault. Windows, Linux and the web have no share extension, and a
self-hosted server rebuilds vault.json from the fields it knows, so it
would drop the setting; a server vault keeps Quick Notes until znserver
mirrors the field. No "active note's folder" mode: a share is filed with no
note in view.
The in-app manual and the website docs describe it.
Verified in the packed app over CDP: searching Settings for "safari" lands
on the row; Specific folder with Clippings saved to vault.json, the sheet's
vault list and heading said Clippings, and a share was filed in
inbox/Clippings/; switching to Primary location relabelled the vault Inbox.
Unit tests cover the resolver and labels, the vault.json round trip, the
destination list, and the Settings row on macOS and its absence elsewhere.
…ne out, the second takes the whole note (#1006)
… vault chooses where shares land
… on it (#1003) The release workflow created the GitHub release public and latest in its first job, then uploaded each platform's installers and update feed as its build finished. On 2.78.0 that left releases/latest pointing at a release with no latest.yml for 39 minutes and no latest-arm64.yml for 50. Every in-app update check in that window asked releases/latest for its feed and got a 404; a Windows user on 2.71.0 hit it and filed #1003. The app now says "still being published" in that case, but the window itself was ours. The release is still created public at the start, because the AUR, Nix and Homebrew steps and the boundary packages read per-tag download URLs mid-run, and a draft's assets answer 404 to the public. What changes is --latest=false at create, and a promote-latest job after the platform jobs that marks the release latest only when all five feeds (latest.yml, latest-arm64.yml, latest-mac.yml, latest-linux.yml, latest-linux-arm64.yml) are on it. It runs after a failed platform job too (!cancelled()), so a dispatch that rebuilds the missing platform and uploads the last feed promotes the release from its own run. It never takes latest from a newer release, so rebuilding an old tag by dispatch leaves latest alone. It does not change how notes are published (gh release edit without --latest leaves latest where it is), and the feed list must change with the platform matrix. A run cancelled and finished by hand from its artifact skips promotion, so that path now ends with gh release edit vX.Y.Z --latest; the release runbook says so.
… land Default shares location stayed hidden for a vault on a self-hosted server, and the Share sheet always said "Save to Quick Notes" for one, because the server dropped vault.json's sharesLocation on every settings save. znserver 2.79 keeps the field and advertises supportsSharesLocation. The Settings row now shows for a server workspace when its server advertises that capability, and stays hidden behind an older server, where a choice would be lost on the next save. Shares were already filed from the server's own setting (fileRemote reads it), so with a 2.79 server they land in the chosen folder; what was wrong was only what the sheet said. The sheet now names the connected server's choice, falls back to Quick Notes when the server is older or cannot be read, and is refreshed after a server vault's settings are saved and when a server connects. It deliberately does not remember a disconnected server's choice: that server's shares wait for the connection anyway, and its label is corrected as it connects. The in-app manual says server 2.79 is needed. Tests: a Settings case shows the row for a server with supportsSharesLocation and hides it for one without (the old gate hid it for every server). Typecheck clean; shared-domain 2188, app-core 3677, quicklook 18 and desktop 1241 pass.
…ine (#1007) #1007 asked for tasks in date order in the right sidebar, with overdue work and milestones marked. The Calendar panel shows one week of one month; nothing showed what is coming up across the vault at a glance. The Timeline is a fifth right-hand panel beside Outline, Connections, Comments and Calendar. Overdue tasks sit on top in the danger color with an "Nd late" pill, Today is the accent ring ("Today · Wed 28"), each later day with work gets a node, and a run of empty days folds into one "N quiet days" line. The header counts what the rail shows ("9 open · 2 overdue"), the switch reaches 2 weeks (the default), a month (to the same date next month) or All, and the filter takes the Tasks filter syntax, note-tags map included (#1008). Tasks past the range are counted at the foot and a click shows them all; "N undated in Tasks" opens the Tasks view. It writes no rule of its own. The input is the list every Tasks surface starts from (display filter, the daily-note date inference the Calendar panel uses, filterTasks), groupTasks decides which tasks are live Today and Upcoming work, and isOverdue decides lateness. So a [/] task is on the rail with its half-filled box, while @waiting and forwarded tasks stay in their own Tasks groups, as the list keeps them. The rail is a pure builder in lib/timeline-rail; the four task-line parsers are untouched. A task tagged #milestone draws as a diamond on a day labelled "· milestone", its row stands out, and it says how many open tasks are due before it, counted since the previous milestone (the first one counts from the top of the rail, overdue included). Counting from today instead made every later milestone repeat the earlier ones' work; the rail already shows the running position, so the number says what it cannot: the size of this milestone's own stretch, each task counted once. It counts what the filter leaves, so it matches the rows above the diamond. A file task's body tag counts, as in the filter. The tag is special nowhere else. Rows reuse the task pieces: TaskStateBox, the shared task menu (with the inline Edit the calendar has; taskTail and the priority marks moved to lib so both panels and the Tasks list share one copy), the note hover preview, and toggleTaskFromList, so a ticked task leaves the rail the way it leaves the calendar's list. Keys: Mod+7, leader n, :timeline and Toggle Timeline in the palette, all in the keymap catalog and rebindable. Leader n opens the panel with the keyboard in it, as leader c does for the calendar. With Vim on, j/k, gg/G, x, m (task menu) and / (filter) work; with Vim off only the arrows, Home/End, Enter and Escape, the hints in the footer show only with Vim on, and the handler stands down while a menu or dialog is open. Escape or Left hands the keyboard back. The handler is the panel's own onKeyDown, which runs after VimNav's capture handler; VimNav stands down for the panel except for Ctrl+W and the leader, so leader n closes it from inside and leader h hints over its rows. The panel claims focusedPanel 'timeline' on its own capture handlers, after the pane's claim of 'editor'. It resizes, tucks into the rail, joins the Alt+hjkl and Ctrl+W cycle after the calendar, closes with :closepanel, and its open state lives where the other panels' does: sticky per pane, or per note in the workspace snapshot with "Keep panels when switching notes" off. Its width joins panelWidths (290 by default), which stay on the device like every panel width. The range is portable, timeline_range under [view] in config.toml beside tasks_view_mode: it is how you like to read your tasks, not a fact about a screen. The filter is session state, like the Tasks filter. The panel is one of the surfaces that keep the task cache fresh, and it rescans when it opens after a while closed, not on a quick remount while a note loads. Phones: their shells hide the pane toolbar and turn any right panel into a sheet with Done, so nothing new shows there. The web client gets the panel as desktop does. Docs: the in-app manual has a Timeline card with the #milestone convention, Mod+7 and Space n in the shortcut lists, <Space> n and :timeline in the Vim commands, and the Timeline in the panel cycle, the task menu and :closepanel texts. Tests: timeline-rail (overdue, late days, today, day nodes, quiet-day folds, range cuts, milestone counts, undated, the inferred date, the groups), keymaps (defaults, no clash, Cmd+7 vs Ctrl+7, rebind and unbind), pane-panels snapshot, the focus cycle, store and config.toml round trips, and TimelinePanel component cases in the style of CalendarPanel.test.ts. Typecheck clean; shared-domain 2188, app-core 3715, quicklook 18 and desktop 1241 pass. Driven over CDP in the built app (69 checks) and the web client at 390 px; the Vim editor, sidebar Vim and editor-improvements smoke suites pass on the same build.
…s out by date, overdue and milestones marked (#1007)
#971 asked for Move Note to Trash, Archive, Restore and Favorite on a key. None of them had a slot: the keymap table binds the app's built-in actions, 141 of them, while the palette has 187 commands, and a "note.trash" line under [keymaps] in config.toml was dropped by the normalizer without a word, although the manual said every shortcut was remappable. Commands now get keys of their own, in a map of their own: commandKeymaps, command id to binding, beside keymapOverrides and never mixed with it. One id lives in both worlds, nav.back (the `h` back-out action and the Go Back command), which is why the two cannot share a map. Every command with a fixed id is bindable and starts unbound; the ones the palette makes per vault (a saved Tasks filter, a workflow, a remote profile, an assistant's edit) are left out, since their ids come and go. A generated catalog in shared-domain lists the bindable ids with a stable title (bindTitle for the toggles whose title flips with state), guarded against drift by a test that rebuilds it from buildCommands(). A binding is a chord in the keymap table's format ("Mod+Backspace", or a function key; a bare letter would eat typing), or, with Vim mode on, a leader sequence stored as "Leader x t" so it follows a rebound leader. Precedence: a key the user chose wins over every default. The chord listener is on window in the capture phase, installed at import time right after the ignored-keys guard, ahead of VimNav, App's own shortcuts and CodeMirror: a command on Mod+Backspace trashes the note from the editor, where that key would delete to the line start, and one on Mod+L takes the key from Toggle checkbox. It stands down for What's New, Settings (and every recorder), the palettes, dialogs, menus, an open completion list and hint mode, and only listens in the main window, not quick capture or a floating note. A held chord runs once. Leader sequences are the exception: they run in VimNav after every built-in leader key, because built-in leader keys always win, and they show in the which-key hints. With Vim off they are inert. When a command's when() says no, nothing runs and a toast says why. The note family carries its reason ("Restore from Trash only works on a note in the Trash"), the three shared guards (an open note, a note in the editor, an open tab) get theirs in one place, and the rest say "isn't available here". Settings, Keymap gains a fifth group, Commands: title, category and id per row, found by any of those or the keywords, with the same Change, Unbind and Reset. A command that already shows a leader chord keeps it ("also Space l s"). The recorder moved out of SettingsModal into KeymapRecorder and onto ui/Modal; for a command, Enter saves, Escape cancels and Backspace clears, since its binding is never one of those keys. Conflicts are checked both ways: a key another of the user's own bindings holds is refused, a built-in default is allowed with a warning naming the action that loses it, and a sequence starting on a built-in leader key is refused (Space t x, since Space t is the template picker). Action rows show "Taken by command" and the like. Settings search finds a command row by its title. In the palette, Alt+Enter on a row opens that same recorder over the palette rather than jumping to Settings: the query and row stay put, Escape comes back to them, the row shows its key at once, and nothing is two Escapes away. The footer names the key. A bound key shows on its row ahead of any key the command already had, a sequence only with Vim on. Themes…, Switch Vault… and Run Workflow… gained a run() that opens the palette on their list, so they work from a key and from the ex line. On the ex line, `:bind <keys> <command.id>` takes Vim notation (<C-BS>, <D-BS>, <leader>xa, <Space>xa) or the Settings spelling, with the same refusals as the recorder. `:unbind <action.id>` keeps working, and `:unbind command <command.id>` takes a command's key away; a bare id that names no action but a command means the command, so nav.back stays the action and `:unbind command nav.back` is the command. Tests: parsing, normalizing (unknown ids, nav.back, leader refusals, order), conflicts both ways, chord and leader matching, the recorder steps, :bind and :unbind parsing and effects, dispatch with every bail-out and the when() toast, VimNav sequences and hints with Vim on and off, the palette's Alt+Enter flow, the Settings Commands group, and the catalog drift guard.
…nds] (#971) commandKeymaps joins the portable preferences, so a key given to a command syncs with the rest of the dotfile and a hand edit applies live. The writer puts it in its own table under the action overrides: [keymaps.commands] "note.trash" = "Mod+Backspace" "note.archive" = "Leader x a" followed by every bindable command as a commented reference line with its category and title (`# "note.trash" = "" # Note: Move Note to Trash`). Every command starts unbound, so the reference shows "" and the header says what a key may be. It is static, so the file reads the same bytes on every OS (#901). "" is a deliberate unbind, as in [keymaps], and is marked. The reader splits [keymaps]: action ids stay overrides, the commands sub-table becomes commandKeymaps. As a courtesy to anyone who followed the issue's example, a command id written straight under [keymaps] is read as a command key when no action has that id; nav.back, which both have, stays the back-out action. The canonical table wins over such a line, and since the app rewrites the file in canonical form on launch, the line moves into [keymaps.commands] the next time the file is written. The renderer validates what it reads like any other pref: unknown ids and keys it cannot read are dropped, and so is a sequence that starts on a built-in leader key, which could never be reached. The web client keeps these keys in its stored preferences, the same place as the overrides. Tests: the round trip with an unbind, the reference, the courtesy read and its precedence, byte-identical output across platforms, and a launch that moves a bare id into its table on disk.
The manual promised that every shortcut was remappable while the palette's commands had no keys at all. Now it can say so truthfully, and it says how: - a How-To, "Give any command a key": Alt+Enter in the palette, the Commands group in Settings, chords and leader sequences, what Vim off does, why built-in leader keys win, the precedence over defaults and the editor's keys, the toast, :bind and :unbind command, [keymaps.commands]; - Alt+Enter (in the command palette) among the global shortcuts; - a :bind entry on the ex line, and :unbind command beside :unbind; - the Commands group, the two-way conflict rules and Reset all in the Keymaps settings section; - [keymaps.commands] and the courtesy read in the configuration file entry.
… in Settings, config.toml, the palette and the ex line (#971)
…eir actions are bound to (#972) #972 found two ways a list view's binding went dead without a word. The sequence normalizer dropped `Mod`. `"nav.delete" = "Mod+Backspace"` was stored as a bare Backspace, so plain Backspace started deleting in the Archive and Trash views (behind their confirms) and Cmd+Backspace did nothing. Keydowns never spell `Mod` (they say Ctrl, Alt, Meta), which is why it was thrown away. Now the stored token keeps `Mod`, the portable "Cmd on macOS, Ctrl elsewhere" that the shortcut format already uses, so one config.toml means the same chord on a Mac and a Linux box; matching resolves it per platform (resolveSequenceToken, applied by getSequenceTokens and advanceSequence for every caller). A letter under Mod is uppercased like one under Ctrl. Vim's sequence form follows: Mod is <D-> on macOS and <C-> elsewhere, where it used to be <D-> everywhere. Entries already saved as the bare key stay as they are; nothing guesses which ones were meant to carry Mod. Then every list view opened with `if (e.metaKey || e.ctrlKey || e.altKey) return`, ahead of its nav.* checks, so a chord bound to one of its actions never reached it. One helper replaces that bail (lib/list-view-keys.ts): - a typed key reaches the view exactly as before, so bare x, d, j and k are untouched; - a chord reaches it only when it is the key of one of the view's own actions, and never when a global or tab shortcut, or with Vim on the pane prefix, holds it: those keep their key everywhere; - a chord never answers the view's hard-coded keys, so a binding on Alt+ArrowDown runs its action instead of moving the cursor; - with Vim off typed keys stay off, as the Vim-off rule says, while a chord bound to an action works in either mode. Converted: Archive, Trash, Quick Notes, Tags, Tasks (list), Unresolved Links, the Timeline panel and the Workflows list. Left alone on purpose, because none of them answers a bindable action and their letters are hard-coded: the Kanban board, the Tasks calendar, the calendar panel, the database grid, the Workflows canvas (its own Cmd+A comes before the bail) and the icon and color pickers, which are modals. AtlasView, What's New, the context menu, the date picker and the editor helpers keep theirs too. A binding that can still never fire is now said out loud instead of saved silently. Settings, Keymap marks the row "Never fires: Search notes has this key", and the recorder warns before Save; it does not refuse, since the user may be about to move the other key. The recorder also records a Backspace or Delete held with Ctrl, Alt or Cmd: Backspace always cleared the recording, so Mod+Backspace could not be recorded at all. A bare Backspace still clears. Tests: tokens with and without Mod on macOS, Linux and Windows, the bare entry left bare, resolved sequences and Vim sequences, the shared guard (typed keys, unrelated chords, a user chord, a global shortcut and the pane prefix in the way), and the Settings warning.
…h, and Move Note to Trash follows the sidebar (#972) The sidebar is where notes are walked with j and k, and it was the one list with no key to trash the row under the cursor: nav.delete was handled by the Archive, Trash and Workflows views only, so the mouse or the palette (which acts on the open note, not the row) were the only ways. Now, with the sidebar holding the keyboard: - `x` (nav.delete) with Vim on, and Mod+Backspace in either mode (a new action, Move sidebar note to Trash, nav.sidebarTrash, rebindable and unbindable like any other) act on the row under the cursor. With Vim off `x` does nothing, as every single-key list shortcut. - A note moves to the Trash after the same confirm the Archive view and the palette show. Escape on it leaves everything as it was. - When that note is part of a multi-selection, the whole selection goes, with one confirm ("Move 3 notes to Trash?"), through the batch path the selection menu uses, and the selection clears. - A note already in the Trash would be deleted for good, after the Trash view's confirm. The sidebar does not list trashed notes today (Trash is a row that opens its view), so this only guards the path. - A folder is left alone and a note says to use its menu. Deleting a folder takes everything inside and cannot be undone, while the same key on a note is a reversible move; one key meaning both on neighbouring rows is how a j/k walk ends in a lost folder. Any other row (a tag, the Trash row) gets "Only a note can go to the Trash from here". The cursor then sits on the row that took the note's place, or the one before it at a folder's end: the anchoring in lib/sidebar-cursor.ts already does that once the row is gone. What it did not cover is the open note: trashing it closes its tab, the next tab becomes the open note, and the sidebar's reveal effect read that as a new pick and pulled the cursor onto it. Measured in the built app: without a hold the cursor jumped from P23 to P20. The sidebar now holds its cursor while one of these actions runs. The editor is not focused afterwards. The typed `x` is handled in VimNav's sidebar handler, after the leader and pane-prefix machinery, so `Space x` still reaches a leader binding; it is consumed there, so a list view left active behind the sidebar never acts on its own row for it. The chord form is a capture listener App installs at import time, after the command keymap dispatcher (a key the user gave a command still wins) and ahead of every component, since the Archive and Trash views listen on the window too. It stands down for dialogs, menus, palettes, Settings and text fields, steps aside for a global shortcut on the same chord, and runs once per press. Move Note to Trash and Move Note to Archive, from the palette or a key the user gave them (#971), follow the same row while the sidebar holds focus and its cursor is on a note, selection rule included, instead of the open note. Elsewhere, or on a row that is not a note, they act on the open note as before. Their when() works with no note open, and a refusal says why ("This note is already in the Trash"). The sidebar's selection is React state, so it registers a small host (lib/sidebar-note-actions.ts) that hands over a snapshot, clears it after a batch and holds the cursor. Tests: the plan for a single row, a selection with and without the cursor row, a selection with folders, a trashed row, archiving; the chord listener (both platforms, Vim off, a user's nav.delete chord, focus, dialogs, text fields, a global shortcut, auto-repeat, order against a later window listener); the cursor-row runner; and the palette commands following the sidebar.
…st chords work (#972) - a How-To, "Trash notes from the sidebar": Mod+Backspace in either mode and `x` with Vim on, the one confirm for a selection, Escape, where the cursor lands, why folders are left to their menu, Move Note to Trash and Move Note to Archive following the sidebar, and the action's name in Settings; - a How-To, "Ctrl, Alt and Cmd keys in list views": a chord bound to a list action works with Vim on or off, `Mod` is portable, a global shortcut keeps its key, and what "Never fires" in Settings means; - `x` and Mod+Backspace in the sidebar shortcut table, which now says Mod+Backspace works with Vim off too; - the Tasks, tags and trash views note that a rebound chord works in either mode.
…ar, Move Note to Trash follows it, and list views hear their chords (#972)
…idebar keeps the keyboard With Vim mode off, a focused sidebar went through App's catch-all for panels, which treats every panel as a flat list: the arrows moved the cursor, Home and End jumped, Enter clicked the row, and ← or Escape handed the keyboard back to the editor. A folder could not be expanded or collapsed from the keyboard (→ did nothing, ← left the sidebar), no key opened a row's menu, and Page Up / Page Down did nothing. The handler that knows the sidebar's rows (folders, tags, date groups, databases), VimNav's handleSidebarKey, only listened with Vim on. That handler now serves the sidebar in both modes. With Vim off, a listener VimNav installs once routes the focused sidebar's keys to it, and the handler keeps the typed keys of its actions (j, k, l, h, o, m, /, gg, G, x) off in that mode through listViewBindingMatches, the rule every list follows. With Vim off: - ↑ / ↓ move the cursor, Home / End jump to the ends, and Page Up / Page Down move a screenful of rows (what the scroll box shows, less one). - → and Enter open the row, as l does: a note opens and the editor takes the keyboard, a folder expands. - ← collapses a folder, or from a note folds its folder and lands on it, as h does. It no longer leaves the sidebar; Escape does. - Shift+F10 and the Context Menu key open the row's menu. - Mod+Backspace trashes the cursor row, as before (#972). The listener stands down for chords, dialogs, menus, palettes, Settings, text fields and self-keyed surfaces, and the catch-all leaves the sidebar to it. It is installed once rather than per mode so it keeps its early place among the window's capture listeners, ahead of a list view left active behind the sidebar. Enter on a note now opens it the way Vim's Enter and l do (a kept tab), where the catch-all clicked the row (a preview tab). Vim mode is unchanged: there the gating is matchesSequenceToken, as before, and Home, End and the page keys are left alone (gg and G are its keys). The three Vim smoke suites and #972's sidebar driver pass on the built app. Reaching the sidebar with Vim off: Alt+H works. Focus Sidebar from the command palette did not, in either mode. Closing the palette gives focus back to the editor, the editor claims its panel again, and the palette's refocus, which read the panel after the command had run, then took the keyboard straight back. Measured over CDP: the sidebar took DOM focus, and the editor took it back a millisecond later. The palette now reads the panel a command moved the keyboard to as it ran, before that close lands. Commands that do not move the panel as they run see no change. A plain click on a note still opens it and hands the keyboard to the editor (#599), so Mod+Backspace right after one is the editor's delete to line start, undoable, never a trash. The clicks that mean the row keep the keyboard in the sidebar, where Mod+Backspace acts on the cursor row: Cmd-click and Shift-click (the selection), a folder row, a row's menu. Tests: with Vim off, the arrows, Home / End and the page keys move, Enter opens and hands over, → expands and ← collapses without leaving, ← steps from a note to its folder, Escape returns, the menu keys open the cursor row's menu, every typed key is left alone, chords pass, and nothing answers unless the sidebar holds the keyboard or while a dialog, a menu or a text field has it. With Vim on, j / k, G, gg, l, / and Escape as before, and Home, End and the page keys untouched.
When the sidebar's cursor sat on a row that was part of the multi-selection, the row drew the selection's tint and nothing else: NoteLeaf and TreeRow pick one class for a selected row and never looked at the cursor. A cursor walking through a selection vanished on every selected row, and x or Mod+Backspace then acted on a row that showed no cursor. #972's driver measured it without meaning to: with three notes Cmd-clicked, no row in the sidebar wore the cursor. A selected row under the cursor now keeps the tint and adds the cursor's ring, `vim-cursor-on-selected` (an inset 2px ring in the theme accent at 65%), the class the note list already uses for the same case. The ring stacks with the tint instead of replacing it, follows each theme's accent, and needs no new token or arbitrary value. Both row kinds share one helper. Only a class changes, so the windowed sidebar's DOM contract (the data-sidebar-* attributes the keys and range-select read) is untouched. Checked in the built app with P20 to P22 selected and the cursor on P21, in Gruvbox dark, Apple light, Catppuccin Latte (the loudest accent, peach) and Catppuccin Mocha: the cursor row carries the ring over the same tint as its selected neighbours, and they carry none. Tests: the helper adds the ring only under the cursor and keeps the tint either way, and the stylesheet's ring is an inset accent outline with no background of its own.
… tree, and the cursor shows on a selected row
…ments answer their keys With Vim mode off, these four panels went through App's catch-all for panels, which treats each as a flat list: ↑ / ↓, Home / End, Enter (not in comments), and ← or Escape back to the editor. Measured in the built app against Vim on, they lacked: - Note list: no menu keys (Shift+F10, the Context Menu key); ← went to the editor where h goes to the sidebar; → did nothing; End stopped at the last row on screen instead of the last note, because the list renders only the rows near the viewport. - Connections and outline: → did not open the row. - Comments: →, Enter, Backspace and Delete did nothing. - All four: Page Up / Page Down did nothing. The handlers that know these panels are VimNav's, and they only listened with Vim on. As 9b070af did for the sidebar, each now serves its panel in both modes: the listener VimNav installs once for Vim off routes the focused panel's keys to the handler Vim mode uses, and the handler keeps the typed keys of its actions off in that mode through listViewBindingMatches (the comments panel's own letters, o e r a d n +, through a vimMode check). The catch-all served only these panels and the sidebar, so it is gone, with the panel-rows helpers only it used. With Vim off, in each panel: - ↑ / ↓ move, Home / End jump to the ends, Page Up / Page Down move a screenful. - → and Enter open the row, as l does: the note list and connections open the note and hand the keyboard to the editor, the outline jumps to the heading, comments jump to the comment's text. - ← goes back as h does: from the note list to the sidebar, from the other three to the editor. Escape returns to the editor. - Shift+F10 and the Context Menu key open the row's menu in the note list, the one panel of the four whose rows have one. - Backspace and Delete delete the selected comment, after a confirm. - Letters do nothing. A page in the note list, which renders only the rows near the viewport, is the rows its box shows, less one, so the row the cursor leaves stays on screen. It is counted by the step from one row to the next and the height inside the box's padding: the note list's rows sit 4 px apart in a padded box, and counting them by their own height paged one row too far. The sidebar's page goes through the same helper and stays what it was (its rows touch and its box has no padding). Connections, the outline and comments render every card, and the cards differ in height, so a page there is the furthest card that still fits on one screen with the one the cursor leaves: a tall card never makes the cursor skip one the screen has not shown. Enter on a note in the note list now opens it the way Vim's Enter and l do (a kept tab), where the catch-all clicked the row (a preview tab). Two deliberate changes with Vim on: - Deleting a comment from the keyboard asks first, in both modes: d, Backspace and Delete with Vim on, Backspace and Delete with Vim off ("Delete this comment? This cannot be undone."). A deleted comment has no Trash and no undo, and Backspace is the key most often pressed by accident. Escape on the confirm leaves the comment and the cursor where they were. The card's own Delete button, a deliberate click, still deletes at once, without asking. - Enter or l on a file row in the note list (an image beside the notes) opens it in a file tab, the way its click does. Vim mode handed the file to selectNote, which loaded the PNG into the editor as a note's text; the catch-all clicked the row, so Vim off got it right and would have inherited the bug. Otherwise Vim mode is unchanged: its gating is still matchesSequenceToken, and Home, End and the page keys are left alone (gg and G are its keys). The Vim-off listener also yields to the surfaces Vim mode yields to by DOM focus (the calendar and timeline panels, the selection toolbar, an Excalidraw canvas): the store can still name a right-side panel while one of them holds focus. Reaching the panels: Alt+H and Alt+L reach each one with Vim off and the panel keeps the keyboard. The palette has no Focus command for any of the four (it has Focus Sidebar and Focus Editor; Focus Pane Left and its siblings move between editor splits), so the refocus 9b070af fixed for Focus Sidebar has nothing to hit here. Checked in the built app over CDP, Vim off, in each panel: reached with Alt+H / Alt+L, walked, paged (the row left behind still on screen), End and Home, → opens, ← goes back, Escape returns, letters do nothing; Shift+F10 and the Context Menu key open the note list's menu; Backspace and Delete in comments ask, Escape keeps the comment, Enter deletes it on disk; the card's Delete button deletes without asking (58 checks). Vim on: j / k, G, gg, m, h, l in each panel, Page Down and End left alone, d and Backspace ask, l on a file row opens a file tab (24 checks). The three Vim smoke suites pass on the same build. Tests: per panel, with Vim off the arrows, Home / End and the page keys move, → and Enter open, ← and Escape go back, every typed key is left alone, and nothing answers under a dialog or a menu or from a text field; the note list's menu keys open the cursor row's menu and its file row opens through its click; comments' Backspace and Delete ask and delete only on yes. With Vim on, the letters as before, Home, End and the page keys untouched, d / Backspace / Delete ask. The page helpers: the page keys only with Vim off and never as a chord, a page counted by row step inside the padding, and a tall card never skipped. 17 of the new panel tests fail on the code before this change. The manual says how each panel answers with Vim off, and that deleting a comment from the keyboard asks first.
…, outline and comments answer their keys
With Vim mode off the command palette still showed keys that only Vim mode has: leader chords (Space d for today's note, Space l s for Favorite), the Ctrl+W pane prefix on the Focus Pane commands, the fold keys (z c, z M), Ctrl+O / Ctrl+I on Go Back and Go Forward, and ex commands (:w, :format, :cover!, :split, :tasks, :tag, :unresolved). None of those keys does anything with Vim off, which is the leak the Vim-off hint audit closed everywhere else on screen. A few rows already guarded their leader chord by hand; most did not. The palette's key helpers now carry the rule once: leader chords, the pane prefix, ex commands and the vim.* keys show only with Vim mode on. Go Back and Go Forward show their global keys (Alt+Left / Alt+Right) with Vim off instead of nothing, since those work in both modes. Keys a user gave a command (#971) are untouched by this. Test: with Vim off no palette row shows a leader chord, the pane prefix, an ex command or a Vim-group key (17 rows did before), with Vim on they still show, and Go Back / Go Forward show the global keys.
… on the next one Deleting the active comment clears activeCommentId in the store, and the comments panel puts a cleared cursor on the first comment, so after d / Backspace / Delete the cursor jumped back to the top of the list and a run of deletes could not walk down it. The keyboard delete now notes the comment after the deleted one in the panel's order (the one before it, when the last goes) and makes it active once the delete has run. Cancelling the confirm still leaves the cursor where it was, and the card's own Delete button is unchanged. Tests: after deleting c1 the cursor is on c2; after deleting the last comment it is on the one before. Both fail on the previous code.
…our machine
Adib changed the Unsplash design: each person pastes their own Unsplash
Access Key and the app asks api.unsplash.com directly. The zennotes.org
proxy and its one shared key are gone from the app (the website's
feat/unsplash-covers stays unmerged), so the release waits on nothing
from Unsplash, and one person's searches never spend anyone else's hour.
The key goes in under Settings, Typography, in a new Covers section that
now also holds the Note covers placement row. Paste and press Enter (or
Save): the key is checked first with one request, GET /photos?per_page=1,
and kept unless Unsplash answers 401, so a mistyped key or a Secret Key
is refused on the spot ("Check that you copied the Access Key, not the
Secret Key"). A key that could not be checked (no connection, the hour
spent) is kept, and the line says why. Test asks again and says what is
left of the hour ("48 of 50 requests left this hour"); Clear removes it.
The field is write-only, so a saved key is never drawn back. The buttons
stay enabled while a request is out, and when the one that had the
keyboard disables itself (Clear, or Save once the field empties) the
field takes the keyboard rather than the page. Settings search finds the
row by "unsplash", "cover" and "api key", and Set Unsplash Access Key…
in the palette opens Settings in the field.
Desktop main keeps the key where the Cloud credential lives: sealed by
Electron safeStorage in the secrets file under userData (secret-store),
never in config.toml, vault.json, a log, an error, or anything the
renderer keeps. The renderer hears only whether a key is set, where it
is kept and whether it could be. Without OS encryption (a Linux
compositor Chromium does not recognize, as on Niri) the key is refused
rather than written in the clear, and the row suggests
--password-store=gnome-libsecret.
Search, the check and the download ping go from main to
api.unsplash.com with Authorization: Client-ID <key> and Accept-Version:
v1, with no cookies, a bare user agent, no language and no referrer;
the key travels in the header, never the URL. Search asks for
landscape, safe-search photos, 24 a page, and the ping is a GET of the
photo's download_location with the key. The 40-a-minute cap per run
stays, and now keeps a renderer loop from spending the person's own 50
an hour. The answer is Unsplash's own JSON, so shared-domain unsplash.ts
reads it directly (user.links.html, links.download_location,
alt_description) and keeps the proxy's rules: images only from
images.unsplash.com, links only to unsplash.com, a ping only to an
api.unsplash.com download endpoint, no Unsplash+ photo. The client
itself (status, save, test, clear, search, ping, the cap and the
argument checks) moved to shared-domain as createUnsplashClient, so
desktop main and the web client run the same code with their own fetch
and key store. ZENNOTES_UNSPLASH_PROXY_URL becomes
ZENNOTES_UNSPLASH_API_URL, honored as before only unpackaged or with
ZEN_PERF=1.
Without a key the Unsplash tab sends nothing and shows a setup panel in
place of photos: what the key is for, the three steps (create a New
Application at unsplash.com/developers, copy its Access Key, paste it
in Settings), Open Settings and the developers page. The field keeps
focus as it does over the tiles: ↑/↓ choose, Enter runs, so Space l u
then Enter is the whole way to the key field, with Vim on or off. A
refused key (401) reads "Unsplash did not accept your access key" with
Change key; a spent hour (403 with X-Ratelimit-Remaining at 0) explains
that a new Unsplash app allows 50 requests an hour until Unsplash
approves it for production, or names the allowance Unsplash reports;
no connection is a plain sentence.
The web client keeps its key in the browser, under its own
localStorage entry that only the bridge reads, and calls Unsplash from
the browser. api.unsplash.com answers any origin, allows the
Authorization header in its preflight and exposes X-Ratelimit-Remaining,
and znserver's CSP already allows https: in connect-src, so the key
never reaches the self-hosted server or zennotes.org. A browser has no
keychain to offer, the web client's own session is a cookie and not in
that storage, and the key is worth an hour of photo searches and can be
revoked at unsplash.com/oauth/applications, so the tab stays on the web
rather than going dark there. The phones still have no tab.
Not here: the website's docs and privacy text are on its
feat/unsplash-own-key branch.
Verified in the built app over CDP (56 checks), both stores isolated
and --use-mock-keychain so the real Keychain is never touched, against a
local stand-in for api.unsplash.com: Space l u and :cover unsplash fog
without a key showing the panel and sending nothing; ↓ and Enter
opening unsplash.com/developers; Enter on Open Settings landing in the
highlighted key field; a malformed key refused without a request and a
wrong one refused after one, neither stored; the right key saved sealed
(the secrets file holds it encrypted, no file under the run holds it as
typed, nor the page or localStorage); Tab to Test; a search with
Client-ID, no cookie, referrer or key in the URL, and the Unsplash+
photo left out; Enter writing the banner and credit, one ping with the
key, the credit on hover; the rate-limited, refused-key (Change key
into Settings, then Test) and offline messages; Clear from the
keyboard and the panel coming back; the palette command; Settings
search by unsplash, cover and api key.
Tests: the key store against a stand-in safeStorage (round trip,
cleared, sealed on disk, refused without encryption, never in config or
an answer), the API client (headers and query, 401, 403 with and
without allowance left, network, the ping's target, the check, save,
clear, the cap, argument checks), the no-key tab (the panel, no search,
Enter into Settings, ↓ Enter to the developers page, Change key, a key
that works), and the messages. shared-domain 2,198, app-core 3,992,
desktop 1,250 and quicklook 18 pass; typecheck clean.
How to test locally: build the desktop app and run
electron apps/desktop/out/main/index.js. Open a note and press Space l u:
the tab shows how to get a key. Press Enter, paste the Access Key of an
app made at unsplash.com/developers and press Enter: the line says
Saved, with the requests left this hour. Escape, Space l u, type "fog",
pick a photo with Enter: the note gains banner, banner_credit and
banner_credit_url, and hovering the cover shows "Photo by ... on
Unsplash". Back in Settings, Clear: the tab asks for a key again.
…adds, straight to Unsplash
An Unsplash cover leaves four lines in a note's frontmatter, and the first is a URL a few hundred characters long. Adib opened such a note in Edit mode and called the properties card too much text to look at. The cover drawn above the note already shows the picture and its credit, so in Edit and in Split's editor (Live Preview) those lines now fold into one muted line in the card, where the first of them was: cover · Unsplash photo by Kalen Emsley cover · lisbon.jpg an image in the vault cover · Dusk, cover · The Great Wave a gradient, a gallery photo cover · image from example.com any other web image cover nothing more to say The other properties keep their rows, so a note with only cover keys shows a card of one line, and tags and due dates stay as they were. Which lines fold comes from the shared cover reader, not a second key list. readNoteCoverLines (shared-domain) finds the line of each key the cover owns (banner, banner_y, banner-y, banner_credit, banner_credit_url, or the cover family when a note uses Obsidian's cover:), with the indented lines a YAML value runs on to, and noteCoverFieldKeys is now the one list that setNoteCover and removeNoteCover clear too. The line names a gradient or a gallery photo by the title the picker shows, matched by the file name the pick wrote (builtinCoverForAsset). A cover nothing draws folds nothing, and neither does a pane with covers hidden in Settings, where the raw lines are the only place the cover shows. Source mode and Preview are unchanged. The lines open while the selection touches one of them and fold again when it leaves, the rule Live Preview uses for a heading's #, and a note being read (a phone with the keyboard down) keeps them folded. j/k and the arrow keys step into the folded line one logical line at a time, the way they step into a rendered math block: k from under the card lands on the last cover line, j from above on the first, with Vim on or off, in normal and insert mode. A banner_y written later under other keys takes no row and opens the same way. A click on the line opens it with the caret at the start of the first cover line. Opening the lines from below grew the card above the caret and drew the caret two to four rows lower than the line it stepped onto. The transaction that opens or closes the fold now notes where the head's line is drawn, and the scroll that follows puts it back there, after Vim's scrolloff. A note shorter than the window gets an empty box under the text to make that room, gone again once the view fits the note. At the very top of a note nothing can scroll further up, so closing the lines downward there still pulls the text up by the folded height. It is decoration only. The document never changes, so select-all, copy, find and undo see the raw lines, and no file is written. The in-app manual says all this in the two cover guides and the Note covers setting. Verified with unit tests (the line reader, the line's text for each cover kind, opening and folding, Vim and arrow stepping, the click, the scroll back), typecheck, test:run, the three Vim smoke suites, and 53 CDP checks on the built app: Vim on and off, insert mode, Split, source mode, a short note, a mixed note and a banner_y under tags.
… line in Edit mode
…ugh one handle CodeQL flagged a file-system race in drainShareInbox (js/file-system-race, high): it stat()ed a capture to enforce the 2 MB cap, then read it with a separate readFile, so a capture swapped or grown between the two calls could slip past the cap. The share extension writes these files into the app's own Application Support folder, so the window was narrow, but the check should hold anyway. The drain now opens the capture once, reads its size from that handle and reads the text from the same handle, and sets aside anything over the cap whether the stat or the text says so. A capture that is gone or locked is still left for the next drain, as before. Test: a capture over 2 MB is set aside into the unreadable folder and never filed (there was no test for the cap before).
The 2.79.0 release PR (#1010) failed on three test problems, none of them in the product: - commands.test.ts checked "no Vim key in the palette with Vim mode off" by comparing displayed keys, and on Linux and Windows Open File's Mod+O and Vim's Ctrl+O both read Ctrl+O, so the global key was flagged. The six commands that show a Vim-group key are now checked by id. - test-app-core-browser.mjs replaced a note's text with Mod+A then typing. Since #1006 the first Mod+A leaves a note's leading `# Title` out (the second takes the whole note), so a note that starts with a title kept it and the saved file never matched. Every step that replaces a note's text now selects all twice; on a note without a title both presses select everything, so nothing else changes. - unsplash.test.ts walked a directory with stat() then readFile() on each path, which CodeQL reports as a file-system race even in a test; it now reads the directory's entry types instead.
noteFolderSubpath stopped being used in commands.ts during the 2.79.0 cycle (CodeQL js/unused-local-variable on the release PR).
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.
Release 2.79.0: the Timeline panel (#1007), a key for every palette command (#971), Share to ZenNotes on macOS, Unsplash covers with your own key, a cover's frontmatter folded to one line, a Status bar switch, sidebar trash by key (#972), the keyboard with Vim mode off in the sidebar and every side panel, and fixes for #1003, #1006 and #1008. The release workflow now marks a release latest only once all five update feeds are on it.
Gates on 77c2afb: ci-sweep since v2.78.0 found no failed jobs; hazard scan clean; production audit has no high advisories; typecheck and test:run (shared-domain 2203, app-core 4016, desktop 1250, quicklook 18) after a fresh npm ci; build:prod; a Developer ID signed pack whose Share and Quick Look extensions verify; the packaged launch check (page target, drawn, 1.5 s, version 2.79.0); vim-editor, sidebar-vim and editor-improvements all passed.
Full notes go on the GitHub release.