1205 lines
44 KiB
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>
|