Extract a Long Claude Conversation

It seems to come out of nowhere. After a bit of a back and forth conversation with Claude, just as I'm getting somewhere, a little purple notification appears next to the chat textarea.

Tip: Long chats cause you to reach your usage limits faster.

It is written in a friendly tone, but the message is clear. Time to wrap up this chat or you're going to run out of invisible usage credits.

Let's look at how to extract this conversation.

This will be useful for at least two things. First, the extracted conversation is its own artifact that we may want to share, save, or even input into another LLM. Second, we can bring this chat transcript with us to a fresh Claude conversation as a robust starting context so that we can pick up where we left off.

The most obvious solution is to directly highlight and copy/paste the entire contents of the conversation from the desktop or web app. The results are unsatisfying though. You cannot tell who is saying what and the formatting is entirely lost, especially for code blocks.

The next idea I had was to ask Claude to come up with a JavaScript snippet I could paste and run from the devtools console. After an initial proof of concept, I was able to iterate on the result to make sure it did the following things:

  • Properly extract multi-line messages
  • Identify and label the speaker (Me or Claude) for each part of the conversation
  • Re-format code blocks with language indication while removing Copy button text

This IIFE extracts the entire conversation, does some formatting, outputs some details to the console as it goes, and then ultimately puts the entire formated transcript on the system clipboard. After running this, it is ready to paste into a google doc, a new file, or a new LLM conversation.

Here is the script:

(function() {
  // Find all message elements
  const userMessages = document.querySelectorAll('div[data-testid="user-message"]');
  const claudeMessages = document.querySelectorAll('div.font-claude-message');
  
  // Create an array to store all messages with their types and positions
  const allMessages = [];
  
  // Process user messages
  userMessages.forEach(userMsg => {
    // Find the closest element with data-test-render-count attribute to get the timestamp
    const container = userMsg.closest('[data-test-render-count]');
    const position = container ? Array.from(document.body.querySelectorAll('[data-test-render-count]')).indexOf(container) : -1;
    
    // Get all paragraph texts within the user message
    const paragraphs = Array.from(userMsg.querySelectorAll('p.whitespace-pre-wrap.break-words'))
      .map(p => p.textContent.trim())
      .join('\n\n');
    
    allMessages.push({
      type: 'User',
      content: paragraphs,
      position: position
    });
  });
  
  // Process Claude messages
  claudeMessages.forEach(claudeMsg => {
    // Find the closest element with data-test-render-count attribute
    const container = claudeMsg.closest('[data-test-render-count]');
    const position = container ? Array.from(document.body.querySelectorAll('[data-test-render-count]')).indexOf(container) : -1;
    
    // Collect content from paragraphs, lists, and handle code blocks specially
    const contentElements = [];
    
    // Process all content elements
    claudeMsg.querySelectorAll('p.whitespace-pre-wrap, pre, ol, ul').forEach(element => {
      if (element.tagName.toLowerCase() === 'pre') {
        // Handle code blocks
        let codeContent = '';
        
        // Try to find the language label
        const langLabel = element.querySelector('.text-xs');
        const language = langLabel ? langLabel.textContent.trim() : '';
        
        // Find the actual code content (exclude the Copy button text)
        const codeElement = element.querySelector('code');
        if (codeElement) {
          codeContent = codeElement.textContent.trim();
        } else {
          // Fallback: get all text except the copy button text
          const copyButton = element.querySelector('button');
          if (copyButton) {
            copyButton.remove(); // Temporarily remove button to get clean text
          }
          codeContent = element.textContent.trim();
          if (copyButton && copyButton.parentNode) {
            element.appendChild(copyButton); // Restore button
          }
        }
        
        // Format with triple backticks and language
        contentElements.push('```' + language + '\n' + codeContent + '\n```');
      } else {
        // Regular paragraphs and lists
        contentElements.push(element.textContent.trim());
      }
    });
    
    const content = contentElements.join('\n\n');
    
    allMessages.push({
      type: 'Claude',
      content: content,
      position: position
    });
  });
  
  // Sort messages by their position in the document
  allMessages.sort((a, b) => a.position - b.position);
  
  // Format the conversation
  const conversation = allMessages
    .map(msg => `${msg.type}:\n${msg.content}`)
    .join('\n\n');
  
  // Create a temporary textarea element
  const textarea = document.createElement('textarea');
  textarea.value = conversation;
  textarea.setAttribute('readonly', '');
  textarea.style.position = 'absolute';
  textarea.style.left = '-9999px';
  document.body.appendChild(textarea);
  
  // Select the text and copy it
  textarea.select();
  document.execCommand('copy');
  
  // Clean up and provide feedback
  document.body.removeChild(textarea);
  console.log('✅ Conversation copied to clipboard successfully!');
  console.log('Preview:');
  console.log(conversation.slice(0, 200) + (conversation.length > 200 ? '...' : ''));
  console.log(`Total messages: ${allMessages.length}`);
})();

For now, I'm going to continue to copy/paste this script as is into the Chrome Console whenever I need it. You could imagine wrapping this up into a custom browser extension, but I don't quite have that need myself yet.

If you're using Claude primarily from the desktop app like I do, you have two options for using the script. You can open Claude in the browser, find and open the conversation, and then run it from the devtools. Or you can use the Share button to get a web URL for the conversation and do the same from there.

The fun thing about websites is that they can always change. If you're trying the above script and you find it isn't quite working, perhaps because Claude adjusted the markup for the site slightly, then ask Claude to help you fix it. Paste the script into a new Claude conversation, point out specific details of what is going wrong, and iterate to a new working version.

Let me know if this JavaScript snippet helped you out or if you have another solution that I should try out.

Tell us about your project

We build good software through good partnerships. Reach out and we can discuss your business, your goals, and how VisualMode can help.