Greasy Fork is available in English.
Shows timestamps for Claude conversation messages
// ==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);
}
})();