Skip to content

fix(export): stop claiming to export while the save dialog is open - #366

Merged
thcp merged 1 commit into
mainfrom
fix/export-dialog-busy-state
Aug 12, 2026
Merged

fix(export): stop claiming to export while the save dialog is open#366
thcp merged 1 commit into
mainfrom
fix/export-dialog-busy-state

Conversation

@thcp

@thcp thcp commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Closes #338.

save_audio_file did two things in one command: show the native picker, then stream the file. The frontend awaited the whole thing, so the button read "Exporting..." from the moment it was clicked, including however long the dialog sat open. Nothing was being exported during that phase.

Split into pick_export_destination and download_to_path. The busy state is now entered from a callback the download helpers fire when bytes actually start moving, so the label describes the transfer alone.

One deviation from the issue, and it matters

#338 suggests download_to_path(url, path). A path parameter would be an arbitrary write primitive for anything running in the WebView — it could stream any localhost URL to any location on disk. Until now the destination has only ever come from the native dialog, and this command has already been hardened twice for exactly this class of problem (#138 SSRF, #139 memory).

So the transfer takes a token, not a path. The picked PathBuf stays in Rust; JS holds an opaque, single-use token.

The token does not need to be unguessable, which is worth being explicit about: every live token maps to a path the user already approved in a dialog, so guessing one only ever yields another approved destination. A monotonic counter is sufficient and no new dependency is needed. Unconsumed picks are capped at 16 so an abandoned export cannot accumulate.

save_audio_file stays as a thin wrapper over both halves for the lane download links, which have no busy state to mislabel.

Also

  • Cancelling gets simpler, not just better labelled. No busy state is ever entered, so there is none to unwind.
  • A picking guard stops a second export being queued while the dialog is open. The dialog is app-modal on a real desktop, but the guard should not depend on that, since busy is deliberately still false during this phase.
  • Removed downloadCurrentStems — exported but never called, and the only _triggerDownload caller in a loop, which would have meant one save dialog per stem.

Verification

Reintroduced the defect (entering the busy state before the dialog is answered): 3 of the 4 new tests fail. That is the check that these tests are worth having.

New coverage, using the two-phase Tauri stub from #365:

test asserts
label during the picker still "Export Mix", not busy, and download_to_path not yet called
cancellation no busy state, no transfer, no error, menu still usable
second export while picking only one pick_export_destination call
addressing the transfer receives {token, url} and no path

All 6 pre-existing export tests were updated for the two-phase flow and still pass.

Two of my own test assumptions were wrong and I corrected them: the panel stays open during the dialog phase (only entering the busy state closes it), so re-opening it toggled it shut, and one test still asserted on the old save_audio_file command name.

Suites: Rust 34, e2e 13, JS 48, installer 52, Python 539 passed. cargo fmt clean; clippy shows the same 5 findings as main, none new. test_all_stems_zip_ogg fails identically on clean main (local ffmpeg lacks libvorbis).

Follow-up, deliberately not here

Progress reporting. The streaming loop already knows the byte count and could emit events now that the transfer is its own command, but it needs a channel and UI. Worth its own issue rather than growing this one.

save_audio_file did two things in one command: show the native picker,
then stream the file. The frontend awaited the whole thing, so the
button read "Exporting..." from the moment it was clicked, including the
entire time the dialog sat open. Nothing was being exported during that
phase, and a user who took a while choosing a folder was simply told
something untrue.

Split into pick_export_destination and download_to_path. The busy state
is now entered from a callback the download helpers fire when bytes
actually start moving, so the label describes the transfer alone.

The transfer takes a token, not a path. #338 suggested
download_to_path(url, path), but a path parameter would hand anything
running in the WebView the ability to write an arbitrary localhost URL
to an arbitrary location on disk -- the destination has until now only
ever come from the native dialog. Instead the picked PathBuf stays in
Rust and JS holds an opaque single-use token. The token does not need to
be unguessable: every live token maps to a path the user already
approved in a dialog, so a monotonic counter is enough and no new
dependency is needed. Unconsumed picks are capped so an export the user
abandons cannot accumulate.

save_audio_file stays as a thin wrapper over both halves for the lane
download links, which have no busy state to mislabel.

Cancelling gets simpler rather than just better labelled: no busy state
is ever entered, so there is none to unwind.

Removes downloadCurrentStems, which was exported but never called. It
was also the only _triggerDownload caller in a loop, which would have
meant one save dialog per stem.

Verified by reintroducing the defect (entering the busy state before the
dialog is answered): 3 of the 4 new tests fail. The suite also covers
cancellation, the guard against queueing a second export while the
picker is open, and that the transfer is addressed by token rather than
by path.

Closes #338
@thcp
thcp marked this pull request as ready for review August 12, 2026 19:43
@thcp
thcp merged commit 2f81799 into main Aug 12, 2026
10 checks passed
@thcp
thcp deleted the fix/export-dialog-busy-state branch August 12, 2026 19:43
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.

Desktop export shows "Exporting..." while the save dialog is still open

1 participant