claude-sesh
Version:
Session explorer for Claude Code - find, search, and resume all your past sessions
1,799 lines (1,594 loc) • 135 kB
JavaScript
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