Skip to content
KONIGI Patterns

AI Assistants / Messages and responses / Edit and resend

1 of 6 ›

Edit and resend

People need to correct an earlier question without losing the conversation that followed it.

Updated September 12, 2026

Problem

When a question contains a mistake, asking it again leaves the original exchange in the conversation. People need to correct the question without losing useful work.

Solution

Let people edit a sent message and continue from the revised version. Keep the original message and its replies available as a separate version of the conversation.

Show a clear version control on the edited message so people can return to the earlier exchange. Explain what will happen to later replies before the edit is sent.

Open the editor with the original text and attachments intact. Let people cancel without sending, and make clear which version of the conversation the assistant will use next.

Use when

People can correct an earlier question and need to keep the conversation that followed it.

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

Keeping alternate conversations prevents data loss but makes navigation more complex. People can lose track of which version they are reading. An edited conversation also needs its original messages preserved if it serves as a record of what happened.

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 requests another answer without changing the question. Message turn holds the edit control, and Composer provides the editing field.

Conversation history must preserve alternate versions. Context meter reflects the messages included in the active conversation.

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 screenshots yet for this pattern. Browse Products to see other examples.