stop putting the sent prompt back in the composer on escape

It was built on a belief carried over from Claude Code's terminal: that interrupting means the agent
never read the prompt, so handing it back lets you say it differently. That is not what happens here —
the prompt is delivered and read before escape can land, the transcript keeps it, and the agent answers
it on the next turn. So the composer refilled with something already sent, and sending it again sent it
twice.

Escape means "stop, I'll say it differently" or just "stop". Neither wants the old text back. Focus
still returns to the composer, which serves both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 22:40:09 +01:00
co-authored by Claude Opus 5
parent eb6f73ff40
commit 908ab1ca6a
@@ -80,9 +80,6 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComp
// the list growing under an animation or a row measuring taller than its estimate, and reading it as
// intent is what used to switch auto-scroll off mid-turn and leave it off.
const gestureAtRef = useRef(0);
// The prompt currently in flight, kept verbatim so a stop can put it back in the composer.
const lastSentRef = useRef('');
// Latest values for the (once-attached) scroll listener, without re-subscribing on every page load.
const loadOlderRef = useRef(loadOlder);
loadOlderRef.current = loadOlder;
@@ -125,26 +122,25 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComp
);
attachmentManager.clearAttachments();
lastSentRef.current = input; // verbatim, not the trimmed prompt — this is what comes back on a stop
setInput('');
userScrolledRef.current = false;
if (textareaRef.current) textareaRef.current.style.height = 'auto';
};
/**
* Stop, and hand the prompt back. Interrupting almost always means "not like that" — you want to say
* it differently — and retyping it from the transcript is busywork. The text returns exactly as typed,
* newlines and all.
* Stop the turn, and leave the composer alone.
*
* It never overwrites: if you've started composing something else while it ran, that wins and the old
* prompt is dropped. Losing what you just typed to a stop you pressed would be the worse failure.
* This used to hand the sent prompt back, on the theory — carried over from Claude Code's terminal —
* that interrupting means the agent never read it, so you should get it back to say differently. That
* is not what happens here. The prompt is delivered and read before Escape can land; the transcript
* keeps it and the agent answers it on the next turn. Restoring it therefore refilled the composer
* with something already sent, and sending it again sent it twice.
*
* Escape means one of two things — "stop, I'll say it differently" or just "stop" — and neither wants
* the old text back. Focus returns to the composer, which serves both.
*/
const stopGeneration = () => {
stopTurn();
const sent = lastSentRef.current;
lastSentRef.current = '';
if (!sent || input.trim()) return;
setInput(sent);
requestAnimationFrame(() => textareaRef.current?.focus());
};