feat(sidebar): new, rename and delete, without leaving the app - #41
Merged
Merged
Conversation
Toril could open and save a note but not organize one: renaming meant going to the file manager and coming back. This adds new note, new folder, rename and delete to the files pane, and in doing so gives two already-built, already-tested pieces of Rust their first caller — trashbin shipped in v1.0.0 with no UI at all, and snapshots::rekey was written for exactly this rename. The rules go in crates/fileops rather than the command layer, because they are not obvious and they are mostly Windows: reserved device names that appear to work and then behave like hardware, names ending in a dot or space that Windows silently strips (so the file that appears is not the one we returned a path for), and a case-only rename that a case-insensitive filesystem reports as a collision with the file itself. Every operation refuses to clobber and requires its target inside the open folder, mirroring trashbin's boundary for the delete direction. Containment is checked with canonicalize and never used to build a result. On Windows that returns a \\?\C:\... path, which does I/O fine and matches nothing else in the app — not the sidebar tree, not an open tab, not a note's history key — so it would quietly split one note into two identities. Pinned by a test. Delete offers Undo instead of asking first: the file moves into .trash/, so the act is reversible and a confirmation would be friction in front of it. The one thing trash cannot bring back is an unsaved buffer, and that is the one case that stops and asks. The section 3 surface here is rename. The watcher reports it as delete then create, which is the shape removedOnDisk exists to catch — left alone, an open tab decides its file vanished and offers to recreate it, resurrecting the old note beside its new name. doRenameEntry re-points every affected tab (including tabs under a renamed folder), bumps the removal epoch for each old path so an in-flight reconcile cannot apply a stale "missing" verdict, and clears removedOnDisk — all in one synchronous block. base is deliberately untouched: a rename changes no bytes, so the merge base is still exactly right. The context menu is DOM, not native: a native popup cannot be driven by the headless gates, and native dialogs are the one thing documented to hang the app on the Linux dev box. vaultscan now keeps an empty directory. It pruned any folder without markdown in it, which meant New Folder created something that immediately disappeared and could not be put a note into. Asset-only folders stay pruned. Gates: cargo test -p fileops (28), tests/sidebar.test.ts, tests/contextmenu.test.ts. Two real defects were caught writing them — a window blur listener registered with capture, which sees every element's blur and so closed the menu the instant it focused its own first item; and the canonicalized-path leak above. Not verified on a device: the browser harness could not be driven in this session (the Chrome extension was not connected). Checklist in docs/ON-DEVICE-VERIFICATION.md section E. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Branch 12b (the chrome rework) shipped on main on 2026-08-12 as PR #36 — panes.ts, rail.ts, resizer.ts and the tokens/chrome/editor stylesheet split are all there — but its checkbox was never ticked, and the status block listed it under "landed outside the movement ladder" when it is a numbered branch in the ladder. Two records of the same thing, both wrong in different directions. The v0.2.0-alpha release point under branch 5 was overtaken by the v1.0.0 tag. The prose above it already says so; the unticked checkbox did not, which left the document implying a version still to be cut. Marked as overtaken and kept for what it meant, rather than deleted — the release point is the record of when the data-safety floor became handable to a stranger, and that is worth keeping even though the number is gone. Branch 12 now names its PR. It was ticked as shipped while nothing was on main and no branch was even pushed, which is the failure mode this document exists to prevent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.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.
Movement II branch 12 of
ROADMAP.md. Stacked on #40 — the diff here is the singlesidebar commit; GitHub will retarget this to
mainwhen #40 merges.Toril could open and save a note but not organize one: renaming meant going to the file
manager and coming back. This adds new note, new folder, rename and delete to the
files pane, and in doing so gives two already-built, already-tested pieces of Rust their
first caller —
trashbinshipped in v1.0.0 with no UI at all, andsnapshots::rekeywaswritten for exactly this rename.
Where the rules live
crates/fileops, not the command layer, because they are not obvious and they are mostlyWindows: reserved device names that appear to work and then behave like hardware, names
ending in a dot or space that Windows silently strips (so the file that appears is not the
one we returned a path for), and a case-only rename that a case-insensitive filesystem
reports as a collision with the file itself. Every operation refuses to clobber and
requires its target inside the open folder.
Containment is checked with
canonicalizeand never used to build a result. On Windowsthat returns
\?\C:\…, which does I/O fine and matches nothing else in the app — not thesidebar tree, not an open tab, not a note's history key — so it would quietly split one
note into two identities. Pinned by a test.
Two judgement calls
Delete offers Undo instead of asking first. The file moves into
.trash/, so the actis reversible by construction and a confirmation would be friction in front of it. The one
thing trash cannot bring back is an unsaved buffer, and that is the one case that stops and
asks.
The menu is a DOM menu, not a native one. A native popup cannot be driven by the
headless gates, and native dialogs are the one thing documented to hang the app on the Linux
dev box.
§3 surface: rename
The watcher reports a rename as delete-then-create, which is the shape
removedOnDiskexists to catch — left alone, an open tab decides its file vanished and offers to recreate
it, resurrecting the old note beside its new name.
doRenameEntryre-points everyaffected tab (including tabs under a renamed folder), bumps the removal epoch for each old
path so an in-flight reconcile cannot apply a stale
missingverdict, and clearsremovedOnDisk— all in one synchronous block.baseis deliberately not reset: a renamechanges no bytes, so the merge base is still exactly right.
Gates
cargo test -p fileops— the Windows name rules and the near-misses that must still beaccepted (
console.md,com10.md), containment against a traversal dressed up as asubdirectory, no-clobber on every create/rename while a case-only rename still succeeds,
and the caller's-spelling regression above.
tests/sidebar.test.ts— the menu offers only what is wired, a rejected name keeps thefield open with the message, blur cancels rather than commits, and a background refresh
cannot delete what is being typed.
tests/contextmenu.test.ts— one menu at a time, dismissal actually removes its documentlisteners, and the menu closes before its handler runs.
Full run on this branch: 565 TS tests, 130 crate tests,
cargo fmt --checkandcargo clippy --workspace --all-targetsclean.Still open from this branch
Drag-to-move in the tree, multi-select, and a trash browser —
list_trashhas a command andan
ipc.tswrapper but no UI, so Undo reaches only the most recent delete. On-deviceverification is §E of
docs/ON-DEVICE-VERIFICATION.md.🤖 Generated with Claude Code