Display timestamps on Claude.ai messages
// ==UserScript==
// @name Claude with TimeStamp
// @name:en Claude with TimeStamp
// @name:pt-BR Claude com TimeStamp
// @namespace https://greasyfork.org/scripts/claude-with-timestamp
// @version 1.2.0
// @description Display timestamps on Claude.ai messages
// @description:pt-BR Exibe timestamps nas mensagens do Claude.ai
// @author Pedrero
// @license MIT
// @match *://claude.ai/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const CFG = {
format: '{yyyy}-{MM}-{dd} {HH}:{mm}',
interval: 2000,
className: 'cwd-timestamp',
};
function fmt(isoString) {
const d = new Date(isoString);
const pad = (n, l = 2) => String(n).padStart(l, '0');
return CFG.format
.replace('{yyyy}', d.getFullYear())
.replace('{MM}', pad(d.getMonth() + 1))
.replace('{dd}', pad(d.getDate()))
.replace('{HH}', pad(d.getHours()))
.replace('{mm}', pad(d.getMinutes()))
.replace('{ss}', pad(d.getSeconds()));
}
async function getOrgUuid() {
const cached = GM_getValue('cwd_org_uuid', null);
if (cached) return cached;
const r = await fetch('https://claude.ai/api/account');
if (!r.ok) return null;
const data = await r.json();
const uuid = data?.memberships?.[0]?.organization?.uuid;
if (uuid) GM_setValue('cwd_org_uuid', uuid);
return uuid;
}
async function fetchMessages(orgUuid, convUuid) {
const url = `https://claude.ai/api/organizations/${orgUuid}/chat_conversations/${convUuid}?tree=True&rendering_mode=messages`;
const r = await fetch(url);
if (!r.ok) return [];
const data = await r.json();
return (data.chat_messages || []).map(m => ({
sender: m.sender,
created_at: m.created_at,
}));
}
function getConvUuid() {
const match = location.pathname.match(/\/chat\/([0-9a-f-]{36})/i);
return match ? match[1] : null;
}
function getMessageBlocks() {
const blocks = [];
document.querySelectorAll('div[data-test-render-count]').forEach(el => {
if (el.querySelector('div[data-is-streaming]')) {
// Turno do assistant
blocks.push({ el, sender: 'assistant' });
} else if (el.querySelector('[data-testid="user-message"]') || el.querySelector('div[class*="justify-end"]')) {
// Turno do human (texto puro ou com anexo)
blocks.push({ el, sender: 'human' });
}
});
return blocks;
}
function injectTimestamp(el, isoString, sender) {
if (el.querySelector('.' + CFG.className)) return;
const span = document.createElement('span');
span.className = CFG.className;
span.textContent = fmt(isoString);
span.dataset.sender = sender;
// Injeta antes do primeiro filho real (div.mb-1), fora de qualquer container oculto
const firstChild = el.firstElementChild;
if (firstChild) {
el.insertBefore(span, firstChild);
} else {
el.appendChild(span);
}
}
let lastConvUuid = null;
let cachedMessages = [];
async function render() {
const convUuid = getConvUuid();
if (!convUuid) return;
const blocks = getMessageBlocks();
if (convUuid !== lastConvUuid || cachedMessages.length === 0 || blocks.length > cachedMessages.length) {
const orgUuid = await getOrgUuid();
if (!orgUuid) return;
const msgs = await fetchMessages(orgUuid, convUuid);
if (msgs.length === 0) return;
cachedMessages = msgs;
lastConvUuid = convUuid;
}
const humanMsgs = cachedMessages.filter(m => m.sender === 'human');
const assistantMsgs = cachedMessages.filter(m => m.sender === 'assistant');
let hi = 0, ai = 0;
for (const { el, sender } of blocks) {
if (sender === 'human') {
if (humanMsgs[hi]) { injectTimestamp(el, humanMsgs[hi].created_at, 'human'); hi++; }
} else {
if (assistantMsgs[ai]) { injectTimestamp(el, assistantMsgs[ai].created_at, 'assistant'); ai++; }
}
}
}
GM_addStyle(`
.cwd-timestamp {
display: block;
font-size: 0.72em;
color: #888;
margin-bottom: 4px;
margin-top: 8px;
font-family: ui-monospace, monospace;
letter-spacing: 0.02em;
user-select: none;
opacity: 0.7;
pointer-events: none;
}
.cwd-timestamp[data-sender="human"] {
text-align: right;
padding-right: 4px;
}
.cwd-timestamp[data-sender="assistant"] {
text-align: left;
}
`);
let renderTimer = null;
function scheduleRender() {
clearTimeout(renderTimer);
renderTimer = setTimeout(render, 600);
}
const _push = history.pushState.bind(history);
const _replace = history.replaceState.bind(history);
history.pushState = (...a) => { _push(...a); cachedMessages = []; scheduleRender(); };
history.replaceState = (...a) => { _replace(...a); cachedMessages = []; scheduleRender(); };
window.addEventListener('popstate', () => { cachedMessages = []; scheduleRender(); });
new MutationObserver(scheduleRender).observe(document.body, { childList: true, subtree: true });
render();
setInterval(render, CFG.interval);
})();