|
// ==UserScript== |
|
// @name Claude Chunked Paste |
|
// @namespace https://intellectronica.net/ |
|
// @version 1.0 |
|
// @description Intercepts paste events in Claude and chunks large text to avoid attachment conversion |
|
// @author You |
|
// @match https://claude.ai/* |
|
// @grant none |
|
// ==/UserScript== |
|
|
|
(function() { |
|
'use strict'; |
|
|
|
// ============ CONFIGURATION ============ |
|
const CHUNK_SIZE = 500; // Characters per chunk - modify this as needed |
|
const CHUNK_DELAY = 50; // Milliseconds between chunks (increase if needed) |
|
// ======================================= |
|
|
|
function findTextArea() { |
|
// Claude uses a contenteditable div, not a textarea |
|
const editor = document.querySelector('[contenteditable="true"]'); |
|
return editor; |
|
} |
|
|
|
function insertTextAtCursor(element, text) { |
|
// Ensure the editor is focused |
|
element.focus(); |
|
|
|
// Split by newlines and insert line by line |
|
// This uses execCommand which handles contenteditable properly |
|
const lines = text.split('\n'); |
|
|
|
lines.forEach((line, index) => { |
|
if (line.length > 0) { |
|
document.execCommand('insertText', false, line); |
|
} |
|
if (index < lines.length - 1) { |
|
// Insert a paragraph break for newline |
|
document.execCommand('insertParagraph', false, null); |
|
} |
|
}); |
|
} |
|
|
|
function triggerInputEvent(element) { |
|
// Dispatch input event so Claude registers the change |
|
element.dispatchEvent(new InputEvent('input', { |
|
bubbles: true, |
|
cancelable: true, |
|
inputType: 'insertText' |
|
})); |
|
} |
|
|
|
async function insertChunkedText(element, text) { |
|
const chunks = []; |
|
for (let i = 0; i < text.length; i += CHUNK_SIZE) { |
|
chunks.push(text.slice(i, i + CHUNK_SIZE)); |
|
} |
|
|
|
console.log(`[Claude Chunked Paste] Pasting ${text.length} chars in ${chunks.length} chunks of up to ${CHUNK_SIZE} chars`); |
|
|
|
for (let i = 0; i < chunks.length; i++) { |
|
insertTextAtCursor(element, chunks[i]); |
|
triggerInputEvent(element); |
|
|
|
if (i < chunks.length - 1) { |
|
await new Promise(resolve => setTimeout(resolve, CHUNK_DELAY)); |
|
} |
|
} |
|
|
|
console.log('[Claude Chunked Paste] Paste complete'); |
|
} |
|
|
|
const HANDLED = Symbol('chunked-paste-handled'); |
|
|
|
function handlePaste(event) { |
|
// Prevent double-handling from multiple listeners |
|
if (event[HANDLED]) return; |
|
|
|
const editor = findTextArea(); |
|
if (!editor || !editor.contains(event.target)) { |
|
return; // Not pasting into the Claude editor |
|
} |
|
|
|
const clipboardData = event.clipboardData || window.clipboardData; |
|
if (!clipboardData) return; |
|
|
|
const text = clipboardData.getData('text/plain'); |
|
if (!text || text.length <= CHUNK_SIZE) { |
|
return; // Small paste, let it through normally |
|
} |
|
|
|
// Mark as handled |
|
event[HANDLED] = true; |
|
|
|
// Prevent default paste and stop all other handlers |
|
event.preventDefault(); |
|
event.stopPropagation(); |
|
event.stopImmediatePropagation(); |
|
|
|
// Neuter the clipboardData so any handlers that still fire get nothing |
|
Object.defineProperty(event, 'clipboardData', { |
|
get: () => ({ |
|
getData: () => '', |
|
types: [], |
|
files: new DataTransfer().files, |
|
items: new DataTransfer().items |
|
}) |
|
}); |
|
|
|
// Insert chunked text |
|
insertChunkedText(editor, text); |
|
|
|
return false; |
|
} |
|
|
|
// Attach listener with capture to intercept before Claude's handler |
|
// Also attach to window for earlier interception |
|
window.addEventListener('paste', handlePaste, true); |
|
document.addEventListener('paste', handlePaste, true); |
|
|
|
console.log(`[Claude Chunked Paste] Loaded - chunk size: ${CHUNK_SIZE} chars`); |
|
})(); |