Problem / motivation
Today, once a user has sent a message in a Scott conversation, the only way to refine the question is to send a follow-up. There's no way to:
- Fix a typo and re-run the same turn.
- Reframe an earlier question and have Scott re-answer with the same retrieval context.
- "Branch off" a chosen point in a long conversation without starting a brand-new thread.
This is the standard edit & regenerate pattern shipped by ChatGPT, Claude.ai, and most modern chat UIs. Adding it would make iterative question-refinement (a common Scott workflow) much faster, and would also be useful for the PR #100 verification step "ask Scott a question that hits a freshly-enriched chunk" — being able to re-run a question after a background enrichment lands turns the verification into one click instead of typing a fresh thread.
UX
-
Each user message bubble grows a pencil-icon button visible on hover (or always, on touch).
-
Clicking it swaps the bubble's static text for an editable composer pre-filled with the original message.
-
Two buttons under the editor: Save & resend and Cancel.
-
If the edited message is the last user message in the thread, save & resend silently erases the assistant's reply (if present) and re-runs the agent from this turn.
-
If the edited message is mid-conversation (i.e. there are messages after the assistant's reply to this turn), Save & resend opens a confirm dialog:
Editing this message will erase the rest of the conversation (N more messages). Continue?
On confirm: erase all subsequent user + assistant messages from the thread (Postgres + assistant-ui state), then re-run the agent from the edited turn. On cancel: stay in edit mode.
-
Cancel restores the original message text and exits edit mode without changes.
What assistant-ui already gives us
MessagePrimitive.If type=\"user\" — for conditionally rendering edit affordance on user bubbles only.
MessageRuntime.edit() / useMessageRuntime() — the in-memory state transition; flips a message into edit mode.
ComposerPrimitive.Edit — the in-place edit composer; renders the pre-filled textarea + Save / Cancel buttons.
- The runtime's existing reload-from-message semantics — when a user message is edited, downstream messages are dropped and the agent re-runs.
So the primary work is CSS + a confirmation dialog, not protocol-level surgery. The remote thread adapter (frontend/src/threadAdapter.ts) already round-trips messages to Postgres via chat/views.py:api_conversation_history (PUT/POST), so message deletion on edit will propagate to the server through the existing save-on-mutation hook.
Backend implications
Conversation / Message are stored as a tree (parent-id chains) in chat/models.py. The existing ExportedMessageRepository round-trip already handles partial trees on save. Likely no backend change required — when the edit causes assistant-ui to truncate the in-memory tree and resave, the API endpoint will accept the smaller tree and the pruned messages become orphaned (or get cleaned up depending on the replace=True semantics already in api_conversation_history).
If we want a stronger guarantee, add an idempotent "replace from message X onward" endpoint, but that's only worth it if the in-place save proves lossy in testing.
Implementation sketch
frontend/src/chat.tsx — extend UserMessage() to render an Edit button, gated behind MessagePrimitive.If type=\"user\". Wire its onClick to useMessageRuntime().composer.beginEdit().
- New
EditComposer component using ComposerPrimitive.Edit for the textarea + Save / Cancel buttons.
- Branch the Save handler:
- Walk the thread state to find messages strictly after this turn.
- If
count > 0, open a Radix-style <AlertDialog> (already a Tailwind 4 / shadcn-style dependency in this project).
- On confirm, call
composer.send() (which assistant-ui implements as edit + truncate + rerun).
- Style the pencil icon as the existing chat aesthetic (warm-brown primary, hover-only on desktop, always-on for touch).
- Tests: a Playwright spec under
chat/tests.py (or frontend/) that covers (a) edit last message → no dialog, (b) edit mid-message → dialog → confirm → tree truncates, (c) cancel restores text.
Out of scope
- Conversation branching / forking (keep both the original and the edited branch, with a UI to switch). Cleaner long-term but heavier; skip for v1.
- Edit assistant messages. Keeping user-only is the standard pattern and avoids correctness implications for citation indices.
- Edit history (audit trail of past versions of an edited message). Nice-to-have; only worth it if anyone asks.
Related
Problem / motivation
Today, once a user has sent a message in a Scott conversation, the only way to refine the question is to send a follow-up. There's no way to:
This is the standard edit & regenerate pattern shipped by ChatGPT, Claude.ai, and most modern chat UIs. Adding it would make iterative question-refinement (a common Scott workflow) much faster, and would also be useful for the PR #100 verification step "ask Scott a question that hits a freshly-enriched chunk" — being able to re-run a question after a background enrichment lands turns the verification into one click instead of typing a fresh thread.
UX
Each user message bubble grows a pencil-icon button visible on hover (or always, on touch).
Clicking it swaps the bubble's static text for an editable composer pre-filled with the original message.
Two buttons under the editor: Save & resend and Cancel.
If the edited message is the last user message in the thread, save & resend silently erases the assistant's reply (if present) and re-runs the agent from this turn.
If the edited message is mid-conversation (i.e. there are messages after the assistant's reply to this turn), Save & resend opens a confirm dialog:
On confirm: erase all subsequent user + assistant messages from the thread (Postgres + assistant-ui state), then re-run the agent from the edited turn. On cancel: stay in edit mode.
Cancel restores the original message text and exits edit mode without changes.
What
assistant-uialready gives usMessagePrimitive.If type=\"user\"— for conditionally rendering edit affordance on user bubbles only.MessageRuntime.edit()/useMessageRuntime()— the in-memory state transition; flips a message into edit mode.ComposerPrimitive.Edit— the in-place edit composer; renders the pre-filled textarea + Save / Cancel buttons.So the primary work is CSS + a confirmation dialog, not protocol-level surgery. The remote thread adapter (
frontend/src/threadAdapter.ts) already round-trips messages to Postgres viachat/views.py:api_conversation_history(PUT/POST), so message deletion on edit will propagate to the server through the existing save-on-mutation hook.Backend implications
Conversation/Messageare stored as a tree (parent-id chains) inchat/models.py. The existingExportedMessageRepositoryround-trip already handles partial trees on save. Likely no backend change required — when the edit causes assistant-ui to truncate the in-memory tree and resave, the API endpoint will accept the smaller tree and the pruned messages become orphaned (or get cleaned up depending on thereplace=Truesemantics already inapi_conversation_history).If we want a stronger guarantee, add an idempotent "replace from message X onward" endpoint, but that's only worth it if the in-place save proves lossy in testing.
Implementation sketch
frontend/src/chat.tsx— extendUserMessage()to render an Edit button, gated behindMessagePrimitive.If type=\"user\". Wire itsonClicktouseMessageRuntime().composer.beginEdit().EditComposercomponent usingComposerPrimitive.Editfor the textarea + Save / Cancel buttons.count > 0, open a Radix-style<AlertDialog>(already a Tailwind 4 / shadcn-style dependency in this project).composer.send()(which assistant-ui implements as edit + truncate + rerun).chat/tests.py(orfrontend/) that covers (a) edit last message → no dialog, (b) edit mid-message → dialog → confirm → tree truncates, (c) cancel restores text.Out of scope
Related
chat/views.py:api_conversation_historyis the round-trip surface that already supports replace-style saves.