Skip to content
KONIGI

AI Assistants / Turn and response / Edit and resend

1 of 6

Edit and resend

The question was badly worded, and asking again pushes the bad exchange down the transcript instead of replacing it.

Updated September 12, 2026

Problem

The answer is wrong because the question was wrong. Asking again appends a second question to the transcript. The bad exchange stays above it as permanent context, and the model reads it on every subsequent turn.

Solution

Let the viewer edit their own turn in place and run the conversation forward from there. This addresses the actual cause where regenerate only rerolls the symptom, and it’s the stronger of the two corrections almost every time.

The mechanism has one consequence that decides the whole design: everything after the edited turn is now answering a question that no longer exists. Three ways to handle that, and only one is honest.

  1. Keep the later turns. Cheapest and incoherent. The transcript now shows answers to a question nobody asked, and the model reads them as context.
  2. Delete the later turns. Coherent and destructive. A viewer who edits turn two to fix a typo loses eight turns of good work, and the delete is usually irreversible.
  3. Branch. The edited turn becomes a sibling of the original, and the conversation continues down the new limb while the old one stays reachable. A pager on the user turn, the same device regenerate uses on the assistant turn.

Branching is the correct answer and it makes the conversation a tree, which has to be explained without drawing a tree. The pager on the edited turn does most of that work: a small 2/2 says a previous version exists and is one click away. HAX guideline 16 applies to the moment of commit, since pressing save here can discard a long tail of conversation and the viewer deserves to know that before it happens rather than after.

Edit and resend is also the only correction control that improves the record. Regenerate leaves the flawed question in place permanently, so the transcript stays a bad artifact even when the final answer is good. Editing produces a conversation someone can read later and learn from, which matters once these get shared, exported or used as documentation.

The small details carry it. The edit control belongs on the user turn rather than in a menu, the textarea should open pre-filled with the cursor at the end, and Escape should abandon the edit without sending. Where a turn carried attachments, the edit has to keep them or say clearly that it didn’t.

Use when

Conversations run long enough that the history has value, and the viewer can identify what was wrong with their own question.

Don’t use when

The turn triggered a side effect. Once a message has sent an email, filed a ticket or written to a system of record, editing it rewrites a history that no longer matches the world, and an append-only correction is the honest shape.

Trade-offs

Branching preserves everything and hides the structure, so a viewer can lose track of which limb they’re on and why an earlier answer seems to have vanished. Truncating is comprehensible and throws away work. Editing history also makes the transcript a poor audit record, since what the model actually received is no longer what the page shows. The control also competes for space on the user turn, which the message turn pattern wants to keep compact.

Checklist

  • What happens to the turns after the edited one?
  • Is the previous version still reachable, and is that visible without hovering?
  • Does the viewer learn what will be discarded before they commit?
  • Does the edit box open pre-filled with the cursor at the end?
  • Does Escape abandon the edit cleanly?
  • Are attachments from the original turn carried over?
  • Can the viewer tell which branch they are currently on?
  • What does an export or a shared link contain when branches exist?
  • Is the control available on the keyboard?
  • Is editing blocked on turns that caused a side effect?

Compare

ChatGPT puts an edit control on the user turn and pages between versions, so the branch is preserved and presented as a small counter rather than a visible tree. Claude offers the same in-place edit and pairs it with model switching at the retry, so a correction can change the question and the answerer at once. Perplexity treats a revised question closer to a new search than an edit of the thread, which suits a product where each answer is largely independent. Slack cannot offer the pattern at all in the same form, since an AI exchange lives in a channel where editing a message rewrites a shared record other people have already read.

Regenerate is the other correction and rerolls the answer instead of the question. Message turn is what gets edited and has to hold the control. Composer is the editor that opens in place. Conversation history is where branches have to survive being closed and reopened. Context meter is affected, since abandoned branches may still occupy the window.

Edit and resend anatomy A user turn opened for editing with a pager showing a previous version, a warning that the turns below will be branched away, and a tree diagram of the two limbs with only the selected one visible in the transcript. Fix the question, not the answer 2 of 2 esc to abandon the 6 turns below move to the other branch original question, kept edited question, shown the tree exists; only the selected limb is rendered 1 2 3 4 1 EDIT IN PLACE Pre-filled, cursor at the end, escape to abandon without sending. 2 THE PREVIOUS VERSION A small counter says an earlier question exists and is one click away. 3 SAY WHAT IS DISCARDED Pressing save can take eight turns of good work with it. Say so before, not after. 4 THE BRANCH POINT Keeping both limbs beats deleting one, and the structure stays hidden behind a pager. Editing improves the record. A rerolled answer leaves the flawed question in the transcript forever.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

Fixing the question rather than arguing with the answer. Editing a user turn branches everything below it, so the count of turns about to move has to be on screen before the reader commits.

shadcn
npx shadcn@latest add textarea button
Tokens
--background--foreground--card--muted-foreground--border--input--status-warn--chart-1
2 of 2
esc to abandon

the 6 turns below move to the other branch

EditTurn.tsxA user turn that opens in place, pages between its versions, and says how many turns a save will move before it moves them.

import { useEffect, useRef, useState, type KeyboardEvent } from "react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";

type Props = {
  /** Every version of this question, oldest first. Editing appends one; it
   *  never replaces, because the old limb of the conversation still hangs
   *  off the old wording. */
  versions: string[];
  /** How many turns sit below this one. Required, because saving moves all of
   *  them to the other branch and the viewer has to read that before they
   *  commit, never after. */
  turnsBelow: number;
  onSave: (text: string) => void;
  onCancel?: () => void;
  /** Which version the transcript continues from. */
  onSelect?: (index: number) => void;
  defaultEditing?: boolean;
  /** Which version shows first. Defaults to the newest. */
  defaultIndex?: number;
};

export function EditTurn({ versions, turnsBelow, onSave, onCancel, onSelect, defaultEditing = false, defaultIndex }: Props) {
  const n = versions.length;
  const [i, setI] = useState(defaultIndex ?? n - 1);
  const [editing, setEditing] = useState(defaultEditing);
  const [draft, setDraft] = useState(versions[i]);
  const ref = useRef<HTMLTextAreaElement>(null);

  const go = (next: number) => { setI(next); setDraft(versions[next]); onSelect?.(next); };

  // A saved edit lands on the new version, the same as a regenerate lands on
  // the new answer.
  const seen = useRef(n);
  useEffect(() => {
    if (n > seen.current) go(n - 1);
    seen.current = n;
  }, [n]);

  // Pre-filled with the cursor at the end. The viewer is here to change a
  // word or add a clause, and the caret at the start makes them arrow past
  // the whole question first.
  useEffect(() => {
    const el = ref.current;
    if (!editing || !el) return;
    el.focus();
    el.setSelectionRange(el.value.length, el.value.length);
  }, [editing]);

  const cancel = () => { setEditing(false); setDraft(versions[i]); onCancel?.(); };
  const save = () => {
    const text = draft.trim();
    if (!text || text === versions[i]) { cancel(); return; }
    setEditing(false);
    onSave(text);
  };
  const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
    if (e.key === "Escape") { e.preventDefault(); cancel(); }
    if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); save(); }
  };

  const pager = n > 1 && (
    <div className="flex items-center gap-1 text-[11px]">
      <Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={() => go(i - 1)} disabled={i === 0} aria-label="previous version">‹</Button>
      <span className="tabular-nums text-chart-1">{i + 1} of {n}</span>
      <Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={() => go(i + 1)} disabled={i === n - 1} aria-label="next version">›</Button>
    </div>
  );

  return (
    <div className="flex flex-col items-end">
      <div className="w-[328px] rounded-lg border bg-background p-3">
        {editing ? (
          <Textarea ref={ref} value={draft} onChange={(e) => setDraft(e.target.value)} onKeyDown={onKeyDown} rows={2}
            aria-label="Edit your question" className="min-h-0 resize-none border-0 p-0 text-sm shadow-none focus-visible:ring-0" />
        ) : (
          <p className="text-sm leading-relaxed text-foreground">{versions[i]}</p>
        )}
        <div className="mt-3 flex items-center gap-2 text-[11px] text-muted-foreground">
          {pager}
          {editing ? (
            <>
              <span className="ml-auto">esc to abandon</span>
              <Button size="sm" className="h-6 px-2 text-[11px]" onClick={save}>save</Button>
            </>
          ) : (
            <Button variant="ghost" size="sm" className="ml-auto h-6 px-2 text-[11px]" onClick={() => setEditing(true)}>edit</Button>
          )}
        </div>
      </div>

      {/* The cost of saving, stated where the save button is. */}
      {editing && turnsBelow > 0 && (
        <p className="mt-2 text-[11px] text-status-warn" role="status">
          the {turnsBelow} {turnsBelow === 1 ? "turn" : "turns"} below {turnsBelow === 1 ? "moves" : "move"} to the other branch
        </p>
      )}
    </div>
  );
}

demo.tsxHow it is called: two versions, opened for editing on the second, six turns below. Save appends a third.

import { useState } from "react";
import { EditTurn } from "./EditTurn";

/**
 * A question already edited once, open for a second edit, with six turns of
 * conversation hanging below it. Saving appends a third version and the new
 * limb starts empty, so the warning goes away with it.
 */
export default function Demo() {
  const [versions, setVersions] = useState([
    "Summarise the lease.",
    "Summarise the lease: term, rent, the break clause and what happens to the deposit.",
  ]);
  const [turnsBelow, setTurnsBelow] = useState(6);
  const save = (text: string) => {
    setVersions([...versions, text]);
    setTurnsBelow(0);
  };
  return (
    <div className="rounded-lg border bg-card p-4">
      <EditTurn versions={versions} turnsBelow={turnsBelow} defaultEditing defaultIndex={1} onSave={save} />
    </div>
  );
}
What it renders. Identical markup in both panes, with only the token values changing.

Examples

No captures reference this pattern yet. Captures arrive product by product; see Products for what's in the gallery so far.