Skip to content

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
adibhanna merged 12 commits into
mainfrom
v2.46.0
Sep 8, 2026

Conversation

@adibhanna

@adibhanna adibhanna commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Release 2.46.0: seven features and three fixes. Cut with docs: draft the 2.46.0 release notes and post (2131393) and chore(release): 2.46.0 (da59c37); main fast-forwarded and tag v2.46.0 pushed on 2026-09-08.

  • Ignore the no-op key a tap-hold remapper sends (closes Ability to make the editor ignore no-op keys. #732). Kanata/QMK/ZMK layers emit a stray key (KanaMode on Linux) with every keystroke, and each one reset jk, dd, leader chords and hints. A new ignoredKeys portable pref (ignored_keys under [editor]), a Settings → Keymap row with a recorder, and :ignorekey feed one capture-phase guard on window, installed at App module load so it precedes every other listener; it swallows keydown, keyup and keypress and stands aside while the recorder captures.
  • The Kanban Folder board groups by each note's folder, or by one folder's children (closes group Kanban by vault folder, not just the four built-in folders #730). Every folder that holds tasks is a column (system folders first, subfolders alphabetical, no empty columns); a new kanbanFolderRoot pref (Settings, :folderroot, the header chip, kanban_folder_root in 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.
  • Discuss a note with an assistant through its comments (closes Allow LLM to create/read comments #738). Comments thread and carry a name (author, absent for the vault owner; parentId for a reply), the panel shows threads with a reply box on a, and the MCP server gains list_comments, add_comment, reply_to_comment and resolve_comment, signing with the connected client's name; zn comment mirrors 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.
  • A shortcut can be removed outright, not just remapped. Every row under Settings, Keymap gains an Unbind button next to Change and Reset (the recorder's Clear became Unbind; Backspace still clears a recording). An unbound row reads Unbound with the Custom badge, nothing fires the action until Reset or a new key, and it never counts as a conflict. The which-key hints, the command palette, tooltips and the manual stop advertising the key. Vim users get :unbind action.id, with a toast naming the old key; bare :unbind or 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.
  • Custom templates on remote vaults (closes [feat] Support custom templates in remote vaults #723; groundwork first explored by @flokchvtr on their web-feature-parity branch). The server serves the vault's .zennotes/templates/ through four authenticated routes that mirror the desktop module byte for byte (slug rules, flat-directory and .md-only validation, adr to adr-2 de-duplication, rename removes the previous file, atomic writes under the vault lock, the note-size limit as 413) and advertises supportsCustomTemplates. 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 new templates scope, 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.
  • Save a Tasks filter under a name and recall it (closes save and recall filters in the Tasks view #731). A query is saved as the [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), F in 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.
  • Pick any date from the @ menu (closes Add a Dedicated @date Field for Selecting Arbitrary Dates #743). The list now ends with Date… (@date narrows 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 create and the MCP tools follow primaryNotesLocation: root (closes CLI and MCP ignore primaryNotesLocation: root when creating notes #745). The CLI and MCP decided the mode from the vault's layout first and consulted vault.json only when the layout was ambiguous, so a vault switched to root mode with old notes still in inbox/ kept getting new notes filed there while the app created them at the root, and vault_info said inbox. 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.
  • Display math renders inside callouts (closes Typst math rendering does not work correctly inside Obsidian-style callouts. #748). A $$…$$ 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.
  • Typst formulas are the size KaTeX formulas are (closes typst formulas have a smaller font size #746). The Typst SVG was sized at a plain 1em while KaTeX draws Computer Modern at 1.21em (its own stylesheet), so the same formula came out a fifth smaller under Typst. The SVG now uses the same factor in both views; the Math size setting still scales on top. Also found there: the theme recolor only knew fills, so square root bars and fraction lines (black strokes in Typst's export) vanished on dark themes; strokes are recolored too.

The unbind's stored form is an override equal to the empty string: UNBOUND_BINDING in packages/app-core/src/lib/keymaps.ts, kept by normalizeKeymapOverrides and returned by getKeymapBinding instead of the default. Any consumer of a binding must treat "" as "no key"; editor keymap entries go through keyBindingsFor, 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.ts composes the operations from readNote plus the new listComments/writeComments backend members (local sidecar, remote /api/comments/*); server.ts captures clientInfo.name on 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: savedTaskFilters in PORTABLE_PREF_KEYS, a map table in the desktop writer, validated on the way in by normalizeSavedTaskFilters; the helpers in lib/saved-task-filters.ts keep insertion order through overwrite and rename because that order is the chip order and the file order. tasks.savedFilters is a new view-actions keymap entry (catalog and keymaps.ts); the palette entries are built per open because buildCommands runs when the palette mounts.

The calendar is a prompt in the app's sense: promptDate() in lib/date-prompt-requests.ts is the calendar twin of promptApp, DatePickerHost sits next to the prompt and confirm hosts, and the modal carries data-prompt-modal so VimNav and the list views hand over the keyboard. The date math is pure in lib/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.go is a synced copy of apps/desktop/src/main/templates.ts (a template's id is custom:<filename stem>, so both must land the same filename), and the Go watcher also gained watchSubdirs for 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 real zn mcp session driven as claude-code (sixteen checks: keyboard comment, assistant reply and anchored thread arriving live, panel reply with a, 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.

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.
@adibhanna adibhanna changed the title Release 2.46.0: a shortcut can be removed outright, not just remapped Release 2.46.0: unbind a shortcut outright, and custom templates on remote vaults Sep 8, 2026
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.
@adibhanna adibhanna changed the title Release 2.46.0: unbind a shortcut outright, and custom templates on remote vaults Release 2.46.0: unbind shortcuts, custom templates on remote vaults, root-mode CLI, math in callouts Sep 8, 2026
…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
@adibhanna adibhanna changed the title Release 2.46.0: unbind shortcuts, custom templates on remote vaults, root-mode CLI, math in callouts Release 2.46.0: unbind shortcuts, custom templates on remote vaults, a calendar in the @ menu, root-mode CLI, math in callouts Sep 8, 2026
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
@adibhanna adibhanna changed the title Release 2.46.0: unbind shortcuts, custom templates on remote vaults, a calendar in the @ menu, root-mode CLI, math in callouts Release 2.46.0: unbind shortcuts, saved Tasks filters, custom templates on remote vaults, a calendar in the @ menu, root-mode CLI, math in callouts Sep 8, 2026
…#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
@adibhanna adibhanna changed the title Release 2.46.0: unbind shortcuts, saved Tasks filters, custom templates on remote vaults, a calendar in the @ menu, root-mode CLI, math in callouts Release 2.46.0: comment threads an assistant can join, unbind shortcuts, saved Tasks filters, custom templates on remote vaults, a calendar in the @ menu, root-mode CLI, math in callouts Sep 8, 2026
…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
@adibhanna adibhanna changed the title Release 2.46.0: comment threads an assistant can join, unbind shortcuts, saved Tasks filters, custom templates on remote vaults, a calendar in the @ menu, root-mode CLI, math in callouts Release 2.46.0: comment threads an assistant can join, a Kanban board per folder, unbind shortcuts, saved Tasks filters, custom templates on remote vaults, a calendar in the @ menu, root-mode CLI, math in callouts Sep 8, 2026
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
@adibhanna adibhanna changed the title Release 2.46.0: comment threads an assistant can join, a Kanban board per folder, unbind shortcuts, saved Tasks filters, custom templates on remote vaults, a calendar in the @ menu, root-mode CLI, math in callouts 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 Sep 8, 2026
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.
@adibhanna
adibhanna merged commit da59c37 into main Sep 8, 2026
@adibhanna
adibhanna deleted the v2.46.0 branch September 8, 2026 20:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment