Documentation
Persistence, commits and undo
How results are keyed and kept, when they go stale, how values are written, and how undo works.
Where results live
Results live in memory, in the grid, for as long as the aiFill prop is set. They aren't saved anywhere, and a reload starts with no suggestions. Only accepted values persist, because they are written to your data through your own edit handlers.
If you want to keep more, the callbacks carry everything: onCommit gives each written cell's previous and next value with its metadata (the model that answered, the fingerprints, the answer and the timings), and onResult gives every decided result. Restoring suggestions after a reload isn't supported.
Row identity
Results are keyed by (rowId, columnId), never by display position:
- Rows are identified by
rows.getRowId(row), and columns byGridColumn.id. Display positions are worked out only when needed: when a cell is drawn, and when a value is written. rows.getRowIndex(rowId)is optional. When you provide it, its answer is checked againstgetRowId, so a wrong index can't redirect a read or a write. Without it, AI Fill scansgetRowIdover the rows once and reuses the map for the current task.- Sorting, filtering and moving columns move nothing. Results stay with their ids and are drawn wherever their row and column are now. An answer that arrives after a sort lands on its own row.
- A decided result on a row that is filtered out keeps its record, and "Accept all eligible" leaves it alone until the row is displayed again, except in the cases listed under Limitations.
Staleness
- Edits in the grid. Editing a source cell makes the row's results in the AI columns that list it
stale, whether they are pending or decided. Editing an AI cell makes its resultmanualand stale: it is never applied automatically or accepted. - Changes outside the grid. After changing rows without the grid's edit handlers (a server push, a form elsewhere), call
api.notifyRowsChanged(rowIds?). A changed input makes a result stale, a changed destination makes it manual and stale, and a result whose row is gone is dropped. You don't have to call it for safety: every answer and every write re-checks the inputs and the destination. - A late answer for a stale cell is stored as stale and never shown as a suggestion.
- Stale results are never re-run by themselves. Re-run them from the menu ("Re-run N stale"), the inspector, or
api.rerunStale(). - Accepted results keep their commit id and are never suggested again, including after an undo. Filling again is always explicit.
The checks use fingerprints. The question fingerprint covers the primitive, instructions, context, option ids and descriptions, level descriptions and their order, Noul criteria and the sources. The input fingerprint covers the state sent. Changing the policy, the output mapping or the presentation doesn't change either, so those changes re-decide stored answers with no request.
The commit path
Accepting, Choose and auto-applying write through your edit handlers, the way a paste does. For each result, right before writing:
- The row is found again by id. A row that is gone is
row-missing, and its result is dropped. - The value is turned into a cell with
output.toCell(by default, by the destination's kind: text, number, boolean, or the cells package's dropdown). A value that doesn't fit istype-mismatch. - The inputs are fingerprinted again and the destination compared with its value when the request was made. A changed input makes the result stale; a changed destination makes it manual and stale.
- The destination must be writable (an editable kind, not
readonly), and theoverwritepolicy and the fill's scope must allow the write: an empty cell always; a populated one never under"never"or in the-emptyscopes, only by a user action (accept or Choose) in theselectionscope under"suggest", and under"apply"in theselectionandcolumnscopes, auto-apply included. - Your
validateCell(location, next, current)runs:falseblocks the write, and a returned cell is written instead.
A result that fails any other guard isn't written and keeps its status. getCellState(...).blocked says why, and onError gets one commit-blocked (or type-mismatch) error per reason, listing the cells. A result is written at most once. A grid without onCellEdited or onCellsEdited can't commit.
One batch
The results that pass are written together, in one tick:
- If the grid has no selection, AI Fill first sets one covering the written cells, through your
onGridSelectionChange(undo stacks such asuseUndoRedoneed one). onCommit({ commitId, source, edits })is called, with each edit's previous and next cell.onCellsEdited(items)is called, if you pass it.- Unless it returned
true,onCellEdited(location, cell)is called for each item.
A Choice semantic outcome without a value, and an uncertain Noul, have nothing to write: accepting one marks it accepted and writes nothing.
Auto-apply
In a "Fill and apply" run (fill(scope, { mode: "apply" }), or "Fill and apply…" in the menu), a result that passes the column's autoApply gate is written as soon as it arrives, through the same path, with source: "auto-apply" and status applied. It must pass every guard; one that doesn't stays suggested. Review results, manual cells and populated cells under overwrite: "suggest" are never applied automatically, and neither is a result re-decided after a policy change. The run's summary (onRunEnd's counts, getRunState().last and the status bar) counts a result that auto-apply wrote as applied, and one that a guard kept from being written as suggested. Cancelling a "Fill and apply" run first writes the results that have already arrived and pass auto-apply, so they count as applied too.
Undo
With useUndoRedo
useUndoRedo from @specstory/ai-data-grid-source works with AI Fill when it's wired the standard way:
import { useUndoRedo } from "@specstory/ai-data-grid-source";
const undo = useUndoRedo(ref, getCellContent, setCellValue);
<DataEditor
ref={ref}
aiFill={aiFill}
getCellContent={getCellContent}
onCellEdited={undo.onCellEdited}
gridSelection={undo.gridSelection ?? undefined}
onGridSelectionChange={undo.onGridSelectionChange}
/>;A bulk accept is then one undo step: Cmd/Ctrl+Z restores every cell, redo writes them again once, and no suggestion comes back. Its known limits apply unchanged:
- It records edits by display position, so an undo after re-sorting or filtering writes to positions, not to the rows that were accepted. Use
revertCommitfor an id-safe undo. - If your
onCellsEditedreturnstrue, the per-cellonCellEditedcalls don't happen, souseUndoRedonever sees the edits. The same is true of paste.
revertCommit
api.revertCommit(commitId) writes a commit's previous values back by row id, through the same onCellsEdited / onCellEdited batch (setting a selection first when there is none), and returns how many cells it restored. It doesn't call onCommit or run the commit guards above; its own checks follow. It leaves a cell alone when its row is gone, when it no longer holds the committed value (someone edited it since), when it is read-only now, or when validateCell rejects the old value. A commit is reverted at most once, and its results stay accepted.
Your own undo stack
onCommit gives every edit's previous and next cell, keyed by row id and column id, which is enough for a custom undo stack to do what revertCommit does.