Recording (audio and MIDI) mutated the document directly in the AudioEvent
handlers, outside the action system — so it was never undoable, and dirty-
tracking leaned on an ad-hoc `media_modified` flag that the MIDI stop handler
forgot to set (hence: recording a MIDI clip didn't trigger the save-on-close
prompt).
Recording is temporal (the clip streams into the document live over the take),
so it can't be applied by one synchronous execute(). Instead, commit the
finished take as an *already-applied* action:
- ActionExecutor::push_applied(action) — registers an action whose effect is
already present (clears redo, bumps the epoch so the doc reads as modified,
pushes to the undo stack) WITHOUT re-running execute()/execute_backend().
Undo then removes the content via rollback/rollback_backend; redo re-adds it.
- AddClipInstanceAction::already_applied(...) — constructs the action pre-seeded
into its post-execute state (executed + the existing backend clip id) so the
first undo can remove the live-recorded clip from both doc and backend, and
redo re-adds it through the normal path.
- Both recording stop handlers now finalize, then push_applied this action.
Keeping the clip in the document (not a transient) matters for streaming-to-
disk and keeps the doc the single source of truth.
Recordings now bump the epoch like every other edit, so the media_modified
flag is dropped for recordings (kept only as a defensive fallback if the action
can't be built). Whole workspace compiles; 299 core tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First half of the autosave/recovery feature — the write side.
Every ~45s while the document is dirty, write the full current state into a
per-session recovery .beam in the app data dir (directories::ProjectDirs data
dir + /recovery/session-<uuid>.beam). Fully background: reuses the existing file
worker, so the pool serialization / encode / DB write all happen off the UI
thread. The only UI-thread cost is one document clone — and build_save_command
now stamps the UI layout onto the *snapshot clone* rather than the live document
(the old prepare_document_for_save mutated live state via Arc::make_mut, which
could deep-clone the whole document mid-frame). Removed that dead helper.
Dirtiness is tracked centrally via ActionExecutor::epoch() (now also bumped on
undo/redo, not just execute) plus a pending_event flag set by non-action changes
(imports, finished recordings). The baseline is rebased on new/load/manual-save
so a freshly-loaded or just-saved project stays quiet. Idle-after-edit still gets
one snapshot via a single request_repaint_after wakeup; completion is polled
lazily (no forced repaints during the write).
on_exit deletes the session's recovery file, so a leftover file on next launch
means an unclean shutdown — the hook the recovery prompt (next commit) keys on.
`RasterStrokeAction`/`RasterFillAction` stored the whole before+after RGBA frame
(~16 MB/action at 1080p → up to ~1.6 GB at the 100-action cap). They now store a
`RasterDiff` — only the changed bounding box's pixels before and after — computed
once in `new()` from the full buffers, which are then dropped. A brush dab shrinks
from ~16 MB to tens of KB; a full-canvas fill is unchanged (its bbox is the frame).
Paging interaction: a diff overwrites just the bbox, so the keyframe's pixels must
be resident when undo/redo applies. A clean evicted frame's container bytes equal
its current logical state, so the editor faults the target frame in (synchronously)
before undo/redo via a new `Action::raster_resident_hint` + `peek_undo/redo_raster_hint`.
Dirty frames are never evicted, so they're already resident. If a base is somehow
not resident the apply is skipped (logged), never resized-and-corrupted.
Unit tests cover exact before/after round-trip, blank-first-stroke, no-op, and the
non-resident-base skip.