infinite-agents-public/claude_code_devtools/claude_devtool_2.html

1205 lines
44 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Session Cache Manager - Claude Code DevTools</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--border-color: #30363d;
--text-primary: #c9d1d9;
--text-secondary: #8b949e;
--accent-blue: #58a6ff;
--accent-green: #3fb950;
--accent-red: #f85149;
--accent-yellow: #d29922;
--accent-purple: #bc8cff;
--code-bg: #161b22;
--hover-bg: #30363d;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
}
header {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
padding: 2rem;
text-align: center;
}
header h1 {
font-size: 2rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--accent-blue);
}
.tagline {
color: var(--text-secondary);
font-size: 1.1rem;
}
main {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
section {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
}
h2 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: var(--accent-blue);
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.5rem;
}
h3 {
font-size: 1.2rem;
margin: 1.5rem 0 1rem 0;
color: var(--accent-purple);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.stat-card {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 1rem;
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: var(--accent-green);
font-family: 'Courier New', monospace;
}
.stat-label {
font-size: 0.9rem;
color: var(--text-secondary);
margin-top: 0.5rem;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1.5rem;
}
.control-group {
flex: 1;
min-width: 250px;
}
label {
display: block;
margin-bottom: 0.5rem;
color: var(--text-secondary);
font-size: 0.9rem;
font-weight: 500;
}
input[type="text"],
input[type="file"],
textarea {
width: 100%;
padding: 0.75rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-family: 'Courier New', monospace;
font-size: 0.9rem;
}
input[type="text"]:focus,
textarea:focus {
outline: none;
border-color: var(--accent-blue);
}
textarea {
min-height: 150px;
resize: vertical;
font-size: 0.85rem;
line-height: 1.4;
}
button {
padding: 0.75rem 1.5rem;
background: var(--accent-blue);
color: white;
border: none;
border-radius: 6px;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
button:hover {
background: #4a8fd8;
transform: translateY(-1px);
}
button:active {
transform: translateY(0);
}
button.secondary {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
}
button.secondary:hover {
background: var(--hover-bg);
}
button.danger {
background: var(--accent-red);
}
button.danger:hover {
background: #dc3d3d;
}
button.success {
background: var(--accent-green);
}
button.success:hover {
background: #32904a;
}
.button-group {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.sessions-list {
max-height: 500px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-tertiary);
}
.session-item {
padding: 1rem;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
transition: background 0.2s;
}
.session-item:hover {
background: var(--hover-bg);
}
.session-item:last-child {
border-bottom: none;
}
.session-item.selected {
background: var(--hover-bg);
border-left: 3px solid var(--accent-blue);
}
.session-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.session-id {
font-family: 'Courier New', monospace;
color: var(--accent-blue);
font-size: 0.9rem;
}
.session-date {
color: var(--text-secondary);
font-size: 0.85rem;
}
.session-meta {
display: flex;
gap: 1rem;
font-size: 0.85rem;
color: var(--text-secondary);
}
.session-meta span {
display: flex;
align-items: center;
gap: 0.3rem;
}
.badge {
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
}
.badge.messages {
background: var(--accent-blue);
color: white;
}
.badge.size {
background: var(--accent-green);
color: white;
}
.search-results {
margin-top: 1rem;
}
.result-item {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 1rem;
margin-bottom: 1rem;
}
.result-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.result-content {
background: var(--code-bg);
padding: 1rem;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
white-space: pre-wrap;
word-break: break-word;
max-height: 200px;
overflow-y: auto;
}
.highlight {
background: var(--accent-yellow);
color: var(--bg-primary);
padding: 0.1rem 0.2rem;
border-radius: 2px;
}
.empty-state {
text-align: center;
padding: 3rem;
color: var(--text-secondary);
}
.empty-state svg {
width: 64px;
height: 64px;
margin-bottom: 1rem;
opacity: 0.5;
}
.docs {
background: var(--bg-tertiary);
}
.doc-content ul,
.doc-content ol {
margin-left: 1.5rem;
margin-top: 0.5rem;
}
.doc-content li {
margin-bottom: 0.5rem;
}
.doc-content p {
margin-bottom: 1rem;
}
.doc-content code {
background: var(--code-bg);
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
color: var(--accent-purple);
}
footer {
text-align: center;
padding: 2rem;
color: var(--text-secondary);
border-top: 1px solid var(--border-color);
}
footer a {
color: var(--accent-blue);
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
.progress-bar {
width: 100%;
height: 6px;
background: var(--bg-tertiary);
border-radius: 3px;
overflow: hidden;
margin-top: 0.5rem;
}
.progress-fill {
height: 100%;
background: var(--accent-blue);
transition: width 0.3s;
}
.storage-warning {
background: var(--accent-yellow);
color: var(--bg-primary);
padding: 1rem;
border-radius: 6px;
margin-bottom: 1rem;
font-weight: 500;
}
.storage-error {
background: var(--accent-red);
color: white;
padding: 1rem;
border-radius: 6px;
margin-bottom: 1rem;
font-weight: 500;
}
@media (max-width: 768px) {
main {
padding: 1rem;
}
header {
padding: 1.5rem;
}
header h1 {
font-size: 1.5rem;
}
.stats-grid {
grid-template-columns: 1fr;
}
.controls {
flex-direction: column;
}
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: var(--bg-tertiary);
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--hover-bg);
}
</style>
</head>
<body>
<header>
<h1>Session Cache Manager</h1>
<p class="tagline">Persistent storage for Claude Code session transcripts and metadata</p>
</header>
<main>
<!-- Storage Statistics -->
<section class="tool-interface">
<h2>Cache Statistics</h2>
<div id="storageWarning"></div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="statSessions">0</div>
<div class="stat-label">Cached Sessions</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statMessages">0</div>
<div class="stat-label">Total Messages</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statSize">0 KB</div>
<div class="stat-label">Storage Used</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statOldest">Never</div>
<div class="stat-label">Oldest Session</div>
</div>
</div>
<!-- Storage Usage Progress -->
<div>
<label>Storage Quota Usage (Estimated 5-10MB limit)</label>
<div class="progress-bar">
<div class="progress-fill" id="storageProgress"></div>
</div>
</div>
</section>
<!-- Cache Session Interface -->
<section>
<h2>Cache New Session</h2>
<div class="controls">
<div class="control-group">
<label>Session ID (auto-generated if empty)</label>
<input type="text" id="sessionId" placeholder="session-2025-10-09-001">
</div>
<div class="control-group">
<label>Session Name/Description</label>
<input type="text" id="sessionName" placeholder="Bug fix in authentication module">
</div>
</div>
<div class="control-group">
<label>Session Data (JSONL format - paste transcript lines)</label>
<textarea id="sessionData" placeholder='{"uuid":"msg-1","sessionId":"session-1","timestamp":"2025-10-09T18:00:00Z","message":{"role":"user","content":"Hello"},"type":"user"}
{"uuid":"msg-2","sessionId":"session-1","timestamp":"2025-10-09T18:00:30Z","message":{"role":"assistant","content":"Hi!"},"type":"assistant"}'></textarea>
</div>
<div class="button-group">
<button class="success" onclick="cacheSession()">Cache Session</button>
<button class="secondary" onclick="loadSessionFile()">Load from File</button>
<input type="file" id="fileInput" accept=".jsonl,.json,.txt" style="display: none;">
</div>
</section>
<!-- Search Interface -->
<section>
<h2>Search Cached Sessions</h2>
<div class="controls">
<div class="control-group">
<label>Search Query (searches session names, IDs, and content)</label>
<input type="text" id="searchQuery" placeholder="Type to search..." oninput="searchSessions()">
</div>
</div>
<div class="search-results" id="searchResults"></div>
</section>
<!-- Session List -->
<section>
<h2>Cached Sessions</h2>
<div class="button-group" style="margin-bottom: 1rem;">
<button class="secondary" onclick="refreshSessionList()">Refresh List</button>
<button class="success" onclick="exportAllSessions()">Export All</button>
<button class="secondary" onclick="importSessions()">Import Cache</button>
<button class="danger" onclick="clearAllCache()">Clear All Cache</button>
</div>
<div class="sessions-list" id="sessionsList"></div>
</section>
<!-- Selected Session Viewer -->
<section>
<h2>Session Details</h2>
<div id="sessionDetails" class="empty-state">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<p>Select a session from the list above to view details</p>
</div>
</section>
<!-- Documentation -->
<section class="docs">
<h2>About This Tool</h2>
<div class="doc-content">
<h3>Purpose</h3>
<p>Session Cache Manager provides persistent browser-based storage for Claude Code conversation transcripts. Store sessions locally, search across cached conversations, and manage your development session history without external dependencies.</p>
<h3>Features</h3>
<ul>
<li><strong>LocalStorage Persistence</strong> - All data stored in browser using Web Storage API</li>
<li><strong>Session Metadata</strong> - Track session IDs, names, timestamps, message counts, and sizes</li>
<li><strong>Real-time Search</strong> - Search across session names, IDs, and message content</li>
<li><strong>Storage Analytics</strong> - Monitor cache size, quota usage, and storage health</li>
<li><strong>Export/Import</strong> - Backup and restore cache data as JSON</li>
<li><strong>Quota Management</strong> - Warning system for approaching storage limits</li>
<li><strong>JSON Serialization</strong> - Efficient storage of complex session objects</li>
</ul>
<h3>Web Research Integration</h3>
<p><strong>Source:</strong> <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API" target="_blank">MDN Web Storage API Documentation</a></p>
<p><strong>Techniques Applied:</strong></p>
<ul>
<li><strong>setItem/getItem Pattern</strong> - Using proper <code>localStorage.setItem()</code> and <code>localStorage.getItem()</code> methods instead of direct property access for reliable storage operations</li>
<li><strong>JSON Serialization</strong> - Implementing <code>JSON.stringify()</code> for storing complex session objects and <code>JSON.parse()</code> for retrieval, enabling storage of nested data structures</li>
<li><strong>Feature Detection</strong> - Using try/catch pattern to detect localStorage availability before usage, with graceful degradation if unavailable</li>
<li><strong>Storage Events</strong> - Leveraging storage event listeners for cross-tab synchronization (planned for multi-instance coordination)</li>
<li><strong>Quota Management</strong> - Calculating storage usage and implementing warning system based on estimated 5-10MB localStorage limits</li>
</ul>
<h3>Usage</h3>
<ol>
<li><strong>Cache a Session</strong> - Paste JSONL transcript data, add optional session ID/name, click "Cache Session"</li>
<li><strong>Load from File</strong> - Click "Load from File" to import a JSONL transcript file directly</li>
<li><strong>Search Sessions</strong> - Type in search box to filter sessions by name, ID, or content</li>
<li><strong>View Details</strong> - Click any session in the list to view full message details</li>
<li><strong>Export/Import</strong> - Backup entire cache with "Export All" or restore with "Import Cache"</li>
<li><strong>Monitor Storage</strong> - Check statistics dashboard for cache health and quota usage</li>
<li><strong>Clear Cache</strong> - Use "Clear All Cache" to remove all stored sessions (with confirmation)</li>
</ol>
<h3>Technical Implementation</h3>
<ul>
<li><strong>Storage Key Pattern</strong> - Uses <code>claude_cache_session_</code> prefix for all session keys</li>
<li><strong>Metadata Index</strong> - Maintains <code>claude_cache_index</code> for efficient session listing</li>
<li><strong>Data Structure</strong> - Each session stores: id, name, timestamp, messages array, metadata</li>
<li><strong>Search Algorithm</strong> - Case-insensitive search across session metadata and message content</li>
<li><strong>Size Calculation</strong> - Estimates storage using <code>JSON.stringify(value).length * 2</code> (UTF-16 encoding)</li>
</ul>
<h3>Storage Format</h3>
<p>Session data is stored as JSON with this structure:</p>
<ul>
<li><code>id</code> - Unique session identifier</li>
<li><code>name</code> - User-provided session description</li>
<li><code>timestamp</code> - Cache creation timestamp (ISO 8601)</li>
<li><code>messages</code> - Array of parsed JSONL message objects</li>
<li><code>messageCount</code> - Total number of messages</li>
<li><code>firstMessage</code> - Timestamp of first message</li>
<li><code>lastMessage</code> - Timestamp of last message</li>
</ul>
</div>
</section>
</main>
<footer>
<p>Claude Code DevTools | Generated via web-enhanced infinite loop</p>
<p>Web Source: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API" target="_blank">MDN Web Storage API</a></p>
</footer>
<script>
// LocalStorage availability check (MDN Feature Detection pattern)
function storageAvailable() {
try {
const storage = window.localStorage;
const testKey = '__storage_test__';
storage.setItem(testKey, testKey);
storage.removeItem(testKey);
return true;
} catch (e) {
return false;
}
}
// Constants
const STORAGE_PREFIX = 'claude_cache_session_';
const INDEX_KEY = 'claude_cache_index';
const ESTIMATED_STORAGE_LIMIT = 5 * 1024 * 1024; // 5MB conservative estimate
// Initialize
document.addEventListener('DOMContentLoaded', function() {
if (!storageAvailable()) {
showStorageError('LocalStorage is not available in your browser. This tool requires localStorage to function.');
return;
}
refreshStatistics();
refreshSessionList();
setupFileInput();
setupStorageEventListener();
});
// MDN Pattern: Storage event listener for cross-tab sync
function setupStorageEventListener() {
window.addEventListener('storage', function(e) {
if (e.key && e.key.startsWith(STORAGE_PREFIX) || e.key === INDEX_KEY) {
console.log('Storage changed in another tab:', e.key);
refreshStatistics();
refreshSessionList();
}
});
}
// Get session index (MDN Pattern: JSON.parse for complex data retrieval)
function getSessionIndex() {
const indexData = localStorage.getItem(INDEX_KEY);
if (!indexData) return [];
try {
return JSON.parse(indexData);
} catch (e) {
console.error('Failed to parse session index:', e);
return [];
}
}
// Save session index (MDN Pattern: JSON.stringify for complex data storage)
function saveSessionIndex(index) {
try {
localStorage.setItem(INDEX_KEY, JSON.stringify(index));
} catch (e) {
console.error('Failed to save session index:', e);
handleStorageError(e);
}
}
// Cache a new session (MDN Pattern: setItem with JSON.stringify)
function cacheSession() {
const sessionId = document.getElementById('sessionId').value.trim() ||
`session-${new Date().toISOString().split('T')[0]}-${Date.now()}`;
const sessionName = document.getElementById('sessionName').value.trim() || 'Untitled Session';
const sessionData = document.getElementById('sessionData').value.trim();
if (!sessionData) {
alert('Please provide session data (JSONL format)');
return;
}
// Parse JSONL
const lines = sessionData.split('\n').filter(line => line.trim());
const messages = [];
for (let line of lines) {
try {
const message = JSON.parse(line);
messages.push(message);
} catch (e) {
console.error('Failed to parse line:', line, e);
}
}
if (messages.length === 0) {
alert('No valid JSON messages found. Please check your JSONL format.');
return;
}
// Create session object
const session = {
id: sessionId,
name: sessionName,
timestamp: new Date().toISOString(),
messages: messages,
messageCount: messages.length,
firstMessage: messages[0]?.timestamp || null,
lastMessage: messages[messages.length - 1]?.timestamp || null
};
// Store session (MDN Pattern: setItem + JSON.stringify)
try {
const storageKey = STORAGE_PREFIX + sessionId;
localStorage.setItem(storageKey, JSON.stringify(session));
// Update index
const index = getSessionIndex();
const existingIndex = index.findIndex(item => item.id === sessionId);
if (existingIndex >= 0) {
index[existingIndex] = {
id: sessionId,
name: sessionName,
timestamp: session.timestamp,
messageCount: messages.length
};
} else {
index.push({
id: sessionId,
name: sessionName,
timestamp: session.timestamp,
messageCount: messages.length
});
}
saveSessionIndex(index);
// Clear form
document.getElementById('sessionId').value = '';
document.getElementById('sessionName').value = '';
document.getElementById('sessionData').value = '';
alert(`Session cached successfully!\nID: ${sessionId}\nMessages: ${messages.length}`);
refreshStatistics();
refreshSessionList();
} catch (e) {
console.error('Failed to cache session:', e);
handleStorageError(e);
}
}
// Load session from file
function loadSessionFile() {
document.getElementById('fileInput').click();
}
function setupFileInput() {
document.getElementById('fileInput').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(event) {
document.getElementById('sessionData').value = event.target.result;
// Auto-populate session ID from filename
const fileName = file.name.replace(/\.(jsonl|json|txt)$/, '');
if (!document.getElementById('sessionId').value) {
document.getElementById('sessionId').value = fileName;
}
};
reader.readAsText(file);
// Reset input
e.target.value = '';
});
}
// Search sessions
function searchSessions() {
const query = document.getElementById('searchQuery').value.toLowerCase().trim();
const resultsDiv = document.getElementById('searchResults');
if (!query) {
resultsDiv.innerHTML = '';
return;
}
const index = getSessionIndex();
const results = [];
for (let item of index) {
// Search in name and ID
if (item.name.toLowerCase().includes(query) ||
item.id.toLowerCase().includes(query)) {
results.push({
session: item,
matchType: 'metadata',
snippet: item.name
});
continue;
}
// Search in message content (MDN Pattern: getItem + JSON.parse)
try {
const sessionData = localStorage.getItem(STORAGE_PREFIX + item.id);
if (sessionData) {
const session = JSON.parse(sessionData);
for (let msg of session.messages) {
const content = JSON.stringify(msg.message?.content || '').toLowerCase();
if (content.includes(query)) {
const snippetStart = Math.max(0, content.indexOf(query) - 50);
const snippetEnd = Math.min(content.length, content.indexOf(query) + 100);
results.push({
session: item,
matchType: 'content',
snippet: '...' + content.slice(snippetStart, snippetEnd) + '...'
});
break; // Only show first match per session
}
}
}
} catch (e) {
console.error('Search error:', e);
}
}
// Render results
if (results.length === 0) {
resultsDiv.innerHTML = '<div class="empty-state"><p>No results found</p></div>';
} else {
resultsDiv.innerHTML = results.map(result => `
<div class="result-item">
<div class="result-header">
<span class="session-id">${escapeHtml(result.session.id)}</span>
<span class="session-date">${new Date(result.session.timestamp).toLocaleString()}</span>
</div>
<div style="margin-bottom: 0.5rem;">
<strong>${escapeHtml(result.session.name)}</strong>
<span class="badge messages">${result.session.messageCount} messages</span>
</div>
<div class="result-content">${highlightQuery(escapeHtml(result.snippet), query)}</div>
</div>
`).join('');
}
}
// Highlight search query in results
function highlightQuery(text, query) {
const regex = new RegExp(`(${escapeRegExp(query)})`, 'gi');
return text.replace(regex, '<span class="highlight">$1</span>');
}
// Refresh session list
function refreshSessionList() {
const index = getSessionIndex();
const listDiv = document.getElementById('sessionsList');
if (index.length === 0) {
listDiv.innerHTML = '<div class="empty-state"><p>No cached sessions yet</p></div>';
return;
}
// Sort by timestamp (newest first)
index.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
listDiv.innerHTML = index.map(item => {
const size = calculateSessionSize(item.id);
return `
<div class="session-item" onclick="viewSession('${escapeHtml(item.id)}')">
<div class="session-header">
<span class="session-id">${escapeHtml(item.id)}</span>
<span class="session-date">${new Date(item.timestamp).toLocaleString()}</span>
</div>
<div style="margin-bottom: 0.5rem;">
<strong>${escapeHtml(item.name)}</strong>
</div>
<div class="session-meta">
<span class="badge messages">${item.messageCount} messages</span>
<span class="badge size">${formatBytes(size)}</span>
</div>
</div>
`;
}).join('');
}
// View session details
function viewSession(sessionId) {
try {
const sessionData = localStorage.getItem(STORAGE_PREFIX + sessionId);
if (!sessionData) {
alert('Session not found');
return;
}
const session = JSON.parse(sessionData);
const detailsDiv = document.getElementById('sessionDetails');
detailsDiv.innerHTML = `
<div style="margin-bottom: 1rem;">
<h3>${escapeHtml(session.name)}</h3>
<div class="session-meta">
<span>ID: <code>${escapeHtml(session.id)}</code></span>
<span>Cached: ${new Date(session.timestamp).toLocaleString()}</span>
<span>Messages: ${session.messageCount}</span>
</div>
</div>
<div class="button-group" style="margin-bottom: 1rem;">
<button class="success" onclick="exportSession('${escapeHtml(session.id)}')">Export Session</button>
<button class="danger" onclick="deleteSession('${escapeHtml(session.id)}')">Delete Session</button>
</div>
<div style="max-height: 400px; overflow-y: auto;">
${session.messages.map((msg, idx) => `
<div class="result-item">
<div class="result-header">
<span>${msg.message?.role || msg.type || 'unknown'}</span>
<span>${msg.timestamp ? new Date(msg.timestamp).toLocaleString() : 'No timestamp'}</span>
</div>
<div class="result-content">${escapeHtml(JSON.stringify(msg.message?.content || msg, null, 2))}</div>
</div>
`).join('')}
</div>
`;
} catch (e) {
console.error('Failed to view session:', e);
alert('Error loading session details');
}
}
// Delete session
function deleteSession(sessionId) {
if (!confirm(`Delete session "${sessionId}"?`)) return;
try {
localStorage.removeItem(STORAGE_PREFIX + sessionId);
const index = getSessionIndex();
const newIndex = index.filter(item => item.id !== sessionId);
saveSessionIndex(newIndex);
document.getElementById('sessionDetails').innerHTML = `
<div class="empty-state">
<p>Session deleted</p>
</div>
`;
refreshStatistics();
refreshSessionList();
} catch (e) {
console.error('Failed to delete session:', e);
alert('Error deleting session');
}
}
// Export session
function exportSession(sessionId) {
try {
const sessionData = localStorage.getItem(STORAGE_PREFIX + sessionId);
if (!sessionData) return;
const blob = new Blob([sessionData], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${sessionId}.json`;
a.click();
URL.revokeObjectURL(url);
} catch (e) {
console.error('Export error:', e);
alert('Error exporting session');
}
}
// Export all sessions
function exportAllSessions() {
try {
const index = getSessionIndex();
const allSessions = {};
for (let item of index) {
const sessionData = localStorage.getItem(STORAGE_PREFIX + item.id);
if (sessionData) {
allSessions[item.id] = JSON.parse(sessionData);
}
}
const exportData = {
exportDate: new Date().toISOString(),
sessionCount: index.length,
sessions: allSessions
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `claude_cache_export_${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
} catch (e) {
console.error('Export error:', e);
alert('Error exporting sessions');
}
}
// Import sessions
function importSessions() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(event) {
try {
const importData = JSON.parse(event.target.result);
const sessions = importData.sessions || {};
const sessionIds = Object.keys(sessions);
if (!confirm(`Import ${sessionIds.length} sessions? This will overwrite existing sessions with the same IDs.`)) {
return;
}
let imported = 0;
const index = getSessionIndex();
for (let sessionId of sessionIds) {
const session = sessions[sessionId];
try {
localStorage.setItem(STORAGE_PREFIX + sessionId, JSON.stringify(session));
// Update index
const existingIndex = index.findIndex(item => item.id === sessionId);
if (existingIndex >= 0) {
index[existingIndex] = {
id: sessionId,
name: session.name,
timestamp: session.timestamp,
messageCount: session.messageCount
};
} else {
index.push({
id: sessionId,
name: session.name,
timestamp: session.timestamp,
messageCount: session.messageCount
});
}
imported++;
} catch (e) {
console.error(`Failed to import session ${sessionId}:`, e);
}
}
saveSessionIndex(index);
alert(`Successfully imported ${imported} sessions`);
refreshStatistics();
refreshSessionList();
} catch (e) {
console.error('Import error:', e);
alert('Error importing sessions. Please check file format.');
}
};
reader.readAsText(file);
};
input.click();
}
// Clear all cache
function clearAllCache() {
if (!confirm('Delete ALL cached sessions? This cannot be undone!')) return;
if (!confirm('Are you absolutely sure? All session data will be lost!')) return;
try {
const index = getSessionIndex();
for (let item of index) {
localStorage.removeItem(STORAGE_PREFIX + item.id);
}
localStorage.removeItem(INDEX_KEY);
alert('Cache cleared successfully');
refreshStatistics();
refreshSessionList();
document.getElementById('sessionDetails').innerHTML = `
<div class="empty-state">
<p>Cache cleared</p>
</div>
`;
} catch (e) {
console.error('Clear cache error:', e);
alert('Error clearing cache');
}
}
// Refresh statistics
function refreshStatistics() {
const index = getSessionIndex();
const totalMessages = index.reduce((sum, item) => sum + item.messageCount, 0);
const totalSize = calculateTotalStorageSize();
const oldestSession = index.length > 0 ?
new Date(Math.min(...index.map(item => new Date(item.timestamp)))).toLocaleDateString() :
'Never';
document.getElementById('statSessions').textContent = index.length;
document.getElementById('statMessages').textContent = totalMessages.toLocaleString();
document.getElementById('statSize').textContent = formatBytes(totalSize);
document.getElementById('statOldest').textContent = oldestSession;
// Update progress bar
const percentage = (totalSize / ESTIMATED_STORAGE_LIMIT) * 100;
document.getElementById('storageProgress').style.width = percentage + '%';
// Show warning if approaching limit
const warningDiv = document.getElementById('storageWarning');
if (percentage > 90) {
warningDiv.innerHTML = '<div class="storage-error">Storage almost full! Please clear some sessions.</div>';
} else if (percentage > 70) {
warningDiv.innerHTML = '<div class="storage-warning">Storage usage is high. Consider exporting and clearing old sessions.</div>';
} else {
warningDiv.innerHTML = '';
}
}
// Calculate total storage size
function calculateTotalStorageSize() {
let totalSize = 0;
const index = getSessionIndex();
for (let item of index) {
totalSize += calculateSessionSize(item.id);
}
return totalSize;
}
// Calculate session size (MDN Pattern: estimate UTF-16 encoding)
function calculateSessionSize(sessionId) {
const sessionData = localStorage.getItem(STORAGE_PREFIX + sessionId);
if (!sessionData) return 0;
// UTF-16 uses 2 bytes per character
return sessionData.length * 2;
}
// Format bytes to human readable
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
}
// Handle storage errors (MDN Pattern: quota exceeded handling)
function handleStorageError(error) {
if (error.name === 'QuotaExceededError' || error.code === 22) {
showStorageError('Storage quota exceeded! Please clear some sessions or export and delete old data.');
} else {
showStorageError('Storage error: ' + error.message);
}
}
function showStorageError(message) {
const warningDiv = document.getElementById('storageWarning');
warningDiv.innerHTML = `<div class="storage-error">${escapeHtml(message)}</div>`;
}
// Utility functions
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
</script>
</body>
</html>