Conversation
…ve a settings save The main process rewrites vault.json through a normalizer that lists the view keys it keeps, and three keys added later were never put on that list: assetSortOrder, kanbanFolderRoot and kanbanStatuses. Any settings save from the desktop (a new favorite, a folder icon) dropped them from the file, so the renderer's per-vault choice silently never stuck. They are now carried through, kanbanStatuses reduced to its string entries. The comment on the normalizer says that every VaultViewSettings key must be listed, since the type cannot enforce it. Found while writing the settings merge for #816, which saves through the same path; the round-trip test gains a case for the three keys. Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4e0-a062-700d-8add-505c3d057740 Co-authored-by: Amp <amp@ampcode.com>
…setting (#816) When this device and another one both changed a vault's settings since the last sync, sync parks the cloud's copy as .zennotes/vault.cloud-conflict.json and keeps this device's in use. Until now the only place that said so was a card in Settings > Cloud with two whole-file buttons, so the question sat there until the user happened to look, and answering it meant choosing between "my favorites" and "their folder icons" when they wanted both. @uNyanda hit exactly that (#816). The question now opens on its own right after the sync that found it. It lists each settings section that differs, this device's value beside the cloud's, down to the field that changed, and offers This device or Cloud per section, plus one button for all. Keys the cloud's file carries that this build has no setting for are listed under "Not used on this device" and never applied. Decide later applies nothing: the status bar reads "Settings need review" until answered, and Space r, the Review Cloud conflicts palette command or the status bar's Review brings the question back, the file queue first when both are pending. The card in Settings > Cloud stays, with a "Compare and choose…" button into the same dialog; its two whole-file answers behave as before. The pure core lives in shared-domain (vault-settings-conflict.ts): diffVaultSettings, mergeVaultSettings and unknownVaultSettingsKeys, with SECTIONS typed as Record<keyof VaultSettings, true> so a new settings key fails to typecheck until it is added to the prompt or knowingly left out. The desktop's settingsConflict() now includes the parsed parked file as cloud_settings, optional in the bridge contract: a host without it keeps the whole-file question, and a parked file that does not parse is still reported. store.setVaultSettings resolves to a boolean so callers can tell a refused save from a completed one. The Cloud demo fixture's /demo/arm also bumps the cloud's vault.json so the flow can be reproduced locally. Verified in the built app over CDP against the fixture, both stores isolated: the prompt opened on its own, Decide later left both files untouched, Space r reopened it, and Cloud for two sections plus Apply produced exactly that mix in vault.json with the parked copy gone. Not exercised live: the web client and the mobile shells. Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4e0-a062-700d-8add-505c3d057740 Co-authored-by: Amp <amp@ampcode.com>
…ain by itself (#812) The startup update check runs once, 8 seconds after launch, and never rescheduled itself. Launch before the network is up and that one check failed, Settings > About read "Update error", the "update needs attention" toast fired, and nothing tried again until the user pressed Check for Updates. On the package-manager path the message was just "fetch failed": undici keeps the real cause (ENOTFOUND, ENETUNREACH) in error.cause. @uNyanda reported it from an AUR install (#812). A check that cannot reach GitHub at all is no longer an error. The updater enters a new offline phase ("Waiting for network" in About, the cause named in the message, no toast) and waits: net.isOnline() is read every 15 s without making a request, and the check runs again on a down-to-up transition. When the link is up but GitHub still cannot be reached (a captive portal, a router with no WAN) it retries 30 s after the failure, doubling up to every 15 minutes, and it makes a real attempt at least every 15 minutes whatever isOnline() says, so a notifier that is wrong about this machine cannot silence the check. Any phase other than offline or checking ends the wait, so a manual check owns its result. electron-updater's own error event is ignored for network-class errors so the state never flashes error before the check's catch decides. Errors that are not the network (a 404, a bad signature) still show as errors, now with the most specific line of the cause chain instead of "fetch failed". isNetworkUnreachableError walks message, code and cause five levels deep for the Node, undici and Chromium net::ERR_* spellings, including undici's bare "fetch failed". The bridge contract gains the offline phase; a host that does not know it shows no badge, notice or action. Verified with fake timers and a mocked net.isOnline (link down: no request until the link is back; link up: retries at 30, 60 and 120 s; the 15-minute attempt; a 404 does not wait; a manual check while waiting), and live in the built app over CDP with the feed on a closed local port: "Waiting for network (connect ECONNREFUSED ...)" and no toast, then, with the feed up and nobody touching the app, "Update available" 25 s later. Not exercised live: the electron-updater path and a real link transition. Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4e0-a062-700d-8add-505c3d057740 Co-authored-by: Amp <amp@ampcode.com>
…d of replaying it (#813) Delete Untitled.md on the desktop while the Trash already holds an older Untitled.md and the note is trashed as trash/Untitled 2.md, a move that changes folder and name in one step. Trash is synced, so the phone had the older trash/Untitled.md too, and its Storage Access Framework plugin did such a move as "move the document, then rename it". The move step keeps the source name, so Android refused the intermediate trash/Untitled.md with "rename failed: Already exists". @uNyanda reported the sync stuck there for good, the deleted note coming back on the phone with every Sync now (#813). The plugin's ordering is fixed in the Android repo (rename first, in the source folder, then move). What this commit fixes is why the failure kept coming back. pullChanges saved its cursor only after a whole batch had been applied, but the files a change touched stayed changed when a later change threw. Every retry then replayed the earlier upsert of Untitled.md from the old cursor, re-creating the note the user had just deleted, and failed again on the same move. The loop now keeps a second state, the newest one that can be persisted on its own, and saves it before letting the error escape, so a retry resumes at the change that failed. That state only advances when no coalesced upsert is still waiting for the change that lands it: a skipped revision is reduced into the live state but never written (#661), and persisting past it would make this device vouch for bytes it does not have. The success path still saves the final state once; the conflict branches are untouched. For the reporter this alone turns the loop into a clean retry: delete the note on the phone once more, Sync now, and the move and delete become no-ops without a source. Verified with two coordinator tests: the #813 sequence (an upsert then a move that throws; the cursor lands on 2, not 1, and after the local delete the retry succeeds with cursor 4 and re-creates nothing), and a guard that progress is never persisted past a coalesced revision whose landing change has not run, which fails against a naive save-after-every-change implementation. Not driven against a live phone. Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4e0-a062-700d-8add-505c3d057740
…d still rolls over (#817) Open today's daily note with "Roll over unfinished tasks to today" on and nothing to roll yet, then type a task into yesterday's note and open today's again: the task stayed where it was until the next day. @naingyeminn reported it as the setting doing nothing at all (#817), which is what it looks like when today's note is always opened first. The rollover kept a once-per-day marker in localStorage, written after every run including the ones that found nothing, so the first open of the day locked the feature for the rest of it. The marker existed to avoid re-reading years of daily notes on every open, and that cost is real, so it is not simply dropped. Its replacement is a per-vault record of the past daily notes already read and found free of open tasks, each with the updatedAt:size signature of the listing that was scanned. A note edited since, typed in this app, changed by sync or another editor, stops matching and is read again; a note with an open buffer is always read from the buffer, which costs nothing; a note the rollover just trimmed is read once more before it is trusted, so the trim itself can never hide a task. The command-palette run ignores the record and reads everything, as it ignored the marker before. Today's note now takes the tasks before the sources give them up, so a failure midway leaves a task in two notes rather than in none. extractOpenTaskBlocks also matched on lines split at "\n" alone, so a Windows file's trailing "\r" defeated the "$" in TASK_LINE_RE: its tasks showed in the Tasks view, whose scanner normalizes endings, and never rolled over. Matching now runs on "\r"-stripped copies while the remaining body keeps its endings byte for byte. Whether the reporter's files are CRLF is not known; the failure is silent and belongs to the same feature. Verified with a store test that walks the reported sequence (first open moves nothing, a second unchanged open reads no past note, the task typed in between rolls on the next open, the trimmed note is read once more) and a CRLF case in tasks-rollover.test.ts, both red on the old code, and in the built app over CDP with the reporter's steps. Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4e0-a062-700d-8add-505c3d057740
In Vim mode, `:version` printed "Codemirror-vim version: 6.3.0" in red and nothing else. That is the stock ex command of the Vim library ZenNotes embeds, and it answers a question nobody asks: the library's version is fixed by the ZenNotes build, and the line says nothing about the build. @vlcinsky asked for the ZenNotes version and the details a bug report needs (#814). `:version` (and `:ve`) is now ZenNotes' own. It prints the ZenNotes version and host kind, the operating system and architecture, the engine (Electron with its Chromium and Node, or the browser's user agent on the web client), how this copy was installed (macOS app bundle, Mac App Store, NSIS installer, portable exe, AppImage, deb, rpm, pacman, or a package manager or tarball install), and, when the window is connected to a remote workspace, the server's version and address. `:version copy` or `:version!` also puts the lines on the clipboard. The text is shown in the editor's own color for 15 seconds: red is what codemirror-vim uses for errors, and this is not one. Settings > About shows the same lines under the version, as a Version details block with a Copy details button, so a user who does not use Vim mode has the same report one click away. One function, lib/version-report.ts, builds the lines for both surfaces, and leaves out any field the host did not fill in instead of printing "unknown". The OS version and install format only main knows (os-release, resourcesPath, app.isPackaged), so the preload asks for them over one synchronous IPC, cached after the first getAppInfo() call, since that call runs many times at boot for its runtime field alone. The Linux install label reuses the updater's own format detection, so what `:version` prints is the format the updater acts on. Nothing in describeInstall() may throw: the preload asks while the window boots. The codemirror-vim version line is gone on purpose: the library does not expose it at runtime, and the ZenNotes version pins it anyway. Verified with unit tests for installLabel and osReleasePrettyName (desktop updater.test.ts) and buildVersionReport (app-core), and in the built app over CDP: `:version` prints the four lines, `:version copy` puts them on the clipboard, `:ve` resolves to this command, and the About block matches the ex output byte for byte. Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4e0-a062-700d-8add-505c3d057740
…first (#815) `zn open <folder>` bound to a key, a window on that folder already up in another workspace, and the app raised that window: focus jumped back to the workspace the user had just left, tabs and all. @radiorambo wanted what Chrome's `-n` gives: a fresh window on the same notes beside the new workspace, with its own tabs, the first window left where it was (#815). `zn open -n <path>` (or `--new-window`) does that now, in both the bundled Node CLI and the Go `zn`. The CLI hands the app a `--new-window` argv switch ahead of the paths. Chromium ignores switches it does not know and Electron does not define this one, so it reaches `second-instance` untouched, and the app's path collector already skips `-`-prefixed entries, so an older app raises the existing window as before. On a cold start the switch changes nothing: there is no window to reuse yet. What "a second window" means depends on what the path is: - A vault a window has open for real: a second real vault window on the same root, exactly what "Open Vault in New Window" does. A temporary session on that root is not an option, because the ephemeral registry is keyed by root and never unregistered, so it would silently switch the first window's workspace-state and settings writes off and send its deleted notes to the system Trash. - A folder that is not a vault: a second ephemeral session window. - A note inside a vault: a new vault window with the note queued. - A markdown file outside every vault keeps reusing its window even with `-n`: nothing keeps two standalone editors of one file in sync, so the second would overwrite the first's saves. The flag also needed a parser fix, since `zn open --new-window ~/notes` parsed as new-window="~/notes" with no path. `VALUELESS_FLAGS` in cli/args.ts lists the long flags that are switches, and the parser never takes the next token as their value (`--flag=value` still works). Every flag on the list was read only through getBool(), so nothing else changes, and `zn delete --yes inbox/a.md` stops swallowing its path too. The Go CLI's parser mirrors the list. Help: the `zn --help` OPEN row names the flag with an example, and the in-app manual's CLI section gains a card for opening a second window. Verified with unit tests (switch detection, switch-before-positional for every valueless flag, the argv the CLI spawns) and in the built app over CDP with isolated stores: `-n <vault>` gives a second real window on the same root while the first keeps its note; `-n <vault>/Ideas.md` opens the note in a third window with the first untouched; a plain folder reuses without `-n` and gets a second session window with it; closing the second window leaves the first intact. The Go `zn open -n` was run end to end against the same app. Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4e0-a062-700d-8add-505c3d057740
…from anywhere (#810) @saran-ncsu asked for a list of pinned or favorite notes to open from without keeping them around as tabs, and on the phones, which open Recent from the home screen, that same list (#810). ZenNotes already had Favorites: the sidebar section that `Space l s` and the row's context menu fill, stored in vault.json so it travels with the vault and with sync. What it lacked was a place to see them when no note is open, and a way to favorite a note that does not go through the sidebar. The home view now shows them. A Favorites section sits right after Recent, in the order the user arranged, with the icon and color the sidebar gives each entry and, for a note, where it lives ("Projects / Alpha"). A favorited note opens in place. A favorited folder opens that folder in the note list, brings the sidebar back if it was hidden and expands the folder and its ancestors in the tree first: folders start collapsed, and in the unified sidebar the tree is the only place the folder's notes appear, so a highlighted collapsed row would look like nothing happened. j/k (Vim mode) and the arrows walk from Recent into Favorites and Enter opens. The section stays out of the way until something is favorited, and a favorite whose note is gone (trashed, moved away) is skipped, as in the sidebar. Favoriting gains two routes. The command palette has "Add Note to Favorites" / "Remove Note from Favorites" (`note.favorite`; the title follows the active note's state, `when` hides it for trashed notes, the shortcut is resolved from the `vim.leaderToggleFavorite` chord). Hosts get `requestToggleNoteFavorite(host, path)`, wrapped in the usual `requestNoteAction` staleness guard, and `ShellSnapshot.favorites` (a frozen array, rebuilt only when the contents change so a folder-color save does not wake subscribers), which is what the phone shells use to put an "Add to Favorites" row in their ••• sheet and long-press menu, next to Pin. Pin stays the phone's own list order; Favorites is the vault's. The shell halves live in the phone repos. `resolveFavoriteItems(favorites, notes, folders)` moves out of Sidebar.tsx into lib/vault-layout.ts, and both the sidebar and HomeView read the same list through it, so a stale key can never show on one surface and not the other. Home stamps `data-home-section` (recent, favorites, today) on its sections and `data-home-favorite` (note or folder) plus `data-home-note-path` on rows; the phone shells' CSS and their row-swipe resolver key off these instead of counting sections and resolving Recent rows by position, which is what they did before. Pinned tabs are unchanged. The in-app manual's home-view card and the `Space l s` entries name the new surface. Verified with unit tests (vault-layout, commands, note-actions, public-host-api) and in the built app over CDP with isolated stores: a note favorite, a folder favorite and a deleted note's key resolve to two rows with the deleted one skipped; j moves past Recent onto the first favorite and Enter opens it; the folder row restores the hidden sidebar and expands the collapsed folder; unfavoriting removes the row at once; a vault without favorites shows no section. The phone stylesheet was checked against this build and against a core with no section markers; the Android shell was driven on the emulator and the iPhone shell with an XCUITest. Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4e0-a062-700d-8add-505c3d057740
…g mode (android#74) Reported from the Android app by @Alastor1991, where Reading is often the default view (ZenNotes/zennotesandroid#74): tap `[[Note#Heading]]` and the target note opened in edit mode with the cursor on the heading, keyboard and all. The desktop did the same. Every jump inside a note (`[[Note#Heading]]`, `[[Note#^block]]`, a same-note `[[#Heading]]`, a search hit, Ctrl+O / Ctrl+I) went through one path written for the editor: set the cursor, then switch the pane to edit so the cursor could show. A pane in reading mode now plans the jump itself. `planPreviewJump(jump, body)` in lib/preview-outline-jump.ts is the pure decision: a jump carrying `highlightLine` (a task opened from the Tasks view) still goes to edit, on purpose, since the highlight it paints on the source line is something only the editor can show; the `editorScrollMode: 'preserve'` shape that Ctrl+O / Ctrl+I (the phone's Back) send restores the reading view to the `previewScrollTop` it had when the user left, rather than jumping to a line; everything else becomes a line, taken from `editorSelectionAnchor`, and the rendered block for that source line is scrolled to the top of the reading view. A note the user last left in edit mode still opens in edit mode with the cursor on the heading; nothing changes for it. Two details keep the scroll honest. `previewShowsNote(el, path)` guards against scrolling the previous note's blocks: Preview.tsx stamps `data-note-path` on the article in the same step as the DOM swap, because the render is asynchronous and the effect can run before it. And a line plan whose blocks are not rendered yet is parked in `pendingPreviewLineRef` (the #543 cursor-line ref, renamed; same `onRendered` consumer), which is reset on note change before the pending-jump effect runs, so a line the previous note never got to render cannot fire on a later visit. Verified with 14 cases in preview-outline-jump.test.ts (heading, block and same-note links, a search hit, the task jump, the restore shape) and in the built app over CDP with isolated stores and default mode Reading: clicking `[[Reference#Deploy]]` from a scrolled Guide keeps reading mode with the Deploy heading at the top; Ctrl+O returns to Guide in reading mode within 40 px of where it was; a same-note `[[#Appendix]]` and a `[[Reference#^gotcha]]` block link both land the target at the top; a note remembered in edit mode still puts the cursor on the heading. Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4e0-a062-700d-8add-505c3d057740
…s (android#78) @uNyanda (ZenNotes/zennotesandroid#78): Settings → Templates → edit showed the editor and the preview side by side at any width, which on a phone left each about half a screen wide, too narrow to write in or to read. The two panes now follow the viewport. From 768 px up (the app's `md` breakpoint) they share a row as before, each 60vh tall. Below it the editor sits on top at 38vh and the preview under it at 22vh, each scrolling on its own, and the row of variable chips scrolls sideways (`flex-nowrap overflow-x-auto`) instead of wrapping into several lines that pushed the editor down. Viewport heights are the accepted exception to the no-arbitrary-values rule; no new tokens. The dialog itself, its header, name field and buttons are unchanged, so a narrow desktop window gets the stacked layout too. Verified in the built app over CDP, reading the layout from the DOM at 1280 px (two grid columns, 60vh each, chips wrapping) and at 390 px (one column, editor 38vh over preview 22vh, chips in one scrolling row, dialog still centered). Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4e0-a062-700d-8add-505c3d057740
The desktop's managed `zn` install still pinned ZenNotes CLI 0.2.0, so a 2.53.0 build would have shipped a `zn` that raises the existing window on `zn open -n` instead of opening a second one (#815). The pin now names 0.4.0, released today from ZenNotes/tui `6481d17`: the source commit, the four macOS and Linux archive URLs and their SHA-256 digests, each checked against the release's checksums.txt and against the downloaded archives. Verified through `npm run terminal:stage`, the same download, checksum and `--desktop-integration` probe that `apps/desktop/build/after-pack.js` runs at packaging time, for all four platform pairs; the darwin-arm64 binary answers protocol 1, version 0.4.0. `test:terminal` 12 of 12. Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4e0-a062-700d-8add-505c3d057740
Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4e0-a062-700d-8add-505c3d057740 Co-authored-by: Amp <amp@ampcode.com>
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.53.0
Seven reports from the issue tracker plus two that came in through the Android repo and are fixed in the shared core here. Three from @uNyanda (#816, #812, #813), one each from @naingyeminn (#817), @vlcinsky (#814), @radiorambo (#815) and @saran-ncsu (#810); zennotesandroid#74 from @Alastor1991 and zennotesandroid#78 from @uNyanda. Every issue is closed with a commit-linked comment.
Features
:versionprints what a bug report needs, with the same block in Settings → About (:versionshall show more information, e.g., zennotes version #814)zn open -nopens a second window instead of raising the first (CLI flag -n to open multiple windows of same vault, folder or file #815). The Goznshipped it as ZenNotes CLI 0.4.0; the bundledznpin moves from 0.2.0 to 0.4.0 in this releaseFixes
Verified before this PR
npm run typecheck7 of 7, nothing from cache;npm run test:run: shared-domain 1,683, app-core 2,542 (1 skipped), desktop 886 (4 skipped), nothing from cache.cd apps/desktop && npm run build:prodfresh, thennpm run pack; the packaged app launched with both stores isolated: CDP page target in 1.5 s, version 2.53.0, the bundledzninside the app is 0.4.0 from ZenNotes/tui6481d17.test:vim-editor,test:sidebar-vim,test:editor-improvements, all green with no renderer console errors.Docs: in-app help updated in the commits; the website mirror is the branch
website/docs-2.53, merged with the release.