A rollback reverts the local data with a rebase, which is not a commit,
so the parts of the workspace that follow commits missed it:
- Comment pins moved in the reverted action stayed where they were, on
screen and on the server, and pins on a board the rollback removed
kept pointing to it. They go back to the reverted position, and pins
on a removed board move to the page root.
- Focus mode stayed on with no shape left when the rollback removed the
focused ones, and the next shape drawn turned it on again. It ends
when its last shape goes, and the selection drops removed shapes.
- Component thumbnails kept the rejected look. The thumbnails of the
reverted mains are rendered and uploaded again, and an upload whose
shapes changed during the render is skipped.
- Under WASM, an open text editor reset to the reverted content but the
canvas kept the old layout. It is laid out again.
- A library sync whose save failed showed no update notice again once
the release undid it. The notice is checked again after that
rollback.
The backend rejects a save after the call that made the changes has
returned, and the workspace then undoes them. A plugin, or the MCP
agent, went on working with shapes that were no longer there and never
learned why.
The rollback event now names the files that lost commits and the pages
and page shapes their changes touched, and plugins get it as the new
`contentrollback` event (`ContentRollback` type) through a subscription
to the store stream, so no rollback is dropped by debouncing.
The MCP plugin keeps the rollbacks it is told about and sends them with
its next task response, failed or not. The server collects them per tool
call and puts a warning first in that tool's response, naming what was
affected; the instructions tell the agent to read the pages again and
redo what is missing.
A rollback can remove or change shapes the UI still refers to.
- The selection drops shapes that are gone and the color picker that
edited them closes.
- The undo history is cut at the reverted action; it stays when no
entry holds a reverted change. Undo and redo commits carry real undo
changes and the history index they moved from, so a reverted Ctrl+Z
puts the change and the index back.
- An open path edition whose shape the rollback changed closes without
saving the editor's stale copy. The v2 text editor loads the reverted
text again; the WASM editor already showed it.
- The Alt measurements look the hovered shape up in the current data.
- Plugin setters, resize() and rotate() on a removed shape throw
"Shape not found", whatever the plugin's throwValidationErrors flag.
A save whose changes failed validation was rejected as a whole: autosave
stopped, the edits made after it piled up unsent, and a reload lost
them all, the valid ones included.
The frontend now sends each commit of a save as its own group with its
commit id (:changes-with-metadata). When applying and validating all of
them at once fails, the backend searches by bisection for the first
commit that fails, keeps the commits before it and answers with
:rejected (that commit and every later one). Only the kept changes are
stored, snapshotted and broadcast, so other users never see the rejected
ones. Flat :changes and groups without ids stay all-or-nothing; database
and network errors still fail the whole request. A rejection without an
error type is reported as an internal error.
The frontend rolls the file back to what the server holds: it undoes
every pending commit of that file, applies again the remote changes that
arrived meanwhile, and redoes the kept commits. Undoing only the
rejected commits would write stale values over a peer's later change to
the same attribute. The rejected and later commits leave the queue, the
buffer and the undo history, and a warning that hides after ten seconds
tells the user. The pure bookkeeping lives in
app.main.data.persistence.commits.
The pending notice of a commit the rollback dropped no longer leaves the
save status at pending, which made "Pin this version" hang and fail.
Debug tools drive these cases by hand: debug.commitInvalidChange() makes a
save fail validation, holdSaves() delays the answers, failNextSave() and
failNextRollback() fail them on purpose (backend flag debug-save-faults),
and persistenceState() shows the queue.