Skip to content

Release 2.52.0: panels each note remembers, undo history that stays with each note, and a note that keeps its width - #807

Merged
adibhanna merged 19 commits into
mainfrom
v2.52.0
Sep 17, 2026
Merged

adibhanna merged 19 commits into
mainfrom
v2.52.0

Conversation

@adibhanna

Copy link
Copy Markdown
Contributor

ZenNotes 2.52.0

Started as the 2.51.2 patch branch and grew into a minor. Two requests from @uNyanda, a mobile Browse action, and ten fixes, most of them for keys that went somewhere other than where the user was looking. Every issue is closed with a commit-linked comment.

Features

  • Feat(panels): each note can remember its own panels, and they come back after a restart (Save Panel States Independently Per Note #794)
  • Fix(editor) + Feat(editor): each note keeps its own undo history across tab switches, and with :set undofile / "Keep undo history after quitting" it survives quitting too (opt-in, desktop only, stored with the app, never in the vault) (Add Support for undofile Setting #793)
  • Feat(browse): a folder or database can move to another folder (public Browse API for the mobile shells; no desktop caller)

Fixes

Verified before this PR

  • npm run typecheck 7 of 7; npm run test:run: shared-domain 1,666, app-core 2,498, desktop 860.
  • npm run pack, then the packaged app launched with both stores isolated: CDP page target in under a second, version 2.52.0, the new main-process undo store loads inside the asar.
  • Smoke against the release build: test:vim-editor 12 of 12, test:sidebar-vim 12 of 12, test:editor-improvements 19 of 19.
  • Every fix reproduced on the previous build first and verified in the built app over CDP with real key events; twelve captioned clips recorded.

Docs: in-app help updated in the commits; the website mirror is the branch website/docs-2.52, merged with the release.

After a visual-block I, A or c, Escape applied the text to every row but
left Vim in insert mode (#803). CodeMirror runs every keymap from a single
DOM handler placed at the first keymap provider, and the editor mounts one
(markdown snippets) ahead of vim(), so every keymap binding sees a key
before the Vim plugin does. Two Escape bindings were spending it.

defaultKeymap binds Escape to simplifySelection, which reports the key
handled whenever there is more than one range or a non-empty one. Since
block edits became real multi-cursors (#792) that is exactly the state Vim
is in when Escape should end the edit: CodeMirror collapsed the cursors and
Vim never saw the key. The same binding made visual block need two presses,
and left a v or V selection through the selection listener with the cursor
on CodeMirror's head instead of the character Vim keeps it on. Escape now
defers to Vim in insert, replace and visual mode. Normal mode keeps the
native command, because there Vim itself hands Escape back to the editor so
stray extra cursors can still collapse.

The completion keymap's closeCompletion reports the key handled while any
source is merely pending, and every typed character puts all sources into
that state for the activateOnTyping debounce (about 100ms) even when none
will match. An Escape pressed right after the last character was lost to a
completion nobody could see, with a single cursor too: easy to hit with
Escape on Caps Lock, and what kept a block insert stuck for fast typists
even once the first binding was out of the way. A pending query is still
cancelled, but only a visible popup keeps the key. A visible popup still
takes the first Escape and stays in insert mode, as before.

The #792 suite asserted a return to normal mode and passed falsely: it
mounted vim() ahead of any keymap, the reverse of the app, so Vim got
Escape first. The mount now mirrors the app's handler order, and with the
fix removed nine tests fail.

Fixes #803.
…scripts

Four scripts that drive the built app over CDP could no longer start:
vim-editor-smoke, sidebar-vim-smoke, editor-improvements-smoke and
perf-editor-scroll all died with "Cannot find module 'electron'" before
launching anything, so `npm run test:vim-editor` and its siblings were dead.

Each one did createRequire(import.meta.url) and required electron from the
repo root. electron is declared by the desktop workspace and is no longer
hoisted to the root node_modules, and hoisting was never a contract. They
now resolve it from apps/desktop/package.json, the pattern
perf-desktop-runtime.mjs already used. ws and the other root-level requires
are untouched, and pdf-export-smoke.py already looked electron up from the
desktop workspace.

With this, test:vim-editor passes 12 of 12, test:editor-improvements 19 of
19, and perf:editor-scroll runs to completion. test:sidebar-vim launches
too; the failures it then reported were a real sidebar bug, fixed next.
In a folder large enough to be windowed (240 notes and up), walking the
sidebar with j/k, opening a note with Enter and coming back with Ctrl+W h
lost the cursor: nothing was highlighted, the next j showed nothing, the
one after landed on Assets, and k on Trash.

Every sidebar row carries a data-sidebar-idx handed out by a mutable
counter, and the Vim cursor is stored as one of those numbers. Only
Sidebar's own render restarts the counter, but FolderTreeRoot,
FolderTreeContents and SubTree read it during theirs and hold state of
their own, so they also render alone. A lone render read whatever the
counter had been left at, and two things leave it wrong:

- A full Sidebar render ends on the sidebar's total row count.
  FolderTreeContents re-renders alone on every sidebar focus change (its
  progressive entry limit resets when `progressive` flips with
  sidebarFocused), so the notes jumped from 10..1009 to 1010..2009 and the
  stored cursor matched no row. It stayed hidden in the simple keyboard
  flow by luck: the focus effect usually writes the cursor in the same
  batch and forces a full render. It bites when the cursor is already on
  the target row, which is exactly "return to the note I just opened".
- React also runs Sidebar's body and then bails out without rendering its
  children. That restarts the counter and leaves it just past Sidebar's
  inline rows, so the next lone render numbered notes from 8 and collided
  with Quick Notes (8) and Inbox (9).

The second case is why a "Sidebar rendered" flag on the counter, the first
shape this fix took, cannot tell the two kinds of render apart. Only a prop
can, because props change exactly when the parent really rendered the child.
useStableSidebarIdxBase remembers the counter value a tree component starts
from and restarts from it whenever the component renders again with the
same idxPass. Each parent creates a new pass per render, and the prop is
required in TreeRenderProps so the compiler flags a call site that forgets
it. DateNotesNav holds no state and needs no guard.

`npm run test:sidebar-vim` was reporting this as 4 failures of 12 and had
not drifted: with the fix it passes 12 of 12 with no selector changes. The
fourth failure was a knock-on, since with no cursor the script fell back to
idx 0 and its gg wait became trivially true. The script did have one real
gap: it never set ZENNOTES_CONFIG_DIR, so it read the developer's real
config.toml and only passed where that file happened to enable Vim. It now
seeds its own, like every other CDP script.
Running "Search Text in Vault…" from the command palette with the editor
focused opened the search with the caret still in the note, so the query was
typed into the note instead (ZenNotes/zennotesandroid#65). "Search Notes…"
and "Open Note Outline…" did the same. Reported on Android in Edit mode, but
the cause is in the shared core and desktop had it too.

After a command runs, the palette hands DOM focus back to the editor unless
it knows the command left something open on top. That list named Settings
and three palettes and was missing the note search, vault text search and
outline palettes. focusEditorNormalMode focuses on the next animation frame,
which is after the opened palette's mount effect focused its input, so the
editor took focus back a moment later. The overlays are lazy-loaded, which
hid this on the first open of a session (the chunk mounted after the
refocus) and broke every later one. In Read mode the editor is hidden and
cannot take focus, which is why only Edit mode showed it.

The list now lives in command-palette-mode.ts as COMMAND_OVERLAY_FLAGS
behind shouldRefocusEditorAfterCommand, and a test requires every `…Open`
boolean in the store to be classified, so a new overlay cannot silently
regress to typing into the note behind it.

Store flags cannot see everything: "Publish Note" opens its dialog through
publish-note-requests without awaiting it, and the editor was taking focus
behind that dialog as well. The caller now reports it next to the Cloud
conflict review, as `dialogOutsideStoreOpen`. Prompts, confirms and date
pickers need no entry, because their commands await the answer and they are
closed by the time this runs.

Verified on a desktop build with real key events, three runs per command
since the first one after launch was the lucky one: the five palettes keep
focus in their input and the note stays untouched, the Publish Note dialog
keeps focus, and a command that lands on the editor is still handed focus.
…he note

With Settings open, typing still edited the note behind it. It happened
whether Settings was opened with Mod+, or from the command palette, because
nothing pulled focus back: Settings simply never took it. It is the one
dialog that draws its own backdrop and panel instead of sitting in the
shared Modal shell, so it never got what the shell gives every other
dialog, where opening moves focus into the panel, Tab cycles inside it, and
closing hands focus back to the opener.

Wrapping Settings in <Modal> would have changed how it looks (backdrop
blur, top offset, corner radius, border against ring), so the shell's focus
logic moves out of ModalRoot into two exported helpers, useDialogFocus and
trapDialogTab. ModalRoot uses them itself, so the 21 dialogs on the shell
behave exactly as before, and Settings calls them on its own panel rather
than keeping a copy. The panel also gains dialog semantics (role,
aria-modal, a label) and tabIndex -1 so it can be a focus target.

Opening lands on the settings search, the first thing a keyboard user
reaches for. On a touch device a focused input would raise the on-screen
keyboard over the list the user is about to tap, so the panel takes focus
there instead, the same rule PromptModal already follows. Escape, the
backdrop and Done still close through closeSettings, which returns focus to
the editor as before.

Verified on a desktop build with real key events, Vim off and on, and with
a touch device emulated: focus lands inside Settings from both ways of
opening it, typing stays there and never reaches the note, 70 Tab presses
never leave the window, Escape closes it and the caret is back in the note.
Settings opens on its search field, but the search itself still needed
the mouse. Enter in the field did nothing, the arrows did not move
through the results, and once focus had moved to a control there was no
key to get back to the search: `/` and Cmd+F, the keys that search
everywhere else in the app, were ignored inside Settings. That is what
#108 asked for back in 2.2.0.

In the field, Enter now opens the picked result, and the arrows (or
Ctrl+J / Ctrl+K and Ctrl+N / Ctrl+P, the same keys the palettes use)
walk the results, opening each one as a click would: right category,
right sub-tab, the setting scrolled into view and flashed. The walk
stops at both ends instead of wrapping, and the picked row is kept on
screen when the list is longer than its column.

From anywhere in Settings, Mod+F returns to the search with its text
selected, and so does `/` in Vim mode. The single key follows the house
rule: it is off when Vim mode is off, and a `/` typed into any field is
just a character.

The find key is handled on the Settings panel, not at the window. The
three shortcut recorders in Settings capture keys at the window and have
to win while they are recording, so recording `/` as an ignored key
still records it.

Fixes #108
…g a note

With Trash, Tasks, Help or an asset in the active tab, Cmd+2, Cmd+3 and
Shift+Cmd+C (and the calendar toggle) still flipped that pane's
Connections, Outline, Comments and Calendar. There is no panel to see on
those tabs, so nothing appeared to happen, and the change turned up on
the next note instead: a panel you never asked for was open, or the one
you had open was gone.

The four toggle listeners now only act when the pane's active tab is a
note. The test is the kind of tab, not whether content has loaded, so a
shortcut pressed while a note is still opening is kept. Closing the
right panel with its own shortcut is unchanged, since closing something
you cannot see does no harm.
…toml

`keepViewModeAcrossNotes` has been in PORTABLE_PREF_KEYS since it shipped
(#543), which is the list of preferences that are supposed to travel
between machines in `~/.config/zennotes/config.toml`. It never did. The
desktop writer only emits keys that have a field mapping, the key had
none, so it was portable in name only: set it on one machine and the
file, and every other machine reading that file, never heard about it.

It is now `keep_view_mode_across_notes` under `[editor]`. A key missing
from an existing file keeps the local value when the file is read, so
nobody's current choice is reset; the option simply appears in the file
the next time the app rewrites it.

To keep this from happening to the next preference, a test now requires
every key in PORTABLE_PREF_KEYS to come back out of a freshly written
config. Without the mapping it fails and names the key.
The Connections, Outline, Comments and Calendar panels have always
belonged to the pane: open the Outline and it stays open on every note
you move to. That suits browsing, but #794 asks for the other habit,
where a project note lives with its Outline and Connections open and a
scratch note lives with nothing, and each comes back the way it was
left.

Panels following the pane stays the default, so nothing changes for
anyone who does not ask. Turn off Settings > Editor > "Keep panels when
switching notes" (or run "Remember Panels per Note" from the command
palette) and each pane keeps one set of panels per note instead. A note
you have not opened yet starts with none. It is the same shape as the
per-note Edit / Split / Preview memory and its "Keep view mode" switch,
on purpose: one idea, two settings that read alike.

What it chooses:

- The memory lasts for the session, like the per-note view mode. It is
  never written into the note, which is a plain file. If it should
  outlive a restart, the workspace snapshot that already restores tabs
  and layout is where it belongs; that is left for a follow-up.
- A note's panels follow it through a rename or a move, are dropped
  when the note is deleted, and are copied to a new split.
- Flipping the setting never rearranges the screen. The panels on view
  are carried into whichever store takes over, and only later note
  switches behave differently.
- With the automatic calendar on, a daily or weekly note whose calendar
  you closed stays closed when you come back to it. That needs a
  "dismissed" bit, recorded only in per-note mode, so the default mode
  keeps auto-opening the calendar on every arrival exactly as before
  and the #502 state machine is untouched.
- The preference is portable: `keep_panels_across_notes` under `[view]`
  in config.toml.

EditorPane swaps its four useState calls for one hook, usePanePanels,
with the same names and setter shapes, so every toggle and auto-open
below it is unchanged. The setters keep a stable identity and read the
live preference and path through refs, because the editor hands them to
CodeMirror handlers that are built once per mount. The store action
runs its updater outside zustand's set(): the panel toggles write to
the store from inside their updater, and a write nested in a set()
callback is clobbered when the callback returns.

Both "Keep ..." settings also gain the Settings search entries and the
sub-tab registration the older one never had, so searching for either
opens the Writing tab on the right row.

Closes #794
With "Keep panels when switching notes" off, each note remembered its
own Connections, Outline, Comments and Calendar, but only until you
quit. Hand testing showed that as the one place the feature felt
unfinished: set up a project note with its Outline, a reading note with
Comments, relaunch, and every note was bare again while the tabs and
splits around them had come back.

The memory now rides in the workspace snapshot, the same record that
already restores tabs, layout and the sidebar, locally and as
`.zennotes/workspace.json` for a synced vault. Nothing is written into
the notes themselves, which stay plain files. A calendar you closed on
a daily note stays closed after a restart too, because the "dismissed"
bit travels with the rest.

What it is careful about, since that file syncs between machines and
versions and can be edited by hand:

- It stays small. Only panes that still exist are written, only what is
  open is spelled out (a note with Outline open is `{"outline": true}`),
  a note with nothing open is not stored at all, and each pane keeps the
  200 most recently set notes. Setting a note's panels again moves it to
  the back of that queue.
- Nothing in it is trusted on the way back in: unknown panes are
  dropped and every field is coerced to its type. A snapshot written
  before this change simply has no panels.
- Once the note listing is in, memory for notes that no longer exist is
  retired, with the same empty-listing guard the tab check uses, so dead
  paths do not pile up after deletes on another machine.
- Toggling a panel changes nothing else about the workspace, so that
  action asks for the save itself instead of waiting for a tab change.

The default mode is untouched: with the setting on, panels belong to the
pane for the session and nothing is restored at launch, exactly as in
2.51.1. Per-note view modes (Edit / Split / Preview) are still
session-only; this change does not extend to them.
…#793)

Make a few edits in a note, switch to another tab, come back, press u:
nothing. Every undo step on the note was gone, and the same happened
after a visit to Trash or Tasks, or after closing the note and opening
it again.

It was deliberate. A pane has ONE editor, and showing another note
swaps that editor's document. Since #247 the swap also empties the undo
history, because a history that crosses the swap lets Cmd+Z paste the
previous note over the current one and save it. The price was that
leaving a note for a second threw away everything you could undo in it.

The history is no longer discarded. It is set aside under the note it
belongs to (vault root and path, so it follows the note into another
pane and survives closing the tab) and handed back when that note
returns to an editor: CodeMirror's history field can be read out of one
state and seeded into another. The editor is also torn down whenever a
pane shows something that is not a note, so that is treated as leaving
the note too.

#247 stays fixed, by construction: a history only ever goes back onto
the note it was taken from, and only while that note still reads
exactly as it did then. Undo steps are edits at character positions, so
on any other text they would corrupt it. If the note changed somewhere
else in the meantime (another pane, sync, an external editor), it starts
a clean history, which is what every switch did before. The fifty most
recently left notes are kept.

There is no setting: this is how an editor is expected to behave. It
covers the in-session half of #793. The other half, undo that survives
quitting the app (Vim's undofile proper), is not part of this change.
…o history

Rename the note you are editing and the caret jumped to the top of it,
the scroll reset, and its undo history was gone, although you never left
the note. Moving it to another folder, or renaming the folder it lives
in, did the same.

A pane has one editor, and it took any new path for a different note.
That is the moment it deliberately drops all three, because for another
note they are meaningless and the undo history is dangerous (#247). A
rename changes the path too, and nothing told the editor that the note
under the new path was the one it was already showing. The rename also
reaches it as two updates: the path first, then the title heading
rewritten to match, and each of them emptied the history on its own.

The store now keeps a short log of "this path is now that path",
appended in the same update that rewrites the paths, at the one remap
every note rename, note move, folder rename and delete goes through. A
path change that log explains is the same note: the editor keeps the
caret, the scroll and the history, and carries the remembered tab
position along. Each editor remembers the newest entry it has accounted
for, so an OLD rename can never make a real note switch look like one;
that would be #247 again, and a genuine switch still goes through the
full reset.

The heading rewrite is why the second half was needed. When a note's
text changes underneath the editor it used to replace the whole
document, which tells CodeMirror that every position is gone: the caret
can only be clamped, and every stored undo step collapses to nothing (a
control test shows exactly that). It now applies only what changed, the
span between the common prefix and suffix, and lets CodeMirror map the
caret and the history through it. That reaches past renames: another
pane typing in the same note, a rename elsewhere rewriting a link in
it, or the file changing on disk used to silently empty the note's undo
history, and now leave it working. Text with carriage returns keeps the
whole replace, because CodeMirror folds \r\n into one line break and
string offsets would not be document offsets.

Two smaller pieces: the per-tab restore re-applies remembered offsets a
frame later, which put the caret back on a stale offset once the
heading had shifted the text, so a rename skips it. And undo histories
set aside by the previous commit follow their note, so one that is
moved, or whose folder is renamed, while it is off screen keeps its
undo as well.
…fit (#805)

Split the window, open the Outline and Connections in one pane, and
the note between them was a strip a few characters wide. Add Comments
and it had no room at all. The four right-hand panels each have a width
the user chose (200 to 640 px), they never shrink, all of them can be
open at once, and nothing reserved any room for the note. A full-width
window hides that. A half-width pane does not: in a 632 px pane two
panels left the note 84 px.

The note comes first now. It keeps 320 px (520 in Split mode, which
shows source and preview side by side), and the panels give way in two
steps. First they shrink, in proportion, down to the smallest width a
panel can be dragged to. If that is still too much, the panels opened
longest ago are tucked into a slim rail at the pane's right edge until
the rest fits.

Tucked is not closed. Nothing the user opened is lost, and the per-note
panel memory from #794 keeps recording what was opened rather than what
happened to fit. A click in the rail brings a panel forward, and so
does its usual shortcut: Cmd+3 on a tucked Outline shows it, and a
second press closes it, which is what the key has to mean when the
panel is open but not on screen. The toolbar tooltips say "Show" for a
tucked panel for the same reason. Code that asks for a panel that is
already open (jumping to a comment opens Comments) brings it forward as
well, so it can never land on something the user cannot see.

Tucked panels are not rendered. Vim pane navigation finds the panels
through the DOM, so it skips them with no change of its own, and a
panel that is tucked while it holds the keyboard hands it back to the
editor, as does a click in the rail.

A pane with room for everything behaves exactly as before, and so does
the first paint, since an unmeasured pane changes nothing. In a pane
too narrow for a note and a panel both, the panel that was just asked
for still shows, at its smallest useful width.

Fixes #805
27c09c9 made each note keep its undo history while the app is open.
This is the other half of #793: reopen a note tomorrow and u / Mod+Z
and redo still step through yesterday's edits.

It is a setting, "Keep undo history after quitting" under Settings >
Editor, and it is off by default. Undo history is made of the text you
deleted, and writing that to disk is a choice the user should make. In
Vim mode it is the option Vim users already type: `:set undofile`,
`:set noundofile` and `:set undofile?` work, through Vim.defineOption,
and there is a command palette entry as well. It travels in config.toml
as `persist_undo_history` under `[editor]`.

Where it lives is the part that needed deciding. Not in the vault:
inside `.zennotes/` it would sync to other machines and land in the
history of a git-backed vault. It sits under the app's own user-data
folder, one file per note, the split Vim makes between a file and its
entry in `undodir`:

  <userData>/undo-history/<sha256(vault)[:16]>/<sha256(note path)[:32]>.json

That makes it a desktop feature. The bridge gains three OPTIONAL methods
and a `supportsUndoFile` capability, so the web client and the mobile
shells need no change and do not show the setting; they keep undo
history while they are open, as before.

The main process follows the usual trust model. The vault comes from
main-process state, never from the renderer (with the remote profile id
mixed in for a remote workspace), and the note path only ever feeds a
hash, so nothing the renderer sends can steer a read or a write outside
that folder. Payloads over 2 MB are dropped, writes go through a temp
file and a rename so a quit mid-write cannot leave half a file, and a
sweep once per launch, off the boot path, expires histories nobody came
back to for 90 days and keeps the 400 most recent per vault.

The renderer saves where it already sets a history aside (leaving a
note, the editor being torn down) and on beforeunload for the note that
is on screen, which the host finishes after the window is gone, the way
it finishes the note saves fired from the same event. It reads a saved
history only into an editor that still shows that note with nothing to
undo yet, so a history kept in memory, or an edit made while the file
was being read, wins.

The rule from 27c09c9 decides whether a saved history may be used at
all: only on the text it was taken from. A note can change between two
launches in ways the app never sees (sync, git, another editor), so the
file carries a fingerprint of the text, and a mismatch means a clean
history. Over 1.5 M characters the OLDEST undo steps are dropped first.

A save never erases. The same note can be open in a second pane that
has just saved its history, and an empty one would have wiped it (found
while testing). Only renaming the open note and turning the setting off
erase anything, and turning it off erases everything, in every vault:
the files do not outlive the setting that asked for them. That lives in
an App effect rather than the setter, because config.toml can turn the
preference off too.
Type `[[`, move to a note in the picker, press `|`, and you got `[[|]]`:
the highlighted note was gone and the picker had closed. The picker's
own tip reads "Type | to change display text", but `|` was never one of
its keys. It was typed as plain text, and the wikilink source stops
matching as soon as the text holds a `|`, so the list closed and the
name it was offering went with it.

`|` is now an accept key for note, asset and database suggestions, next
to Enter, Ctrl+Y and Tab: it takes the highlighted suggestion and leaves
the caret behind a `|` inside the brackets. `[[`, Linux, `|` gives
`[[Linux|]]` ready for the display text, and a half-typed name is
completed first, the way Enter completes it. The `@` note picker shares
the suggestion kind, so it gains the key too.

The key is matched as a character, not as a chord. It is Shift+\ on a
US keyboard, Option+7 on a German Mac and AltGr+< on a German PC, which
reports Ctrl and Alt together, so the modifiers cannot be pinned down;
only a bare Ctrl or a Cmd chord is certainly not typing.

Picking a new note for a link that already has display text keeps that
text, so a second `|` there would break the link. The existing display
text is selected, ready to be typed over. A `#section` the link already
carries stays in front of the new `|`.

Everywhere else `|` is still just a character: in plain text, in a table
row, in pickers that are not wikilinks, and after a name that has no
suggestions, so `[[Brand new idea|alias]]` types as it always did. With
suggestions showing, `|` now takes the highlighted one even if a new
name was meant, which is what Enter already did; Esc first keeps the
typed name, and the manual says so. An exact title match ranks first,
so typing a full existing name and then `|` gives the same link as
before.

`closedLinkTail` and the new edit live in a store-free module,
`cm-wikilink-tail`, because `cm-wikilinks` reads the store and the key
handler, and its jsdom tests, should not.

Fixes #804
The mobile shells can rename and delete a folder or database from
Browse, and move a note, but could not move a folder or database:
desktop does it by dragging in the sidebar, which has no equivalent on
a phone, and the public Browse API had no action for it. A Play Store
review of the Android app asked for exactly that.

requestMoveBrowseDirectory(host, directory) prompts for a new parent
with the same inbox[/path] values as the move-note prompt, then calls
the store action the sidebar drag already uses, so open tabs (database
tabs included), folder icons and colors, favorites and manual order
follow the move. The leaf name is kept, and with it a database's .base
suffix.

The prompt offers existing notes-area folders only: never the
directory itself, anything under it, a database, or the archive. A
destination that already holds the name is refused in the prompt, the
current parent is a no-op, and the host still refuses to overwrite.
The field starts empty on purpose: the touch prompt filters its list
by the field's value, so a prefilled path would hide every other
folder. Pins stay host-owned; the README says a host that pins folders
re-keys them after a move.

Verified by tests only, not in a running app: ten cases in
browse-actions.test.ts, each rule checked by removing it and watching
a test fail. The shells add their "Move to…" rows when they adopt
this core.
…aid out

`test:editor-improvements` failed one run in five at the release gate
with "cursor position survives Preview and Edit mode switches", and the
detail gave it away: the caret's left was 0. A caret at the start of
that line sits around x=400, never 0. Preview mode tears the editor
down and Edit mode builds a new one, and for a frame the new caret
element exists without a layout box, so its rect is all zeros. The poll
accepted the first sample that had the right active line, and sometimes
that sample fell into the gap.

Both samples now wait for a caret with a real box. Nothing in the app
changed: the position was restored correctly in every run, including the
failing one.
Comment thread apps/desktop/src/main/undo-history-store.ts Fixed
CodeQL flagged the new undo store on the release PR (js/file-system-race,
high): readUndoHistory checked the file's size by path and then read it
by path again, so the file could be swapped between the check and the
read. It is the class 5cd4eb5 fixed for the CLI launcher.

The file is now opened once, and both the size check and the read go
through that handle. A missing file is still "no history", and anything
over the cap is still ignored.
@adibhanna
adibhanna merged commit f90161f into main Sep 17, 2026
13 of 14 checks passed
@adibhanna
adibhanna deleted the v2.52.0 branch September 17, 2026 19:22
@adibhanna
adibhanna restored the v2.52.0 branch September 17, 2026 19:23
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.

2 participants