ZenNotes 2.25.0: databases meet your notes
Databases learned to discover the vault: type
[[in any cell to link a note, point a Select column at a folder or a #tag and its options fill themselves, and the new Note link columns store real wikilinks whose chips open the note. Your agents get the same powers, because theznCLI now speaks database: pipe a transcript intozn base addand it becomes a row with its record page, locally or against your self-hosted server, with the open app catching up live. Around the editor: nested lists finally read at a glance (a full tab size per level, with optional indent guides), the pinned reference pane renders frontmatter as the proper properties card, and a YAML quoting bug that could corrupt record pages is gone. Installers for macOS, Windows, and Linux (x64 + arm64) are attached below.
✨ New
-
Databases discover your notes. Until now a database column only offered whatever options you had typed into it by hand; linking the notes you already have meant hard-coding every project and person (asked for by @lawtj in #500, and exactly their meeting-notes use case). Three surfaces shipped together, all fed by the same ranking engine as the editor's
[[completion:Typing
[[while editing a text cell opens note search and inserts a[[wikilink]]. A Select or Multi-select column can source its options from your vault: the column's⋯menu grows Options: all notes / notes in folder… / notes with tag…, the cell popover then suggests matching notes live, and a picked note is saved as an ordinary option so boards, filters, and older builds carry on unchanged. And the new Note link / Note links column types store real wikilinks: cells edit through a searchable picker, chips show each note's title, click one and the note opens; a title with commas in it survives, which the comma-joined multi-select encoding never could.Every picker is keyboard-first: type to search, ↑/↓ or Ctrl+N/P, Enter picks, Escape closes. The select popover finally gained full list keyboard navigation along the way (it was mouse-only before). Older ZenNotes builds open these schemas cleanly: unknown column types render as plain text and round-trip untouched.
How to test locally: create a database with a Select column, open the column's
⋯menu, choose "Options: notes in folder…" and point it at a folder of notes. Open a cell in that column: the popover lists those notes with no typing; Enter picks one. Add a field, retype it to "Note link", and pick a note: the chip opens the note on click. In any text cell, typesee [[and watch the note list appear. -
The
znCLI speaks database. Agents and scripts could not touch a database: record notes are not rows until the CSV knows them, and frontmatter only flows table to page (asked for by @lawtj in #556, the same reporter as the note-discovery work above, both filed from real agent workflows). A newzn basefamily closes the gap:list,create,rows,get,add, andset, referencing a database by title or path and a row by id or title value.zn base add --set Name=... --body -turns a piped meeting transcript into a row WITH its record page (frontmatter mirrored, body preserved);zn base setedits fields with the grid's exact semantics, minting select options and re-mirroring the page. Every command works on a local vault and, via--server, against a self-hosted server, because the whole family is composed from the same shared database operations the web and desktop remote clients use. The running app's watcher picks CLI writes up live: add a row from a script and watch it appear in the open grid.How to test locally:
printf 'Notes from standup.' | zn base add Meetings --set "Name=Standup" --set "Project=Acme" --body - --vault <your vault>, thenzn base rows Meetings --vault <your vault>; open the app and the row is there, page and all. Same commands with--server host:7878 --token <t>against a server. -
Nested lists you can actually read. Deep list levels rendered almost flush against each other, especially for notes written with two-space indents, so telling level three from level four on a large monitor was squinting work (asked for by @Brachistocronium in #491, Obsidian comparison included). Two changes, one knob: every nested level now renders a full Tab size deep, whatever the note's source spacing (the raw markup never changes, and notes already indented that wide do not shift), and vertical indent guides mark each level, on by default with a Settings → Editor toggle. The Tab size slider (1 through 8) governs both, so the reporter's "8 wide" is a slider away. The setting travels in config.toml with your other portable prefs.
How to test locally: open a note with a 2-space-indented nested list. Before: levels hug each other. After: each level steps 4 columns with a guide line per level. Settings → Editor → Tab size to 8 widens every step; the Indent guides toggle removes the lines while keeping the width.
🐛 Fixes
-
Pinned reference notes show their properties, not a wall of dashes. Pinning a note as a reference put its YAML frontmatter on screen raw: the
---fences rendered as horizontal rules, and the markdown parser read the closing fence as a setext underline, so the properties above it inflated into a giant Heading 2 while the main editor showed the same note as a tidy metadata card. The reference pane builds its own copy of the editor's markdown setup, and the copy was missingfrontmatterStyle, the plugin that tags the leading YAML block for the card styling. The plugin is now in the pane, so both views share the identical rendering path: compact key-value rows, muted keys, hidden fences, in Edit mode and Preview alike. (#553, reported by @naingyeminn)How to test locally: open a note whose file starts with YAML frontmatter (
---/author: you/---), right-click its tab title, choose Pin as Reference. Before: the pane shows---lines and the properties as huge heading text. After: the same properties card as the main editor, and the pane's Preview mode hides the frontmatter entirely. -
Record pages can no longer be corrupted by a bracketed cell value. Frontmatter scalars written by the record-page mirror (and by task files) under-quoted: a cell holding
[[Project X]]or- itemlanded unquoted in YAML, where every reader, including ZenNotes' own parser, takes it for a list. The sharedyamlValuenow quotes leading-indicator values while leaving mid-value brackets bare, so existing pages don't churn. Groundwork for the note-link columns above, but the bug was reachable today by typing[[x]]into any text cell. -
The filter menu survives schemas from the future. A database whose schema names a field type this build doesn't know (written by a newer version) rendered its cells as plain text but THREW in the filter menu. Unknown types now filter like text, matching how they render.
🧰 For contributors
PinnedReferencePaneduplicatesEditorPane's markdown editing bundle by hand, and this bug was the copy drifting. Known remaining divergences, deliberate for now because they are behavior rather than presentation:orderedListRenumber,forwardOnCheckboxArrow,codeBlockFontPlugin. If a second drift bug lands here, extract the shared bundle instead of patching the copy again.- Wikilink matching/ranking moved out of
cm-wikilinks.tsinto the pure, store-freelib/link-candidates.ts; the editor sources are now thin CodeMirror adapters over it. Any new[[surface (cells, future query autocomplete, mobile) must consume this module, not fork the scoring. note/noteMulticells encode as space-joined[[target]]wikilinks (splitNoteLinks/joinNoteLinksin shared-domaindatabase-transforms): bracket-delimited on purpose so comma titles survive. Select cells deliberately keep plain-string values; seedocs/ideas/database-note-links.mdfor the full decision log, including what was deliberately NOT built (board grouping by note fields, backlink indexing of CSV cells).DbField.optionsSourceis picker-only metadata: older builds ignore and round-trip it. The Go server needs no changes for any of this; it serves.baseinternals as plain files.- Record-level doc mutations (
setCell,addRow,ensureSelectOption,composePageBody,recordTitle) moved to shared-domaindatabase-records; app-core re-exports them. The CLI backends exposedatabaseOps()composed via@shared/database-ops: local binds new vault-ops raw-file IO pluslistDatabaseDirs(listFolders hides.baseon purpose; the composition needs exactly those), remote binds the CLI server client with the absence-aware reader.RemoteRequestErrornow carries the HTTP status for that reader. - CLI stdin rule learned the hard way:
zn base addreads stdin ONLY on an explicit--body -. Auto-draining a non-TTY stdin (the capture pattern) hangs forever when the spawning process leaves its pipe open, which is exactly how agent tooling invokes CLIs.
Keyboard-first and local-first, as always.