Claude Timestamp Revealer

Show timestamps for Claude conversation messages

// ==UserScript==
// @name         Claude Timestamp Revealer
// @namespace    http://tampermonkey.net/
// @version      3.0
// @license      MIT
// @description  Show timestamps for Claude conversation messages
// @author       Baseline Claude Sonnet 4, enhanced by Baseline ChatGPT-5, debugged by Wayne L. "Grasshopper" Pendley's "Syntactico" persona, who is sourced from ChatGPT-4o
// @match        https://claude.ai/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    const CONFIG = {
        messageSelector: '.group\\/conversation-turn, .font-claude-response, [data-testid="user-message"]',
        timestampClass: 'claude-timestamp',
        storageKey: 'claudeTimestamps',
        observerDelay: 750
    };

    let timestampData = new Map();

    // Load existing timestamp data
    function loadTimestampData() {
        try {
            const stored = localStorage.getItem(CONFIG.storageKey);
            if (stored) {
                const parsed = JSON.parse(stored);
                timestampData = new Map(Object.entries(parsed));
            }
        } catch (e) {
            console.warn('Failed to load timestamp data:', e);
        }
    }

    // Save timestamp data
    function saveTimestampData() {
        try {
            const obj = Object.fromEntries(timestampData);
            localStorage.setItem(CONFIG.storageKey, JSON.stringify(obj));
        } catch (e) {
            console.warn('Failed to save timestamp data:', e);
        }
    }

    // Inject CSS styles with !important to override Claude's styles
    function injectStyles() {
        const style = document.createElement('style');
        style.textContent = `
            .${CONFIG.timestampClass} {
                font-size: 15px !important;
                color: #555 !important;
                opacity: 1 !important;
                margin-bottom: 8px !important;
                margin-top: 4px !important;
                font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace !important;
                display: block !important;
                line-height: 1.4 !important;
            }
        `;
        document.head.appendChild(style);
    }

    // Create timestamp element
    function createTimestamp(date) {
        const timestamp = document.createElement('div');
        timestamp.className = CONFIG.timestampClass;
        const year = date.getUTCFullYear();
        const month = String(date.getUTCMonth() + 1).padStart(2, '0');
        const day = String(date.getUTCDate()).padStart(2, '0');
        const hours = String(date.getUTCHours()).padStart(2, '0');
        const minutes = String(date.getUTCMinutes()).padStart(2, '0');
        const seconds = String(date.getUTCSeconds()).padStart(2, '0');
        timestamp.textContent = `${year}-${month}-${day} ${hours}:${minutes}:${seconds} UTC`;
        return timestamp;
    }

    // Generate message hash for identification
    function generateMessageHash(element) {
        const textContent = element.textContent?.trim() || '';
        const position = Array.from(element.parentNode?.children || []).indexOf(element);
        return `${textContent.substring(0, 50)}_${position}`;
    }

    // Process messages and add timestamps
    function processMessages() {
        const messages = document.querySelectorAll(CONFIG.messageSelector);

        messages.forEach(message => {
            if (message.querySelector(`.${CONFIG.timestampClass}`)) return;

            const hash = generateMessageHash(message);
            let timestamp;

            if (timestampData.has(hash)) {
                timestamp = new Date(timestampData.get(hash));
            } else {
                timestamp = new Date();
                timestampData.set(hash, timestamp.toISOString());
                saveTimestampData();
            }

            const isClaude = message.classList.contains('font-claude-response');
            const isUser = message.getAttribute('data-testid') === 'user-message';

            if (isClaude || isUser) {
                const timestampElement = createTimestamp(timestamp);
                const firstChild = message.firstElementChild;
                if (firstChild) {
                    message.insertBefore(timestampElement, firstChild);
                } else {
                    message.prepend(timestampElement);
                }
            }
        });
    }

    // Intercept network requests to capture real timestamps
    function interceptNetworkData() {
        const originalFetch = window.fetch;
        window.fetch = function(...args) {
            return originalFetch.apply(this, args).then(response => {
                if (response.url.includes('/chat_conversations/') || 
                    response.url.includes('/messages')) {
                    const clonedResponse = response.clone();
                    clonedResponse.json().then(data => {
                        extractTimestampsFromResponse(data);
                    }).catch(() => {});
                }
                return response;
            });
        };
    }

    // Extract timestamps from API responses
    function extractTimestampsFromResponse(data) {
        if (!data) return;
        const messages = data.messages || data.chat_messages || [];

        messages.forEach(msg => {
            if (msg.created_at || msg.timestamp) {
                const timestamp = new Date(msg.created_at || msg.timestamp);
                const content = msg.content || msg.text || '';
                const hash = `${content.substring(0, 50)}_`;
                for (const [key, value] of timestampData.entries()) {
                    if (key.startsWith(hash)) {
                        timestampData.set(key, timestamp.toISOString());
                        break;
                    }
                }
            }
        });

        saveTimestampData();
        setTimeout(processMessages, 100);
    }

    // Setup mutation observer
    function setupObserver() {
        const observer = new MutationObserver((mutations) => {
            let shouldProcess = false;
            mutations.forEach((mutation) => {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    for (const node of mutation.addedNodes) {
                        if (
                            node.nodeType === 1 &&
                            (node.matches?.(CONFIG.messageSelector) ||
                             node.querySelector?.(CONFIG.messageSelector))
                        ) {
                            shouldProcess = true;
                            break;
                        }
                    }
                }
            });
            if (shouldProcess) {
                setTimeout(processMessages, CONFIG.observerDelay);
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
        return observer;
    }

    // Initialize the script
    function init() {
        console.log('Claude Timestamp Revealer: Initializing...');
        loadTimestampData();
        injectStyles();
        interceptNetworkData();
        setTimeout(processMessages, 1000);
        setupObserver();
        setInterval(processMessages, 10000);
        console.log('Claude Timestamp Revealer: Ready');
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        setTimeout(init, 100);
    }
})();