Conversation
…838) From @uNyanda (#838, Niri on Wayland, 2.54.1): on a Kanban board wider than the window, dragging a card to the board's left or right edge never scrolled it, so a card could not reach an off-screen column in one drag. Released at the edge, it landed in whatever column was under the pointer. Reproduced in the built app with eight custom-status columns: a card held at the right edge left the board at scrollLeft 0 of 1736, and the release wrote @status:review. The same wall stood at the left edge, at the bottom of a long column, and for a dragged column header. The board moves cards and headers with pointer events rather than HTML5 drag-and-drop, so it gets none of the browser's built-in drag autoscroll. lib/drag-autoscroll.ts adds it: a pointer held in a 64 px band along a scroller's edge scrolls it, easing up to 1200 px/s at the edge and holding that speed past it, so an overshoot into the sidebar keeps going. A frame loop does the scrolling, because a resting mouse sends no pointermove, and it carries sub-pixel steps so the slow end of the band still moves. A card drag scrolls the board sideways and the column under the pointer up and down; a header drag scrolls the board. A scroll under a still pointer slides a different column beneath it, so a capture scroll listener on the board re-aims the drop highlight and the insertion line. That also fixes a highlight that already went stale when the board was wheel-scrolled mid-drag. A drop now puts the cursor on the dropped card, and a dropped column takes the cursor with it, the way a click, a right-click, Shift+H/L and `<` / `>` already do. That is load-bearing, not polish: the focus-follow effect scrolls the cursor's card into view, and with the cursor still in the first column the board jumped from 1736 back to 21 the moment a card landed on the far side. The cursor is set before the move, because the move flushes the board rebuild synchronously and that commit already runs the scroll. The Tasks Kanban entry in the manual now covers the edge scroll and where the cursor lands after a drop.
…ame says why (#839) From @vinivinoo (#839, a root-mode vault): New note from the sidebar, type "test" in the title field, Enter, and the note stayed "Untitled". Every retry failed the same way, and nothing on screen said why. Reproduced in the built app: a creation date left at .zennotes/note-metadata/test.md.metadata.json with no note beside it refused every rename onto "test" with "The destination folder or its comments already exist", and the store's renameNote only wrote that to the console. Since creation dates moved into sidecars (b6c3b9d), every note saved in the app has one, and anything that moves a note without it leaves the old one behind: a workflow run (the applier moves the Markdown file only, and the reporter runs a "Move Topics" workflow), the MCP rename, a file manager, git, sync. Creating a note at that path already discarded the stale date; renaming or moving onto it refused, and a test pinned that refusal. Renames and moves onto a path with no note now discard a leftover creation date the same way creating a note there does. One relocateNote in desktop main backs rename, move, archive, trash and restore, and the MCP copy's moveWithCreationMetadata does the same for a single note. A folder moved onto a leftover tree of dates still refuses: dropping a whole tree is a bigger call than one date. Leftover comments still refuse, since adopting another note's discussion and deleting it are both wrong, but the error now names the file to move aside. A name that belongs to a real note says "A note named “test” already exists in this folder". Every rename in the UI goes through the store's renameNote, which now shows the reason in a toast instead of failing silently. The refusal test flips to expect the rename (desktop and MCP), and new tests cover a renamed note never inheriting a stale date, a move and a trash onto one, both refusal messages, and the toast. Not done here: making the workflow applier and the MCP rename carry the sidecars, so they stop leaving them behind. That needs the workflow journal to cover the sidecars for undo, and lands separately.
…d undo puts them back (#839) Follow-up to #839. The workflow applier moved a note with a bare fs.rename, so a move, rename, archive or trash step left the note's comments (.zennotes/comments/<note>.comments.json) and creation date (.zennotes/note-metadata/<note>.metadata.json) behind under the old name: the discussion no longer showed on the note, the moved note took the run's time as its creation date, and the leftovers blocked or polluted the next rename onto that name, which is how #839's reporter got there. The MCP server carried the date but not the comments, and never removed comments on delete, Empty Trash or folder delete. relocateNote, relocateFolderTrees and the comments paths move from vault.ts into an Electron-free note-sidecars.ts, with the refusal message shared, so desktop main, the MCP server and the applier follow one policy. The MCP server now moves, trashes, restores and renames folders through them and removes comments where the desktop does. In the applier, a path op reads both sidecars before it journals anything, records them with the note, then moves them after it. Leftover comments at the destination fail the run, which rolls it back, with the file named; a leftover date is journalled and replaced. The entries name the note, not the sidecar file, which is derived at restore time after resolveVaultPath, so a ledger still cannot aim a write anywhere but a note's own sidecar. They carry no `path` and sit in a separate `sidecars` ledger array: an older ZenNotes or znserver reading the ledger resolves every journal path as a note and would fail the undo on a .zennotes one; this way it skips them and still undoes the notes. Undo writes a moved note back as a new file, so a note that never had a date file came back with the undo's time as its date; the run now writes the date down before moving such a note (not in a temporary folder session). Journalling up to six files per move with a sync each made a 400-note move three times slower, so a path op now appends its entries with one sync, down from two before sidecars, and undo renames an untouched sidecar back once its bytes match instead of rewriting it. Undo prunes the sidecar folders it empties, because a folder rename refuses any existing destination tree. A rollback cause that already ends in a full stop no longer doubles it. Tests: note-sidecars.test.ts runs the same rename, move, Trash, folder and delete cases against desktop and MCP (the 8 MCP ones fail on the old code), and workflow-apply.test.ts gains 20 cases for each path op, undo, rollback, leftovers, the crash journal, old and forged ledgers, drift, the sync count and the rename on undo (19 fail on the old applier; the old-ledger case passes on both by design). Not done here: znserver applies prepared change sets that cannot express sidecars, so a workflow move on a remote vault still leaves the comments behind. Two older applier bugs found on the way are separate: text ops reset the creation date of a note with no date file, and undoing a move of a symlinked note deletes the link's target.
…, not a copy, and keeps the file behind it A note can be a symlink to a file kept elsewhere, outside the vault included; the app lists and edits it like any other note. A workflow move, rename, archive or trash renames the link itself (fs.rename in movePathInVault), so the file behind it never moves. Undo did not know that. Its journal held only the bytes read through the link, so for the destination (before: null) it took the link for a file the run had created, followed it and deleted the target, a file the vault may never have owned; for the source it wrote the bytes back as a plain file; and the link itself stayed at the destination, pointing at nothing. Found while giving the applier sidecars (1cdb0e8) and reproduced in the built app before this change: after Undo the outside file was gone. A note's journal entry now records the text of the symlink its path was before the run, read at first touch like the bytes, in the crash journal and the ledger alike. Undo moves that same link back: it finds it by its text among the paths the run filled, renames it home, and only then compares the bytes. A path the run filled is emptied by removing what sits there as itself, never through a link. The one target undo still removes is behind a recorded link that pointed at nothing before the run, whose target the run created by writing through it, and only while that link still has the same text. A link is never created from what a ledger says. An edited or synced ledger could otherwise plant a link to anywhere and write through it, which is the write primitive resolveVaultPath exists to rule out; a test pins that. Ledgers from before the text was recorded still undo, and without it the note comes back as a copy while the file behind the link is left alone, where the old code deleted it. uniqueRel now treats a link that points at nothing as taken, since the rename would have replaced it with nothing to put back. Nine tests in the symlinked-notes block cover undo, rollback, an edit through the link before the move, a note moved twice, a relative link, the crash journal and its recovery, an old ledger, a forged one and a dead link at the destination; eight fail on 1cdb0e8. Not done here: moving a relative link to a different folder depth breaks it in the forward direction, in the app's own move as much as here.
… creation date alone Since creation dates moved into sidecars (b6c3b9d, 2.51), a note the app has never saved has no date file and shows its file's birth time. Saving replaces the file (temp file plus rename), so writeNote writes the date down first. The applier's text ops (add-tag, remove-tag, set-frontmatter, append, prepend, write-section, write, apply-template) replaced the file the same way without doing that: every such note they touched took the run's time as its creation date, and undo, which writes the bytes back as a new file, reset it again to the undo's time. Found while giving the applier sidecars (1cdb0e8), confirmed in a throwaway test (the date moved by exactly the wait before the run) and reproduced in the built app before this change. keepCreationDate writes the date down through prepareNoteCreation when the note has no date file, and both writers call it before they journal: applyTextOpToVault for an existing note, and sidecarsToCarry for a move, where the block that did this inline moves into the helper. A date file already there is left alone, readable or not, so a corrupt one cannot fail a run the way it fails a save; nothing is written in a temporary folder session, which writeNote never writes app state into either. It is the one write not journalled first, safely: it records a date the note already had, where the app already looks for it. A note the run creates keeps its own birth time. A date file left behind by an earlier note of that name is journalled as a sidecar entry and removed, as createNote removes it, so undo puts it back with the rest. journalPathOp becomes journalOp and absorbs journalTouch, since a text op now journals a sidecar too. Eleven tests cover six text ops through the run and the undo, the date written down before the note, a date file left alone, a temporary folder session, a created note over a leftover and its undo, and a created note with no date file of its own; eight fail on 25f4040.
On a vault served by znserver, the app plans a workflow run and sends the server the file changes to make; a move arrives there as a delete and a write. The server carries a note's comments on its own renames and moves, but a workflow move left them behind under the old name, detached from the note, the way the desktop applier did before 1cdb0e8: the changes name only notes, and a note's comments live in .zennotes, which a change may not name. The prepared run now also lists its moves, one per path op as it lands, suffix and all, in order. The matching change in ZenNotes/znserver carries each note's comments with its move, refuses a destination where an earlier note's comments still sit, and records what it moved in the desktop's ledger format so undo puts it back. The web bridge and the desktop's remote client pass the prepared run through unchanged, so nothing else moves here. A server from before the field ignores it and moves the Markdown alone; an older app sends no moves and gets what it always got.
…t its file A link's relative text is read from the link's own folder, so a note that is a link like ../sources/Real.md, moved from inbox/ into inbox/Topics/, named inbox/sources/Real.md from there and opened empty. That is what mv and Finder do, and it was what every move in ZenNotes did: the app's own moves and the MCP server's (renameDirectory in note-sidecars.ts, a plain fs.rename) and the workflow applier's. Seen while fixing the applier's undo of a moved symlink (25f4040). rebaseMovedLink runs after every such rename: a relative text is re-based on the file it named from the old folder, so it names that file from the new one, written as a new link renamed over the old so there is no moment without one; an absolute text needs nothing. Inside renameDirectory it also covers every rollback that renames a link back. The applier's undo matches a moved link by the file it resolves to rather than by its text, since the text changed on the way out, and re-spells it with the recorded text only when that text names the very file the link now names: the same reach, spelled as it was. Links inside a moved folder move with their folder and are not followed; a folder moved to another depth still breaks the ones that pointed out of it. Tests: the depth-changing move and its undo, a text path.relative would not have produced, and the same move on the desktop and the MCP server.
On a vault whose primary notes live at the root, the Workflows "Notes in folder" source could not start from those notes. The engine matched a folder by directory, and the root's own directory is the empty name it refuses on purpose (`folder ""` once matched the whole vault, Trash and Archive included); `folder inbox` matched nothing there, because no note has an `inbox` directory; and the folder picker offered only the directories notes sit in, the buckets deliberately left out. The reporter typed `inbox`, got "0 notes", and had no way to say "all my notes". The engine already read `folder trash` and `folder archive` as the Trash and the Archive wherever the vault keeps them. Now all four system names work that way, for the `folder` source and the `in` filter alike: a note matches by its directory, or by the bucket the reader classified it into. `folder inbox` is the primary notes area, the root itself on a root-mode vault and the inbox wherever it was mapped on a remapped one; a directory name still narrows to that directory, and `where folder = ...` still compares the directory. The picker leads with the four names, each hinted with the sidebar's label for it, and `inbox` reads "Vault root" on a root-mode vault, so typing "root" finds it: the combobox options grew a hint that is rendered and searched. The step descriptions, the manual's grammar card and the website docs say so. Deliberately not changed: the `move` step's destination is still a directory name.
… shells (#842) The New note form that the note search opens from its Create row shows hardware-keyboard hints: "↑↓ pick", "↵ create" and "esc back" in the footer beside Back and Create, and a "Shift+↵" chip inside the Open it button when the name already exists. On a phone they are noise, in a footer that sits right above the soft keyboard. The phone shells hide every other palette footer with one CSS rule, but this footer also holds the buttons, so that rule must not match it, and the hints had no hook of their own. Now the hint group and the chip carry `data-keyboard-hints`. The shells hide whatever carries that marker on the phone layout (one rule in each `mobile.css`, landing as a PR in each shell); a tablet with a keyboard keeps the hints, and nothing changes on the desktop. The alternative, dropping the hints when the host is ios or android, would have taken them from those tablets too. Not changed: the key handling. Enter, Escape and the arrows keep working wherever there is a keyboard.
) Since 2.20 a workflow's `trigger:` line accepted `on note-saved` and its siblings, and the manual said what they did: nothing. They parsed, and the workflow ran when you ran it. A user wrote `all | contains type: topic | move Topics` under `trigger: on note-saved` and, reasonably, expected the note to move when they saved it. Now they fire, in a deliberately narrow form. Only edits made in this app fire: the store rings a small bus (`lib/note-events`) from its own write paths (a save, a creation, a rename or move, an archive, trash or restore, and a save that gave the note a tag), and a dispatcher (`lib/workflow- events`) installed at vault open turns that into a run a moment after the note settles. A change that arrives by sync or from another device does not fire: two synced desktops both firing on each other's writes would run every step once per device and bounce an `append` between them forever, which is why the design document wants one executor per vault; that model stays a design, and this shape can grow into it without changing what a workflow file means. What a run writes never fires either: the host applies it, and nothing about it passes through the store's save path. A run sees only the note that fired: every source resolves over it, so the reporter's line means "file this note under Topics when it says so" and nothing is read across the vault on every autosave. `on <event> where <field> <op> <value>` is planned by the engine as `current | where ...` over the same reader. There is no confirmation: a run that changed nothing says nothing, a note that is dirty again waits for its next save, another dirty note the run would write is left alone and named, a condition the engine cannot read is reported once, and the receipt is the palette's toast with its Undo. An editor open on a note the run moves follows it, the way it follows a rename made in the app: `followWorkflowMoves` shields the promised paths from the watcher's unlink echo while the host applies the run, then carries the tab, the buffer and the undo history to where the note landed, checked against the disk byte for byte so a suffixed landing is never mistaken for the note. The run record remembers the moves, so Undo carries the editor back; the canvas's own Run and Undo do the same. Settings, Workflows gains "Event triggers", a portable pref (`workflow_event_triggers` under `[view]`), so every trigger on a device can be silenced at once; `status: draft` and `trigger: manual` still silence one. Schedules still parse without firing, and a folder rename does not fire for the notes inside it.
On a vault whose notes live at the root, `:mv` (and Move... from the context menu, the note list, the Archive view, the palette and a multi-selection) opened on `inbox` and offered `inbox/Areas`, `inbox/Projects`, folders the vault does not have. The prompt spoke in the bucket's terms: a destination had to start with `inbox` or `archive`, suggestions were built as `<bucket>/<subpath>`, and the initial value came from the path's first segment, which on a root vault is a real folder name and so fell back to `inbox`. Every folder of the notes area is filed under the inbox bucket whatever the layout, so the words were right for the host and wrong for the person reading them. Now the prompt speaks the way the sidebar does. Destinations are folders of the notes area as it shows them (`Areas`, `Work/Research`, nothing in front), empty for the notes root, labelled "Vault root" when the notes live there and with the inbox's label otherwise, and `archive/...` for the Archive, by that name even when the archive folder is called something else on disk. The prompt opens on the note's current folder in that spelling, an empty field moves to the root, and the reserved names (Quick Notes, Trash, their directory names) are refused with one message instead of being turned into folders. The older `inbox/Work` spelling the manual taught still lands in Work; on a root vault too, unless a real folder named `inbox` sits at the root, in which case it means that folder. Folder and database moves use the same vocabulary. `moveNoteVocabulary(settings, labels, folders)` carries these facts, every caller passes it, and only null is the Cancel now that empty is an answer. The manual's three mentions and the website were reworded.
A user with Vim mode off clicked a folder in the sidebar and got a pane ring around the whole sidebar and an `m` chip on the row, for a key that did nothing. The same shape was everywhere a surface hints at its keys: the note list's ring, the connections panel's strip and cursor card (`j/k`, `p`), the comments panel's strip and the badges under a comment (`n a e r d`), and tooltips promising `(a)`, `(o)`, `(x)`, `(c)`, `(3)`, `(or m)` and `:w`. Each was gated on "panel focused and cursor row" but never on Vim mode, while every one of those keys lives in VimNav, which stands down entirely with Vim off. To someone who never asked for Vim it read as a terminal. Every such hint now reads `vimMode` next to where it renders (or takes it as a prop where the component has no store: TasksRow, TaskStateBox and the Kanban card, whose checkbox tooltip now names the list's real toggle binding through `toggleKey` and nothing with Vim off). The comments strip is no longer mounted with Vim off, so it does not hold a blank row open under the header. The cursor styling stays in both modes: the highlighted row is the row you clicked; only what names a key goes. With Vim on nothing changes. Two Tasks surfaces were the exception to the rule the lists follow (with Vim off, single-letter keys are disabled and only arrows, Enter, Escape, Tab and Space navigate): the Kanban board and the Tasks calendar kept h/l, j/k, x, g, the bracket and angle keys and the calendar's e, m, dd, T and a live with Vim off, and their hint lines said so. They now get the same one-line guard the lists have, and their lines name what is live: arrows, Space, Enter, Tab, drag and right-click. The calendar panel beside a note does the same, since its `m` grab mode showed an "h/j/k/l pick a day" banner. TasksCalendar's handler effect also lists `vimMode` in its dependencies, which the existing `i`/`c` gating had missed. Verified over CDP in the built app with both stores isolated (docs/releases/v2.55.0/tools/check-vim-off-hints.mjs, local): two launches, Vim off then on, 28 checks reading the DOM and, for the board, the note file after `x` and Space. app-core suite and `npm run typecheck` clean.
The "Tasks, tags, and trash views" section of the manual listed j/k, x, i, c and friends with no word on Vim mode, while every one of them is Vim mode's: with it off the lists keep only the arrows, Enter and Escape, plus Shift+J/K in the list, Tab on the calendar and Space on the board and the calendar, and the rest is in the right-click menu. Now the section says so, and "Switch Tasks to Kanban (button or `3`)" names Vim mode for the digit, which was always gated on it. Follows cdccd55, which stopped the app naming those keys with Vim off.
Every workspace package moves to 2.55.0 together, so the desktop app, the web client, the share viewer and the shared packages report one version, and the lockfile follows.
Three assertions in workflow-apply.test.ts compared the text of a symlink a run had put back with `../sources/Real.md`, and failed on the Windows runner with `..\sources\Real.md`: Windows stores a symlink's target with backslashes whatever text created it, so `readlink` there never returns the forward slashes the test wrote. The run itself is right, it wrote the recorded text as is. A `linkText` helper reads the target with forward slashes on every platform and the three assertions use it. Found by the 2.55.0 release PR's Windows job; no app code changes.
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.
ZenNotes 2.55.0, cut from branch
v2.55.0.mainfast-forwards to this branch once the checks pass; the tagv2.55.0goes on the release commit.Feature
trigger: on note-saved,on note-created,on note-movedandon tag-addedrun a workflow for the note that changed, about a second and a half after it settles, with a receipt that carries Undo and a kill switch under Settings → Workflows ([Bug] Workflow triggers do not work #843).Fixes
mchip, the connections and comments strips, tooltips and hint lines; the Kanban board and the Tasks calendar now follow the same Vim-off rule as the lists.Docs
Release notes, demo clips and the verification log live in
docs/releases/v2.55.0/(local).