Greasy Fork is available in English.

Claude AI with Date

Shows timestamps for Claude conversation messages

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         Claude AI with Date
// @namespace    http://tampermonkey.net/
// @version      4.2
// @license      MIT
// @description  Shows 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, repaired by Claude Sonnet 4.6
// @match        https://claude.ai/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // =========================================================
    // FETCH INTERCEPTION — runs synchronously at script start,
    // before DOMContentLoaded, to catch early API calls.
    // =========================================================
    const _pendingApiData = [];
    let _apiDataHandler = null;

    const _originalFetch = window.fetch;
    window.fetch = function(...args) {
        return _originalFetch.apply(this, args).then(response => {
            const url = response.url || (typeof args[0] === 'string' ? args[0] : args[0]?.url || '');
            if (url.includes('/chat_conversations/') || url.includes('/messages')) {
                response.clone().json().then(data => {
                    if (_apiDataHandler) {
                        _apiDataHandler(data);
                    } else {
                        _pendingApiData.push(data);
                    }
                }).catch(() => {});
            }
            return response;
        });
    };

    // =========================================================
    // CONFIG
    // =========================================================
    const CONFIG = {
        userSelector:  '[data-testid="user-message"]',
        claudeSelector: '.font-claude-response',
        allSelector:   '[data-testid="user-message"], .font-claude-response',
        timestampClass:   'claude-timestamp',
        provisionalAttr:  'data-provisional',
        observerDelay: 750
    };

    let timestampData = new Map();   // "idx_N" -> ISO string
    let currentConversationId = null;

    // =========================================================
    // STORAGE — scoped per conversation ID, new prefix "claudeTs_"
    // avoids inheriting stale data from v3.3 / v4.0 / v4.1
    // =========================================================
    function getConversationId() {
        const match = window.location.pathname.match(/\/chat\/([a-zA-Z0-9_-]+)/);
        return match ? match[1] : '__global__';
    }

    function getStorageKey(convId) {
        return `claudeTs_${convId}`;
    }

    function loadTimestampData() {
        const convId = getConversationId();
        currentConversationId = convId;
        try {
            const stored = localStorage.getItem(getStorageKey(convId));
            timestampData = stored
                ? new Map(Object.entries(JSON.parse(stored)))
                : new Map();
        } catch (e) {
            timestampData = new Map();
        }
    }

    function saveTimestampData() {
        try {
            localStorage.setItem(
                getStorageKey(currentConversationId),
                JSON.stringify(Object.fromEntries(timestampData))
            );
        } catch (e) {}
    }

    function migrateOldStorage() {
        try {
            // Remove legacy global key from v3.x
            localStorage.removeItem('claudeTimestamps');
            // Old per-conversation keys (claudeTimestamps_{id}) are harmless — leave them.
        } catch (e) {}
    }

    // =========================================================
    // 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;
            }
            .${CONFIG.timestampClass}[${CONFIG.provisionalAttr}="true"] {
                opacity: 0.4 !important;
            }
        `;
        document.head.appendChild(style);
    }

    // =========================================================
    // TIMESTAMP FORMATTING — local time with TZ abbreviation
    // =========================================================
    function getTimezoneAbbr(date) {
        try {
            const parts = Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).formatToParts(date);
            const part = parts.find(p => p.type === 'timeZoneName');
            return part ? part.value : '';
        } catch (e) {
            return '';
        }
    }

    function formatDate(date) {
        const pad = n => String(n).padStart(2, '0');
        const tz = getTimezoneAbbr(date);
        return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
               `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` +
               (tz ? ' ' + tz : '');
    }

    function createTimestampElement(date, provisional) {
        const el = document.createElement('div');
        el.className = CONFIG.timestampClass;
        el.setAttribute(CONFIG.provisionalAttr, provisional ? 'true' : 'false');
        el.textContent = formatDate(date);
        return el;
    }

    // =========================================================
    // DOM HELPERS
    // =========================================================
    function getOrderedMessages() {
        // Returns all user + Claude message elements in DOM order.
        return Array.from(document.querySelectorAll(CONFIG.allSelector))
            .sort((a, b) =>
                a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
            );
    }

    // =========================================================
    // PROCESS MESSAGES
    // Index-based matching: API message[N] -> DOM element[N].
    // No content hashing, no markdown stripping needed.
    // =========================================================
    function processMessages() {
        const messages = getOrderedMessages();

        messages.forEach((message, index) => {
            const existing = message.querySelector('.' + CONFIG.timestampClass);

            // Skip if already has a confirmed (non-provisional) timestamp.
            if (existing && existing.getAttribute(CONFIG.provisionalAttr) !== 'true') return;

            // Remove provisional placeholder so we can re-stamp.
            if (existing) existing.remove();

            const storedTs = timestampData.get('idx_' + index);
            let timestamp, provisional;

            if (storedTs) {
                timestamp = new Date(storedTs);
                provisional = false;
            } else {
                // No API data yet — show dimmed current time as placeholder.
                // Do NOT store in localStorage; it will be replaced when API data arrives.
                timestamp = new Date();
                provisional = true;
            }

            const el = createTimestampElement(timestamp, provisional);
            const firstChild = message.firstElementChild;
            if (firstChild) {
                message.insertBefore(el, firstChild);
            } else {
                message.prepend(el);
            }
        });
    }

    // =========================================================
    // API RESPONSE HANDLER
    // Stores timestamps by index, then clears provisional
    // placeholders and re-stamps with correct times.
    // =========================================================
    function extractTimestampsFromResponse(data) {
        if (!data) return;
        const messages = data.chat_messages || data.messages || [];
        if (!messages.length) return;

        let updated = false;
        messages.forEach((msg, index) => {
            const ts = msg.created_at || msg.updated_at || msg.timestamp;
            if (ts) {
                timestampData.set('idx_' + index, new Date(ts).toISOString());
                updated = true;
            }
        });

        if (updated) {
            saveTimestampData();
            // Clear all provisional placeholders so processMessages re-stamps them.
            document.querySelectorAll(
                `.${CONFIG.timestampClass}[${CONFIG.provisionalAttr}="true"]`
            ).forEach(el => el.remove());
            setTimeout(processMessages, 100);
        }
    }

    // =========================================================
    // OBSERVERS — DOM mutations + SPA navigation
    // =========================================================
    function setupObserver() {
        // Watch for new message elements.
        const domObserver = new MutationObserver((mutations) => {
            let shouldProcess = false;
            for (const mutation of mutations) {
                if (mutation.type === 'childList') {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === 1 &&
                            (node.matches?.(CONFIG.allSelector) ||
                             node.querySelector?.(CONFIG.allSelector))) {
                            shouldProcess = true;
                            break;
                        }
                    }
                }
                if (shouldProcess) break;
            }
            if (shouldProcess) setTimeout(processMessages, CONFIG.observerDelay);
        });
        domObserver.observe(document.body, { childList: true, subtree: true });

        // Watch for SPA navigation (URL changes without page reload).
        let lastHref = window.location.href;
        new MutationObserver(() => {
            if (window.location.href !== lastHref) {
                lastHref = window.location.href;
                loadTimestampData();
                setTimeout(processMessages, CONFIG.observerDelay);
            }
        }).observe(document.body, { childList: true, subtree: true });
    }

    // =========================================================
    // INIT
    // =========================================================
    function init() {
        migrateOldStorage();
        loadTimestampData();
        injectStyles();

        // Register handler and drain the buffer of any API data
        // that arrived before init() ran.
        _apiDataHandler = extractTimestampsFromResponse;
        _pendingApiData.forEach(data => extractTimestampsFromResponse(data));
        _pendingApiData.length = 0;

        setTimeout(processMessages, 1000);
        setupObserver();
        setInterval(processMessages, 10000);
    }

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