Skip to content

fix: preserve undo history when pasting into the chat input - #498

Open
dungdong-aws wants to merge 1 commit into
Amazon-Q-Developer:mainfrom
dungdong-aws:fix/preserve-undo-on-paste
Open

dungdong-aws wants to merge 1 commit into
Amazon-Q-Developer:mainfrom
dungdong-aws:fix/preserve-undo-on-paste

Conversation

@dungdong-aws

@dungdong-aws dungdong-aws commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Problem

Pasting into the chat input could not be undone — Ctrl/Cmd+Z after a paste did nothing.

The paste handler cancelled the event and inserted the clipboard text itself with
range.insertNode(document.createTextNode(text)). A script-created text node is not
recorded on the browser's native undo stack, so the paste was invisible to undo.

Why the handler existed, and why it is now redundant

It was not about stripping formatting. It was added in 52b8162 (Feb 2025) as part of the
cursor-tracking work for the context selector, which converts character offsets to DOM
positions:

// updateCursorPos()
return preCaretRange.toString().length;

// insertElementToGivenPosition() — direct children only, text nodes only
for (const node of this.promptTextInput.childNodes) { ... }
if (currentPos + length >= safePosition && node.nodeType === Node.TEXT_NODE) {
  range.setStart(node, offset);
}

That mapping only holds while the input's childNodes are a flat sequence of text nodes and
span.context pills. At the time the element was contenteditable="true", where a native
paste could inject <div> / <br> / <span> structures and break the invariant — so forcing
the clipboard content into exactly one flat text node was correct.

Two months later, b64288b changed the element to contenteditable="plaintext-only", which
makes the browser guarantee that same invariant. From that point the handler was
redundant, and its only remaining effect was suppressing the native undo entry.

Fix

Stop cancelling the paste and remove the manual insertion (net −26/+3 in the handler). The
existing input handler still fires after a native paste and already covers onInput,
removeContextPlaceholderOverlay and checkIsEmpty — one more than the paste path previously
did.

This also removes a latent bug: if ((selection?.rangeCount) != null) was always true
(rangeCount is a number, so 0 != null), meaning getRangeAt(0) could throw when there was
no range.

Why not execCommand('insertText')

That is the obvious alternative and it does not work in the VS Code webview. Measured from
inside a real paste, with the input focused:

execCommand insertText -> false | hasFocus: true
activeElement: mynah-chat-prompt-input | path: MANUAL FALLBACK (not undoable)

Because a failed execCommand falls through to the manual insertion, that approach fails
silently: undo stays broken and the behaviour is identical to no fix at all. It works in a
plain browser, so a browser-only check passes while the IDE stays broken.

This means #492, which takes the execCommand route for the same bug, will not fix it in the
VS Code webview. Happy to consolidate — closing whichever the maintainers prefer.

Testing

Added a regression test asserting the paste event is not cancelled. jsdom cannot model
undo, so it guards the actual cause rather than simulating the symptom.

Verified manually in Amazon Q for VS Code against a locally built Flare bundle:

  • paste, then Ctrl/Cmd+Z → the paste is undone
  • pasting rich text still inserts plain text (plaintext-only preserved)
  • context pills still land correctly: pasted multi-line text, placed the cursor mid-text,
    typed @ and picked an item — the pill is inserted at the trigger offset, confirming the
    offset→DOM mapping is intact without the manual insertion

Known limitation (pre-existing, out of scope)

Context pills are inserted by script (range.insertNode in insertElementToGivenPosition),
so they are not on the native undo stack either. After this change, undo reverts a paste but
skips a pill. That is not a regression — previously neither was undoable — but it is now
visible as an inconsistency. Making pill insertion undoable would require every scripted
mutation in the prompt input to become a native editing command or be tracked in a custom undo
stack, which is a larger design change.

Risk

Relies on contenteditable="plaintext-only" being honoured by the host engine. It is in
Chromium-based hosts (VS Code, JetBrains JCEF). Worth a sanity check on Eclipse's SWT browser,
where a fallback would be needed if formatting survives a paste.

The paste handler cancelled the event and inserted the clipboard text itself
via a script-created text node. That insertion is not recorded on the
browser's native undo stack, so Ctrl/Cmd+Z after a paste did nothing.

The input is contenteditable="plaintext-only", so the browser already inserts
clipboard content as plain text -- the manual insertion was reimplementing
that, and cancelling the event was what broke undo. Letting the native paste
run keeps it undoable; the existing `input` handler still fires afterwards and
covers onInput / removeContextPlaceholderOverlay / checkIsEmpty.

Note document.execCommand('insertText', ...) is not a viable alternative: it
returns false inside the VS Code webview even with the input focused, which
silently falls back to the same non-undoable path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants