Skip to content

fix(studio): safe markdown-folder persistence that follows outside edits - #108

Merged
tomasz-zajac-oss merged 3 commits into
mainfrom
fix/md-folder-persistence
Sep 30, 2026
Merged

tomasz-zajac-oss merged 3 commits into
mainfrom
fix/md-folder-persistence

Conversation

@tomasz-zajac-oss

Copy link
Copy Markdown
Collaborator

Makes models stored as a folder of Markdown files safe to use alongside other editors and git. Before this, Studio could silently lose descriptions, delete files it didn't write, and overwrite edits made outside it.

Data-loss fixes

  • Descriptions lost after a rename or move. Descriptions of folder models load lazily. An element never opened in the panel lost its description on the second save after a rename, a move, or its first child, because its body path went stale. Body paths now follow each write.
  • Deleting files Studio didn't write. Pruning removed every top-level .json (e.g. package.json) and anything under nodes/ (e.g. a hand-written README.md). Now only the manifest, the known sidecars and node files with id/type frontmatter are ever pruned. "Save as folder…" asks before writing into a folder that has other files but no model.
  • Overlapping saves. Two saves overlapping (slow disk, large model) each pruned the other's files, so a renamed element could lose both its old and its new file. Disk saves now run one at a time per document and collapse into the newest state. localStorage saves stay synchronous, so the page-hide flush still works.
  • Load landing in the wrong document. A slow load that finished after a document switch landed in the new document, and autosave then overwrote it. Loads are now sequenced. An edit still in the 400 ms debounce is saved to the document being left.

Outside edits

  • MdFolderSession (packages/common/src/formats/mdFolderSync.ts, platform-free) remembers each model file's content and stamp (mtime + size) as last read or written:
    • it writes only files that changed;
    • it refuses to write if the folder changed on disk since the last read;
    • poll() reports such changes.
  • Electron: one session per folder in the main process, polled every second, same as the existing --file watcher, because native fs events are unreliable on external volumes. New watchFolder / onFolderChanged host capabilities.
  • Web: one session per directory handle, polled every 2 s.
  • Autosave: watches the active folder document and reloads it on an outside edit, keeping the open view and selection. The disk wins: an in-app edit not yet saved is dropped by the reload.
  • Only model files are read, so a folder inside a repo no longer walks node_modules. Folder writes can't leave the model folder.

Tests

  • Unit: session behaviour, including on a real temp directory; the regressions above; watching and reload. 6 of the 8 regression tests in mdFolderSafety.test.ts fail on the old code; the other 2 exercise logic in @radical/common that wasn't reverted.
  • E2E (apps/e2e/tests/studio/folders.spec.ts), 6 scenarios. The directory picker returns an OPFS directory that the test also edits directly, standing in for another editor. Turning off the rename fix or the watcher fails exactly the matching scenario.
  • These specs run in a persistent browser profile. Chromium crashes the whole browser when an off-the-record page (Playwright's default) reads a stored directory handle back from IndexedDB.

npm test, npm run typecheck and the full e2e suite pass locally (e2e: 51 passed; the 4 known-issues tests are test.fail as before). The Electron build bundles the new main-process code. Not tried by hand in the desktop app.

Still open

Listed in docs/IMPROVEMENTS.md under "Markdown-folder persistence (review 2026-09-30)", among them:

  • opening a folder still rewrites it (normalisation);
  • no atomic writes or flush on quit;
  • save failures are silent;
  • disk-wins with no merge;
  • a folder or file model reloads onto the full canvas, ignoring the view in a deep link;
  • possible Chrome Incognito crash in folder mode (not checked).

🤖 Generated with Claude Code

Tomasz Zajac and others added 3 commits September 30, 2026 18:19
… edits

Data-loss fixes:
- An element whose description was never opened lost it on the second save
  after a rename or move: its lazily-loaded body path went stale. Body paths
  now follow each write (serializeToMdFolderWithPaths).
- Pruning deleted any top-level .json and anything under nodes/. Only the
  manifest, the known sidecars and node files with id/type frontmatter are
  pruned now (staleMdFolderFiles); "Save as folder…" asks before writing into
  a folder that holds other files but no model.
- Overlapping saves could prune each other's files. Disk saves run one at a
  time per document and collapse into the newest payload; localStorage saves
  stay synchronous for the page-hide flush.
- A slow load finishing after a document switch loaded into the new
  document, which autosave then overwrote. Loads are sequenced; an edit still
  in the debounce window is saved to the document being left.

Outside edits:
- MdFolderSession (@radical/common) keeps a baseline of the model files: it
  writes only changed files, refuses to write over files changed on disk
  since the last read, and polls for such changes. Electron keeps one per
  folder in the main process (1 s polling, like the --file watcher; native
  events are unreliable on external volumes); the web build keeps one per
  directory handle (2 s).
- Autosave watches the active md document and reloads it on an outside
  edit, keeping the open view and selection. The disk wins.
- Only model files are read (no walking node_modules); folder writes are
  confined to the model folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Save as folder, reload, renames that move unopened elements' files, edits
made outside Studio, and files Studio must leave alone. The directory picker
returns an OPFS directory the test can also edit. Runs in a persistent
profile: Chromium crashes when an off-the-record page reads a stored
directory handle back from IndexedDB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tomasz-zajac-oss
tomasz-zajac-oss merged commit a475018 into main Sep 30, 2026
4 checks passed
@tomasz-zajac-oss
tomasz-zajac-oss deleted the fix/md-folder-persistence branch September 30, 2026 16:54
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