Fix slow prompt paste and focus-mode model submenu scroll (#1898)

## Fixes
- Slow paste in prompt box: `execCommand("insertText")` is quadratic per
line in Chromium - now inserts the clipboard as a single text node
(linear)
- Model selector submenu not scrolling in focus mode: the modal's scroll
lock swallows wheel/touch on body-portaled panels - flyout now stops
propagation (same pattern as mention dropdown)
This commit is contained in:
BFlat
2026-08-13 07:42:18 +07:00
committed by GitHub
parent a10b49b4c6
commit 85e4c15598
2 changed files with 46 additions and 2 deletions
@@ -616,6 +616,12 @@ function SubmenuFlyout({
data-modal-outside-safe=""
onMouseEnter={cancelClose}
onMouseLeave={scheduleClose}
// The focus-mode modal's scroll lock (react-remove-scroll) blocks
// any wheel/touch event that bubbles to document from outside the
// dialog — and this body-portaled panel counts as outside. Stop
// propagation so the list's native scrolling keeps working.
onWheel={(e) => e.stopPropagation()}
onTouchMove={(e) => e.stopPropagation()}
style={{
position: "fixed",
top: position.top,
@@ -1497,9 +1497,47 @@ export const MentionTextarea = forwardRef<HTMLDivElement, MentionTextareaProps>(
(e: React.ClipboardEvent<HTMLDivElement>) => {
e.preventDefault();
const text = e.clipboardData.getData("text/plain");
document.execCommand("insertText", false, text);
const el = editorRef.current;
const sel = window.getSelection();
if (!text || !el || !sel?.rangeCount) return;
// Not execCommand("insertText"): Chromium runs a separate editing
// command per pasted line (each re-canonicalizing positions over the
// whole document), so large multi-line prompts froze the tab for
// seconds. Splice in a single text node instead — whitespace-pre-wrap
// renders its "\n"s — and let handleInput do the usual serialize +
// rebuild. This skips the native undo stack, but any rebuild
// (mentions, newlines) already invalidated it.
try {
const range = sel.getRangeAt(0);
// A boundary inside an atomic chip would drop the pasted text into
// the contenteditable=false span, where serialization ignores it —
// snap such boundaries to the chip's outside edge.
const startChip = chipContaining(el, range.startContainer);
const endChip = chipContaining(el, range.endContainer);
if (range.collapsed) {
if (startChip) {
range.setStartAfter(startChip);
range.collapse(true);
}
} else {
if (startChip) range.setStartBefore(startChip);
if (endChip) range.setEndAfter(endChip);
}
range.deleteContents();
const node = document.createTextNode(text);
range.insertNode(node);
range.setStartAfter(node);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
} catch (err) {
console.debug("handlePaste: DOM changed during insert", err);
return;
}
handleInput();
},
[],
[handleInput],
);
// Copy/cut must yield plain text with mention labels intact ("@Name"),