import { useMemo, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useDeleteSession, useImportZip, useSessions } from '../../hooks/useSession'; import type { SessionSummary, SessionHealth } from '../../types'; import { SessionCard } from './SessionCard'; import { SessionFilter } from './SessionFilter'; export type SessionSortKey = 'recent' | 'oldest' | 'most_records' | 'most_subagents'; export type HealthFilter = 'all' | SessionHealth; export type SourceFilter = 'all' | 'local' | 'imported'; function workspaceKey(s: SessionSummary): string { if (!s.workDir) return '(no workspace)'; return s.workDir.split('/').slice(-2).join('/'); } function sortSessions(sessions: readonly SessionSummary[], key: SessionSortKey): SessionSummary[] { switch (key) { case 'recent': return sessions.toSorted((a, b) => b.updatedAt - a.updatedAt); case 'oldest': return sessions.toSorted((a, b) => a.createdAt - b.createdAt); case 'most_records': return sessions.toSorted((a, b) => b.mainWireRecordCount - a.mainWireRecordCount); case 'most_subagents': return sessions.toSorted((a, b) => (b.agentCount - 1) - (a.agentCount - 1)); } } export function SessionRail() { const { data, isLoading, error } = useSessions(); const deleteSession = useDeleteSession(); const importZip = useImportZip(); const navigate = useNavigate(); const { sessionId } = useParams<{ sessionId: string }>(); const [search, setSearch] = useState(''); const [sortKey, setSortKey] = useState('recent'); const [healthFilter, setHealthFilter] = useState('all'); const [sourceFilter, setSourceFilter] = useState('all'); const filtered = useMemo(() => { if (!data) return []; const q = search.trim().toLowerCase(); return data.filter((s) => { if (healthFilter !== 'all' && s.health !== healthFilter) return false; if (sourceFilter === 'local' && s.imported) return false; if (sourceFilter === 'imported' && !s.imported) return false; if (!q) return true; const hay = [ s.sessionId, s.title ?? '', s.lastPrompt ?? '', s.workDir ?? '', s.importMeta?.originalName ?? '', ] .join(' ') .toLowerCase(); return hay.includes(q); }); }, [data, search, healthFilter, sourceFilter]); const importedCount = useMemo(() => (data ?? []).filter((s) => s.imported).length, [data]); async function handleImport(file: File) { try { const result = await importZip.mutateAsync(file); void navigate(`/sessions/${result.sessionId}`); } catch (importError) { window.alert(`Import failed: ${importError instanceof Error ? importError.message : String(importError)}`); } } const grouped = useMemo(() => { if (sortKey !== 'recent') return null; const map = new Map(); for (const s of filtered) { const k = workspaceKey(s); const existing = map.get(k); if (existing === undefined) { map.set(k, [s]); } else { existing.push(s); } } return [...map.entries()] .map(([group, items]) => { const sorted = items.toSorted((a, b) => b.updatedAt - a.updatedAt); return [group, sorted] as const; }) .toSorted(([, a], [, b]) => { const ua = a[0]?.updatedAt ?? 0; const ub = b[0]?.updatedAt ?? 0; return ub - ua; }); }, [filtered, sortKey]); const flat = useMemo( () => (grouped === null ? sortSessions(filtered, sortKey) : null), [filtered, sortKey, grouped], ); async function handleDeleteSession(session: SessionSummary) { const label = session.title ?? session.lastPrompt ?? session.sessionId; if (!window.confirm(`Delete session "${label}"?\n\nThis removes its files from KIMI_CODE_HOME.`)) { return; } try { await deleteSession.mutateAsync(session.sessionId); if (sessionId === session.sessionId) { void navigate('/'); } } catch (deleteError) { window.alert(deleteError instanceof Error ? deleteError.message : String(deleteError)); } } return ( ); }