您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add additional metadata to Claude messages: index, branch, timestamp, UUID
// ==UserScript== // @name Claude Message Info // @namespace http://tampermonkey.net/ // @version 0.0.13 // @description Add additional metadata to Claude messages: index, branch, timestamp, UUID // @author MRL // @match https://claude.ai/* // @license MIT // ==/UserScript== (function() { 'use strict'; // ============================================= // API FUNCTIONS // ============================================= /** * Extracts conversation ID from current URL */ function getConversationId() { const match = window.location.pathname.match(/\/chat\/([^/?]+)/); return match ? match[1] : null; } /** * Gets organization ID from browser cookies */ function getOrgId() { const cookies = document.cookie.split(';'); for (const cookie of cookies) { const [name, value] = cookie.trim().split('='); if (name === 'lastActiveOrg') { return value; } } throw new Error('Could not find organization ID'); } /** * Fetches conversation data from Claude API */ async function getConversationData() { const conversationId = getConversationId(); if (!conversationId) { return null; } const orgId = getOrgId(); const response = await fetch( `/api/organizations/${orgId}/chat_conversations/${conversationId}?tree=true&rendering_mode=messages&render_all_tools=true` ); if (!response.ok) { throw new Error(`API request failed: ${response.status}`); } return await response.json(); } // ============================================= // BRANCH BUILDING FUNCTIONS // ============================================= /** * Builds conversation tree structure */ function buildConversationTree(messages) { const messageMap = new Map(); const rootMessages = []; // Create message map messages.forEach(message => { messageMap.set(message.uuid, { ...message, children: [] }); }); // Build parent-child relationships messages.forEach(message => { const messageNode = messageMap.get(message.uuid); const parentUuid = message.parent_message_uuid; if (parentUuid && parentUuid !== "00000000-0000-4000-8000-000000000000" && messageMap.has(parentUuid)) { const parent = messageMap.get(parentUuid); parent.children.push(messageNode); } else { rootMessages.push(messageNode); } }); return { messageMap, rootMessages }; } /** * Finds main branch path from current_leaf_message_uuid */ function findMainBranchPathFromLeaf(tree, currentLeafUuid) { if (!currentLeafUuid) { return []; } const mainPath = []; let currentMessage = tree.messageMap.get(currentLeafUuid); while (currentMessage) { mainPath.unshift(currentMessage); // Add to beginning const parentUuid = currentMessage.parent_message_uuid; if (parentUuid === "00000000-0000-4000-8000-000000000000" || !parentUuid) { break; } currentMessage = tree.messageMap.get(parentUuid); } return mainPath; } /** * Finds main branch path from message with maximum index */ function findMainBranchPathFromMaxIndex(tree) { // Find message with maximum index let maxIndexMessage = null; let maxIndex = -1; tree.messageMap.forEach(message => { if (message.index > maxIndex) { maxIndex = message.index; maxIndexMessage = message; } }); if (!maxIndexMessage) return []; // Build path backwards through parent_message_uuid const mainPath = []; let currentMessage = maxIndexMessage; while (currentMessage) { mainPath.unshift(currentMessage); const parentUuid = currentMessage.parent_message_uuid; if (parentUuid === "00000000-0000-4000-8000-000000000000" || !parentUuid) { break; } currentMessage = tree.messageMap.get(parentUuid); } return mainPath; } /** * Gets all branch information including branch points */ function getAllBranchInfo(tree) { const branches = []; let branchCounter = 0; // Find main path from MAX INDEX (for determining Main/Side) const mainBranchPath = findMainBranchPathFromMaxIndex(tree); const mainBranchUuids = new Set(mainBranchPath.map(msg => msg.uuid)); function traverseBranches(node, currentPath = [], branchStartIndex = 0) { const newPath = [...currentPath, node]; if (node.children.length === 0) { // Leaf node - this is a complete branch branchCounter++; // Check if this branch is main const isMainBranch = newPath.every(msg => mainBranchUuids.has(msg.uuid)) && newPath.length === mainBranchPath.length; branches.push({ branchId: node.uuid, branchIndex: branchCounter, fullPath: newPath, branchStartIndex: branchStartIndex, isMainBranch: isMainBranch }); } else if (node.children.length === 1) { // Single child - continue same branch traverseBranches(node.children[0], newPath, branchStartIndex); } else { // Multiple children - branch point node.children.forEach((child, childIndex) => { // For first child, continue current branch // For other children, start new branches from this point const newBranchStart = childIndex === 0 ? branchStartIndex : newPath.length; traverseBranches(child, newPath, newBranchStart); }); } } tree.rootMessages.forEach(root => { traverseBranches(root, [], 0); }); return { branches, mainBranchUuids: mainBranchUuids // Based on MAX INDEX }; } /** * Creates a Map of messageUuid to branch info */ function createMessageBranchMap(branches) { const messageBranchMap = new Map(); branches.forEach(branch => { branch.fullPath.forEach(msg => { if (!messageBranchMap.has(msg.uuid)) { messageBranchMap.set(msg.uuid, { branchIndex: branch.branchIndex, isMainBranch: branch.isMainBranch }); } }); }); return messageBranchMap; } // ============================================= // DOM MANIPULATION // ============================================= /** * Formats timestamp for display */ function formatTimestamp(isoString) { const date = new Date(isoString); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}`; } /** * Injects metadata into a DOM element */ function injectMetadata(container, messageData, mainBranchUuids, messageBranchMap, domPosition, totalDomMessages) { // Check if already added if (container.querySelector('.claude-timestamp-metadata')) { return; } // Determine if main or side branch BY SPECIFIC MESSAGE UUID // Not by branch info, because a branch can contain both main and side messages const isMain = mainBranchUuids.has(messageData.uuid); const branchStatus = isMain ? 'Main' : 'Side'; // Get branch number const branchInfo = messageBranchMap.get(messageData.uuid); const branchNumber = branchInfo ? branchInfo.branchIndex : '?'; // Check if message was canceled const isCanceled = messageData.stop_reason === 'user_canceled'; const canceledText = isCanceled ? ' | CANCELED' : ''; // Create metadata element const metadata = document.createElement('div'); metadata.className = 'claude-timestamp-metadata'; metadata.style.cssText = ` position: absolute; top: -15px; right: 8px; font-size: 10px; color: var(--text-400, #94a3b8); opacity: 0.7; padding: 2px 6px; background: var(--bg-200, rgba(0, 0, 0, 0.05)); border-radius: 4px; /* backdrop-filter: blur(4px); */ z-index: -1; pointer-events: none; user-select: none; white-space: nowrap; `; const timestamp = formatTimestamp(messageData.created_at); metadata.textContent = `[${domPosition}/${totalDomMessages}] #${messageData.index} | Branch ${branchNumber} | ${branchStatus}${canceledText} | ${timestamp}`; const tooltipLines = [ `DOM Position: ${domPosition} of ${totalDomMessages}`, `API Index: ${messageData.index}`, `Branch: ${branchNumber}`, `Status: ${branchStatus}`, `Created: ${new Date(messageData.created_at).toLocaleString()}`, `UUID: ${messageData.uuid}` ]; if (isCanceled) { tooltipLines.push('Stop Reason: User Canceled'); } metadata.title = tooltipLines.join('\n'); // Find where to insert const groupDiv = container.querySelector('.group.relative'); if (groupDiv) { groupDiv.style.position = 'relative'; groupDiv.appendChild(metadata); } } /** * Removes all existing metadata badges */ function clearMetadata() { document.querySelectorAll('.claude-timestamp-metadata').forEach(el => el.remove()); } // ============================================= // INJECTION LOGIC // ============================================= /** * Main function to inject timestamps into all messages */ async function injectTimestamps(retryCount = 0) { try { // console.log('[Claude Timestamps] 🔄 Starting injection...'); // Get conversation data from API const conversationData = await getConversationData(); if (!conversationData || !conversationData.chat_messages || conversationData.chat_messages.length === 0) { console.log('[Claude Timestamps] ❌ No messages found in API'); return false; } // Build conversation tree const tree = buildConversationTree(conversationData.chat_messages); // Get all branch information (uses MAX INDEX for Main/Side determination) const { branches, mainBranchUuids } = getAllBranchInfo(tree); // Create message to branch mapping const messageBranchMap = createMessageBranchMap(branches); // Find active branch from current_leaf_message_uuid (for DOM matching) const activeBranch = findMainBranchPathFromLeaf(tree, conversationData.current_leaf_message_uuid); if (activeBranch.length === 0) { console.log('[Claude Timestamps] ❌ No active branch found'); return false; } // Get DOM elements const messageContainers = document.querySelectorAll( '.flex-1.flex-col.gap-3 > div[data-test-render-count]' ); if (messageContainers.length === 0) { // Retry if DOM not ready yet (max 3 attempts) if (retryCount < 3) { console.log(`[Claude Timestamps] ⏳ DOM not ready, retrying... (${retryCount + 1}/3)`); setTimeout(() => injectTimestamps(retryCount + 1), 500); return false; } else { console.log('[Claude Timestamps] ❌ No message containers found in DOM after 3 retries'); return false; } } console.log(`[Claude Timestamps] 📊 API (current_leaf): ${activeBranch.length} messages, DOM: ${messageContainers.length} elements`); // Clear old metadata clearMetadata(); // Match and inject const totalDomMessages = messageContainers.length; activeBranch.forEach((msg, index) => { if (index < messageContainers.length) { injectMetadata( messageContainers[index], msg, mainBranchUuids, messageBranchMap, index + 1, totalDomMessages ); } }); // console.log(`[Claude Timestamps] 📊 Injected timestamps for ${Math.min(activeBranch.length, messageContainers.length)} messages`); // console.log(`[Claude Timestamps] 📊 Active branch (from current_leaf): ${activeBranch.length} messages`); console.log(`[Claude Timestamps] 📊 Main branch UUIDs (from max index): ${mainBranchUuids.size} messages`); console.log(`[Claude Timestamps] 📊 Total branches found: ${branches.length}`); return true; } catch (error) { console.error('[Claude Timestamps] ❌ Error:', error); return false; } } // ============================================= // DEBOUNCE // ============================================= function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // ============================================= // INITIALIZATION // ============================================= function init() { console.log('[Claude Timestamps] 🚀 Initializing with API interception...'); // Initial injection setTimeout(() => { console.log('[Claude Timestamps] Initial injection after page load'); injectTimestamps(); }, 1000); // Watch for URL changes let lastUrl = location.href; new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; console.log('[Claude Timestamps] 🔗 URL changed, re-injecting...'); clearMetadata(); setTimeout(() => injectTimestamps(), 1500); } }).observe(document, { subtree: true, childList: true }); // Watch for new user messages (they appear immediately when sent) let lastMessageCount = 0; const userMessageObserver = new MutationObserver(debounce(() => { const messageContainers = document.querySelectorAll( '.flex-1.flex-col.gap-3 > div[data-test-render-count]' ); // If new messages appeared, inject timestamps if (messageContainers.length > lastMessageCount) { console.log('[Claude Timestamps] 📨 New user message detected, updating...'); lastMessageCount = messageContainers.length; setTimeout(() => injectTimestamps(), 300); } }, 200)); // Observe message list for new messages const observeMessages = () => { const messageList = document.querySelector('.flex-1.flex-col.gap-3'); if (messageList) { userMessageObserver.observe(messageList, { childList: true, subtree: false }); } }; setTimeout(observeMessages, 1000); // Watch for streaming completion (data-is-streaming attribute changes) const streamingObserver = new MutationObserver(debounce((mutations) => { for (const mutation of mutations) { if (mutation.type === 'attributes' && mutation.attributeName === 'data-is-streaming') { const target = mutation.target; const isStreaming = target.getAttribute('data-is-streaming'); // When streaming completes (changes to "false") if (isStreaming === 'false') { console.log('[Claude Timestamps] ✅ Streaming completed, updating metadata...'); setTimeout(() => injectTimestamps(), 500); } } } }, 300)); // Start observing for streaming changes const observeStreaming = () => { const messageContainers = document.querySelectorAll('[data-is-streaming]'); messageContainers.forEach(container => { streamingObserver.observe(container, { attributes: true, attributeFilter: ['data-is-streaming'] }); }); }; // Initial observation setTimeout(observeStreaming, 1000); // Re-observe when new messages appear new MutationObserver(debounce(() => { observeStreaming(); }, 500)).observe(document.body, { childList: true, subtree: true }); // Watch for version switching clicks document.addEventListener('click', debounce((e) => { const target = e.target.closest('button'); if (target) { // Check if it's a version navigation button const hasArrowIcon = target.querySelector('svg path[d*="M13.2402"]') || target.querySelector('svg path[d*="M6.13378"]'); if (hasArrowIcon) { console.log('[Claude Timestamps] 🔄 Version switch detected'); setTimeout(() => injectTimestamps(), 500); } } }, 100), true); } // Start when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();