Skip to content

feat(editor): separate project saves from library exports#352

Merged
ChrisBeWithYou merged 2 commits into
mainfrom
agent/save-export-workflow
Jul 22, 2026
Merged

feat(editor): separate project saves from library exports#352
ChrisBeWithYou merged 2 commits into
mainfrom
agent/save-export-workflow

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

This draft separates ordinary project persistence from publishing a playable library copy.

  • A newly imported/created song remains an unsaved in-memory editing session.
  • First Save uses the native file picker to choose the editable .feedpak project name and location.
  • Later saves reuse that session-bound file handle.
  • Save builds a private, session-scoped package; it does not create or overwrite a library item.
  • Build feedpak becomes Export to Library and remains an explicit action.
  • Exporting no longer clears unsaved project changes.
  • The editor-owned Back button now uses the existing Save / Don't Save / Cancel transition guard.

The backend keeps /build backward compatible: omitted/library destination exports to the configured library, while destination: "session" creates an immutable temporary save generation for /session/export. Temporary paths stay private and superseded generations are cleaned up.

Why

Importing content should not silently create a library package. The expected document workflow is:

  1. Import or create an unsaved project.
  2. Save the editable project to a user-selected .feedpak.
  3. Export to the library only when the user intentionally wants to publish/update the playable copy.

This also keeps quick edits lightweight: Save does not open export settings or run export-only tone preflight.

Important draft boundaries

This is intentionally not merge-ready until the cross-repo contracts below are resolved:

  1. Native Open for external projects. The editor can save a project outside the library, but the current Open flow only browses library-relative files. The host needs an Open-file contract (or an upload/path-token flow) so a saved external .feedpak can be reopened without first copying it into the library.
  2. App-wide leave/quit protection. New, Open, and the editor's own Back button are guarded. Arbitrary shell navigation is not cancellable today, and Electron beforeunload would silently refuse to close rather than show the required dialog. The host needs an awaited navigation guard and the desktop main process needs a close/save/discard/cancel handshake.
  3. Loaded-library compatibility path. Existing library feedpaks still Save in place and do not yet participate in the new project/export identity model. That migration needs a product decision alongside native Open.
  4. Export Center/presets. This PR establishes the Save-versus-Export boundary and explicit Library target; named export profiles, quick re-export, preflight, and export-staleness UI remain follow-up work.

Safety and regression coverage

  • File handles are bound to their initiating session, so a new project cannot overwrite the previous project's selected file.
  • Save As captures and revalidates its session across awaits.
  • Browser download fallback stays marked unsaved because download completion cannot be verified.
  • Session packages use generation-unique paths so overlapping saves cannot overwrite a file being streamed.
  • Session temporary filesystem paths are not returned to the browser.

Validation

  • npm test — 311 passed
  • python -m pytest — 384 passed, 5 skipped
  • focused save/export tests — 15 JS + 4 picker + 2 Back guard + 3 backend passed
  • npm run lint — 0 errors, 3 pre-existing warnings
  • git diff --check origin/main...HEAD

Summary by CodeRabbit

  • New Features
    • Save now preserves an editable project, while Export to Library publishes a playable copy.
    • Imported projects remain unsaved until explicitly saved.
    • Exporting no longer clears unsaved project changes; export again to refresh the library copy.
    • Back navigation now prompts with Save, Don’t Save, or Cancel when needed.
  • Bug Fixes
    • Prevented saves from being applied to the wrong project after switching sessions.
  • Documentation
    • Updated the README, user guide, changelog, and in-app guidance to explain the revised workflow.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ChrisBeWithYou, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 27 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7c3705ef-ddf0-4bbd-be23-4a8cce298d52

📥 Commits

Reviewing files that changed from the base of the PR and between c0f9caa and 7530f7c.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • README.md
  • routes.py
  • src/file-ops.js
  • tests/create_save_routing.test.mjs
  • tests/test_create_save_destination.py
📝 Walkthrough

Walkthrough

The editor now treats Save as editable project persistence and Export to Library as publishing. Create-mode builds support separate destinations, external saves are session-scoped, Back navigation is guarded, and documentation and tests reflect the revised workflow.

Changes

Save and Export Workflow

Layer / File(s) Summary
Destination-aware backend builds
routes.py, tests/test_create_save_destination.py
Build requests accept session or library destinations, store session exports separately, preserve legacy library output, and validate responses and filesystem effects.
Client save and library export flow
src/create.js, screen.html, src/menu-bar.js, src/replace-audio.js, src/stem-tracks.js
Client build handling separates save preparation from library export, including destination payloads, export-only side effects, labels, and status messages.
Session-scoped external saving and navigation guards
src/file-ops.js, src/main.js, screen.html, tests/create_save_routing.test.mjs, tests/editor_back_guard.test.mjs, tests/first_save_picker.test.mjs
External save handles are tied to sessions, Save uses current handles, host saving calls editorSave, and Back navigation waits for the transition guard.
Save and export documentation
CHANGELOG.md, README.md, docs/USER-GUIDE.md
Documentation distinguishes editable project saving from library publishing and updates refresh and navigation guidance.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Editor
  participant BuildAPI
  participant SessionExport
  participant Library
  Editor->>BuildAPI: request session save or library export
  BuildAPI->>SessionExport: write session package for session destination
  BuildAPI->>Library: write DLC package for library destination
  BuildAPI-->>Editor: return destination-specific result
  Editor->>SessionExport: download session package when saving
Loading

Possibly related PRs

Suggested reviewers: byrongamatos

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: separating editable project saves from library exports.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/save-export-workflow

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ChrisBeWithYou
ChrisBeWithYou marked this pull request as ready for review July 22, 2026 22:20
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #359

coderabbitai Bot added a commit that referenced this pull request Jul 22, 2026
Docstrings generation was requested by @ChrisBeWithYou.

* #352 (comment)

The following files were modified:

* `routes.py`
* `src/create.js`
* `src/file-ops.js`
* `src/main.js`
* `src/replace-audio.js`
* `src/stem-tracks.js`
* `tests/test_create_save_destination.py`

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
routes.py (1)

8771-8789: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate title/artist sanitization and suggested_name computation.

The same title/artist/safe_t/safe_a/suggested_name logic is computed once inside _build_sloppak() (to pick the "library" output path) and again in the outer scope after the executor call (to populate export_filename/the response filename). Currently the results match, but the duplication is a drift risk — a future edit to one copy (e.g. adding the truncation/trailing-dot stripping that create_sloppak's _truncate_utf8 already applies) could silently diverge from the other and desync the exported download name from what's actually inside the pack.

♻️ Suggested consolidation — compute once, return it from the closure
         def _build_sloppak():
             ...
             safe_t = re.sub(r'[<>:"/\\|?*]', '_', title)
             safe_a = re.sub(r'[<>:"/\\|?*]', '_', artist)
             suggested_name = (
                 f"{safe_t}_{safe_a}.feedpak" if safe_a
                 else f"{safe_t}.feedpak"
             )
             if destination == "session":
                 output = (Path(session["dir"])
                           / f"project-save-{time.time_ns()}.feedpak")
             else:
                 dlc_dir = get_dlc_dir()
                 if not dlc_dir:
                     raise RuntimeError("DLC folder not configured")
                 output = dlc_dir / suggested_name
-            return _write_sloppak_pak(...)
+            return _write_sloppak_pak(...), suggested_name

         try:
-            output_path = await asyncio.get_event_loop().run_in_executor(
+            output_path, suggested_name = await asyncio.get_event_loop().run_in_executor(
                 None, _build_sloppak
             )
         ...
-        title = meta.get("title", "Untitled")
-        artist = meta.get("artistName") or meta.get("artist", "Unknown")
-        safe_t = re.sub(r'[<>:"/\\|?*]', '_', title)
-        safe_a = re.sub(r'[<>:"/\\|?*]', '_', artist)
-        suggested_name = (
-            f"{safe_t}_{safe_a}.feedpak" if safe_a
-            else f"{safe_t}.feedpak"
-        )
         previous_export = session.get("export_path")

Also applies to: 8821-8828

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@routes.py` around lines 8771 - 8789, Consolidate the duplicated title/artist
sanitization and suggested_name computation used by _build_sloppak() and the
outer export response flow. Compute the filename once inside _build_sloppak(),
return it alongside the generated output/path data, and reuse that returned
value when setting export_filename and the response filename so both remain
synchronized.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 59-62: Update README.md lines 59-62 to state that durable Save and
reused handles apply only when the native picker is available, while browser
fallback downloads a copy without saving the project. Update CHANGELOG.md lines
12-19 to document the browser fallback behavior or explicitly scope destination
reuse to the native desktop contract.

In `@routes.py`:
- Around line 8829-8846: Update the cleanup block around previous_export so a
previously stored export file is unlinked whenever the destination changes away
from the prior session export, including session-to-library transitions; do not
gate this cleanup on destination == "session". Preserve the existing path
comparison and OSError handling, and add a regression test covering a session
build followed by a library build that verifies the original temporary package
is removed.

In `@src/file-ops.js`:
- Around line 936-939: Update the save routing around the saved assignment in
src/file-ops.js: prioritize S.createMode so archive create sessions call
saveCDLC({ skipExternal: true }), while retaining editorSaveAsSloppakConfirm()
only for non-create archive sessions; update tests/create_save_routing.test.mjs
in lines 60-77 with an archive create Save As case asserting /build receives
destination: 'session' and no library export occurs.

---

Nitpick comments:
In `@routes.py`:
- Around line 8771-8789: Consolidate the duplicated title/artist sanitization
and suggested_name computation used by _build_sloppak() and the outer export
response flow. Compute the filename once inside _build_sloppak(), return it
alongside the generated output/path data, and reuse that returned value when
setting export_filename and the response filename so both remain synchronized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a636417d-8ccd-4bbe-b3c2-6b2e2612c4bc

📥 Commits

Reviewing files that changed from the base of the PR and between ba4f924 and c0f9caa.

📒 Files selected for processing (15)
  • CHANGELOG.md
  • README.md
  • docs/USER-GUIDE.md
  • routes.py
  • screen.html
  • src/create.js
  • src/file-ops.js
  • src/main.js
  • src/menu-bar.js
  • src/replace-audio.js
  • src/stem-tracks.js
  • tests/create_save_routing.test.mjs
  • tests/editor_back_guard.test.mjs
  • tests/first_save_picker.test.mjs
  • tests/test_create_save_destination.py

Comment thread README.md Outdated
Comment thread routes.py
Comment thread src/file-ops.js Outdated
@ChrisBeWithYou
ChrisBeWithYou merged commit d26e30a into main Jul 22, 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