2f817990af
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