History & transactions
Undo, redo, and grouping edits into one step.
Every successful command is one undo step, with no setup.
project.undo(); // true if there was something to undo
project.redo(); // true if there was something to redo
project.canUndo();
project.canRedo();
project.clearHistory();| Fact | Detail |
|---|---|
| What's recorded | Document changes only. setPlayhead and setSelection never create undo steps |
| Exactness | Each step stores the inverse patches of its change, so undo restores the previous document exactly |
| Limit | 500 steps by default; createProject(init, { historyLimit }) changes it. The oldest steps drop first |
| New edits | Dispatching after an undo clears the redo stack |
| Loading | createProject(saved) starts with empty history |
Transactions
transaction(fn, label?) turns every command dispatched inside fn into one undo step.
project.transaction(() => {
project.dispatch({ type: "clip/move", payload: { clipId: "intro", startUs: 2_000_000 } });
project.dispatch({ type: "track/rename", payload: { trackId: "main", name: "Main" } });
}, "Move and rename");
project.undo(); // reverts bothIt's all or nothing. If anything inside fn throws, every change made so far in the transaction is rolled back, and the error is rethrown:
try {
project.transaction(() => {
project.dispatch({ type: "clip/move", payload: { clipId: "intro", startUs: 3_000_000 } });
project.dispatch({ type: "clip/move", payload: { clipId: "missing", startUs: 0 } }); // throws
});
} catch {
// "intro" is back where it was; no undo step was added
}Use transactions for any user action that takes several commands: a ripple delete, pasting several clips, applying a template.
applyCommands(project, commands) does the same for an array of commands and returns a result instead of throwing, including the index of the command that failed. It's built for AI agents: Tools from the command catalog.
Labels
The label appears on the history event, so you can show "Undo Move and rename" in a menu:
let lastLabel: string | undefined;
project.events.on("history", ({ kind, label }) => {
if (kind === "commit") lastLabel = label;
});kind | When |
|---|---|
commit | A command or transaction finished |
undo | undo() ran; label is the step undone |
redo | redo() ran |
Single dispatches have no label. To label one, wrap it in a transaction.
Undo/redo buttons
canUndo() and canRedo() are methods, not state. Re-read them when history changes:
const undoButton = document.querySelector<HTMLButtonElement>("#undo")!;
const redoButton = document.querySelector<HTMLButtonElement>("#redo")!;
function refresh() {
undoButton.disabled = !project.canUndo();
redoButton.disabled = !project.canRedo();
}
project.events.on("history", refresh);
refresh();