Skip to content

Release v2.4.0#206

Merged
adibhanna merged 67 commits into
mainfrom
release/v2.4.0
Jun 19, 2026
Merged

Release v2.4.0#206
adibhanna merged 67 commits into
mainfrom
release/v2.4.0

Conversation

@adibhanna

Copy link
Copy Markdown
Contributor

Merge the v2.4.0 release branch into main. Tag v2.4.0 after merging to trigger the installer build + GitHub release.

Highlights

Release prep in this branch

  • All package versions bumped to 2.4.0.
  • Typecheck is green (added RemoteServerClient.createExcalidraw; fixed assetEmbeds test fixtures).
  • release.yml now uploads *.tar.gz (the AUR source asset).

Closes a long list of issues — see the release notes for the full set.

adibhanna added 30 commits June 12, 2026 09:48
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.
victor-rm and others added 26 commits June 17, 2026 08:30
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)
@adibhanna
adibhanna merged commit bf2b1f6 into main Jun 19, 2026
6 checks passed
@adibhanna
adibhanna deleted the release/v2.4.0 branch June 19, 2026 22:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants