import React, { useState, useEffect, useMemo } from "react"; import type { SessionEntry } from "../lib/types"; import { ChevronRight, ChevronLeft, ChatBubble } from "./Icons"; interface Props { sessions: SessionEntry[]; loading: boolean; selectedId?: string; onSelect: (id: string) => void; } export function SessionList({ sessions, loading, selectedId, onSelect }: Props) { const [selectedProject, setSelectedProject] = useState(null); // Group by project (memoized to avoid recomputing on unrelated rerenders) const grouped = useMemo(() => { const map = new Map(); for (const session of sessions) { const group = map.get(session.project) || []; group.push(session); map.set(session.project, group); } return map; }, [sessions]); // Auto-select project when selectedId changes useEffect(() => { if (selectedId) { const match = sessions.find((s) => s.id === selectedId); if (match) { setSelectedProject(match.project); } } }, [selectedId, sessions]); if (loading) { return (
{[...Array(5)].map((_, i) => (
))}
); } if (sessions.length === 0) { return (

No sessions found

Sessions will appear here once created

); } // Session list for selected project if (selectedProject !== null) { const projectSessions = grouped.get(selectedProject) || []; return (
{truncateProjectName(selectedProject)}
{projectSessions.map((session, idx) => { const isSelected = selectedId === session.id; return ( ); })}
); } // Project list return (
{[...grouped.entries()].map(([project, projectSessions]) => { const latest = projectSessions.reduce((a, b) => (a.modified || a.created) > (b.modified || b.created) ? a : b ); const count = projectSessions.length; const totalMessages = projectSessions.reduce((sum, s) => sum + s.messageCount, 0); return ( ); })}
); } /** * Best-effort decode of Claude Code's project directory name back to a path. */ function formatProjectName(project: string): string { if (project.startsWith("-")) { return project.replace(/^-/, "/").replace(/-/g, "/"); } return project; } /** Show last 2 path segments for compact display. */ function truncateProjectName(project: string): string { const full = formatProjectName(project); const segments = full.split("/").filter(Boolean); if (segments.length <= 2) return full; return segments.slice(-2).join("/"); } function formatSessionDuration(ms: number): string { const minutes = Math.floor(ms / 60000); if (minutes < 1) return "<1m"; if (minutes < 60) return `${minutes}m`; const hours = Math.floor(minutes / 60); const rem = minutes % 60; if (rem === 0) return `${hours}h`; return `${hours}h ${rem}m`; } function formatRelativeTime(dateStr: string): string { if (!dateStr) return ""; const d = new Date(dateStr); if (isNaN(d.getTime())) return dateStr; const now = Date.now(); const diffMs = now - d.getTime(); if (diffMs < 0) return "just now"; if (diffMs < 60_000) return "just now"; if (diffMs < 3_600_000) { const mins = Math.floor(diffMs / 60_000); return `${mins}m ago`; } if (diffMs < 86_400_000) { const hours = Math.floor(diffMs / 3_600_000); return `${hours}h ago`; } if (diffMs < 604_800_000) { const days = Math.floor(diffMs / 86_400_000); return `${days}d ago`; } return d.toLocaleDateString(undefined, { month: "short", day: "numeric" }); }