UNPKG

claude-sesh

Version:

Session explorer for Claude Code - find, search, and resume all your past sessions

1,799 lines (1,594 loc) 135 kB
import express from 'express'; import { parser, formatModelName } from '../services/parser.js'; import { enricher } from '../services/enricher.js'; import { cardsService } from '../services/cards.js'; import { bookmarksService } from '../services/bookmarks.js'; import { LeaderboardService } from '../services/leaderboard.js'; const leaderboardService = new LeaderboardService(parser); export function startServer(port = 3847) { const app = express(); // Enable JSON body parsing app.use(express.json()); // Serve static dashboard app.get('/', (_req, res) => { res.send(getDashboardHTML()); }); // API endpoints app.get('/api/sessions', (req, res) => { const limit = parseInt(req.query.limit, 10) || 50; const project = req.query.project; const date = req.query.date; const search = req.query.search; let sessions; if (date) { sessions = parser.getSessionsByDate(date); } else if (project) { sessions = parser.getProjectSessions(project, limit); } else { sessions = parser.getAllSessions(limit); } // Search filter (includes user tags and notes from bookmarks) if (search) { const q = search.toLowerCase(); sessions = sessions.filter(s => { const bookmark = bookmarksService.getBookmark(s.id); const userTags = bookmark?.tags || []; const userNote = bookmark?.note || ''; return s.projectName.toLowerCase().includes(q) || s.summaries.some(sum => sum.toLowerCase().includes(q)) || s.filesWritten.some(f => f.toLowerCase().includes(q)) || s.filesEdited.some(f => f.toLowerCase().includes(q)) || userTags.some(t => t.toLowerCase().includes(q)) || userNote.toLowerCase().includes(q); }); } // Return lightweight session data for list view (exclude toolCalls which can be huge) const formatted = sessions.map(s => { const enrichedData = enricher.getEnrichedData(s.id); const bookmark = bookmarksService.getBookmark(s.id); return { id: s.id, projectPath: s.projectPath, projectName: s.projectName, startedAt: s.startedAt, endedAt: s.endedAt, durationSeconds: s.durationSeconds, model: s.model, messageCount: s.messageCount, toolCallCount: s.toolCallCount, totalTokens: s.totalTokens, estimatedCostUsd: s.estimatedCostUsd, summaries: s.summaries, filesWritten: s.filesWritten?.slice(0, 5), // Limit to first 5 filesEdited: s.filesEdited?.slice(0, 5), // Limit to first 5 errors: s.errors?.slice(0, 3), // Limit to first 3 // Formatted fields modelFormatted: formatModelName(s.model), durationFormatted: formatDuration(s.durationSeconds), costFormatted: `$${s.estimatedCostUsd.toFixed(2)}`, tokensFormatted: formatTokens(s.totalTokens), startedAtFormatted: s.startedAt.toLocaleString(), dateKey: s.startedAt.toISOString().split('T')[0], timeFormatted: s.startedAt.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }), enriched: enrichedData, // Bookmark data starred: bookmark?.starred || false, userTags: bookmark?.tags || [], userNote: bookmark?.note || '', }; }); res.json(formatted); }); app.get('/api/sessions/:id', (req, res) => { const session = parser.getSession(req.params.id); if (!session) { res.status(404).json({ error: 'Session not found' }); return; } res.json({ ...session, modelFormatted: formatModelName(session.model), durationFormatted: formatDuration(session.durationSeconds), costFormatted: `$${session.estimatedCostUsd.toFixed(2)}`, tokensFormatted: formatTokens(session.totalTokens), }); }); app.get('/api/sessions/:id/resume', (req, res) => { const context = parser.buildResumeContext(req.params.id); if (!context) { res.status(404).json({ error: 'Session not found' }); return; } res.json({ context }); }); app.get('/api/projects', (_req, res) => { const projects = parser.getProjects(); const projectData = projects.map(p => { const sessions = parser.getProjectSessions(p, 100); return { path: p, name: p.split('/').pop(), sessionCount: sessions.length, totalTokens: sessions.reduce((sum, s) => sum + s.totalTokens, 0), totalCost: sessions.reduce((sum, s) => sum + s.estimatedCostUsd, 0), lastActive: sessions[0]?.startedAt || null, }; }); res.json(projectData); }); app.get('/api/stats', (_req, res) => { const sessions = parser.getAllSessions(500); const stats = calculateStats(sessions); res.json(stats); }); // NEW: Heatmap endpoint app.get('/api/heatmap', (req, res) => { const weeks = parseInt(req.query.weeks, 10) || 52; const heatmapData = parser.getHeatmapData(weeks); res.json(heatmapData); }); // NEW: Timeline endpoint app.get('/api/timeline', (req, res) => { const days = parseInt(req.query.days, 10) || 30; const timelineData = parser.getTimelineData(days); res.json(timelineData); }); // NEW: Leaderboard endpoint app.get('/api/leaderboard/:type', (req, res) => { const type = req.params.type; const limit = parseInt(req.query.limit, 10) || 10; const leaderboard = parser.getLeaderboard(type, limit); res.json(leaderboard); }); // NEW: Resumable sessions endpoint app.get('/api/resumable', (req, res) => { const limit = parseInt(req.query.limit, 10) || 10; const resumable = parser.getResumableSessions(limit); const formatted = resumable.map(s => ({ id: s.id, projectName: s.projectName, projectPath: s.projectPath, startedAt: s.startedAt, durationSeconds: s.durationSeconds, messageCount: s.messageCount, resumeReason: s.resumeReason, resumeScore: s.resumeScore, model: s.model, modelFormatted: formatModelName(s.model), durationFormatted: formatDuration(s.durationSeconds), timeAgo: formatTimeAgo(s.startedAt), })); res.json(formatted); }); // NEW: Daily activity endpoint app.get('/api/activity', (_req, res) => { const dailyActivity = parser.getDailyActivity(); res.json(dailyActivity); }); // NEW: Plans endpoint app.get('/api/plans', (_req, res) => { const plans = parser.getPlanFiles(); res.json(plans.map(p => ({ name: p.name, path: p.path, size: p.size, createdAt: p.createdAt, modifiedAt: p.modifiedAt, preview: p.content.slice(0, 500), }))); }); // NEW: Enrichment endpoints app.get('/api/enrich/stats', (_req, res) => { const stats = enricher.getStats(); res.json({ ...stats, available: enricher.isAvailable() }); }); app.get('/api/enrich/:sessionId', (req, res) => { const enriched = enricher.getEnrichedData(req.params.sessionId); if (!enriched) { res.status(404).json({ error: 'No enriched data found' }); return; } res.json(enriched); }); app.post('/api/enrich/:sessionId', async (req, res) => { try { const enriched = await enricher.enrichSession(req.params.sessionId); if (!enriched) { res.status(400).json({ error: 'Failed to enrich session' }); return; } res.json(enriched); } catch (error) { res.status(500).json({ error: 'Enrichment failed', details: String(error) }); } }); app.get('/api/enriched', (_req, res) => { const all = enricher.getAllEnriched(); res.json(all); }); // Stats Cards API app.get('/api/cards', (req, res) => { const period = req.query.period || 'month'; const cards = cardsService.getAllCards(period); res.json(cards); }); app.get('/api/cards/summary', (req, res) => { const period = req.query.period || 'month'; res.json(cardsService.getSummaryCard(period)); }); app.get('/api/cards/languages', (req, res) => { const period = req.query.period || 'month'; res.json(cardsService.getLanguagesCard(period)); }); app.get('/api/cards/rhythm', (req, res) => { const period = req.query.period || 'month'; res.json(cardsService.getRhythmCard(period)); }); app.get('/api/cards/achievement', (_req, res) => { res.json(cardsService.getAchievementCard()); }); app.get('/api/cards/wrapped', (req, res) => { const period = req.query.period || 'month'; res.json(cardsService.getWrappedCard(period)); }); // Bookmarks API app.get('/api/bookmarks', (_req, res) => { res.json(bookmarksService.getAllBookmarks()); }); app.get('/api/bookmarks/starred', (_req, res) => { res.json(bookmarksService.getStarredSessions()); }); app.get('/api/bookmarks/tags', (_req, res) => { res.json(bookmarksService.getAllTags()); }); app.get('/api/bookmarks/:sessionId', (req, res) => { const bookmark = bookmarksService.getBookmark(req.params.sessionId); res.json(bookmark || { sessionId: req.params.sessionId, starred: false, tags: [], note: '' }); }); app.post('/api/bookmarks/:sessionId/star', (req, res) => { const starred = bookmarksService.toggleStar(req.params.sessionId); res.json({ starred }); }); app.post('/api/bookmarks/:sessionId/tags', (req, res) => { const { tag } = req.body; if (!tag) { res.status(400).json({ error: 'Tag is required' }); return; } const tags = bookmarksService.addTag(req.params.sessionId, tag); res.json({ tags }); }); app.delete('/api/bookmarks/:sessionId/tags/:tag', (req, res) => { const tags = bookmarksService.removeTag(req.params.sessionId, req.params.tag); res.json({ tags }); }); app.post('/api/bookmarks/:sessionId/note', (req, res) => { const { note } = req.body; bookmarksService.setNote(req.params.sessionId, note || ''); res.json({ note: bookmarksService.getNote(req.params.sessionId) }); }); // Global Leaderboard API app.get('/api/global-leaderboard', (req, res) => { const category = req.query.category || 'tokens'; const leaderboard = leaderboardService.getLeaderboard(category); const userStats = leaderboardService.getUserStats(); const submission = leaderboardService.getSubmission(); const achievement = leaderboardService.getAchievementTitle(); res.json({ ...leaderboard, userStats, submission, achievement, percentile: leaderboardService.getPercentile(category) }); }); app.post('/api/global-leaderboard/submit', (req, res) => { const { displayName } = req.body; const submission = leaderboardService.submitStats(displayName || 'anonymous'); res.json({ success: true, submission }); }); app.post('/api/global-leaderboard/remove', (_req, res) => { leaderboardService.removeSubmission(); res.json({ success: true }); }); app.listen(port, () => { console.log(` 🌐 Dashboard running at http://localhost:${port}`); console.log(); // Pre-warm the session cache in background setImmediate(() => { const start = Date.now(); const sessions = parser.getAllSessions(50); console.log(` ⚡ Cache warmed: ${sessions.length} sessions loaded in ${Date.now() - start}ms`); }); }); } function calculateStats(sessions) { const today = new Date().toDateString(); let totalTokens = 0; let totalCost = 0; let totalDuration = 0; let sessionsToday = 0; let tokensToday = 0; const modelMap = new Map(); for (const session of sessions) { totalTokens += session.totalTokens; totalCost += session.estimatedCostUsd; totalDuration += session.durationSeconds; if (session.startedAt.toDateString() === today) { sessionsToday++; tokensToday += session.totalTokens; } const model = formatModelName(session.model); const existing = modelMap.get(model) || { sessions: 0, tokens: 0 }; existing.sessions++; existing.tokens += session.totalTokens; modelMap.set(model, existing); } return { totalSessions: sessions.length, totalTokens, totalTokensFormatted: formatTokens(totalTokens), totalCost, totalCostFormatted: `$${totalCost.toFixed(2)}`, totalDuration, totalDurationFormatted: formatDuration(totalDuration), averageSessionDuration: sessions.length > 0 ? totalDuration / sessions.length : 0, averageDurationFormatted: formatDuration(sessions.length > 0 ? totalDuration / sessions.length : 0), sessionsToday, tokensToday, tokensTodayFormatted: formatTokens(tokensToday), modelBreakdown: Array.from(modelMap.entries()) .filter(([m]) => m && m !== 'Unknown') .map(([model, data]) => ({ model, sessions: data.sessions, tokens: data.tokens, tokensFormatted: formatTokens(data.tokens), })) .sort((a, b) => b.tokens - a.tokens), }; } function formatTimeAgo(date) { const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return 'Just now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays === 1) return 'Yesterday'; if (diffDays < 7) return `${diffDays} days ago`; if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`; return date.toLocaleDateString(); } function formatDuration(seconds) { if (seconds < 60) return `${Math.round(seconds)}s`; if (seconds < 3600) return `${Math.floor(seconds / 60)}m`; const hours = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); return `${hours}h ${mins}m`; } function formatTokens(tokens) { if (tokens < 1000) return tokens.toString(); if (tokens < 1000000) return `${(tokens / 1000).toFixed(1)}K`; return `${(tokens / 1000000).toFixed(2)}M`; } function getDashboardHTML() { return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>claude-sesh</title> <style> :root { --bg: #F0EEE6; --bg-card: #FDFCF9; --bg-hover: #E8E6DE; --text: #141413; --text-secondary: #57534E; --text-muted: #A8A29E; --accent: #C6613F; --accent-light: #D97757; --accent-soft: #FBE0DD; --border: #D6D3CB; --success: #059669; --radius: 12px; --radius-sm: 8px; } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--text); line-height: 1.5; min-height: 100vh; } .app { max-width: 1400px; margin: 0 auto; padding: 32px 24px; } /* Header */ header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 32px; } .logo { display: flex; align-items: center; gap: 14px; } .logo-mark { width: 42px; height: 42px; background: var(--text); border-radius: 10px; display: flex; align-items: center; justify-content: center; font-family: 'SF Mono', 'Monaco', 'Menlo', monospace; font-size: 16px; font-weight: 600; color: var(--bg); letter-spacing: -1px; } .logo h1 { font-size: 24px; font-weight: 600; letter-spacing: -0.5px; } /* Navigation */ nav { display: flex; gap: 4px; background: var(--bg-card); padding: 4px; border-radius: var(--radius); border: 1px solid var(--border); } nav button { padding: 10px 20px; border: none; background: transparent; border-radius: var(--radius-sm); font-size: 14px; font-weight: 500; color: var(--text-secondary); cursor: pointer; transition: all 0.2s; } nav button:hover { background: var(--bg-hover); color: var(--text); } nav button.active { background: var(--accent); color: white; } /* Views */ .view { display: none; } .view.active { display: block; } /* Stats Grid */ .stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 32px; } @media (max-width: 900px) { .stats { grid-template-columns: repeat(2, 1fr); } } .stat { background: var(--bg-card); border-radius: var(--radius); padding: 20px; border: 1px solid var(--border); } .stat-label { font-size: 12px; font-weight: 500; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; } .stat-value { font-size: 28px; font-weight: 600; letter-spacing: -0.5px; } .stat-sub { font-size: 13px; color: var(--text-secondary); margin-top: 2px; } .stat.highlight .stat-value { color: var(--accent); } .stat.success .stat-value { color: var(--success); } /* Heatmap */ .heatmap-container { background: var(--bg-card); border-radius: var(--radius); padding: 24px; border: 1px solid var(--border); margin-bottom: 32px; overflow-x: auto; } .heatmap-container.full-width { padding: 32px; } .heatmap-title { font-size: 16px; font-weight: 600; margin-bottom: 16px; display: flex; align-items: center; justify-content: space-between; } .heatmap { display: flex; gap: 4px; } .heatmap.large { gap: 5px; } .heatmap-week { display: flex; flex-direction: column; gap: 4px; } .heatmap.large .heatmap-week { gap: 5px; } .heatmap-cell { width: 14px; height: 14px; border-radius: 3px; background: var(--bg-hover); cursor: pointer; transition: transform 0.1s, box-shadow 0.1s; } .heatmap.large .heatmap-cell { width: 18px; height: 18px; border-radius: 4px; } .heatmap-cell:hover { transform: scale(1.2); box-shadow: 0 2px 8px rgba(0,0,0,0.15); } .heatmap-cell.level-1 { background: #FBE0DD; } .heatmap-cell.level-2 { background: #F5A89A; } .heatmap-cell.level-3 { background: #D97757; } .heatmap-cell.level-4 { background: #C6613F; } .heatmap-legend { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-muted); } .heatmap-legend span { display: flex; align-items: center; gap: 4px; } .heatmap-days { display: flex; flex-direction: column; gap: 4px; margin-right: 10px; font-size: 11px; color: var(--text-muted); } .heatmap-days.large { gap: 5px; font-size: 12px; } .heatmap-day { height: 14px; display: flex; align-items: center; } .heatmap-days.large .heatmap-day { height: 18px; } /* Tooltip */ .tooltip { position: fixed; background: var(--text); color: white; padding: 8px 12px; border-radius: 6px; font-size: 12px; pointer-events: none; z-index: 1000; display: none; max-width: 250px; } .tooltip.show { display: block; } /* Section */ .section { margin-bottom: 32px; } .section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; flex-wrap: wrap; gap: 8px; } .section-title { font-size: 18px; font-weight: 600; } .section-subtitle { font-size: 13px; color: var(--text-muted); } /* Continue Working Section */ .continue-section { background: linear-gradient(135deg, var(--bg-card) 0%, rgba(207, 135, 77, 0.05) 100%); padding: 20px; border-radius: var(--radius-lg); border: 1px solid var(--border); } .continue-cards { display: flex; gap: 16px; overflow-x: auto; padding-bottom: 8px; } .continue-card { min-width: 280px; max-width: 320px; background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; cursor: pointer; transition: all 0.2s; position: relative; } .continue-card:hover { border-color: var(--accent); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.08); } .continue-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px; } .continue-card-project { font-weight: 600; font-size: 14px; color: var(--text); } .continue-card-badge { font-size: 11px; padding: 2px 8px; border-radius: 12px; background: var(--accent); color: white; white-space: nowrap; } .continue-card-reason { font-size: 13px; color: var(--text-muted); margin-bottom: 8px; } .continue-card-meta { font-size: 12px; color: var(--text-light); display: flex; gap: 12px; } .continue-card-actions { display: flex; gap: 8px; margin-top: 12px; } .continue-btn { flex: 1; padding: 8px 12px; border-radius: var(--radius); font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.2s; border: 1px solid var(--border); background: var(--bg); color: var(--text); } .continue-btn:hover { background: var(--bg-hover); } .continue-btn.primary { background: var(--accent); color: white; border-color: var(--accent); } .continue-btn.primary:hover { background: var(--accent-dark); } /* Search & Filters */ .search-bar { display: flex; gap: 12px; margin-bottom: 24px; flex-wrap: wrap; } .search-input { flex: 1; min-width: 250px; padding: 12px 16px; padding-left: 44px; border: 1px solid var(--border); border-radius: var(--radius); font-size: 14px; background: var(--bg-card) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23A8A29E'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'/%3E%3C/svg%3E") 14px center/20px no-repeat; transition: all 0.2s; } .search-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } .date-picker { padding: 12px 16px; border: 1px solid var(--border); border-radius: var(--radius); font-size: 14px; background: var(--bg-card); cursor: pointer; } .date-picker:focus { outline: none; border-color: var(--accent); } /* Sessions Container with Day Groups */ .sessions-container { display: flex; gap: 24px; } .date-nav { position: sticky; top: 24px; align-self: flex-start; width: 160px; display: flex; flex-direction: column; gap: 4px; } .date-nav-item { padding: 10px 14px; font-size: 13px; color: var(--text-secondary); border-radius: var(--radius-sm); cursor: pointer; transition: all 0.15s; white-space: nowrap; } .date-nav-item:hover { background: var(--bg-hover); color: var(--text); } .date-nav-item.active { background: var(--accent); color: white; font-weight: 600; } .date-nav-item .count { opacity: 0.7; font-size: 11px; margin-left: 4px; } .sessions-main { flex: 1; min-width: 0; } /* Day Group */ .day-group { margin-bottom: 32px; } .day-header { display: flex; align-items: center; gap: 12px; padding: 12px 0; position: sticky; top: 0; background: var(--bg); z-index: 10; border-bottom: 1px solid var(--border); margin-bottom: 12px; } .day-date { font-size: 14px; font-weight: 600; color: var(--text); } .day-count { font-size: 12px; color: var(--text-muted); background: var(--bg-hover); padding: 2px 8px; border-radius: 10px; } /* Sessions */ .sessions { display: flex; flex-direction: column; gap: 16px; } .session { background: var(--bg-card); border-radius: 16px; padding: 0; border: 1px solid var(--border); cursor: pointer; transition: all 0.2s ease; overflow: hidden; position: relative; } .session::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--accent); opacity: 0; transition: opacity 0.2s ease; } .session:hover { border-color: var(--accent); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1); transform: translateY(-2px); } .session:hover::before { opacity: 1; } .session-inner { padding: 20px; } .session-header { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 14px; } .session-avatar { width: 44px; height: 44px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 16px; flex-shrink: 0; color: white; text-transform: uppercase; background: var(--accent); } .session-avatar.model-opus { background: #7C3AED; } .session-avatar.model-sonnet { background: var(--accent); } .session-avatar.model-haiku { background: #10B981; } .session-main { flex: 1; min-width: 0; } .session-title-row { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; } .session-name { font-size: 16px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text); } .session-meta { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } .session-time { font-size: 13px; color: var(--text-muted); display: flex; align-items: center; gap: 4px; } .session-time::before { content: '🕐'; font-size: 11px; } .session-duration { font-size: 13px; color: var(--text-secondary); display: flex; align-items: center; gap: 4px; } .session-duration::before { content: '⏱️'; font-size: 11px; } .session-actions { display: flex; gap: 8px; flex-shrink: 0; opacity: 0; transition: opacity 0.15s ease; } .session:hover .session-actions { opacity: 1; } .session-action-btn { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 6px 10px; font-size: 11px; font-weight: 600; cursor: pointer; transition: all 0.15s ease; display: flex; align-items: center; gap: 4px; } .session-action-btn:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); } .session-badges { display: flex; gap: 8px; flex-shrink: 0; align-items: center; } .session-model { background: var(--accent-soft); color: var(--accent); padding: 5px 12px; border-radius: 20px; font-size: 11px; font-weight: 600; } .session-model.model-opus { background: #EDE9FE; color: #7C3AED; } .session-model.model-haiku { background: #D1FAE5; color: #059669; } .session-sentiment { padding: 5px 12px; border-radius: 20px; font-size: 11px; font-weight: 600; } .sentiment-productive { background: #D1FAE5; color: #059669; } .sentiment-challenging { background: #FEE2E2; color: #DC2626; } .sentiment-exploratory { background: #DBEAFE; color: #2563EB; } .sentiment-maintenance { background: #F3F4F6; color: #6B7280; } /* Star/Bookmark button */ .session-star { background: none; border: none; cursor: pointer; font-size: 20px; padding: 4px; margin: -4px; opacity: 0.3; transition: all 0.15s ease; line-height: 1; } .session-star:hover { opacity: 1; transform: scale(1.15); } .session-star.starred { opacity: 1; } .session:hover .session-star { opacity: 0.6; } .session:hover .session-star.starred { opacity: 1; } /* Summary section */ .session-summary { font-size: 14px; color: var(--text-secondary); line-height: 1.7; margin-bottom: 14px; padding: 12px 14px; background: var(--bg); border-radius: 10px; border-left: 3px solid var(--border); } .session-summary.has-enriched { color: var(--text); border-left-color: var(--accent); background: var(--accent-soft); } /* Tags section */ .session-tags-section { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; } .session-user-tag { background: var(--accent); color: white; padding: 4px 12px; border-radius: 14px; font-size: 11px; font-weight: 600; } .session-tag { background: var(--bg); color: var(--text-secondary); padding: 4px 12px; border-radius: 14px; font-size: 11px; font-weight: 500; border: 1px solid var(--border); } /* Note indicator */ .session-note-indicator { font-size: 12px; color: var(--text-muted); display: flex; align-items: center; gap: 6px; margin-bottom: 14px; padding: 8px 12px; background: #FFFBEB; border-radius: 8px; border: 1px solid #FDE68A; } .session-note-indicator::before { content: '📝'; } /* Stats grid */ .session-stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 14px; } .session-stat-card { background: var(--bg); border-radius: 10px; padding: 12px; text-align: center; } .session-stat-icon { font-size: 18px; margin-bottom: 4px; } .session-stat-value { font-size: 16px; font-weight: 700; color: var(--text); } .session-stat-label { font-size: 10px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; } .session-stat-card.highlight { background: var(--accent-soft); } .session-stat-card.highlight .session-stat-value { color: var(--accent); } /* Files section */ .session-files-section { display: flex; gap: 6px; flex-wrap: wrap; padding-top: 14px; border-top: 1px solid var(--border); } .session-file { display: flex; align-items: center; gap: 4px; padding: 4px 10px; background: var(--bg); border-radius: 6px; font-size: 11px; color: var(--text-secondary); font-family: 'SF Mono', Monaco, monospace; } .session-file.written { border-left: 2px solid #10B981; } .session-file.edited { border-left: 2px solid #3B82F6; } .session-files-more { padding: 4px 10px; font-size: 11px; color: var(--text-muted); } /* Legacy support */ .session-footer { display: none; } .session-stats { display: none; } .session-tags { display: none; } .session-user-tags { display: none; } @media (max-width: 600px) { .session-stats-grid { grid-template-columns: repeat(2, 1fr); } .session-actions { opacity: 1; } } @media (max-width: 900px) { .sessions-container { flex-direction: column; } .date-nav { display: none; } } /* Leaderboard */ .leaderboard-tabs { display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; } .leaderboard-tabs button { padding: 8px 16px; border: 1px solid var(--border); background: var(--bg-card); border-radius: var(--radius-sm); font-size: 13px; font-weight: 500; color: var(--text-secondary); cursor: pointer; transition: all 0.2s; } .leaderboard-tabs button:hover { border-color: var(--accent); color: var(--text); } .leaderboard-tabs button.active { background: var(--accent); color: white; border-color: var(--accent); } .leaderboard-table { background: var(--bg-card); border-radius: var(--radius); border: 1px solid var(--border); overflow: hidden; } .leaderboard-row { display: grid; grid-template-columns: 50px 1fr 120px 100px; padding: 14px 20px; align-items: center; border-bottom: 1px solid var(--border); } .leaderboard-row:last-child { border-bottom: none; } .leaderboard-row.header { background: var(--bg-hover); font-weight: 600; font-size: 12px; color: var(--text-muted); text-transform: uppercase; } .leaderboard-rank { font-size: 16px; font-weight: 600; } .rank-1 { color: #FFD700; } .rank-2 { color: #C0C0C0; } .rank-3 { color: #CD7F32; } .leaderboard-name { font-weight: 500; } .leaderboard-value { font-weight: 600; color: var(--accent); text-align: right; } .leaderboard-secondary { font-size: 13px; color: var(--text-muted); text-align: right; } /* Leaderboard Mode Toggle */ .leaderboard-mode-toggle { display: flex; gap: 4px; padding: 4px; background: var(--bg); border-radius: var(--radius-sm); border: 1px solid var(--border); } .mode-btn { padding: 8px 16px; border: none; background: transparent; border-radius: 6px; font-size: 13px; font-weight: 500; color: var(--text-muted); cursor: pointer; transition: all 0.2s; } .mode-btn:hover { color: var(--text); } .mode-btn.active { background: var(--bg-card); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,0.08); } .leaderboard-mode { display: none; } .leaderboard-mode.active { display: block; } /* Global Leaderboard */ .global-user-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 24px; margin-bottom: 24px; } .global-user-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; } .global-user-title { display: flex; align-items: center; gap: 12px; } .global-user-achievement { font-size: 32px; } .global-user-name { font-size: 18px; font-weight: 600; } .global-user-badge { font-size: 13px; color: var(--text-muted); } .global-user-actions { display: flex; gap: 8px; } .global-submit-btn { padding: 10px 20px; background: var(--accent); color: white; border: none; border-radius: var(--radius-sm); font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.2s; } .global-submit-btn:hover { background: var(--accent-light); } .global-remove-btn { padding: 10px 16px; background: transparent; color: var(--text-muted); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: 13px; cursor: pointer; } .global-remove-btn:hover { border-color: #dc2626; color: #dc2626; } .global-user-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 16px; } .global-stat { text-align: center; padding: 12px; background: var(--bg); border-radius: var(--radius-sm); } .global-stat-value { font-size: 24px; font-weight: 700; color: var(--accent); } .global-stat-label { font-size: 11px; text-transform: uppercase; color: var(--text-muted); letter-spacing: 0.5px; } .global-rank-badge { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; background: var(--accent-soft); color: var(--accent); border-radius: 20px; font-size: 13px; font-weight: 600; } .global-leaderboard-row { display: grid; grid-template-columns: 60px 1fr 120px 100px; gap: 16px; align-items: center; padding: 14px 16px; border-bottom: 1px solid var(--border); } .global-leaderboard-row:last-child { border-bottom: none; } .global-leaderboard-row.header { background: var(--bg-hover); font-weight: 600; font-size: 12px; color: var(--text-muted); text-transform: uppercase; padding: 10px 16px; } .global-leaderboard-row.is-user { background: var(--accent-soft); } .global-rank { font-weight: 700; font-size: 16px; } .global-rank.rank-1 { color: #FFD700; } .global-rank.rank-2 { color: #C0C0C0; } .global-rank.rank-3 { color: #CD7F32; } .global-player { display: flex; align-items: center; gap: 10px; } .global-player-name { font-weight: 500; } .global-player-badge { font-size: 11px; padding: 2px 8px; background: var(--accent); color: white; border-radius: 10px; } .submit-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; opacity: 0; visibility: hidden; transition: all 0.2s; } .submit-modal.visible { opacity: 1; visibility: visible; } .submit-modal-content { background: var(--bg-card); border-radius: var(--radius); padding: 32px; max-width: 400px; width: 90%; } .submit-modal h3 { font-size: 18px; margin-bottom: 8px; } .submit-modal p { color: var(--text-muted); font-size: 14px; margin-bottom: 20px; } .submit-modal input { width: 100%; padding: 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: 14px; margin-bottom: 16px; } .submit-modal-actions { display: flex; gap: 8px; justify-content: flex-end; } /* Timeline */ .timeline-container { background: var(--bg-card); border-radius: var(--radius); border: 1px solid var(--border); padding: 24px; overflow-x: auto; } .timeline { display: flex; flex-direction: column; gap: 16px; min-width: 800px; } .timeline-row { display: flex; align-items: center; gap: 16px; } .timeline-project { width: 150px; font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .timeline-track { flex: 1; height: 28px; background: var(--bg-hover); border-radius: 4px; position: relative; } .timeline-session { position: absolute; height: 100%; border-radius: 4px; cursor: pointer; transition: opacity 0.2s; min-width: 4px; } .timeline-session:hover { opacity: 0.8; } .timeline-session.opus { background: #8B5A2B; } .timeline-session.sonnet { background: var(--accent); } .timeline-session.haiku { background: #A0826D; } .timeline-legend { display: flex; gap: 20px; margin-top: 16px; font-size: 12px; color: var(--text-muted); } .timeline-legend-item { display: flex; align-items: center; gap: 6px; } .timeline-legend-color { width: 12px; height: 12px; border-radius: 3px; } /* Modal */ .modal-backdrop { display: none; position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(6px); z-index: 100; align-items: center; justify-content: center; padding: 24px; } .modal-backdrop.open { display: flex; } .modal { background: var(--bg-card); border-radius: 16px; width: 100%; max-width: 800px; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2); } .modal-header { padding: 24px 28px 0; display: flex; flex-direction: column; gap: 16px; } .modal-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; } .modal-title-section { flex: 1; } .modal-title { font-size: 20px; font-weight: 600; margin-bottom: 4px; } .modal-subtitle { font-size: 13px; color: var(--text-muted); } .modal-close { width: 36px; height: 36px; border: none; background: var(--bg-hover); border-radius: 10px; cursor: pointer; font-size: 20px; color: var(--text-secondary); display: flex; align-items: center; justify-content: center; transition: all 0.2s; flex-shrink: 0; } .modal-close:hover { background: var(--border); color: var(--text); } .modal-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin: 0 -28px; padding: 0 28px; } .modal-tab { padding: 14px 20px; border: none; background: transparent; font-size: 14px; font-weight: 500; color: var(--text-muted); cursor: pointer; position: relative; transition: color 0.2s; } .modal-tab:hover { color: var(--text); } .modal-tab.active { color: var(--accent); font-weight: 600; } .modal-tab.active::after { content: ''; position: absolute; bottom: -1px; left: 0; right: 0; height: 2px; background: var(--accent); border-radius: 1px 1px 0 0; } .modal-body { padding: 24px 28px; overflow-y: auto; flex: 1; } .modal-panel { display: none; } .modal-panel.active { display: block; } /* Modal Summary Panel */ .summary-hero { background: linear-gradient(135deg, var(--accent-soft), #fff); border-radius: var(--radius); padding: 20px; margin-bottom: 20px; } .summary-text { font-size: 16px; line-height: 1.6; color: var(--text); margin-bottom: 12px; } .summary-sentiment { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: 20px; font-size: 12px; font-weight: 500; } .detail-section { margin-bottom: 24px; } .detail-section:last-child { margin-bottom: 0; } .detail-label { font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; } .detail-label-icon { font-size: 14px; } .detail-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } @media (max-width: 600px) { .detail-grid { grid-template-columns: repeat(2, 1fr); } } .detail-item { background: var(--bg); padding: 14px; border-radius: var(--radius-sm); transition: all 0.15s; } .detail-item:hover { background: var(--bg-hover); } .detail-item-label { font-size: 11px; color: var(--text-muted); margin-bottom: 4px; } .detail-item-value { font-size: 17px; font-weight: 600; } /* Lists */ .insight-list { display: flex; flex-direction: column; gap: 8px; } .insight-item { display: flex; gap: 10px; padding: 12px 14px; background: var(--bg); border-radius: var(--radius-sm); font-size: 13px; line-height: 1.5; } .insight-icon { font-size: 14px; flex-shrink: 0; margin-top: 2px; } .problem-item { background: var(--bg); border-radius: var(--radius-sm); padding: 14px; margin-bottom: 8px; } .problem-issue { font-size: 13px; font-weight: 500; color: var(--text); margin-bottom: 6px; display: flex; align-items: flex-start; gap: 8px; } .problem-solution { font-size: 12px; color: var(--text-secondary); padding-left: 22px; } .files-list { display: flex; flex-direction: column; gap: 6px; } .file-item { font-family: 'SF Mono', Monaco, monospace; font-size: 12px; color: var(--text-secondary); padding: 10px 14px; background: var(--bg); border-radius: 8px; word-break: break-all; display: flex; align-items: center; gap: 8px; } .file-item-icon { color: var(--text-muted); } /* Tools Panel */ .tools-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; } .tool-item { background: var(--bg); padding: 12px; border-radius: var(--radius-sm); text-align: center; } .tool-name { font-size: 12px; font-weight: 600; margin-bottom: 4px; } .tool-count { font-size: 18px; font-weight: 700; color: var(--accent); } /* Resume Panel */ .resume-box { background: var(--bg); border-radius: var(--radius); padding: 20px; font-family: 'SF Mono', Monaco, monospace; font-size: 12px; white-space: pre-wrap; color: var(--text-secondary); line-height: 1.6; max-height: 400px; overflow-y: auto; border: 1px solid var(--border); } .btn { padding: 12px 20px; background: var(--accent); color: white; border: none; border-radius: var(--radius-sm); font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; display: inline-flex; align-items: center; gap: 8px; } .btn:hover { background: #A8522F; transform: translateY(-1px); } .btn-secondary { background: var(--bg-hover); color: var(--text); } .btn-secondary:hover { background: var(--border); } .btn-group { display: flex; gap: 10px; margin-top: 16px; } .loading { display: flex; align-items: center; justify-content: center; padding: 48px; color: var(--text-muted); gap: 10px; } @keyframes spin { to { transform: rotate(360deg); } } .spinner { width: 18px; height: 18px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.7s linear infinite; } .empty { text-align: center; padding: 48px 20px; color: var(--text-muted); } .empty-icon { font-size: 40px; margin-bottom: 12px; } /* Command Palette */ .cmd-palette-backdrop { display: none; position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(6px); z-index: 200; align-items: flex-start; justify-content: center; padding: 80px 24px 24px; } .cmd-palette-backdrop.open { display: flex; } .cmd-palette { background: var(--bg-card); border-radius: 16px; width: 100%; max-width: 640px; max-height: 70vh; overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); border: 1px solid var(--border); } .cmd-palette-input-wrapper { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px; } .cmd-palette-icon { font-size: 20px; color: var(--text-muted); } .cmd-palette-input { flex: 1; border: none; background: transparent; font-size: 16px; color: var(--text); outline: none; } .cmd-palette-input::placeholder { color: var(--text-muted); } .cmd-palette-shortcut { font-size: 12px; color: var(--text-muted); padding: 4px 8px; background: var(--bg); border-radius: 6px; font-family: monospace; } .cmd-palette-results { flex: 1