Skip to content

Finish Phase 3: create, rename and delete files - #8

Merged
aashishaxo merged 2 commits into
feat/phase-4-organisationfrom
feat/file-ops
Oct 7, 2026
Merged

aashishaxo merged 2 commits into
feat/phase-4-organisationfrom
feat/file-ops

Conversation

@ewraj

@ewraj ewraj commented Oct 6, 2026

Copy link
Copy Markdown
Owner

The Phase 3 commit left create, rename and delete undone, and the gist's MVP lists creating files. Stacked on #6 (Phase 4) because both touch the session store and the sidebar; retarget to main once #6 is in.

How it works

  • New file at the top of the tree, starting in the open file's folder. Enter creates it and opens it in Edit mode.
  • Rename and delete on each row. Rename happens in place with just the name selected. Delete is confirmed under the list and says what goes with the file.
  • A rename carries the cached text, annotations, bookmarks and reading position to the new id in one transaction.
  • Paths are checked against what GitHub, Windows and macOS all accept, case-insensitively, with a reason you can act on.

The decision to check (open question 4 in the plan): what a source can change follows what it can write back to, the same rule edits already use.

Source New file Rename / delete
Picked folder on disk any file, on disk
GitHub repo, uploaded folder here only, marked new only files created here

An upstream file would come straight back the next time the source opened, so deleting it here would be a lie. Opening the repository again by URL keeps the files you created.

Checked — 266 unit tests (path rules, the rename transaction, the source-kind rules). Plus 22 checks in headless Chrome against both a GitHub source and a writable folder (the origin-private file system, which has the picker's API), verified against the files on disk. Two bugs the browser run found are fixed in the commit: a created file appearing twice, and a rename restoring a new file's empty text.

ewraj added 2 commits October 7, 2026 00:39
The gist's MVP lists creating files, and the Phase 3 commit left create,
rename and delete undone. "New file" sits at the top of the tree and
starts in the open file's folder; type a path, press Enter, and the file
opens in Edit mode - "filename.ts, and then the editor opens it". Rename
and delete are on each row, rename in place with just the name selected,
delete confirmed under the list with what goes with the file said
plainly.

What a source can change follows what it can write back to, as edits
already do. In a picked folder every file can be renamed or deleted, on
disk. In a GitHub repository or an uploaded folder a new file is created
here only, marked "new", and only those can be renamed or deleted: an
upstream file would come straight back the next time the source opened,
and a tree that let you delete it would be lying. Opening a repository
again by URL keeps the files created in it.

A file's id is its path - a re-walked folder can only name files by where
they are - so a rename moves everything keyed to the old id onto the new
one in a single transaction: the cached text, annotations, bookmarks and
the reading position. Notes and bookmarks on a renamed file come with it.
Paths are checked against what a GitHub tree, Windows and a Mac will all
accept, case-insensitively, with a reason the user can act on.

Two bugs found by the browser run and fixed before this commit. The
folder adapter pushed new files onto the very array the app was
rendering, so a created file appeared twice; it keeps its own copy now.
And renaming a new file after typing into it put its empty starting text
back, because the app's copy of the entry was older than the last save;
the stored text now always wins, and entries no longer carry text at all.

Checked in headless Chrome, a GitHub source and a writable folder (the
origin-private file system, which has the picker's API): create, type,
save, note, rename, reload, delete, and a refused duplicate; and on disk,
create, write through, rename and delete, each verified against the
files themselves.
# Conflicts:
#	src/ui/FileTree/FileTree.tsx
@ewraj
ewraj marked this pull request as ready for review October 7, 2026 06:36

@ewraj ewraj left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phase 3 Completed

@aashishaxo
aashishaxo merged commit 1bc0e00 into feat/phase-4-organisation Oct 7, 2026
1 check passed
@aashishaxo
aashishaxo deleted the feat/file-ops branch October 7, 2026 15:41
ewraj added a commit that referenced this pull request Oct 7, 2026
* Plan Phase 4: layers, bookmarks, typed notes, search

The data model has carried Layer, Bookmark, kind 'note' and layerId since
Phase 0, and Phases 2 and 3 never exercised them. So Phase 4 needs no new
store, no schema bump and no second anchoring path: notes ride in the same
placed list as strokes, bookmarks resolve through the same ladder, and
hiding a layer is a render-time filter rather than a reload.

The document also lists the calls made without a conversation - where
layers live, how a note is created, where it is drawn - with the cheapest
way to reverse each one.

* Add goTo, so bookmarks and search have somewhere to send you

One navigation primitive for everything in Phase 4 that points at code:
open the file if it is not open, then land on the line.

A jump is not a restored reading position, and they now behave
differently. Restoring puts the line back at the top of the screen,
where it was being read from. A jump centres the line and moves the
cursor onto it, so the active-line highlight shows where you arrived -
otherwise a jump into the middle of a long file leaves you hunting for
the line you asked for.

goTo takes an optional anchor and resolves it against the file's text
once it has loaded. A bookmark or note in a file that changed since it
was written still lands on its code; when the ladder gives up, the
stored line is used as given.

Checked in headless Chrome: a jump into another file centres line 91 and
lights it, and a jump within the open file does the same.

* Build layers: create, rename, show, hide, delete or merge

Layers live in the palette's overflow menu, which the plan reserved for
them; the menu also takes over hiding the palette, which is what the dots
used to do on their own. One layer per source is active, persisted in
meta, and every new stroke is written into it.

Visibility is a render-time filter. Hiding a layer is a repaint: nothing is
reloaded, nothing in the database changes, and the undo stack is left
alone, so showing it again cannot lose anything. Hidden ink is not drawn
and cannot be erased - rubbing out something you cannot see is
destruction with no feedback. Drawing on a hidden active layer turns it
back on rather than laying down ink that vanishes as it is written.

Deleting asks once, inline, and offers to move the layer's annotations
into another layer instead. That is the plan's "reassign", at the only
granularity there is without a lasso. The merge is one transaction, so a
failure halfway cannot leave annotations in a layer the user was told is
gone. Removing a layer also drops the ink undo stack: an entry holds whole
annotations, and replaying one would resurrect it in the deleted layer.

The layers store now owns the default layer. ensureDefaultLayer shares an
in-flight call per source, because the store and the ink loader used to
race on a fresh source and could each write a "Notes" layer.

Checked in headless Chrome: draw, add a layer and write on it, hide one
and see only its ink go, erase across hidden ink and lose nothing, merge
on delete, reload and find visibility and the active layer as left.

* Give export a menu item, so the backstop can be reached

exportSource has gathered a codebase's records since Phase 0 with nothing
calling it. The plan calls the export the answer to every storage risk -
eviction, quota, a browser that forgets - and an answer nobody can reach
is not one. It is now "Export annotations" in the palette's menu: one JSON
file with the source, its files, layers, annotations and bookmarks, named
after the codebase and the day.

Import is not part of this.

Checked in headless Chrome: draw a stroke, export, and the downloaded file
holds the source, its layer and the stroke.

* Build bookmarks: a ribbon in the margin, a list in the sidebar

Two ways to set one. The ribbon button in the toolbar marks the cursor's
line when the cursor is on screen, and otherwise the top line of what is
showing - a reader who scrolled without clicking means "here", not
wherever the cursor was left, which on a fresh file is line 1. A narrow
margin left of the line numbers takes a click on any line, shows a grey
ghost under the pointer, and is the way in on a tablet.

A new bookmark is named after its own line. "export async function
runJob(job) {" says more than "Bookmark 3" and costs nothing to accept;
it can be renamed in the list, and coloured from the palette's swatches.

Bookmarks are anchored, not numbered. Each one already carried an Anchor,
and now it is used: resolved through the ladder when its file opens,
carried across edits by the same line mapper as ink, and re-anchored on
save. One the ladder cannot place is listed as "moved?" rather than
pointed at a line it no longer describes. The margin's ribbons are a
CodeMirror range set, so between store updates CodeMirror maps them
through edits itself.

The sidebar gains Files and Bookmarks views, the way a PDF reader's does,
instead of more buttons in the one strip of chrome the code shares the
screen with. Bookmark gets the optional updatedAt every other record has.

Checked in headless Chrome: set from the toolbar and the margin, clear
from the margin, mark the top line after scrolling away, rename and
recolour from the list, jump to it from another file, type two lines above
it and watch the ribbon follow, save, reload, and find it on the moved
line.

* Build typed notes: a Note tool, chips at the end of the line

A fourth tool in the palette. Arm it and tap a line to write a note on it;
tap a note - with the tool armed or with none at all - to open it again.
Keeping note-taking in the palette means it works the same way with a
stylus as everything else does.

A note is an annotation like a stroke: kind 'note', in the active layer,
in the same placed list. So it is carried across an edit by the same line
mapper, re-anchored by the same save, hidden with its layer, and sent to
the displaced tray when the ladder gives up - none of that needed writing
twice. It is not on the ink undo stack, where undo means "the last
stroke"; a note has its own Delete, and emptying one deletes it.

Notes are drawn as CodeMirror widget decorations after the code on their
line, not as an overlay positioned by hand, so they scroll with the text
by construction and there is nothing to keep in sync. The chip's height is
held inside the line box: a chip that grew its line would move every
stroke anchored below it, and the check for that is now part of the
browser run.

The card a note is written in opens under its line, or over it when there
is no room, and follows the line as the document scrolls. A tap anywhere
else keeps what was typed - tapping a second line commits the first note
on the way - and Escape is the only way to throw it away. Leaving a file
commits too, before the ink moves on to the next one.

Two collisions found on the way. The mouse-down that follows a tap would
move focus to the page and away from the card the tap had just opened, so
the drawing surface now swallows it while the Note tool is armed. And
Ctrl+Z inside a note field undid the last stroke instead of the last
keystroke; the ink's undo now leaves text fields alone.

The displaced tray shows a note's own words rather than the code beside
it - for typed notes the words are the thing worth keeping.

Checked in headless Chrome: write a note, see the chip on its line with
no line height changed, edit it from the chip, move to another line
mid-note and keep both, discard with Escape, delete by emptying, hide and
show its layer, Ctrl+Z inside the field, type two lines above it and
watch it follow, save, reload, and find it on the moved line.

* Build the annotation index: search every note and bookmark

The plan's test for Phase 4 is "you can find a note you wrote last week by
typing three words", and the matcher is built to that sentence. Every word
typed must match, in any order. Case and accents do not matter, because
nobody remembers how they capitalised a note. The file path counts, so
"runner webcontainer" finds the WebContainer note in runner.ts - where you
wrote something is part of how you remember it. Hits in the words rank
above hits only in the path, and notes above bookmarks.

It is the sidebar's third view, and Ctrl/Cmd+Shift+F opens it from
anywhere with the field focused; Ctrl/Cmd+F stays CodeMirror's search
within the file. With nothing typed the view is the index itself: every
note and bookmark, grouped by file. Results show why they matched - the
words highlighted, and a long note cut down to the part around its first
match. Enter jumps to the best one.

Notes on hidden layers are found and say so. Search is how you find what
you are not currently looking at.

Notes come from the database through the source's layers, a new
listNotes; the open file's are taken from the working set instead, so
their lines follow unsaved edits and a note written a moment ago does not
wait for the next read to be findable. Jumping to a note in a closed file
resolves its anchor through the ladder once the file has loaded.

Sidebar state moved into a small store, because the shortcut has to open
it on the right view from outside it.

The matcher is pure and tested table-style: folding, terms, matching,
ranking, highlighting that round-trips the text exactly, and snippets.
Checked in headless Chrome: four notes across two files and two layers
and a bookmark; one word finds the note and the bookmark that mention it,
three words including one from the path find exactly one note, a note on
a hidden layer is still found, clicking a result opens its file on its
line, and Enter jumps to the first.

* Close the sidebar sheet after choosing where to go, on a phone

Below 720px the sidebar is a sheet over the document rather than a column
beside it. Choosing a file, a bookmark or a search result left it open,
covering the very line just jumped to; on a phone the jump looked like it
had done nothing. It now closes itself after any of the three, on narrow
screens only - beside the code, on a wide screen, it stays where it is.

Checked in headless Chrome at 390px with touch: the sheet starts open,
closes when a file is chosen, and opens again from the toolbar; the
desktop runs for bookmarks and search are unchanged.

* Finish Phase 3: create, rename and delete files (#8)

The gist's MVP lists creating files, and the Phase 3 commit left create,
rename and delete undone. "New file" sits at the top of the tree and
starts in the open file's folder; type a path, press Enter, and the file
opens in Edit mode - "filename.ts, and then the editor opens it". Rename
and delete are on each row, rename in place with just the name selected,
delete confirmed under the list with what goes with the file said
plainly.

What a source can change follows what it can write back to, as edits
already do. In a picked folder every file can be renamed or deleted, on
disk. In a GitHub repository or an uploaded folder a new file is created
here only, marked "new", and only those can be renamed or deleted: an
upstream file would come straight back the next time the source opened,
and a tree that let you delete it would be lying. Opening a repository
again by URL keeps the files created in it.

A file's id is its path - a re-walked folder can only name files by where
they are - so a rename moves everything keyed to the old id onto the new
one in a single transaction: the cached text, annotations, bookmarks and
the reading position. Notes and bookmarks on a renamed file come with it.
Paths are checked against what a GitHub tree, Windows and a Mac will all
accept, case-insensitively, with a reason the user can act on.

Two bugs found by the browser run and fixed before this commit. The
folder adapter pushed new files onto the very array the app was
rendering, so a created file appeared twice; it keeps its own copy now.
And renaming a new file after typing into it put its empty starting text
back, because the app's copy of the entry was older than the last save;
the stored text now always wins, and entries no longer carry text at all.

Checked in headless Chrome, a GitHub source and a writable folder (the
origin-private file system, which has the picker's API): create, type,
save, note, rename, reload, delete, and a refused duplicate; and on disk,
create, write through, rename and delete, each verified against the
files themselves.

* Add an end-to-end suite, and run it in CI (#9)

* Finish Phase 3: create, rename and delete files

The gist's MVP lists creating files, and the Phase 3 commit left create,
rename and delete undone. "New file" sits at the top of the tree and
starts in the open file's folder; type a path, press Enter, and the file
opens in Edit mode - "filename.ts, and then the editor opens it". Rename
and delete are on each row, rename in place with just the name selected,
delete confirmed under the list with what goes with the file said
plainly.

What a source can change follows what it can write back to, as edits
already do. In a picked folder every file can be renamed or deleted, on
disk. In a GitHub repository or an uploaded folder a new file is created
here only, marked "new", and only those can be renamed or deleted: an
upstream file would come straight back the next time the source opened,
and a tree that let you delete it would be lying. Opening a repository
again by URL keeps the files created in it.

A file's id is its path - a re-walked folder can only name files by where
they are - so a rename moves everything keyed to the old id onto the new
one in a single transaction: the cached text, annotations, bookmarks and
the reading position. Notes and bookmarks on a renamed file come with it.
Paths are checked against what a GitHub tree, Windows and a Mac will all
accept, case-insensitively, with a reason the user can act on.

Two bugs found by the browser run and fixed before this commit. The
folder adapter pushed new files onto the very array the app was
rendering, so a created file appeared twice; it keeps its own copy now.
And renaming a new file after typing into it put its empty starting text
back, because the app's copy of the entry was older than the last save;
the stored text now always wins, and entries no longer carry text at all.

Checked in headless Chrome, a GitHub source and a writable folder (the
origin-private file system, which has the picker's API): create, type,
save, note, rename, reload, delete, and a refused duplicate; and on disk,
create, write through, rename and delete, each verified against the
files themselves.

* Add an end-to-end suite, and run it in CI

The plan put Playwright in CI in Phase 0, for the one thing unit tests
cannot see: ink drifting off the code it was written on, which it calls
the project's highest risk. It never arrived. This is it, and it covers
the Phase 3 and 4 flows that cross the editor, the database and the UI.

e2e/ink-scroll measures a stroke against its line at five scroll offsets,
after the sidebar closes and moves the code left, and after a reload, and
fails on a pixel of drift. The rest drive layers, notes, bookmarks,
search, export and file operations the way a reader would, and check the
database and - for a picked folder - the files on disk.

Each test gets a fresh browser context and seeds IndexedDB with exactly
what it needs through e2e/app.ts, then opens it from the recents list, so
nothing touches the network. Database checks poll rather than read once,
because writes land a moment after the click that caused them. The picked
folder is the origin-private file system in a persistent profile: in
Playwright's default off-the-record context, storing a folder handle in
IndexedDB closes the page, which no reader's browser does.

CI runs it as a second job beside verify, uploading traces when it fails;
deploys do not, since main has already passed it in review. The suite is
typechecked with the app. 29 tests, run three times over locally with no
flakes, about 13 seconds a pass.

---------

Co-authored-by: aashishaxo <singhaashish4522@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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