History and Undo
Record component values, step through changes and try suggestions you can revert.
createHistory records changes to component values. stageChanges applies a set of values and gives you a way to restore the previous ones. Both use the agent bridge to change the controls.
For undo inside one component, like a drawing or a board, record its value with createUndoStack instead.
These helpers are experimental. The examples reuse the bridge from the Guide, including its permission policy. Import the helpers from your framework's history entry or @ui-sdk/core/history.
Start Recording
Start recording before the changes you want to keep:
import { createHistory } from "@ui-sdk/react/history"
import { agent } from "./agent"
const history = createHistory(agent)By default, the history records value, checked and pressed changes across the page, from people, agents, and your app. Use within to limit it to one element, such as the Guide's <main>:
const history = createHistory(agent, {
within: () => document.querySelector("main"),
keys: ["value", "checked"],
})within accepts an element or a function that returns one. keys replaces the list of state keys to record. Restoring a key uses its set<Key> action, such as setValue or setChecked, so choose keys the component can set through the bridge.
Keep password, card and one-time-code fields outside the recorded panel. Their values are redacted, so the history can't restore the originals: their entries carry redacted: true, and seeking or undoing leaves those fields as they are and reports them among the changes it couldn't put back.
Read the History
history.entries() returns entries from oldest to newest. Each has an id, component ref, label, state key, value, previous, source and reason.
Use describeEntry when you want a short sentence for each entry:
import { describeEntry } from "@ui-sdk/react/history"
const labels = history.entries().map(describeEntry)For example: "Agent set Project name to Atlas". history.position() tells you how far you are through the entries. It starts at 0; the end is history.entries().length.
history.subscribe(listener) calls a listener when the entries or position change. Read them inside the listener to update your history UI. It returns a function that unsubscribes.
Step Back and Forward
Pass an entry count to seek. These calls return to the start of the recorded history, then apply it again:
await history.seek(0)
await history.seek(history.entries().length)seek clamps the position to the available history. If someone makes a new recorded change after seeking back, the history drops the entries after that position, like undo in an editor.
The history restores recorded component values. It doesn't undo a network request, recreate a removed component or reverse an action such as saving a form. Restoration goes through agent.act, one control at a time, so current permissions, disabled state and change callbacks still apply, and a control that's refused or has left the page doesn't stop the others.
seek resolves with the changes it couldn't put back, each as { ref, key, value }, and with an empty array when everything went back. If nothing went back, the position stays where it was.
The history doesn't record its own restoring, but it records every other change made meanwhile. When someone changes a control the history hasn't restored yet, their change stays, and the control is in the result.
Undo the Agent's Changes
Use undoSource to remove the effect of one source from the applied history:
await history.undoSource("agent")It restores each affected key to its latest value from the other sources, or to its starting value if there isn't one. For example, if a person changes the project name after the agent does, that person's value stays.
The entries it put back remain in the history with reverted: true, and seeking through the history continues to skip them. Like seek, undoSource resolves with the changes it couldn't put back, and their entries stay applied. The same method accepts other source names, such as "keyboard" or "programmatic".
A Button whose handler awaits needs one more step. A change the handler makes after the await happens once the press is over, so it's recorded as "programmatic" and undoSource("agent") leaves it. Wrap it in withSource with the source the handler receives:
import { Button } from "@ui-sdk/react/button"
import { withSource } from "@ui-sdk/react/observe"
function SaveButton() {
return (
<Button
onClick={async (event, { source }) => {
await save()
withSource(source, () => status.setValue("Saved"))
}}
>
Save
</Button>
)
}Here status is an Input's controller. When the agent presses Save, the new status is recorded as the agent's. See Observe for how sources work.
Keep Interactions Together
A drag that commits arrives as one entry. The history also combines consecutive changes with the same component, key, source and reason, except for item-press and imperative changes.
To record every intermediate step instead, turn coalescing off:
const history = createHistory(agent, { coalesce: false })Call history.clear() to forget every entry and keep the controls as they are. Call history.destroy() when you're done to stop recording and remove its listeners.
Try a Suggestion
stageChanges puts suggested values on the real controls. Try a different region in the Guide's Select:
import { stageChanges } from "@ui-sdk/react/history"
const staged = await stageChanges(agent, [
{ ref: "Region", value: "north-america" },
])Each change is { ref, key?, value }. key defaults to "value"; use "checked" for a checkbox. The component must be mounted, expose that key in its snapshot, and advertise the matching set<Key> action.
Staging accepts a ref or an exact label. It uses the first component with a matching label, so use a ref from snapshot() when labels repeat.
Missing components, unavailable setters and refused actions are skipped, and the remaining suggestions are still attempted. staged.applied lists successful calls as { ref, key, before, value }. As with act, a successful call doesn't prove a controlled owner accepted the value or that a change callback didn't cancel it; read the component state when you need to confirm.
If the person rejects the suggestion, restore the captured values:
await staged.revert()revert runs once, in reverse order, through the same action policy and callbacks. It attempts to restore the captured values even if the person has edited those controls since the preview, and resolves with the entries of staged.applied it couldn't put back. A filled password, card or one-time-code field's before reads "[redacted]", so revert leaves that field as staged and includes it in the result. If the person accepts the suggestion, leave the values in place.
To point at a suggested control, use elementOf(ref) from Observe with one of the refs in staged.applied.