Topic 4.6
Editing a Message and Branching
In one line
When a user edits an earlier question or asks for another answer, nothing should be destroyed. Store messages as a tree: an edit or a regenerated answer is a new sibling of the old one, the screen follows one path through the tree, and arrows let the user flip between versions.
Think of it like this
A road with forks. You drive to a junction, turn left, and later decide to go back and turn right instead. The left road is still there, with everything you passed on it, and you can drive it again. Editing a question is going back to a junction and taking the other road: both roads exist, and you are on one of them.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Tree
- Messages that each point to a parent. A message can have several children.
- parentId
- The id of the message this one follows.
nullfor the first message. - Sibling
- A message with the same parent as another: a version of it.
- Branch
- One path through the tree. Editing creates a new branch.
- Selected child
- The version of a message that is currently on screen, per parent.
- Active path
- The messages shown on screen and sent to the model.
- Regenerate
- Ask for another answer to the same question, kept as a new version.
- Truncate
- The simple alternative to branching: an edit deletes everything after it.
Step by step
01Messages that point to a parent
Four messages in a line are a tree with no forks. The second example starts with exactly this chat.
[
{ id: "n1", parentId: null, role: "user", content: "Where is ORD-1043?" },
{ id: "n2", parentId: "n1", role: "assistant", content: "ORD-1043 is being packed." },
{ id: "n3", parentId: "n2", role: "user", content: "Can I cancel it?" },
{ id: "n4", parentId: "n3", role: "assistant", content: "Yes, until it ships." },
]02Edit, ignore, refuse
The first live example runs the operations on a tree and prints the active path. Two edits do nothing: the same text, and empty text. A real edit adds n5 beside n1, and the path becomes only n5 (version 2/2) with no answer yet: the old answers belong to the old question.
Then an answer is added under n5, and "regenerate" adds a second answer under the same question (version 2/2 of the assistant message). Finally the example selects version 1 of the question again and the original four-message chat is back, nothing lost.
03What the model receives
Build the request from the active path, up to and including the user message you are answering. For a regenerate, that cuts off the old answer.
// answer the user message `userId`: send the path that ends at it
function historyFor(userId) {
const path = [];
for (let id = userId; id; id = byId[id].parentId) path.unshift(byId[id]);
return path.map((m) => ({ role: m.role, content: m.content }));
}04The interface
The second live example is a small working screen. The scripted visitor clicks Edit on the first question, types a new text, saves, flips back with the "‹" arrow, and finally opens the editor and saves without changing anything.
Read the console: the edited path has two messages (the new question and its fresh answer), version 1 brings back all four, and the unchanged save does not create a branch.
Try it yourself
- 1
Regenerate and flip
In the first example, add
selected[u3] = nodes.find((x) => x.parentId === u3).id; show("first answer again");on the line right aftershow("after regenerate");(before the step that goes back to version 1).It selects the first of the two answers to the edited question. The path shows
n6with(version 1/2): both answers are still there, and you choose between them. - 2
Edit with trailing spaces
Call
edit(u1, "Where is ORD-1043? ")in the first example.It prints "edit ignored: same text":
trim()makes the texts equal. Without trimming, a stray space would create a whole new branch and a paid request. - 3
Edit the second question
In the second example, make the scripted visitor press the *second*
.editbutton (document.querySelectorAll(".edit")[1].click()).The new version appears under
n2, and the first question and its answer are untouched. Only the messages below the edited one belong to the new branch.
Code & diagrams
Nothing is ever deleted: the tree grows, and only "selected" changes when you flip. The last block is the original four messages, back exactly as they were.
Preview
Console
Nothing logged yet.Expected console output
start: n1 user "Where is ORD-1043?"
n2 assistant "ORD-1043 is being packed."
n3 user "Can I cancel it?"
n4 assistant "Yes, until it ships."
edit ignored: same text
edit refused: empty text
after editing the first question: n5 user "Where is ORD-1042?" (version 2/2)
messages in the tree: 5 | on screen: 1
answer to the edited question: n5 user "Where is ORD-1042?" (version 2/2)
n6 assistant "ORD-1042 shipped yesterday."
after regenerate: n5 user "Where is ORD-1042?" (version 2/2)
n7 assistant "ORD-1042 left Pune yesterday morning." (version 2/2)
back to version 1: n1 user "Where is ORD-1043?" (version 1/2)
n2 assistant "ORD-1043 is being packed."
n3 user "Can I cancel it?"
n4 assistant "Yes, until it ships."The scripted visitor edits the first question, flips back to version 1 with the arrow, then saves without changing anything. The version counter text "2/2" is what a person sees between the arrows.
Preview
Console
Nothing logged yet.Expected console output
start: 4 messages
edited: ["user: Where is ORD-1042? 2/2","assistant: ORD-1042 has shipped."]
version 1: ["user: Where is ORD-1043? 1/2","assistant: ORD-1043 is being packed.","user: Can I cancel it?","assistant: Yes, until it ships."]
saved without a change: 4 messages, still version 1Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Edit in place
In the first example, change the last line of edit from return add(old.parentId, old.role, text.trim()); to old.content = text.trim(); return old.id;.
Myth vs fact
Myth
Editing a message just means changing its text.
Fact
The answers below it were written for the old text. Changing the text in place leaves them contradicting the question, and loses the original.
Myth
A regenerated answer should replace the old one.
Fact
Users often prefer the first. Keep both as versions, and let them flip.
Myth
Branching needs a database with trees.
Fact
A flat array with a parentId on each item, plus a small selected map, is enough, in memory or in localStorage.
Myth
Regenerate re-sends the whole chat including the old answer.
Fact
It must send the path up to the user's question only. Including the old answer makes the model repeat it.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Store the tree as a flat array (or map) by id, not nested objects. Nested trees are awkward to update one node at a time, and flat records fit the reducer from Topic 4.2.
- ▸
Store
selectedwith the chat. Otherwise a reload shows the newest branch instead of the one the user was reading. - ▸
Show *when* a branch is offered: put the arrows on the message that has siblings, not in a global toolbar, and keep them keyboard-reachable with clear labels ("Previous version").
- ▸
A cap on versions per message (say 10) bounds storage and cost. When the cap is hit, drop the oldest unselected sibling and everything beneath it.
Remember this
- 1
The problem this solves: A user spots a typo in their first question, or wants a better answer to the last one. If your chat is a flat list, you have two bad options: overwrite the message (the answers below it now answer a question that no longer exists) or delete everything after it (the user loses work, and cannot go back). A tree keeps both versions, and the screen shows one path through it.
- 2
The tree. *(Recall from Topic 4.1: every message already has a unique
id; that is what aparentIdpoints to.)* Give each message aparentId: the message it follows. The first message hasparentId: null. A message with two or more children has versions: the children are alternatives, such as the original question and its edit, or two different answers to the same question. A flat chat is a tree in which every message has exactly one child. - 3
The path. The screen shows one path: start at the top and, at every message, follow the selected child (store which child is selected for each parent, and default to the newest). The path is also what you send to the model, because the model should see the conversation as the user sees it now, not every branch that ever existed. The first live example prints the path after each operation.
- 4
Edit creates a sibling. Editing message
n1does not changen1. It adds a new user messagen5with the *same*parentId, selects it, and then asks the model for a new answer. The old answers hang belown1, still saved, no longer on the path. Editing in place (changingn1's text) leavesn2, the old answer, standing under a question that is no longer what was asked: the break-it shows this. - 5
Regenerate creates a sibling too. "Give me another answer" adds a new assistant message with the same parent as the old one. The request sent for it ends at the user message: the old answer must not be in it, or the model sees its own previous reply and tends to repeat it.
- 6
Versions on the screen. A message that has siblings shows
‹ 2/3 ›: previous, current and total. The arrows select a sibling, and the rest of the screen follows the selected child's own selected children, so flipping back to version 1 of the question brings its original answers back, later messages included. The second live example has the full interface: Edit, a box, Save and resend, and the arrows. - 7
Rules for edits. If the new text is empty or the same as the old text, do nothing (no empty branch, no wasted request). Only user messages are editable: editing what the *assistant* said would put words in the model's mouth. Don't allow edit or regenerate while an answer is streaming in that chat: either block the buttons, or stop the stream first (Phase 3), because the new request and the old stream would otherwise race (Topic 4.7).
- 8
Costs and limits. Every edit and regenerate is a new request, so it costs tokens, and it counts toward the user's quota. Branches also add messages to storage: a chat that was edited ten times holds ten times that part. Decide how many versions to keep, and let the user delete a branch. The simpler design is to *truncate*: an edit deletes everything after it. That is easier to build and store, and it cannot be undone, which users notice only when they need to.
Explain it without notes
Why does editing a message create a new message instead of changing the old one?
How do you decide what history to send after an edit or a regenerate?
How does the interface show and switch between versions?
What are the costs of supporting branches?
Practice
Write historyFor(nodes, userId) that returns the { role, content } list sent to the model for answering the user message userId.
Write deleteBranch(nodes, id) that removes a node and every node below it, and returns the new array.
Trade-offs
- ↔
A tree keeps every version and costs complexity, storage and requests. Truncate-on-edit is a few lines and cheap but destroys the old continuation. If your users rarely edit, truncate first and add branches when they ask.
- ↔
Letting users edit only the last question is the simplest middle ground: no later messages to reconcile, and the old answer can still be kept as a version.
Done when you can
I can store messages with a parentId and follow a selected path.
I can explain why an edit is a new sibling and not a change in place.
I can build the request for an edit or a regenerate from the active path.
I can show versions with arrows and switch between them.
I can name the costs of branching and a simpler alternative.