Release v2.4.0#206
Merged
Merged
Conversation
Document CSV databases (how-to + core concept + database-grid shortcuts), the comments keyboard flow (Mod+Alt+M, Mod+Shift+C, panel j/k/e/r/d), the always-on Alt+H/J/K/L pane-focus motions, a new Palettes and pickers shortcut section, and the expanded theme family list in Appearance.
Daily and weekly notes can use date-pattern tokens (yyyy, MM, MMM, dd, EEE, ww, ...) for both the directory and the title/filename, with a locale for localized month and weekday names. Literal text is single-quoted, e.g. 'Daily Notes'/yyyy/MM-MMM. Recognition re-formats each note's recovered date and accepts it only when it round-trips, so any token combination resolves -- including titles that encode the day via ISO week + weekday (yyyy-'W'ww-EEE) rather than day-of-month. Changing a pattern snapshots the previous one so existing notes stay recognized, and a Reset to defaults control restores the directory, naming, and locale. Closes #77
IconBtn rendered its label through both the native title attribute and a custom hover bubble, so macOS showed the tooltip text twice. Drop the redundant native title; the styled bubble and aria-label remain, matching the editor toolbar's tooltip pattern.
Release notes and launch copy are personal/local working docs, not repo content. Untrack the v2.4.0 notes (the file stays on disk) and gitignore docs/releases/ so they can't be committed again.
Electron 41 no longer supplies default text for the macOS tab roles (toggleTabBar, selectNextTab, selectPreviousTab, mergeAllWindows), so the Window menu rendered them as blank rows that thinned out on repeat opens. Give each role an explicit label so the rows render their text.
In remote mode the sidebar header showed vault.name -- the server-side vault folder (e.g. "workspace") -- while the vault switcher names the same vault after the connection profile the user created (e.g. "Home"). The header label and its color/initial badge therefore disagreed with the switcher. Bind the header to the current switcher entry's name so the label and badge stay in sync with the dropdown; fall back to vault.name when there is no current entry.
openLocalVault bailed out when the target path equaled the current vault.root, without checking workspaceMode. In remote mode vault.root is the server-reported path, and a localhost server reports the served folder's real on-disk path -- which equals the local vault's own path. Switching back to that local vault was therefore mistaken for reopening the current vault and silently did nothing. Only short-circuit when already in local mode on the same root.
2.3.0 moved hint mode to <leader>h, but VimNav bailed out of the tasks/tags tabs for every key except the old standalone `f`, so the leader -- and therefore hint mode and every other leader command -- was dead there. Let leader input through in those panels; VimNav now arms and consumes the leader before TasksView can see it, so <leader>h works and Space-to-toggle falls back to x (Space still toggles for anyone who remapped their leader off Space). While the hint overlay is open, the tasks/tags handlers (list, calendar, kanban) now yield every key to it, so hint labels that collide with panel keys (j/k/x/o, Esc) are no longer stolen. Also stop Esc from closing the tasks and tags tabs. They are tabs like any note tab, so Esc now only clears an active filter and leaves the tab open; close with :q, the header button, or the OS shortcut.
Remapping the pane-focus shortcuts (global.focusPane*) onto a key CodeMirror also binds -- e.g. Ctrl-h (delete char) or Ctrl-k (kill line) -- ran both: the bubble-phase window handler fired *after* CodeMirror had already executed its command, so the key deleted text and moved focus. Handle these shortcuts in the capture phase and consume the event so the editor never sees a key bound to focus. The handler bails while Settings, a palette, or a modal is open so it can't steal keys from the keybinding recorder. Also fix sidebar->pane navigation: focusPaneOrEdgePanel always moved geometrically from activePaneId, so pressing focus-right from the sidebar jumped from the last active pane and skipped the pane next to the sidebar. When focus is on a horizontal edge panel, move relative to that panel and land on the edge-most pane when crossing into the editor.
…ts (#144) On Linux the editor's right-click menu hardcoded Mac hints ("⌘X" etc.), and the Ctrl clipboard keys collided with codemirror-vim's own Ctrl bindings (Ctrl+A increment, Ctrl+V visual block, Ctrl+X decrement), so they appeared broken. - Menu hints now derive from formatKeyToken('Mod+...'), showing Ctrl on Linux/Windows and ⌘ on macOS. - New 'Yank to system clipboard' Vim setting (clipboard=unnamed): yank/delete/change to the unnamed register also copy to the OS clipboard, while vim's Ctrl bindings keep working. Implemented by wrapping codemirror-vim's register controller (created once, so the patch is stable). Default off. - Highlight on yank (like nvim's vim.highlight.on_yank): the yanked range briefly flashes. The range is read from the editor selection, which codemirror-vim sets to the operator range while the yank runs.
VimNav's capture-phase leader handling armed on Space even while codemirror-vim was mid-command in the focused editor. After f/t/F/T/r (or an operator/count) the next key is the command's argument, so r<Space> (replace with space) and f<Space> (find next space) instead opened the which-key leader UI. Add isVimAwaitingArgument() -- true when codemirror-vim has a partial command buffered (inputState.keyBuffer) or expects a literal next char (expectLiteralNext) -- and skip arming the leader when the editor is focused and awaiting an argument, so Space falls through to codemirror-vim. The leader still works at rest.
electron-builder auto-generated the macOS icon from build/icon.png, producing an .icns that omitted the standard 32x32 (@1x) and 1024x1024 representations. Add a canonical build/icon.icns (all 10 Apple iconset sizes, built from the 1024 source) and point mac.icon at it, so future builds carry a complete icon for every consumer. The reported Raycast case is most likely a stale Raycast icon cache -- the bundle icon is correct and renders in Spotlight/Finder -- but a complete .icns rules out a strict icon reader.
Reflect the keyboard updates shipped this release: hint mode is <leader>h (was shown as f), the Tasks view toggles with x (Space is the leader), Esc clears the filter rather than closing the tab, and the leader works in the Tasks/Tags views. Adds the <Space> h entry to the Vim command list.
Running an ex command with a bad argument (e.g. :mv trash) reported the error with window.alert. In an Electron renderer that native dialog blurs CodeMirror, and after dismissing it the editor never regained focus -- in Vim mode the user could no longer type. Route the :mv/:move, :e/:edit, and :new error paths through a non-blocking, in-editor notification (cm.openNotification) -- the same red bottom-of-editor message codemirror-vim uses for its own errors -- so the editor keeps focus. Falls back to alert + refocus when the editor notification is unavailable.
Sidebar folders rendered in on-disk/insert order and were never name-sorted, so folders named with numeric/char prefixes only fell into place after a restart (a fresh load), and a folder created mid-session appeared last. Note and asset name sorting also used a plain locale compare, so '10 Foo' sorted above '2 Foo'. Add a shared naturalCompare() (numeric-aware, case-insensitive) and use it for folder children (sorted at render time, so new folders land in place immediately) and for the note/asset Name sort modes in the sidebar tree and note list.
The server's ListNotes and ListFolders walked the vault with filepath.WalkDir and returned the error for anything except os.ErrNotExist, so the first permission-denied entry aborted the whole scan -- a vault copied in with root-owned files while the container runs as a non-root user showed up empty, and fixing some files only revealed those. ListAssets already tolerated permission errors; notes and folders did not. Add a shared isSkippableWalkErr helper (skips ErrNotExist and ErrPermission) and use it in all three list functions, so unreadable entries are skipped and the rest of the vault still loads.
Render markdown in Edit mode (live preview), ported from the WYSIWYG work in PR #185 (author: songgnqing) — excluding that PR's breaking database restructure: - Tables as an editable grid (cm-table) + action menu (cm-table-menu) - Blockquote bars, list bullets, horizontal rules (cm-wysiwyg-blocks) - Hashtag chips (cm-hashtags), wikilink rendering (cm-wikilink-render) - Fenced-code cards + language flair (cm-code-block-flair, cm-code-block-font) - Strikethrough across all editor highlight styles (#187) - Task lines: marker hidden when rendered, raw source on the active line (Obsidian-style) Vim-navigable tables: a block cursor over the current character with h/l/w/b/e/0/$ motions, i/a/I/A editing, j/k row moves, and an `m` action menu. Wired via wysiwygExtensions() in the live-preview compartment, gated by the livePreview setting. 400 app-core tests green.
Detect `> [!type] title` blockquotes and render them as colored cards matching the Preview palette (note/info=blue, tip/hint/success=green, warning/warn=yellow, danger/error=red, quote=grey). The typed title shows bold (custom title, or the capitalized type name when none); the `[!type]` token is hidden off the line and revealed when the cursor is on the header for editing. Also fixes nested lists inside callouts/blockquotes breaking out of the box (re-add the box padding on top of the list hanging-indent) and de-italicizes callout body text to match plain blockquotes.
In a table cell (Vim mode), beyond h/l/w/b/e/0/$ navigation: - Editing operators: x, D, C, and operator-pending dd/cc, dw/cw, d$/c$, d0, db. - Char-wise visual mode (v) with motions + text objects, then d/c/y; rendered as a themed overlay (not a native selection, which CodeMirror would mirror across cells and show in the OS color). - Text objects: viw/vaw, vi"/va", vi(/va) (and operators diw/ciw/ca", …), via a pure, unit-tested textObjectRange (word/quote/bracket). - Undo/redo: u and Ctrl-r route to the editor history (committing the pending cell edit first). - Esc in normal mode is a no-op (was jumping out below the table); leave via j/k at the edges. - Cells disable native selection in view mode (re-enabled while editing), so the only highlight is the themed visual overlay.
`global.closeActiveTab` (Cmd+W / Ctrl+W) called closeActiveNote(), which returns early when there's no active note — so on an empty window (all tabs closed) nothing happened and the window never closed. In vim mode Ctrl+W was also always reserved for the `<C-w>` pane prefix, swallowing the empty-window case on Linux. Now: when no tab is open, close the window via window.zen.windowClose(); and only reserve Ctrl+W for vim when a tab is actually open (an empty window has no pane to navigate), so Cmd+W / Ctrl+W closes the window on macOS and Linux, vim on or off.
The `:` in a reference-style definition (`[label]: url`) parses as a LinkMark, which the live-preview plugin hides along with inline-link brackets — so the colon disappeared in Edit mode, leaving a broken-looking `[label] url`. Skip hiding LinkMarks whose parent is LinkReference; inline-link brackets (parent Link/Image) still hide as before. Preview was already correct (the definition is consumed and the reference link resolves).
On a case-insensitive filesystem (macOS/Windows), an inbox folder stored as `Inbox/` (or any system folder with non-canonical casing) made the file browser hide its images and PDFs while still showing its notes. The vault walk's `realpath` (symlink-loop detection) resolves to the on-disk case, so both note and asset paths come back as `Inbox/...`. Notes still appeared because the walk assigns `folder: 'inbox'` authoritatively, but assets carry no folder field and the renderer's `folderForVaultRelativePath` matched system folders case-sensitively, dropping the capital path to `null` -> hidden. Make the renderer classify and strip system-folder prefixes case-insensitively (`folderForVaultRelativePath`, `assetPathWithinFolder`, `notePathWithinFolder`), so a capital `Inbox/` behaves exactly like `inbox/` and notes + their sibling assets group into the same subpath. The real on-disk path is preserved for file ops, keeping this correct on case-sensitive Linux. Adds renderer unit coverage plus an end-to-end test wiring the real main-process listAssets/listNotes to the real renderer classifier.
Deleting a database (or any client removing the .csv) left the renderer trying to re-read the gone file: the watcher's delete event called syncDatabaseFromDisk, and nothing ever removed the database from state or closed its tab, so DatabaseView kept re-requesting it. Each attempt threw "Database not found" from the open-database IPC handler, which Electron logs as "Error occurred in handler for 'vault:open-database'". - The open-database handler now returns null for a missing database (a stale tab outliving its file is not exceptional); real parse/ permission errors still throw. - loadDatabase / syncDatabaseFromDisk treat null as "gone" and forget the database, closing its tab (both the database-tab and .csv asset-tab forms) instead of looping on a missing file. - The watcher's delete event proactively forgets the database, and openDatabase won't open a tab for one that no longer exists. Covers live deletes, a stale tab restored on startup, and external deletes. Adds store tests for all three.
) When typing with an IME (e.g. Japanese), pressing Enter to confirm a conversion also fired our own keydown handlers — committing a rename, selecting a search result, blurring a field, etc. — because the handlers never checked whether a composition was in progress. Add a shared `isImeComposing(e)` helper (standard `isComposing` flag plus the legacy `keyCode === 229` fallback, for both React synthetic and raw DOM events) and guard every text-entry "Enter confirms" handler: the rename prompt and breadcrumb rename, all search/command palettes, database inline renames (view/field/cell/group/tag), settings fields and the combobox picker, the remote-profile and directory-picker modals, quick-capture result pickers, and the tasks filter and Kanban column rename. While composing, these now let the IME own Enter/Arrows/Tab; a fresh Enter still confirms once composition ends. Pure-navigation handlers and modifier combos (e.g. Cmd+Enter) are left as-is, and the onboarding handler already bailed on editable targets.
#179) In an unprivileged LXC container, inotify on a bind-mounted vault can wedge the process (unkillable, host volume locked) rather than returning an error, and errors from fsw.Add() were silently discarded so a restricted environment failed invisibly. - Add ZENNOTES_DISABLE_WATCHER=1 → a no-op watcher (no fsnotify handle, no tree walk, no inotify syscalls). The vault is still fully served; only live auto-refresh stops. This is the reliable escape hatch on restricted hosts because it never makes the syscalls that hang. - New watcher.StartOrDisabled is used at startup and on vault switch: if the watcher can't be created it logs a warning and falls back to the no-op watcher instead of log.Fatal / failing the switch. - Surface fsw.Add failures: the initial walk counts and logs them once (with a pointer to ZENNOTES_DISABLE_WATCHER), and watching a newly created directory logs on error. Document the variable and a "container hangs and won't stop" troubleshooting entry in the Docker how-to. Adds tests for the no-op watcher.
A database is now one folder, `<Name>.base/`, holding `data.csv`, `schema.json` (the sidecar), and its record-page notes as `.md` files. The folder is the database: rename/move/trash are plain folder operations and record-page paths in the schema are stored relative to it, so nothing needs path rewriting. The sidebar renders a `.base` folder as a database — database icon, title without the suffix, click opens the grid, expand reveals its record pages — and the folder menu renames/deletes it as a database. The database header title is editable (double-click) and renames the folder, rehoming the open grid tab. Walks treat a `.base` folder as one unit: the desktop lists it as a database and its record pages as notes, but hides data.csv/schema.json from notes, folders, and assets; the MCP server hides the folder; the self-hosted Go server hides it entirely (databases are desktop-only). A legacy loose `<Name>.csv` plus co-located `.csv.base.json` (with record notes under `<Name>/`) is migrated to the new layout on vault open — idempotent, and only for managed databases (a plain CSV is left alone). Also fixes a couple of grid rough edges: a new/blank row no longer collapses shorter than filled rows, and the rename input is sized to the title. Adds shared-path, main-process (folder ops + migration + walk), and Go-server tests.
Adds a root flake plus packaging/nix/ with separate desktop and server packages that build ZenNotes from source, a dev shell, and install docs. Linux packaging only — no application-code changes. Co-authored-by: Krysteq <justkrysteq@proton.me>
CodeMirror's defaultKeymap binds the emacs-style control chords (Ctrl-d/a/e/f/b/v/h/k/o/t/p/n) as macOS-only bindings, and its key handler outranks the Vim plugin's — so on macOS those chords ran the emacs action (Ctrl-d deleted a character) instead of the Vim motion (<C-d> half-page down). Ctrl-u, which has no emacs binding, already worked, hence the up/down asymmetry. Linux/Windows were unaffected. Add vimAwareDefaultKeymap(): in Vim mode the emacs chords are stripped so Vim handles them natively; with Vim off they're kept (standard macOS editing keys). Applied to every Vim-capable editor surface — the main editor (dynamic via a compartment, so toggling Vim mode updates it live), quick capture, floating/external/template editors, and the pinned reference pane.
Comment bodies in the comments panel rendered as raw text, so Markdown a user typed (## headings, lists, **bold**, `code`) showed as literal source. They now render as sanitized Markdown via the same renderMarkdown pipeline the preview uses, with a compact `comment-prose` variant that scales headings and block spacing down to fit the narrow comment card. The input stays a plain textarea (you type Markdown source).
Creating a note from the sidebar (the note-list "+" button, the folder context menu, and the empty-area menu) dropped the cursor straight into the body with the name left as "Untitled", so it had to be renamed afterward. These paths now pass focusTitle: true — matching the command palette's "New Note" — so focus lands on the title input with "Untitled" selected; Enter then moves to the body. Daily/weekly notes, wikilink-create, and trash-restore are intentionally unchanged.
An Obsidian-style vault whose notes live at the root (and in custom folders) showed up empty when ZenNotes opened it in "inbox" mode — the scan only walks inbox/quick/archive/trash, so top-level content was silently hidden. This commonly happened when a flaky first directory read (iCloud-backed, symlinked folders) fell back to the inbox default. Add a sidebar banner that detects the mismatch — vault in inbox mode but its root holds markdown/non-system folders that only root mode surfaces (rootContentHiddenByInboxMode in the main process, exposed over a new IPC) — and offers a one-click "Switch to Vault root". Replaces the silent empty vault with a clear, actionable message. The banner lives in the sidebar (the unified layout's note tree), not the retired NoteList column. Refreshed on vault open and after settings changes. Covered by vault.test.ts cases.
Folders can now be given a preset accent color (10-color palette) from the sidebar context menu — "Change color…" / "Reset color" — alongside the existing folder icons. The color tints the folder's icon and name and stays readable in every state: a colored row that's selected uses a faint accent tint + ring instead of the saturated fill, so same-hue text doesn't vanish. Stored as `folderColors: Record<"folder:subpath", FolderColorId>` on VaultSettings (persisted to .zennotes/vault.json via the existing setVaultSettings — no new IPC), keyed exactly like folderIcons and rewritten in lock-step on folder rename/delete/duplicate (main process + store).
Add a "Copy Note as Markdown" command (command palette) plus a Vim leader binding (Space l y, under the note-actions group) that copies the active note's full Markdown source — including unsaved edits — to the clipboard. Wired through a copyActiveNoteAsMarkdown store action, registered as the remappable vim.leaderCopyMarkdown keymap, surfaced in the leader-l which-key hint, and documented in the in-app help.
Adds `.excalidraw` drawings alongside notes and databases:
- Listed in the sidebar (not as assets), with the real Excalidraw icon;
they open in an embedded, lazy-loaded @excalidraw/excalidraw editor that
loads/saves the scene JSON and follows the app's light/dark theme.
- Create from the sidebar ("New drawing" in the folder and empty-area
menus) and from the breadcrumb. Rename and move preserve the
`.excalidraw` extension instead of turning a drawing into a .md note.
- The drawing header hides Markdown-only controls (edit/split/preview,
connections, comments, outline, PDF export), keeping archive + trash.
- New vault:create-excalidraw IPC (desktop); the web build degrades until
the Go server learns to list/create drawings.
Also adds a breadcrumb create menu: right-click — or press `m` while a
crumb is focused (e.g. via hint mode) — any folder crumb to create a new
note / drawing / folder in that folder.
Brings the desktop's `.excalidraw` support to the Go server so drawings work on the self-hosted and web client: - ListNotes includes `.excalidraw` files (shown in the sidebar); ListAssets excludes them so they aren't treated as attachments. - readMeta/ReadNote skip Markdown parsing for drawings — a scene's hex color (e.g. #1971c2) no longer registers as a #tag, and the raw JSON never leaks into the excerpt. - Rename, move, duplicate, and move-between-folders preserve the `.excalidraw` extension instead of turning a drawing into a .md note. - New CreateExcalidraw method + POST /excalidraw/create endpoint, backing the web bridge's existing createExcalidraw call (which previously 404'd). - Root-mode detection recognizes a vault of only drawings; full-text search stays .md-only (drawings remain findable by title). Tests: vault listing/create/rename-move parity + an end-to-end HTTP test (login → POST /api/excalidraw/create → appears in /api/notes).
Favorites — pin notes and folders to the top of the sidebar: - Right-click a note or subfolder → Add/Remove from Favorites, or press the Vim leader binding `<leader> l s` on the active note (which-key + remappable). - A FAVORITES section lists them with their real icons; click or Vim-Enter to open. Stored per-vault in VaultSettings.favorites (note path or folder key). - Favorites survive note rename/move and folder rename, and are pruned on folder delete (rewrite logic lives in the store; the Go server carries favorites through folder ops so the web client persists them too). Sidebar reorganized into clear labeled sections (Notion-style): - FAVORITES (top), QUICK ACCESS (Tasks / Quick Notes / Daily+Weekly), NOTES (the tree), TAGS (flows after the notes), and SYSTEM (Archive / Trash / Assets) pinned to the bottom. - The header `+` now opens a create menu for every supported type (note, drawing, template, database, folder) instead of only a note. - Fix: Vim j/k now stops on Daily/Weekly note rows (they were missing navigation indices); Assets is Vim-activatable; removed stray top margin that made the gap above Daily Notes uneven; dropped the Tags chevron.
Lays the groundwork for a third workspace mode, `synced` — a local vault
that bidirectionally syncs with a server hub — while leaving `local` and the
thin-client `remote` modes untouched. No user-facing behavior yet.
Server (M2):
- GET /api/sync/manifest returns {path, hash, size, mtime} per note/.excalidraw
for content-addressed diffing. sha256 hashes are cached in the existing
note-meta-cache (keyed by mtime+size, version bumped 1->2), so an unchanged
vault answers from stat() calls alone. supportsSyncManifest capability added.
- Refactored the notes walk into a shared collectNoteFiles(). Tests for hash
stability/change, database+internal exclusion, and the HTTP endpoint (auth).
Desktop plumbing (M1):
- WorkspaceMode / WindowWorkspaceMode gain 'synced'; PersistedConfig gains
syncedVaults[] (root + server profile + conflict policy + asset mode) with a
normalizer. WindowVaultRegistry.setSyncedVault treats synced as disk-backed
(shares the chokidar watcher) with an onLocalChange fan-out hook for the
engine. store.workspaceModeFrom handles synced.
Reconcile core (M3, pure):
- Three-way diff (local/remote/base content hashes): push, pull, bidirectional
delete-propagation, keep-both conflicts, first-sync union merge, hash-only
(clock-skew-safe) decisions. 16 unit tests cover every decision-table row.
Builds the background replicator on top of the pure reconcile core:
- sync-state.ts — per-device base snapshot at .zennotes/sync-state.json
(atomic writes, intent log for crash recovery).
- scan-local.ts — walks/hashes local note/.excalidraw files, mirroring the
server's collectNoteFiles (inbox/root + quick/archive/trash, excluding
databases, hidden, .zennotes).
- conflict.ts — keep-both conflict-copy naming ("Note (conflict <dev> <ts>).md").
- engine.ts — debounced, single-flight reconcile loop triggered by the local
watcher fan-out, the server /api/watch WS, and a periodic backstop; executes
push/pull/delete/converge/conflict via the existing RemoteServerClient with
atomic, path-safe local writes. Offline/error/conflict status surface.
- RemoteServerClient.getSyncManifest(); SyncManifestEntry + SyncStatus types.
Verified: 22 unit tests (reconcile decision table + an end-to-end engine test
against an in-memory server over real local files: first-sync union merge,
pull, push, delete-propagation, keep-both conflict with no data loss,
idempotency). Not yet wired into app startup (next: activation + status UI).
Wires the sync engine into the running app: - index.ts activation: a synced vault opens disk-backed (all file ops stay local — isRemoteWorkspaceActive stays false) plus a background SyncEngine, started from loadCurrentVaultFromConfig and torn down when switching to local/remote. setSyncedVaultForWindow mirrors setVaultForWindow; the local watcher fan-out (onLocalChange) drives the engine. - New IPC: sync:get-status / now / on-status / attach-server / detach-server / open-vault, exposed through preload + the ZenBridge interface; the web bridge gets inert stubs (the browser is itself the thin client). - Store: syncStatus slice, onSyncStatus subscription + initial fetch, and syncNow / attachSyncServer / detachSyncServer actions. - UI: a cloud sync-status button in the sidebar header (synced / syncing / offline / error + a conflict badge; click to sync now) and vault-menu items to "Back up & sync to <server>" (attach a local vault) or "Stop syncing". Typechecks clean across all packages; 36 desktop/sync tests + app-core tests green; web builds. Runtime not yet exercised end-to-end (next: assets, then databases/comments/settings, then the conflict-resolution UI).
…ts (M5+M6) Unifies all syncable content under one manifest instead of notes-only: - Server: Manifest() now walks collectSyncFiles() — the whole vault tree (notes, drawings, databases, assets, comments, vault.json) excluding only the per-device sync-state, the meta cache, dotfiles, unrelated dot-dirs, and temp files. New POST /api/sync/write (multipart) writes raw bytes to an exact path for binary files (Vault.WriteSyncFile, atomic + size-capped). - Client: scanLocalVault() walks the whole tree to match (also removing the earlier inbox/root-mode parity concern). The engine classifies each path as text (.md/.excalidraw/.json/.csv/.txt) — transferred via writeNote/readNote — or binary, transferred byte-for-byte via writeSyncFile/readSyncFile. Conflict copies handle both. Databases (CSV/JSON), comments, and settings are text, so they sync through the existing endpoints with no special-casing. Tests: 25 sync unit tests (added binary push/pull byte-equality, databases/ comments/settings sync, and a never-sync guard for sync-state/meta-cache); the server manifest test now asserts syncables-in / internal-out. All green; typechecks + web build clean. Note: assets sync as a full mirror for now; the lazy on-demand fetch is a follow-up optimization.
When the sidebar sync indicator shows kept-both conflicts, clicking it opens a
resolver listing each conflict copy with its original. Per conflict the user can
open either version, adopt the copy's content into the original ("Use the copy's
version"), or discard the copy — all via normal vault ops that then sync. The
engine gains dismissConflict() + a sync:dismiss-conflict IPC to clear a resolved
entry from the status list; the store adds resolveSyncConflict(keepMine|keepTheirs)
and an originalFromConflictPath() helper.
Completes the synced-vault feature end to end: notes, drawings, databases,
comments, settings, and assets sync bidirectionally with keep-both conflicts, a
live status indicator, attach/detach, and conflict resolution. Typechecks +
tests + web build all green. Ready for live testing.
Reverts the five feat(sync) commits (731abb7, 757098a, aa22df5, 4fd805b, a38ce62): the server sync manifest + binary write endpoint, the desktop sync engine, app activation/IPC/status UI, full-vault coverage, and the conflict UI. The bidirectional-sync direction needs more design/validation before it ships, so it's parked. The work remains in history and can be restored later by reverting this commit (or cherry-picking those SHAs).
Databases now work in the web client, reusing the exact same shared TS logic the desktop uses so the on-disk format is identical across platforms. - Server (apps/server/internal/vault/vault.go): ListFolders now lists `.base` database folders (then stops descending), mirroring the desktop — that one divergence was why databases never reached the web sidebar. ListNotes and ListAssets still hide the internals. Test updated. - Web bridge (apps/web/src/bridge/http-bridge.ts): implemented the 8 database methods (open/writeRows/writeSchema/create/createRecordPage/rename/list) over HTTP, reusing @shared/database-csv + @shared/databases (parseCsv, parseRows, serializeRows, inferFields, buildDefaultViews, path helpers). Reads/writes use the existing generic /notes/read|write endpoints; `.base` folders are created/ renamed via the folder endpoints — no new server endpoints, no Go re-implementation, so CSV/schema parity is guaranteed. Drawings already worked end-to-end on web (no change). Verified: web typecheck + build, app-core tests (shared CSV unchanged), Go vault/httpserver tests.
…e sidebar Tasks calendar - Daily-note tasks get an implicit due date (the note's date) so they surface on the right day without writing due: tokens; gated by the new "Tasks are due on the note's date" setting. - Add, move, reschedule, and delete tasks directly from the calendar, creating the daily note on demand when needed. - Drag a task to another day (pointer-based) with a move-vs-set-due choice. - Optional "Roll over unfinished tasks to today" (Obsidian-style). - Week agenda under the month grid in the sidebar calendar panel. Vim keyboard - Full keyboard control of the big Tasks calendar and the sidebar calendar panel: h/j/k/l day nav, Tab to pick a task, x/e/dd/m/</>/T/a actions, gg/G, [/] month, gt today. Sidebar - Notion-style restyle (soft active highlight, roomier rows, muted icons). - Per-folder/note/database icon + color customization with inline reset. - Folder hover-chevron and aligned rows. Editor - "Task" slash command; calendar hidden on quick notes. Settings + docs - New daily-notes task settings (server round-trips them as *bool). - Help docs for the tasks calendar and daily-note settings.
Pointing ZenNotes at an existing Obsidian vault silently relocated every loose root-level non-md file — images, PDFs, and Obsidian `.canvas` files — into `assets/` on first open (the loose-asset migration), which a user reported as "a number of different files were changed that I made no edits to." A vault with an `.obsidian/` directory is managed by another app, so its loose files are the user's own, not ZenNotes' legacy attachments. Skip the root-level relocation step in that case; the legacy ZenNotes attachment dirs (which never exist in such a vault) stay handled, so the former-ZenNotes upgrade path is unchanged. Adds end-to-end integrity tests that wire the real main-process vault I/O and the real renderer store for the reporter's exact shape (root mode, nested folders, spaces in filenames, symlinked `~/sync` root): notes never cross-wire content, pure navigation writes nothing to disk, edits autosave to only the edited file, and external watcher changes land under the right path.
Standard `[text](target)` links didn't navigate the way `[[wikilinks]]` do: internal note links opened as broken asset tabs (the asset enhancer tagged every relative link with a zen-asset URL), and external links — especially a scheme-less domain like `[site](google.com)` — did nothing. Now, across the preview pane, the editor live preview, and `gd`: - `[text](Note.md)` and relative paths (`../x.md`, `#heading`) navigate to the note, resolved relative to the current note. - `[site](https://…)` and bare domains (`google.com`, `www.x.com/path`) open in the browser; a scheme-less domain is promoted to https. - In the editor a plain click follows a rendered link (cursor outside it), mirroring wikilinks; clicking a link the cursor is inside edits it. Cmd/Ctrl- click always follows. Markdown links now show the pointer cursor on hover. - `.md` and external links are no longer mis-tagged as vault assets. New pure module `internal-links.ts` (resolveInternalNoteHref / externalLinkUrl / markdownLinkAt / extractLinkAtCursor) shared by all surfaces, with unit tests and a regression test that note/external links aren't tagged as assets.
…adata card The database table is the source of truth for a record's properties, and a record page's frontmatter is a derived "metadata" mirror. Previously the frontmatter was only re-mirrored when the page was opened, so editing a cell or adding a field in the table left an already-open record page stale. Now table changes (a cell value, an added/renamed/removed field) re-mirror the frontmatter of any open record page live, preserving its body; pages that aren't open are still synced lazily on next open. The existing database write-guards keep this from looping through the file watcher. Also redesign the in-editor frontmatter rendering: instead of small grey text it reads as a compact properties card — a subtle rounded container, the `---` fences hidden into quiet rounded edges, and each key shown as a muted label beside its value.
Selecting text in the editor now pops up a floating bubble toolbar: a "Turn into" block menu (Text, Heading 1–3, bulleted/numbered/to-do lists, quote, code), inline formatting (bold, italic, strikethrough, highlight, code, math), a link, and a comment. The footer shows the focused/hovered action's keyboard shortcut on the popup itself. Inline formats also have keyboard shortcuts that work on every platform, in or out of Vim mode — handled in VimNav's window-capture pass so they beat Vim's own Ctrl chords on Linux/Windows, with `Mod` resolving to ⌘ on macOS and Ctrl elsewhere: ⌘B bold, ⌘I italic, ⌘E code, ⌘K link, ⇧⌘S strikethrough, ⇧⌘H highlight, ⇧⌘M math. The toolbar is fully keyboard-navigable: `Mod+/` focuses it, then the arrows — or h/j/k/l in Vim mode — move row-aware (h/l across the format row, j/k between it and "Turn into"); Enter applies, Esc returns to text. `l`/→ on "Turn into" opens its submenu. New pure `cm-format` commands (toggleWrap / wrapLink / setBlockType) with unit tests; new toolbar icons; in-app Help updated.
Hashtags required an ASCII letter to start (`#[a-zA-Z]…`, with `\w` for the
rest), so `#тест`, `#ошибка`, `#标签` and other non-Latin tags were never
recognized and rendered as plain text.
Make the tag character classes Unicode-aware everywhere — first char `\p{L}`
(a letter in any script), rest letters/digits/`_`/`-`/`/` — keeping behavior
otherwise identical (a heading `# x` still isn't a tag; a hex colour like
`#1971c2` still isn't, since the first char must be a letter). Updated in
lockstep across the preview, editor pills, inline render, live sidebar
extraction, tag rename, the desktop scanner + MCP, shared task tags, and the
Go server so desktop and the self-hosted/web client stay byte-for-byte.
Adds regression tests: the editor renders Cyrillic/CJK tags as pills, and the
Go scanner extracts them.
- Bump all package versions 2.3.0 → 2.4.0.
- Fix the typecheck gate (was red, would fail CI build:prod):
- Add RemoteServerClient.createExcalidraw — a real fix: creating a drawing
while connected to a self-hosted server called a method that didn't exist.
- Add the now-required `assetEmbeds` field to older NoteMeta test fixtures.
- release.yml: upload `*.tar.gz` so the AUR source asset is published (was built
then dropped by the upload glob).
- Flatpak: add the 2.4.0 metainfo release entry and point the AppImage URL at
v2.4.0 (sha256 pinned at release once the asset exists).
| if (editable.dataset.rendered === 'false') { | ||
| const raw = editable.dataset.raw ?? editable.textContent ?? '' | ||
| editable.dataset.raw = raw | ||
| editable.innerHTML = renderInlineCell(raw) |
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.
Merge the v2.4.0 release branch into
main. Tagv2.4.0after merging to trigger the installer build + GitHub release.Highlights
.base/folders, work on the web/self-hosted client, and a record page's fields show as live frontmatter "metadata" (table is the source of truth).assets/with a dedicated Assets view.Release prep in this branch
RemoteServerClient.createExcalidraw; fixedassetEmbedstest fixtures).release.ymlnow uploads*.tar.gz(the AUR source asset).Closes a long list of issues — see the release notes for the full set.