Skip to content

Undo, Regenerate and Edit: Recovery Affordances

6 min read · updated August 3, 2026

You cannot make a model reliably right. You can make being wrong cost the user four seconds instead of an afternoon, and that substitution is most of what people mean when they say a product feels trustworthy.

Why recovery carries the trust

Trust in an unreliable component is not a function of its error rate alone; it is a function of error rate multiplied by the cost of an error. A model that is wrong one time in twenty inside an editor with a working undo is pleasant to use. The same model, at the same error rate, wired to send email is unusable. Nothing about the model changed — only the recovery cost.

That gives a concrete design priority. Time spent making recovery instant buys more perceived reliability than time spent shaving a percentage point off the error rate, and unlike the error rate it is entirely within your control.

Three affordances, three different jobs

They are routinely conflated into one “try again” button, which is why interfaces end up with a control that sometimes helps and sometimes cannot possibly help. Each addresses a different property.

AffordanceDescription
RegenerateSame input, new sample. It works only because generation is stochastic — the second draw from the distribution differs. Its expected value collapses to nothing at temperature 0, where the same input genuinely does give the same output. Use it when the answer was badly sampled, not when it was badly asked.
Edit and resendChange the input, discard the branch below it. This is the one that fixes a wrong answer caused by a wrong or ambiguous request, and it is the only one that removes the bad text from the context rather than adding to it.
UndoRevert a side effect that already happened in the world. Nothing to do with the model at all — it addresses the consequence of acting on output, and it is the only one of the three that applies after an agent has done something.

The practical test for which one to offer: ask whether the problem is in the sample, the prompt, or the world. Offering all three indiscriminately trains users to hammer regenerate, which is the expensive option and frequently the one that cannot work.

The regenerate traps

  • It bills again, at full price. A regenerate is a complete new request: the entire prompt is re-sent and re-charged, and the new answer is charged at the output rate. Two regenerates on a long conversation cost three times the original interaction, not one and a bit. See what retries actually add to a bill.
  • It destroys the previous answer. The classic implementation replaces the message in place. Users regenerate, get something worse, and want the first one back — and there is no way to get it, because it was a sample from a distribution and it is gone. This is a data-loss bug wearing an interaction-design costume.
  • Automatic regeneration hides quality problems. If your code silently re-rolls a malformed structured output, the user never learns that the feature is unreliable and neither do your metrics — a feature that succeeds on the third attempt shows up as a success. Count attempts separately from outcomes.
  • At temperature 0 it is a lie. If your request is deterministic by configuration, the regenerate button will return near-identical text and the user will conclude the button is broken. Either raise temperature for that path or do not offer it. The residual non-determinism that survives temperature 0 is not enough to build an affordance on.

Keep every generation

The fix for the second trap is small and worth doing on the first day, because retrofitting it after users have lost answers is a migration. Model a turn as a list of samples with a selected index, rather than as a string:

type Turn = {
  id: string;
  request: Request;        // exact input that produced these
  samples: Sample[];       // append-only; never replaced
  selected: number;        // which one is shown and is in context
};

type Sample = {
  text: string;
  model: string;           // may differ between samples
  finishReason: "stop" | "length" | "filter";
  costMicros: number;
  at: string;
};

// regenerate: append, select the new one, keep the old
function regenerate(turn: Turn, sample: Sample): Turn {
  return { ...turn,
           samples: [...turn.samples, sample],
           selected: turn.samples.length };
}

// edit: a NEW turn, and everything after it is discarded,
// because the old answers were conditioned on the old input
function editTurn(turns: Turn[], index: number, request: Request): Turn[] {
  return [...turns.slice(0, index), { id: newId(), request,
                                      samples: [], selected: 0 }];
}

The interface that falls out of this is the familiar ‹ 2 / 3 › pager under a message. It costs almost nothing to build and it changes the meaning of the regenerate button from a gamble into an exploration, because no click can lose anything.

Note the asymmetry between the two functions. Regenerating appends; editing truncates. That is not a UI convention, it is forced by the mechanics: answers below an edited turn were generated conditioned on text that no longer exists, so keeping them would build a conversation whose history never happened.

Undoing something that already happened

Once an agent has acted, undo stops being a UI state and becomes a compensating action against an external system. Three levels, in descending order of how much you should prefer them:

  • Deferred execution. Hold the effect for a few seconds and show “Sending… Undo”. Within the window the undo is free and total, because nothing has happened yet. This is the only genuinely reliable undo and it is available far more often than people use it.
  • Soft delete plus restore. The action happens against your own data and is reversible by design — archived rather than deleted, status changed rather than overwritten. Requires that you captured the prior state before acting, which is a thing to build into the tool layer rather than the UI.
  • Compensating action. The effect is out in the world and can only be counteracted: refund the charge, post a correction, delete the created record. Partial by nature — a compensated email was still read — so the honest interface says “we sent a follow-up”, not “undone”.

Where none of the three is available, the action is genuinely irreversible and belongs behind an explicit confirmation rather than behind an undo. Which is the whole subject of approval flows: undo and confirmation are the two halves of one decision, and the reversibility of the action is what picks between them.

Deferred execution is worth arguing for specifically, because it is almost always available and almost never used. A three-second hold before an agent sends an email costs the user nothing they will notice and converts the worst category of failure into the cheapest. The objection is that it feels less responsive; the answer is that the interface can report the action as done immediately and simply not have performed it yet, which is the same optimistic-update trick every other part of the product already uses. The only requirement is that the undo window be honest — if the effect has left the building, the affordance must stop saying “undo”.

A last observation about how these three affordances should be labelled. “Try again” is the wrong word for all of them, and it is the most common one on the button, because it describes the user’s intent rather than the system’s action. “Regenerate” says a new sample is coming and implicitly that it may be worse. “Edit” says the input changes. “Undo” says the world changes back. Users who can predict what a control does will pick the right one, and picking the right one is the difference between recovery costing four seconds and costing four generations.

Undo, Regenerate and Edit: Recovery Affordances · Multigrid