10x-media plugins
Undo/Redo

Debugging

The history inspector overlay, what its readout means, and how to reach the history from tests.

The overlay

debug: true adds a third toolbar button that opens a history inspector:

payload.config.ts
undoRedo({
  collections: {
    posts: { debug: true },
  },
})

It lists every entry with the paths that entry changed, marks the current one, and shows a restore button per entry, which is how you step to a specific point without pressing undo repeatedly. Expanding an entry shows the actual diff: path, previous value, new value, and for arrays and blocks the row ids before and after, so a reorder reads as a reorder rather than as an opaque change.

Anything still inside the capture debounce appears at the bottom under Pending, which answers the most common question directly: whether an edit has not been captured yet, or is not being captured at all.

Copy JSON puts the whole stack on the clipboard as entries with their diffs, which is the useful thing to attach to a bug report.

The header reads 3/7 · unsaved · rev 12:

PartMeaning
3/7Current entry, and how many are held.
cleanThe form matches the persisted document.
unsavedIt does not.
no baselineNothing has been saved or loaded yet.
autosave, no baselineThe collection autosaves, so the baseline is deliberately not tracked.
rev NCaptures and restores so far, as a liveness signal.

Entries matching the persisted document are tagged saved. More than one can carry the tag, which is correct: they hold the same state.

The overlay is a development tool. Leave debug off in production; it is off by default, and the extra re-render work only happens when it is on.

Reaching the history from a test

While the controls are mounted, the live history object is on window.__payloadUndoHistory. It is the real object, not a copy, so a Playwright test can assert on it without sleeping on the capture debounce:

a Playwright spec
const entries = await page.evaluate(() => {
  const history = (window as any).__payloadUndoHistory
  return { index: history.index, length: history.stack.length }
})

Waiting on length is what makes an undo/redo test deterministic: entries appear one debounce after the edit, and asserting on the field too early passes for the wrong reason.

await expect.poll(async () =>
  page.evaluate(() => (window as any).__payloadUndoHistory.stack.length)
).toBe(2)

Assert on an exact count rather than a minimum. An extra entry is itself a bug worth failing on, because a phantom capture is what silently truncates the redo tail.

__payloadUndoHistory is a debug handle for tests and console work, not a public API. It is present whenever the controls are mounted, debug or not, and its shape follows the internals.

Common symptoms

Nothing is captured. The field is excluded, by admin.custom, ignorePaths, ignoreFieldTypes, or by being one of Payload's own fields. Open the overlay and check whether the edit shows up under Pending: if it does not, it is filtered rather than merely uncaptured.

One undo reverts a whole paragraph. Typing coalesces into one entry per captureDebounce. Lower it for finer steps.

Undo does nothing inside the editor. Rich text and plain inputs keep their own undo; see Shortcuts.

Old entries disappear. maxHistory evicts from the front. Undo bottoms out inside the retained window, not at the document as it loaded.

On this page