Many thanks to @Balrog2000 for the Tampermonkey script, which is a great workaround until such time as this is fixed.
However, there are some situations where I do actually find the new ChatGPT behaviour helpful, so ideally it should be optional. Hopefully thatāll be how itās implemented eventually - but until then, Iāve modified @Balrog2000ās script such that:
- conventional paste events of any kind will trigger the script and paste normally
- āspecial pasteā events (effectively CTRL-Shift-V / CMD-Shift-V, but more specifically: holding CTRL+Shift / CMD+Shift during any kind of paste event) will disable the script and allow ChatGPTās default behaviour to go ahead.
In case this is helpful for anyone else, too, hereās the script:
// ==UserScript==
// @name ChatGPT: prevent large paste -> pasted.txt (ProseMirror) + bypass Ctrl+Shift+V
// @match https://chatgpt.com/*
// @run-at document-start
// @grant none
// ==/UserScript==
(() => {
"use strict";
const TAG = "[TM-no-pastedtxt]";
const CFG = {
DEBUG: false,
// Intercept if either threshold is hit
TRIGGER_LINES: 120,
TRIGGER_CHARS: 8000,
// Reinsert in chunks
CHUNK_LINES: 40,
CHUNK_DELAY_MS: 5, // increase (e.g. 5ā20) if you ever see dropped chunks
// Try multiple selectors to survive minor DOM changes
EDITOR_SELECTORS: [
"div#prompt-textarea.ProseMirror[contenteditable='true']",
"div#prompt-textarea[contenteditable='true']",
"[contenteditable='true'][data-testid='prompt-textarea']",
],
// Bypass combo:
// - Windows/Linux: Ctrl+Shift held at paste time
// - macOS: Cmd+Shift held at paste time
// (We detect modifier state on paste, so we do NOT rely on seeing "V" keydown.)
BYPASS_REQUIRE_NO_ALT: true,
};
const log = (...a) => CFG.DEBUG && console.log(TAG, ...a);
const attachedEditors = new WeakSet();
// --- modifier tracking (robust bypass) ---
const mods = { ctrl: false, shift: false, meta: false, alt: false };
function updateModsFromEvent(e) {
mods.ctrl = !!e.ctrlKey;
mods.shift = !!e.shiftKey;
mods.meta = !!e.metaKey;
mods.alt = !!e.altKey;
}
function isBypassHeldNow() {
const altOk = CFG.BYPASS_REQUIRE_NO_ALT ? !mods.alt : true;
// Win/Linux: Ctrl+Shift (no Meta)
const winLinux = mods.ctrl && mods.shift && !mods.meta && altOk;
// macOS: Cmd+Shift (no Ctrl)
const mac = mods.meta && mods.shift && !mods.ctrl && altOk;
return winLinux || mac;
}
function onKeyDown(e) {
updateModsFromEvent(e);
}
function onKeyUp(e) {
updateModsFromEvent(e);
}
function findEditorFromEvent(e) {
const path = typeof e.composedPath === "function" ? e.composedPath() : [];
for (const node of path) {
if (!(node instanceof Element)) continue;
for (const sel of CFG.EDITOR_SELECTORS) {
const hit = node.closest?.(sel);
if (hit) return hit;
}
}
// Fallback: activeElement or global query
const ae = document.activeElement;
if (ae instanceof Element) {
for (const sel of CFG.EDITOR_SELECTORS) {
const hit = ae.closest?.(sel);
if (hit) return hit;
}
}
for (const sel of CFG.EDITOR_SELECTORS) {
const hit = document.querySelector(sel);
if (hit) return hit;
}
return null;
}
function chunkByLines(text, nLines) {
const lines = text.split("\n");
const chunks = [];
for (let i = 0; i < lines.length; i += nLines) {
chunks.push(lines.slice(i, i + nLines).join("\n"));
}
return chunks;
}
function safeInsertText(editor, text) {
editor.focus();
// 1) Preferred: execCommand insertText (the most compatible for contenteditable)
try {
document.execCommand("insertText", false, text);
} catch {
// 2) Fallback: DOM Range insertion
const sel = editor.ownerDocument.getSelection?.() || window.getSelection?.();
if (sel && sel.rangeCount > 0) {
const range = sel.getRangeAt(0);
range.deleteContents();
range.insertNode(editor.ownerDocument.createTextNode(text));
// Move caret to end of inserted text node
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
} else {
// 3) Last resort
editor.textContent += text;
}
}
// Nudge frameworks that listen for input
try {
editor.dispatchEvent(
new InputEvent("input", { bubbles: true, data: text, inputType: "insertText" })
);
} catch {
editor.dispatchEvent(new Event("input", { bubbles: true }));
}
}
async function reinsertInChunks(editor, text) {
const chunks = chunkByLines(text, CFG.CHUNK_LINES);
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
safeInsertText(editor, chunk + (i < chunks.length - 1 ? "\n" : ""));
const delay = Math.max(0, CFG.CHUNK_DELAY_MS);
await new Promise((r) => setTimeout(r, delay));
}
}
function onPaste(e) {
const editor = findEditorFromEvent(e);
if (!editor) return;
// --- BYPASS: if "special paste" modifiers are held, allow site/default handler ---
if (isBypassHeldNow()) {
log("bypass held: allowing default paste", { mods: { ...mods } });
return; // IMPORTANT: no preventDefault, no stopPropagation
}
const text = e.clipboardData?.getData("text/plain") ?? "";
if (!text) return;
const lineCount = (text.match(/\n/g) || []).length;
const isLarge = lineCount >= CFG.TRIGGER_LINES || text.length >= CFG.TRIGGER_CHARS;
log("paste", { len: text.length, lines: lineCount, isLarge, mods: { ...mods } });
if (!isLarge) return;
// Prevent ChatGPT's paste handler from seeing it at all
e.preventDefault();
e.stopImmediatePropagation();
void reinsertInChunks(editor, text);
}
// Capture-phase listeners to beat site handlers
// Track modifiers globally (donāt depend on editor detection for key events)
window.addEventListener("keydown", onKeyDown, true);
window.addEventListener("keyup", onKeyUp, true);
document.addEventListener("paste", onPaste, true);
// Attach directly to editor nodes as they appear (more robust vs bubbling weirdness)
const mo = new MutationObserver(() => {
for (const sel of CFG.EDITOR_SELECTORS) {
const editor = document.querySelector(sel);
if (editor && !attachedEditors.has(editor)) {
attachedEditors.add(editor);
editor.addEventListener("paste", onPaste, true);
log("attached to editor node", sel);
}
}
});
mo.observe(document.documentElement, { childList: true, subtree: true });
log("loaded");
})();