// ==UserScript== // @name Claude typing lag fix // @namespace https://lugia19.com // @version 1.2.3 // @description Fix typing lag in long claude chats by replacing the text entry field. // @author lugia19 // @match https://claude.ai/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @run-at document-idle // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; const processedProseMirrors = new WeakSet(); let currentTextarea = null; let draftSaveTimer; let draftDebounce = 300; // 0.3 seconds debounce for draft saving const messageCountThreshold = 200; // Threshold for long conversations const longConversations = new Set(); document.addEventListener('keydown', (e) => { // Check for body element OR thought process containers const isTargetBody = e.target.tagName === 'BODY' && e.target.classList.contains('bg-bg-100') && e.target.classList.contains('text-text-100'); const isThoughtProcessContainer = e.target.tagName === 'DIV' && e.target.classList.contains('h-full') && e.target.classList.contains('overflow-y-auto') && e.target.classList.contains('overflow-x-hidden'); const hasModifiers = e.ctrlKey || e.altKey || e.metaKey; // Blacklist navigation/special keys instead of whitelisting characters const isNavigationKey = [ 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'PageUp', 'PageDown', 'Home', 'End', 'Tab', 'Escape', 'Delete', 'Backspace', 'Enter', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'Shift', 'Control', 'Alt', 'Meta' ].includes(e.key); if (!isNavigationKey && (isTargetBody || isThoughtProcessContainer) && !hasModifiers && currentTextarea) { console.log('๐ŸŽฏ Intercepting typing character:', e.key); e.stopImmediatePropagation(); e.preventDefault(); currentTextarea.focus(); currentTextarea.value += e.key; currentTextarea.dispatchEvent(new Event('input', { bubbles: true })); return false; } }, { capture: true }); //Draft storage function getDraftKey() { if (window.location.pathname.indexOf('/new') != -1) { // New chat, common key return "claude-draft-homepage"; } const match = window.location.pathname.match(/\/chat\/([a-f0-9-]+)/); const uuid = match ? match[1] : null; return uuid ? `claude-draft-${uuid}` : null; } function saveDraft(text) { const key = getDraftKey(); if (!key) return; clearTimeout(draftSaveTimer); draftSaveTimer = setTimeout(() => { if (text.trim()) { GM_setValue(key, text); console.log('๐Ÿ’พ Draft saved for chat:', getDraftKey()); } else { GM_deleteValue(key); console.log('๐Ÿ—‘๏ธ Empty draft deleted for chat:', getDraftKey()); } }, draftDebounce); // 0.5 second debounce } function loadDraft() { const key = getDraftKey(); if (!key) return ''; const draft = GM_getValue(key, ''); if (draft) { console.log('๐Ÿ“‚ Draft loaded for chat:', getDraftKey()); } return draft; } function clearDraft() { const key = getDraftKey(); if (key) { GM_deleteValue(key); console.log('๐Ÿ—‘๏ธ Draft cleared for chat:', getDraftKey()); } } //Actual replacement function replaceProseMirror() { const proseMirrorDiv = document.querySelector('.ProseMirror'); if (!proseMirrorDiv || processedProseMirrors.has(proseMirrorDiv)) { return; } console.log('๐Ÿ“ Replacing ProseMirror with textarea'); processedProseMirrors.add(proseMirrorDiv); // Hide and clear original proseMirrorDiv.innerHTML = ''; proseMirrorDiv.textContent = ''; proseMirrorDiv.setAttribute('contenteditable', 'false'); proseMirrorDiv.setAttribute('tabindex', '-1'); proseMirrorDiv.style.cssText = ` opacity: 0 !important; pointer-events: none !important; position: absolute !important; z-index: -1 !important; height: 0 !important; overflow: hidden !important; `; // Create textarea // In the replaceProseMirror function, update the textarea creation: const simpleTextarea = document.createElement('textarea'); simpleTextarea.className = 'claude-simple-input'; simpleTextarea.style.cssText = ` width: 100%; min-height: 1.5rem; max-height: none; border: none; outline: none; resize: none; overflow: hidden; font-family: inherit; font-size: inherit; line-height: inherit; padding: 0; background: transparent; color: inherit; `; simpleTextarea.placeholder = 'Write your prompt to Claude'; // Auto-resize function function autoResize() { // Reset height to measure scrollHeight accurately simpleTextarea.style.height = 'auto'; // Calculate new height const newHeight = Math.max(24, simpleTextarea.scrollHeight); // 24px minimum (1.5rem) const maxHeight = unsafeWindow.innerHeight * 0.4; // Max 40% of viewport height // Apply the height simpleTextarea.style.height = Math.min(newHeight, maxHeight) + 'px'; // If we hit max height, show scrollbar if (newHeight > maxHeight) { simpleTextarea.style.overflowY = 'auto'; } else { simpleTextarea.style.overflowY = 'hidden'; } } // Add auto-resize to input events simpleTextarea.addEventListener('input', () => { saveDraft(simpleTextarea.value); autoResize(); }); // Load existing draft const existingDraft = loadDraft(); if (existingDraft) { simpleTextarea.value = existingDraft; } // Initial resize setTimeout(autoResize, 0); // Insert textarea proseMirrorDiv.parentNode.insertBefore(simpleTextarea, proseMirrorDiv); currentTextarea = simpleTextarea; // Handle focus hijacking proseMirrorDiv.addEventListener('focus', (e) => { e.preventDefault(); e.stopPropagation(); if (currentTextarea) currentTextarea.focus(); }, true); // Also intercept clicks on the container area const container = proseMirrorDiv.parentNode; container.addEventListener('click', (e) => { // If they clicked in the general area but not on our textarea if (e.target !== currentTextarea && currentTextarea) { console.log('๐Ÿ–ฑ๏ธ Redirecting container click to textarea'); currentTextarea.focus(); } }); // Handle Enter key simpleTextarea.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); const text = simpleTextarea.value.trim(); if (text) { submitMessage(text); } } }); simpleTextarea.focus(); } function replaceSubmitButton() { const originalSubmitButton = document.querySelector('button[aria-label="Send message"]:not(.claude-custom-submit)'); const existingCustomButton = document.querySelector('.claude-custom-submit'); if (!originalSubmitButton || existingCustomButton) { return; // No original button or custom already exists } console.log('๐Ÿ”˜ Replacing submit button'); // Create our button const newSubmitButton = document.createElement('button'); newSubmitButton.innerHTML = originalSubmitButton.innerHTML; newSubmitButton.className = originalSubmitButton.className + ' claude-custom-submit'; newSubmitButton.type = 'button'; newSubmitButton.setAttribute('aria-label', 'Send message'); newSubmitButton.disabled = false; // Replace the button originalSubmitButton.style.display = 'none'; originalSubmitButton.parentNode.insertBefore(newSubmitButton, originalSubmitButton); // Handle click newSubmitButton.addEventListener('click', (e) => { e.preventDefault(); if (currentTextarea) { const text = currentTextarea.value.trim(); if (text) { submitMessage(text); } } }); } function processMarkdownCodeBlocks(text) { // Replace ```language\ncode\n``` with proper HTML return text.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, language, code) => { let lang = language || ''; const escapedCode = code.replace(/&/g, '&') .replace(//g, '>'); return `
${escapedCode}
`; }); } function submitMessage(text) { console.log('๐Ÿ“ค Submitting message'); const proseMirrorDiv = document.querySelector('.ProseMirror'); if (proseMirrorDiv) { // Temporarily re-enable it for submission proseMirrorDiv.setAttribute('contenteditable', 'true'); // Process markdown first const processedText = processMarkdownCodeBlocks(text); // If it has code blocks, use innerHTML (but escaped), otherwise use paragraphs if (processedText !== text) { proseMirrorDiv.innerHTML = processedText; } else { // Original paragraph approach for non-code text proseMirrorDiv.innerHTML = ''; const lines = text.split('\n'); lines.forEach(line => { const p = document.createElement('p'); p.textContent = line || '\u00A0'; proseMirrorDiv.appendChild(p); }); } proseMirrorDiv.dispatchEvent(new Event('input', { bubbles: true })); proseMirrorDiv.dispatchEvent(new Event('change', { bubbles: true })); setTimeout(() => { // Find and click submit... let hiddenSubmit = document.querySelector('button[aria-label="Send message"][style*="display: none"]'); if (!hiddenSubmit) { hiddenSubmit = document.querySelector('button[aria-label="Send message"]:not(.claude-custom-submit)'); } if (hiddenSubmit && !hiddenSubmit.disabled) { hiddenSubmit.click(); } // Disable it again after submission // Clear our textarea and clean up the original setTimeout(() => { console.log('๐Ÿงน Cleaning up after submission'); if (currentTextarea) { currentTextarea.value = ''; currentTextarea.style.height = 'auto'; currentTextarea.style.height = '1.5rem'; currentTextarea.style.overflowY = 'hidden'; currentTextarea.focus(); } // Clear the original proseMirrorDiv.innerHTML = ''; proseMirrorDiv.textContent = ''; proseMirrorDiv.setAttribute('contenteditable', 'false'); // Scroll to bottom with multiple attempts const scrollToBottom = () => { const chatContainer = document.querySelector('.relative.h-full.flex-1.flex.overflow-x-hidden.overflow-y-scroll.pt-6'); if (chatContainer) { chatContainer.scrollTo(0, chatContainer.scrollHeight); } }; scrollToBottom(); // Immediate setTimeout(scrollToBottom, 1000); // 1s setTimeout(scrollToBottom, 2000); // 2s setTimeout(scrollToBottom, 3000); // 3s setTimeout(clearDraft, 200); }, 100); }, 50); } } // Separate polling for each component function checkAndMaintain() { const currentUrlMatch = unsafeWindow.location.pathname.match(/\/chat\/([a-f0-9-]+)/); const currentConvId = currentUrlMatch ? currentUrlMatch[1] : null; // Only enable performance mode if current conversation is in the long conversations set if (currentConvId && longConversations.has(currentConvId)) { const proseMirrorExists = !!document.querySelector('.ProseMirror'); const ourTextareaExists = !!document.querySelector('.claude-simple-input'); const ourButtonExists = !!document.querySelector('.claude-custom-submit'); if (proseMirrorExists && !ourTextareaExists) { replaceProseMirror(); } if (!ourButtonExists) { replaceSubmitButton(); } } } // Monkey patch fetch to monitor conversation updates // Monkey patch fetch const originalFetch = unsafeWindow.fetch; unsafeWindow.fetch = function (...args) { const url = args[0]; if (typeof url === 'string' && url.includes('/chat_conversations/') && url.includes('tree=True')) { console.log('๐Ÿ” Intercepted tree=True call, making tree=False call'); // Extract conversation ID const conversationIdMatch = url.match(/chat_conversations\/([a-f0-9-]+)/); const fetchedConvId = conversationIdMatch ? conversationIdMatch[1] : null; if (fetchedConvId) { // Make our own call with tree=False to get visible message count const visibleMessagesUrl = url.replace('tree=True', 'tree=False'); originalFetch(visibleMessagesUrl, args[1]) .then(response => response.json()) .then(data => { const visibleMessageCount = data.chat_messages?.length || 0; console.log(`๐Ÿ“Š Conversation ${fetchedConvId} has ${visibleMessageCount} visible messages`); if (visibleMessageCount > messageCountThreshold) { longConversations.add(fetchedConvId); console.log('๐Ÿ“ Added to long conversations set (based on visible messages)'); } else { longConversations.delete(fetchedConvId); console.log('๐Ÿ“ Removed from long conversations set'); } }) .catch(err => { console.log('โŒ Failed to fetch visible messages:', err); }); } } return originalFetch.apply(this, args); }; // Start setInterval(checkAndMaintain, 100); })();