Skip to content

feat(sidebar): new, rename and delete, without leaving the app - #41

Merged
evangress merged 2 commits into
feat/release-readinessfrom
feat/sidebar-file-ops
Aug 21, 2026
Merged

evangress merged 2 commits into
feat/release-readinessfrom
feat/sidebar-file-ops

Conversation

@evangress

Copy link
Copy Markdown
Collaborator

Movement II branch 12 of ROADMAP.md. Stacked on #40 — the diff here is the single
sidebar commit; GitHub will retarget this to main when #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 — trashbin shipped in v1.0.0 with no UI at all, and snapshots::rekey was
written for exactly this rename.

Where the rules live

crates/fileops, not 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.

Containment is checked with canonicalize and never used to build a result. On Windows
that returns \?\C:\…, 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.

Two judgement calls

Delete offers Undo instead of asking first. The file moves into .trash/, so the act
is 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 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 not reset: a rename
changes 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 be
    accepted (console.md, com10.md), containment against a traversal dressed up as a
    subdirectory, 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 the
    field 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 document
    listeners, and the menu closes before its handler runs.

Full run on this branch: 565 TS tests, 130 crate tests, cargo fmt --check and
cargo clippy --workspace --all-targets clean.

Still open from this branch

Drag-to-move in the tree, multi-select, and a trash browser — list_trash has a command and
an ipc.ts wrapper but no UI, so Undo reaches only the most recent delete. On-device
verification is §E of docs/ON-DEVICE-VERIFICATION.md.

🤖 Generated with Claude Code

evangress and others added 2 commits August 17, 2026 17:23
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>
@evangress
evangress merged commit fab2afd into feat/release-readiness Aug 21, 2026
4 checks passed
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.

1 participant