Walking into a folder wrote its path with replace, so the whole journey shared one
history entry: Back did not step up a folder, it left the library and landed on
whatever came before it. Each folder is its own entry now, and the two effects that
keep path and selection in step carry a marker so neither overwrites the entry the
other just arrived at. A path naming a folder that has not loaded yet waits for the
folder map to fill instead of falling back to the root.
New folder is one control in both places it appears. The empty state offered a
single click that guessed a destination and blocked itself where it could not; it
now shows the header's menu, under the header's label.
Opening a file already in the workspace skips the fetch-and-add and just goes to
it, and a folder answers to the source filter the way its files do.
It was a pseudo-tab with a view of its own: its own predicate for which files
count, its own empty state, and a carve-out anywhere folders are involved -
folder visibility, the New folder button, the heading. All of it to say "files
with no server copy", which the source filter already says.
Clicking it now sets that filter and nothing else, so it narrows whichever view
you are in instead of taking you somewhere. The tab value goes with the
machinery, and the strings only its empty state read.
A mounted directory listed at most 500 files. The cap was there because the grid
rendered a card for every entry, so a big Downloads folder was slow whether or not
the user scrolled that far - it traded away the rest of the folder to stay usable.
The grid and the list now render only the rows in view plus a spacer at each end,
so DOM size tracks the viewport instead of the folder. Spacers rather than
absolute positioning, so the grid keeps its own auto-fill layout and the list its
row flow; the column count is read off the computed style, leaving the CSS the one
place that decides it. With no measurable scrolling ancestor - a short list, the
first paint, a test environment with no geometry - every item renders, as before.
The cap goes with it: listDirectory returns what the directory holds.
What this does not change is the IPC cost of listing. Each entry is a stat over
the bridge, batched, so a 10k-file directory still pays for 10k stats before the
first card appears.
Both sides restructured FileGrid: folder kinds added disk-listed cards and
kind-aware folder menus, this branch made every item memoized behind one stable
actions dispatcher and took the folder context back out of the items.
The dispatcher stays, and the new behaviour moves onto it. Folder menus keep
their kind gating, deriving editsDisabled from the serverReachable prop rather
than subscribing to the folder context - a subscription inside a memoized item
undoes what the memo buys. Opening a disk-listed file becomes
actions.openDiskFile, so DiskFileCard and DiskFileRow take the dispatcher instead
of a closure rebuilt every render, and are memoized like every other item.
Two deleted outright: an upload branch and a folder lookup whose comments said
what the condition below them said.
The rest kept their fact and lost the rest of the sentence. DiskFileCard gets
back the constraint that makes it unusual - no stub, so no selection or move.
The "Local" tab keeps only the both-halves rule, not the predicate beside it.
The disk-subfolder state says it is never persisted rather than restating its
type. FolderRecord.kind pointed at folderKind twice over; now the accessor holds
the rule and the field points at it.
Nine that carried nothing: four sat above a throw whose message was the comment,
the rest restated the name or type they documented.
Two were wrong rather than redundant. One counted two systems of record where
three stores are loaded. The other explained local-file membership above the
branch that reports server files being left behind.
The restructure's whole claim is that selecting a file redraws the cards whose
selection changed rather than the folder. Nothing enforced it: one inline object
or closure at a call site undoes every bit of it, with no visible symptom until a
folder is large enough to feel it.
Counts the badge row each card renders exactly once, selects one of four, and
expects one card's worth of redraw. With React.memo stripped from FileCard the
same test reports four.
Three compounding costs made a full folder feel sticky:
- Every card and row re-rendered on ANY page state change, because item
components weren't memoized and got fresh closures each render. Items
now take a single stable actions dispatcher (latest-ref backed, so
behavior stays current while identity stays fixed) and are React.memo —
a selection click re-renders the two cards whose selection changed, not
all 500. Selection-aware behavior (drag payloads, multi-move) moved
into the dispatcher so items no longer hold the selection Set, whose
identity changes on every click.
- Each lazily generated thumbnail updated the shared stub immediately,
re-rendering every file-list consumer once per thumbnail — hundreds of
times as a folder fills in. Updates now flush in windows; the card
itself paints instantly from local state.
- Offscreen cards still paid layout and paint. content-visibility lets
the browser skip them; the intrinsic size keeps the scrollbar honest.
Cutting each block to its first sentence sometimes kept the what and dropped the
why, which leaves a comment saying what the signature already says. Those are
deleted where the name covers them, and where the second sentence was the point
it is back: the OS error codes behind isAlreadyExists, why a virtual folder
cannot hang off a server one, the effect that snaps folder selection back to root.
Same facts, a third of the words. Mostly three- and four-line asides saying one
thing, and rhetorical framing around explanations that stand up on their own.
The same three lines explaining which folder kinds can go offline sat above both
the grid card and the list row; one copy carries the reasoning and the other
points at it.
The rest is the module docs on the new stores, saying the same things with less
around them.
Folders now carry a kind, and each kind has its own system of record:
- "server": the backend owns them, as before. The only kind the web
offers — the root New-folder button goes straight to the server dialog
and greys out with the reason (sign in / storage off / unreachable)
when the server can't take one.
- "local": a directory on the machine, mounted read-through on desktop
via the native picker ("Add local folder"). The directory is the source
of truth: the listing is taken fresh from disk (stats batched — a
directory's open time is IPC latency, so the calls overlap), opening a
file loads its bytes into the workbench, and moving, dropping, or
uploading files into the mount writes them to the directory itself —
the app copy is retired only after the bytes verifiably land, taking
superseded versions with it. Names are reduced to a safe basename
before writing; collisions take the OS's " (n)" suffix convention.
Mount records dedupe through a lexical directory key (case-folded for
Windows-style paths, separators unified) and refuse nested or
containing directories — one directory, one row.
- "virtual": browser-owned IndexedDB folders. Dormant by decision:
nothing creates one at the root any more, but existing rows still
render, take subfolders, and hold files.
One kind per subtree, always — each kind has its own store and a mixed
chain would mean an ancestry no single store can vouch for.
Placement is part of creation: a file uploaded while standing in a
folder is born with that folderId, set atomically with the stub — for a
server folder the save-to-server is the sync step, and a failed sync
leaves the file visibly in its folder rather than stranded. moveFilesTo
falls back to storage for ids newer than its render-time snapshot, so
just-born files never silently drop out of a move.
Platform gating goes through build seams (@app): the directory picker,
the disk listing/read/write, and the server-folder blocker — desktop's
blocker speaks in connection modes ("Sign in to Stirling Cloud or
connect a self-hosted server"), seeded from the service's cache so first
paint answers correctly. The one-click New-folder surfaces (sidebar
rail, empty-state CTA) share one flow: the native picker on desktop, a
server folder on the web, disabled with the reason when neither applies.