'use client' import * as React from 'react' import { AlertCircle, AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Clock, Inbox, TerminalSquare, } from 'lucide-react' import { cn } from '../../lib/utils' import { SessionCard, SessionCardSkeleton, formatRelative } from './SessionCard' import { resolveSessionStatus } from './status' import type { SessionStatusKind, SessionSummary } from './types' export interface SessionListProps { sessions: SessionSummary[] isLoading?: boolean error?: string | null /** Grid of cards (default) or a compact table of rows. */ layout?: 'grid' | 'table' /** In `table` layout, group rows under collapsible project headers. */ groupByProject?: boolean /** Slot rendered above the list (filters/search — the app owns them). */ renderFilters?: React.ReactNode /** Inject routing for a card/row (grid: card title/Details become links). */ href?: (id: string) => string onSessionClick?: (session: SessionSummary) => void onOpenTerminal?: (id: string) => void /** Message shown when there are no sessions. */ emptyLabel?: string /** Number of skeletons while loading. Default 6. */ skeletonCount?: number className?: string } const STATUS_ICON: Record> = { active: TerminalSquare, success: CheckCircle2, completed: CheckCircle2, error: AlertCircle, warning: AlertTriangle, idle: Clock, } interface ProjectGroup { project: string sessions: SessionSummary[] } /** Group + sort sessions by project name for the grouped table layout. */ function groupByProjectName(sessions: SessionSummary[]): ProjectGroup[] { const groups = new Map() for (const session of sessions) { const key = session.projectName ?? 'Ungrouped' if (!groups.has(key)) groups.set(key, { project: key, sessions: [] }) groups.get(key)!.sessions.push(session) } return Array.from(groups.values()) } function SessionRow({ session, onSessionClick, onOpenTerminal, }: { session: SessionSummary onSessionClick?: (s: SessionSummary) => void onOpenTerminal?: (id: string) => void }) { const status = resolveSessionStatus(session) const Icon = STATUS_ICON[status.kind] return ( ) } function EmptyState({ label }: { label: string }) { return (

{label}

) } /** * A PURELY presentational list of sessions. It does NO data fetching and issues NO * per-row queries — status is injected on each `SessionSummary`. Renders a card * grid (`layout="grid"`) or a compact table (`layout="table"`, optionally grouped * by project). The app owns filters (pass them via `renderFilters`). */ export function SessionList({ sessions, isLoading = false, error = null, layout = 'grid', groupByProject = false, renderFilters, href, onSessionClick, onOpenTerminal, emptyLabel = 'No sessions yet', skeletonCount = 6, className, }: SessionListProps) { const [collapsed, setCollapsed] = React.useState>(new Set()) const groups = React.useMemo( () => (layout === 'table' && groupByProject ? groupByProjectName(sessions) : []), [layout, groupByProject, sessions], ) const toggle = (project: string) => setCollapsed((prev) => { const next = new Set(prev) if (next.has(project)) next.delete(project) else next.add(project) return next }) let body: React.ReactNode if (isLoading) { body = (
{Array.from({ length: skeletonCount }).map((_, i) => ( ))}
) } else if (error) { body = (

Failed to load sessions

{error}

) } else if (sessions.length === 0) { body = } else if (layout === 'table') { if (groupByProject) { body = (
{groups.map((group) => { const isCollapsed = collapsed.has(group.project) const activeCount = group.sessions.filter((s) => s.isActive).length return (
{!isCollapsed && (
{group.sessions.map((session) => ( ))}
)}
) })}
) } else { body = (
{sessions.map((session) => ( ))}
) } } else { body = (
{sessions.map((session) => ( onSessionClick(session) : undefined} onOpenTerminal={onOpenTerminal} /> ))}
) } return (
{renderFilters} {body}
) }