Release 2.46.0: comment threads an assistant can join, a Kanban board per folder, unbind shortcuts, ignored keys for tap-hold remappers, saved Tasks filters, custom templates on remote vaults, a calendar in the @ menu, root-mode CLI, math in callouts - #749
Merged
Conversation
The only way to get rid of a shortcut you kept hitting by accident was to remap it onto some chord you would never press: the recorder needed a key, the config file needed a value, and an empty override was quietly thrown away in favor of the default. Every keymap row now carries an Unbind button next to Change and Reset, the recorder's Clear became Unbind (Backspace still clears a recording), and an unbound row reads Unbound with the Custom badge until Reset brings the default back. The stored form is an override equal to the empty string. It is a deliberate state, distinct from a missing override (the default applies) and from a recording that has not captured a key yet: normalizeKeymapOverrides keeps it, getKeymapBinding returns it instead of falling back, and the matchers, the conflict check and the user-override scan treat it as a key that does not exist. TOML has no null, so config.toml carries it as "action.id" = "" with a # unbound marker, the reference block explains the convention, and a hand edit applies live like every other config change. Everything that advertises keys follows suit. The which-key hints drop an unbound leader action, the command palette shows no chord when any step of one is unbound, tooltips lose their parenthetical, and the manual prints "Unbound" where the key used to be. CodeMirror files an empty key name without complaint and would run the command on a keydown whose key is empty, so editor keymap entries now go through keyBindingsFor, which returns nothing for an unbound action; VimNav's leader and pane prefixes fall back to the empty string, which never equals a token read off an event, rather than reviving Space and Ctrl+W. Vim users get :unbind action.id, the ex twin of the button: it removes the key and a toast names what it was. Bare :unbind, or an id the catalog does not know, opens Settings on the Keymap page, which lists every id, instead of guessing. Both docs surfaces describe the button, the ex command and the config spelling; the website half lives in its own repository. Verified with unit tests for the model, the config round trip, the half-page keymap and the Settings row, and by driving the built app over CDP through the row Unbind, :unbind with a known and an unknown id, the leader hints before and after, a hand-edited "" picked up live, Tab completion of :unb, and Reset restoring the default.
Settings, Templates said custom templates require a local vault in the self-hosted web client and in desktop remote mode, because nothing served the vault's .zennotes/templates/ over HTTP; only the built-in templates, which are renderer constants, worked there. @ajselzilic asked for the remote half in #723, and @flokchvtr had explored the server and web side on a fork branch after #592. The server now serves that directory through four authenticated routes (list, read, write, delete). The vault layer is a synced copy of the desktop module, byte for byte: the same slug rules, the same flat directory and .md-only validation, the same adr, adr-2 de-duplication and rename-removes-previous behaviour, atomic writes under the vault lock, symlinked escapes refused. A template's id is custom:<filename stem>, so both sides have to land the same bytes on the same filename, and the comment on each copy says so. Writes past the configured note limit answer 413. The server advertises the routes as supportsCustomTemplates; the web bridge and the desktop remote client gate on the flag, so an older server keeps the section read-only with a note naming the remedy and the built-ins keep working, and a local vault behaves exactly as before. Changes travel between clients. Both watchers, the Go one and the desktop's, report .zennotes/templates/ under a templates scope of their own, because with the empty scope the renderer treats a .md path as a note and re-lists the whole tree; the store re-lists templates on that scope and after a change-feed gap. The Go watcher also learned to watch directories created together with a parent that raised one Create event: the first template written into a vault with no .zennotes/ yet created both directories in one go, and the inner one stayed unwatched until a restart. Found on the way: the desktop read the remote workspace info before getCurrentVault connected the server and then kept that copy, so capabilities and bootError stayed null for the whole session and Settings could never learn what the server advertises. init re-reads the info once the vault is resolved. Tests on every layer: Go (slug, path, de-duplication, symlink and listing rules; auth, CRUD, traversal, 413 and the capability; watcher scope and the late walk), desktop (the four routes against a loopback server, the watcher scope), app-core (the scope, the resync fan-out, the post-connect re-read, the Settings gate with and without the flag). Verified live: the web client in a browser against a real server (create, rename, delete, a file dropped into the vault arriving through the WebSocket feed) and the desktop in remote mode over CDP. Both docs surfaces describe it; the website half lives in its own repository.
…ot (#745) With Settings, Vault set to "Vault root", zn create and the MCP create_note tool still wrote new notes into inbox/, and vault_info reported inbox, while zn list and zn search found root-level notes fine. @diazkev314 hit it on 2.45.0 and could not find where the CLI read primaryNotesLocation at all. It did read it, and then let the layout outrank it. The CLI and the MCP server share readPrimaryNotesLocation, which decided the mode from the vault's directory tree first (loose content at the root meant root, notes inside inbox/ meant inbox) and consulted vault.json only when the layout was ambiguous. That rule dates from the day root mode shipped, written so a vault switched in Settings but not yet migrated kept filing new notes next to its old ones. The app does the opposite: an explicit setting is the answer, and the layout is inferred only when vault.json leaves the question unstated. With old notes still in inbox/, the two halves disagreed on the same vault: Settings said root, the app created notes at the root, and the CLI kept writing into inbox/. The file wins now, on every side. An explicit primaryNotesLocation decides, and only a vault without one, or one a sandboxed process cannot read, is inferred from its layout, the same way the app infers it. The count of notes inside inbox/ that used to outrank the file is gone. Reads were already right and are unchanged. Three tests pin the order: the file says root while old notes sit in inbox/, the file says inbox while loose notes sit at the root, and no file at all. Verified through the running app over CDP: the vault switched to Vault root in Settings, then zn create with no --vault (it follows the vault the app has open) and the MCP vault_info and create_note tools over stdio, with both new notes appearing in the app at the root while the old ones stayed in inbox/.
A $$ block inside an Obsidian-style callout rendered nowhere. In the editor it stayed raw, and in the reading view the callout lost its body while, with Typst selected, one failing formula swallowed the rest of the note. @OstrichDowneyJr reported it with Typst; KaTeX has the same two bugs, it just fails more quietly. The editor's live preview only renders a block whose fences own their lines, and it counted the "> " before a callout's fence as prose. It now accepts blockquote markers there, strips them from the formula (they are the quote's, not the math's), and gives the rendered block the callout card's own classes, resolved from the outermost Blockquote node, so the card cm-wysiwyg-blocks draws line by line stays whole around the widget instead of breaking in two. The reading view's fault was in normalizeBlockMathFences, the pre-pass that rewrites editor-legal fence shapes into the form remark-math parses. After copying a canonical block that came earlier in the note it advanced one line and re-scanned that block's closing fence as an opener, then paired it with the callout's "> $$", which it took for content hugging a fence, and wrote a bare $$ outside the quote. The pass now splits every line into its quote prefix and content, matches fences on the content, writes the prefix back on whatever it emits (an empty ">" line where a blank would end the quote), closes a block only at its own quote depth, and moves past a canonical block instead of re-reading its closing fence. The currency guard's raw source slice also drops the markers of continuation lines, so a span demoted to text inside a callout no longer comes back as "$ > x $". Bare "$" on its own line is still not a display block in ZenNotes, with either engine; that is "$$". Reproduced on the pre-fix build in both views and verified on the fixed one in the built app with Typst selected: the callout's block renders inside the card in the editor, the reading view renders both blocks with the right source, and inline math inside callouts is untouched. Tests cover the editor scan and the normalizer, including the fence that merely ends a prose line and the one-line "> $$x^2$$" form.
…t color (#746) With Typst selected, inline math sat visibly smaller than the text around it, where the KaTeX rendering of the same formula fit. @cyperion saw it on 2.45.0 and reported it as a scaling bug, which it is. KaTeX draws Computer Modern at 1.21 times the surrounding text size; its own stylesheet says `.katex { font-size: 1.21em }`, because the family sits small on its em square and a plain 1em reads undersized next to prose. The Typst SVG was sized at that plain 1em: its dimensions come back in points at the 11pt the formula is compiled at, and the wrapper divided by 11 to get em. New Computer Modern shares the metrics, so the same formula came out a fifth smaller, 52 px wide against KaTeX's 63 for `E = h nu` at the default text size. The wrapper now applies KaTeX's factor to both dimensions, in the editor and the reading view, display blocks included, so switching engines no longer changes how big the math is. The Math size setting still scales on top. Found on the way: the recolor that makes Typst glyphs follow the theme only knew fills. Typst exports glyphs as black fills and the rules a formula draws as shapes, a square root's bar and a fraction line, as black strokes, so those stayed black and vanished on a dark theme. Every black fill or stroke is recolored now, in the spellings Typst's export uses. styleSvg is exported for the new test, which pins the factor and both recolors. Measured in the built app before and after on the same note, with the KaTeX rendering as the reference.
Typing @ offered Today, Yesterday, Tomorrow and Now; any other day meant typing it out by hand, which is exactly the case a date picker exists for. The list now ends with Date… (@Date, @cal and @pick narrow to it). Picking it opens a calendar on today with today's cell focused, so the keyboard flow is complete without a Tab: arrows move a day or a week, PageUp and PageDown change the month (Shift: the year), Home and End go to the ends of the week, and Enter inserts the ISO date where the @ stood and returns focus to the note. A digit pressed on the grid moves typing to the field, and the grid follows the typed date as it takes shape; a day the month does not have is refused rather than rolled forward. With Vim mode on, h j k l move and t jumps to today; with it off, letters stay inert, the rule the list views follow. Escape leaves the note as it was before the @. The quick options are unchanged, and the calendar honours the week start setting. The trigger is removed before the calendar opens, so a dismissed picker leaves clean text and a picked date lands exactly where the @ was; the position is re-clamped at insert time in case the document shrank under the modal. The picker is a prompt in the app's sense: it is requested through promptDate(), the calendar twin of promptApp(), hosted next to the prompt and confirm hosts, and carries data-prompt-modal so VimNav and the list views hand over the keyboard. A month change replaces every grid cell, which drops focus to the body before the effect that refocuses runs, so the decision to refocus is recorded by the move itself; the built app caught this where jsdom did not. Both docs surfaces describe the calendar. Verified with unit tests for the date math, the modal (keyboard, paging, typed dates, Vim gating) and the menu's hand-off, and in the built app over CDP with Vim on and off. Closes #743
The filter box narrows the Tasks views to one project, one area, one
context, but it was transient: the same query had to be retyped every
time, so in practice people stopped reaching for it and went looking for
a grouping mode instead. Grouping has one axis; filters compose.
A query is now saved under a name, as the [saved_filters] table in
config.toml ("Project alpha" = "@project:alpha", one line each), so it is
diffable, syncs with the rest of the preferences, and a hand edit applies
live. Recall is the point, so it is cheap from everywhere: a chip row under
the Tasks header shows the saved filters in file order, a click applies
one and a second click clears it; `:filter <name>` applies the saved
query when the text is a saved name (any other text filters literally, as
before); F (Vim mode) opens a picker that narrows as you type; and the
command palette lists every saved filter as "Tasks: name", which opens
the view already filtered from any note. Saving is a chip too: an
unsaved query shows Save filter…, which asks for a name and offers the
existing ones for an overwrite; `:savefilter <name>` does it from the ex
line and `:delfilter <name>` forgets one. A chip's context menu renames or
deletes it.
Names match case-insensitively wherever a user types one, and the stored
spelling is what the chips show. Edits keep the chip order: an overwrite
or a rename stays in place, a new filter goes last. The map is a portable
pref like the keymap overrides, validated on the way in from the file and
from localStorage, so the web client keeps its saved filters in the
browser. The Tasks list keys stay Vim-gated: with Vim off, the chips and
the palette are the way in.
Both docs surfaces describe it. Verified with unit tests for the map
helpers, the config.toml round trip, the palette entries and the store,
and in the built app over CDP with Vim on and off: chips from the file,
the picker, the ex commands, a hand edit picked up live, and the palette
entry from a note.
Closes #731
…#738) A note's comments were a one-voice affair: flat, unsigned, and reachable only from the app. Reviewing a draft with an assistant meant pasting the note into a chat and carrying the answers back by hand, which is exactly the loop the comments panel exists to avoid. Comments now thread and carry a name. A record has an optional author (absent for the vault's owner, the assistant's name otherwise) and an optional parentId that files a reply under a top-level comment; the panel shows each thread as one card with its replies, the avatar and name on every entry, and `a` (or the Reply arrow) opens a reply box under the selected thread with ⌘↵ to send. A reply keeps its thread's anchor, so the editor draws one marker per conversation and re-anchoring moves a thread together. The MCP server gets four tools on the same sidecar: list_comments (each thread with the anchored passage, the line it sits on now, who wrote what, and the replies), add_comment (a new thread anchored to a passage copied from the note, or note-level), reply_to_comment and resolve_comment. Replies are signed with the connected client's name from the initialize handshake (claude-code reads as Claude Code, claude-ai as Claude, codex-cli as Codex), so its words and the user's stay apart in the panel. The server instructions tell a model to hold a review through the threads rather than by editing the body, and to resolve only when the user says so. `zn comment list|add|reply|resolve` exposes the same operations, and both work against a folder or a ZenNotes server. The record shape is one module now, shared-domain/note-comments.ts, used by the desktop main process, the MCP server and the CLI, with the Go server as its mirror; the anchor helpers moved there from app-core. A reply whose parent is missing stays as a comment of its own rather than vanishing, on every side. Both docs surfaces describe threads, the `a` key and the tools. Verified with unit tests (shared normalizer and threading, comment-ops on a temp vault, the MCP tool list and reply attribution, the CLI group, the Go round trip) and in the built app with a real MCP session: a comment from the keyboard, the assistant's reply and its own anchored thread arriving live in the panel, a reply from the panel, and a resolve moving the thread. Closes #738
…e folder's children (#730) "Group by: Folder" grouped by the four built-in folders, so a vault organised as Projects/<name>/... under the inbox got one column holding everything plus an empty Quick. People worked around it with a hand-maintained @status token per task, derived from the note's folder, which drifts the moment a note moves and spends the status field on something the folder tree already knows. The board now gives every folder that holds tasks its own column: the Inbox root, Projects/alpha, Areas, Quick Notes, in system-folder order with subfolders alphabetical, and no empty columns. A new kanban_folder_root (Settings, Tasks, Folder board; `:folderroot Projects` on the board; the header chip; `kanban_folder_root` in config.toml) points the board at one folder: its children become the columns, deeper notes roll up to their child, notes in the root itself get the root's column, and everything else shares a trailing Other folders column. The root is a per-vault view setting like the group-by, and a portable pref like the rest. A note's place is read from its path through the system-folder rules, so a remapped inbox and a root-mode vault classify the same way the sidebar does; column ids are the note directory, so title and order overrides key on something stable, and the id grammars accept paths. Folder columns stay read-only: a card's column is where its note lives, and moving the note is the way to change it. Both docs surfaces describe the board and the root. Verified with unit tests (location classification, column building across inbox mode, root mode, a remapped folder, a root with roll-up and Other folders, archive hiding; the store normalizer and per-vault override; the config round trip) and in the built app over CDP: the default columns, `:folderroot Projects`, the chip, Settings, config.toml, and clearing. Closes #730
Keyboard remappers with tap-hold layers (Kanata, QMK, ZMK) emit a harmless extra key with every keystroke, on Linux usually the Katakana/Hiragana key, which Chromium reports as KanaMode. Editors that map only the keys they know ignore it; here every stray keydown reset a pending sequence: `jk` never left insert mode and typed itself instead, `dd` deleted nothing, a leader chord or hint mode died halfway. Neovim users with home-row mods had no way to get the same tolerance from ZenNotes. Settings, Keymap gains an Ignored keys row: Record a key, press the no-op, and it is listed by name (its DOM key, or its physical code when the layout reports Unidentified). `:ignorekey <key>` adds one from the ex line and bare `:ignorekey` opens the page; `ignored_keys = ["KanaMode"]` under [editor] in config.toml is the portable form. The list is enforced by one guard on `window` in the capture phase, installed when the app module loads rather than from an effect: children's effects run before App's, so a VimNav or editor listener would otherwise see the key first. It swallows keydown, keyup and keypress, so a handler that tracks a held key never sees half a pair, and it stands aside while the recorder is capturing, since that is the one moment the key must be seen. Both docs surfaces describe it. Verified with unit tests (normalization, matching by key and code, the guard blocking element and document listeners while leaving other keys alone, the recorder bypass, single install; the store edits; the config round trip) and in the built app over CDP: with an empty list a stray KanaMode between j and k left the editor in insert mode and between d and d deleted nothing, exactly the report; with the key recorded, both sequences work, config.toml carries the key, and :ignorekey adds and lists. Closes #732
Seven features and three fixes, each with its captioned demo clip and the website caption track, force-added like the 2.44.0 set since docs/releases is ignored by default.
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.46.0: seven features and three fixes. Cut with
docs: draft the 2.46.0 release notes and post(2131393) andchore(release): 2.46.0(da59c37); main fast-forwarded and tag v2.46.0 pushed on 2026-09-08.jk,dd, leader chords and hints. A newignoredKeysportable pref (ignored_keysunder[editor]), a Settings → Keymap row with a recorder, and:ignorekeyfeed one capture-phase guard onwindow, installed at App module load so it precedes every other listener; it swallows keydown, keyup and keypress and stands aside while the recorder captures.kanbanFolderRootpref (Settings,:folderroot, the header chip,kanban_folder_rootin config.toml, per vault) makes one folder's children the columns with deeper notes rolling up and the rest in Other folders. Placement goes through the system-folder rules (remapped folders, root mode); column ids are note directories and the title/order grammars accept them; folder columns stay read-only.author, absent for the vault owner;parentIdfor a reply), the panel shows threads with a reply box ona, and the MCP server gainslist_comments,add_comment,reply_to_commentandresolve_comment, signing with the connected client's name;zn commentmirrors them. The record shape is one shared-domain module used by desktop main, MCP and CLI, with the Go server as its mirror. Replies keep their thread's anchor, so the editor draws one marker per conversation.:unbind action.id, with a toast naming the old key; bare:unbindor an unknown id opens the Keymap page, which lists every id. The unbind travels in config.toml as"action.id" = ""(marked# unbound, documented in the[keymaps]header), and a hand edit applies live..zennotes/templates/through four authenticated routes that mirror the desktop module byte for byte (slug rules, flat-directory and.md-only validation,adrtoadr-2de-duplication, rename removes the previous file, atomic writes under the vault lock, the note-size limit as 413) and advertisessupportsCustomTemplates. The web client and the desktop remote client gate on the flag, so an older server keeps Settings, Templates read-only with a note naming the fix, and built-ins keep working. Both watchers report.zennotes/templates/under a newtemplatesscope, so a template saved in one client shows up in the others, and a change-feed gap re-lists them. Local vaults are unchanged. Also fixed: the desktop kept a pre-connection copy of the remote workspace info, so the renderer never learned what the server advertises.[saved_filters]table in config.toml ("Project alpha" = "@project:alpha"), diffable and synced with the rest of the prefs, and a hand edit applies live. Recall from a chip row under the Tasks header (click applies, click again clears, right-click renames or deletes),:filter <name>(a saved name applies its query; other text filters literally as before),Fin Vim mode (a picker that narrows as you type), or the command palette ("Tasks: name", which opens the view already filtered from any note). Save with the Save filter… chip or:savefilter <name>;:delfilter <name>forgets one. Names match case-insensitively; edits keep the chip order; the web client keeps its list in the browser.@menu (closes Add a Dedicated@dateField for Selecting Arbitrary Dates #743). The list now ends with Date… (@datenarrows to it); Enter opens a calendar on today with today's cell focused, so the keyboard flow is complete: arrows move a day or a week, PageUp/PageDown change the month (Shift: the year), Home/End the ends of the week, Enter inserts the ISO date where the@stood and returns focus to the note. A digit on the grid moves typing to the field and the grid follows the typed date; a day the month does not have is refused. With Vim mode on, h j k l move and t jumps to today; with it off, letters stay inert. Escape leaves the note as it was. The quick options are unchanged; the calendar honours the week-start setting and works wherever@does, web client included.zn createand the MCP tools followprimaryNotesLocation: root(closes CLI and MCPignore primaryNotesLocation: rootwhen creating notes #745). The CLI and MCP decided the mode from the vault's layout first and consultedvault.jsononly when the layout was ambiguous, so a vault switched to root mode with old notes still ininbox/kept getting new notes filed there while the app created them at the root, andvault_infosaidinbox. An explicit setting now wins on every side; the layout is inferred only when the file leaves it unstated, as the app does. Reads were already right.$$…$$block inside an Obsidian-style callout stayed raw in the editor (its>read as prose before the fence) and broke the reading view: the fence normalizer re-scanned an earlier block's closing fence as an opener, paired it with the callout's> $$, and rewrote the note with a bare$$outside the quote that swallowed everything after it. Both views now treat a fence inside a quote as a fence; the editor strips the markers from the formula and styles the block as part of the callout card, and the normalizer looks past markers, closes only at the same quote depth, and moves past canonical blocks. Works with Typst and KaTeX.The unbind's stored form is an override equal to the empty string:
UNBOUND_BINDINGinpackages/app-core/src/lib/keymaps.ts, kept bynormalizeKeymapOverridesand returned bygetKeymapBindinginstead of the default. Any consumer of a binding must treat""as "no key"; editor keymap entries go throughkeyBindingsFor, which returns nothing for an unbound action.Comment records:
packages/shared-domain/src/note-comments.ts(normalizer, threading, anchor helpers) replaces the desktop copy and the app-core anchor module;apps/desktop/src/mcp/comment-ops.tscomposes the operations fromreadNoteplus the newlistComments/writeCommentsbackend members (local sidecar, remote/api/comments/*);server.tscapturesclientInfo.nameon initialize for attribution and the instructions gain a Comments section. A reply whose parent is gone stays as a comment of its own on every side.Saved filters are a portable pref like the keymap overrides:
savedTaskFiltersinPORTABLE_PREF_KEYS, a map table in the desktop writer, validated on the way in bynormalizeSavedTaskFilters; the helpers inlib/saved-task-filters.tskeep insertion order through overwrite and rename because that order is the chip order and the file order.tasks.savedFiltersis a new view-actions keymap entry (catalog andkeymaps.ts); the palette entries are built per open becausebuildCommandsruns when the palette mounts.The calendar is a prompt in the app's sense:
promptDate()inlib/date-prompt-requests.tsis the calendar twin ofpromptApp,DatePickerHostsits next to the prompt and confirm hosts, and the modal carriesdata-prompt-modalso VimNav and the list views hand over the keyboard. The date math is pure inlib/date-picker.ts. The@item removes its trigger before the calendar opens and inserts at the recorded position, re-clamped to the document length. A month change replaces every grid cell (focus drops to the body before the effect runs), so the refocus decision is recorded by the move itself; the built app caught this where jsdom did not.For templates,
apps/server/internal/vault/templates.gois a synced copy ofapps/desktop/src/main/templates.ts(a template's id iscustom:<filename stem>, so both must land the same filename), and the Go watcher also gainedwatchSubdirsfor directories created together with a parent that raised one Create.Verified with unit tests on every layer (Go vault, routes, watcher; desktop remote client and watcher; app-core store and Settings) and live: the built desktop app over CDP for the unbind flows and for remote mode against a local server (create, external arrival through the feed, picker, delete), plus the web client in a browser against the same server (create, rename, delete, external arrival). Ignored keys were verified with jsdom tests of the guard and the store, the config round trip, and in the built app over CDP with realistic timing: the report reproduced with an empty list, fixed once the key was recorded. The folder board was verified with unit tests (placement, column building across layouts, roots, archive hiding, store normalizer, config round trip) and in the built app over CDP (default columns,
:folderroot, chip, Settings, config.toml, clearing). Comment threads were verified with unit tests on every layer (shared, comment-ops on a temp vault, MCP tool list and attribution, CLI group, Go round trip) and in the built app with a realzn mcpsession driven as claude-code (sixteen checks: keyboard comment, assistant reply and anchored thread arriving live, panel reply witha, resolve). The date picker was verified with unit tests (date math, modal, menu hand-off) and in the built app over CDP with Vim on and off; saved filters likewise (map helpers, config round trip, palette entries, store) and over CDP with Vim on and off, including a hand edit of config.toml picked up live and the palette entry from a note. Both docs surfaces are updated; the website half is committed in its own repository and goes out with the release.