import React, { useEffect, useMemo, useState } from "react"; import { FolderOpen, FileClock, X, Search, ChevronRight, ChevronDown, HardDrive } from "lucide-react"; import { C, FD, FM } from "../theme.js"; import { fetchSessions } from "../useAnalysis.js"; // Multi-session browser. Walks the REAL ~/.claude/projects via the local engine // API (cwd read from inside each file, never the lossy folder name — NN#5) and // lets the user open ANY session, not just the bundled POC. Deepest-folder-wins // grouping is by the true cwd the engine reports. function fmtBytes(n) { if (!n) return "0B"; if (n >= 1e6) return (n / 1e6).toFixed(1) + "MB"; if (n >= 1e3) return Math.round(n / 1e3) + "KB"; return n + "B"; } function fmtAge(sec) { if (!sec) return ""; const d = Date.now() / 1000 - sec; if (d < 3600) return Math.max(1, Math.round(d / 60)) + "m ago"; if (d < 86400) return Math.round(d / 3600) + "h ago"; return Math.round(d / 86400) + "d ago"; } // Real session START datetime — the whole point of Shripal's ask: "couldn't tell // which session was which." Renders local like "Jun 04, 21:30". Accepts an ISO // string (session.startedAt, read from inside the file) OR epoch-ms (the mtime // fallback). Invalid/empty input renders nothing rather than "Invalid Date". const _MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; function fmtDateTime(iso_or_ms) { if (!iso_or_ms) return ""; const dt = new Date(iso_or_ms); if (isNaN(dt.getTime())) return ""; const mon = _MONTHS[dt.getMonth()]; const day = String(dt.getDate()).padStart(2, "0"); const hh = String(dt.getHours()).padStart(2, "0"); const mm = String(dt.getMinutes()).padStart(2, "0"); return `${mon} ${day}, ${hh}:${mm}`; } export default function SessionBrowser({ current, onPick, onOpenProject, onClose }) { const [state, setState] = useState({ status: "loading", data: null, error: null }); const [open, setOpen] = useState({}); const [q, setQ] = useState(""); useEffect(() => { let alive = true; (async () => { try { const data = await fetchSessions(); if (alive) { setState({ status: "ready", data, error: null }); // auto-expand the project containing the current session if (data.projects && data.projects.length) { const first = {}; data.projects.slice(0, 1).forEach((p) => (first[p.cwd] = true)); setOpen(first); } } } catch (e) { if (alive) setState({ status: "error", data: null, error: String(e) }); } })(); return () => { alive = false; }; }, []); const projects = useMemo(() => { const ps = state.data?.projects || []; if (!q.trim()) return ps; const needle = q.toLowerCase(); return ps .map((p) => ({ ...p, sessions: p.sessions.filter( (s) => p.cwd.toLowerCase().includes(needle) || (s.sessionId || "").toLowerCase().includes(needle) ), })) .filter((p) => p.cwd.toLowerCase().includes(needle) || p.sessions.length); }, [state.data, q]); return (