"use client"; import { useCallback, useMemo, useState } from "react"; import { UnicodeSpinner } from "../unicode-spinner"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "../ui/dropdown-menu"; type WebSession = { id: string; title: string; createdAt: number; updatedAt: number; messageCount: number; }; export type SidebarSubagentInfo = { childSessionKey: string; runId: string; task: string; label?: string; parentSessionId: string; status?: "running" | "completed" | "error"; }; type ChatSessionsSidebarProps = { sessions: WebSession[]; activeSessionId: string | null; /** Title of the currently active session (shown in the header). */ activeSessionTitle?: string; /** Session IDs with an actively running agent stream. */ streamingSessionIds?: Set; /** Subagents spawned by chat sessions. */ subagents?: SidebarSubagentInfo[]; /** Currently selected subagent session key (if viewing a subagent). */ activeSubagentKey?: string | null; onSelectSession: (sessionId: string) => void; onNewSession: () => void; /** Called when a subagent is selected in the sidebar. */ onSelectSubagent?: (sessionKey: string) => void; /** When true, renders as a mobile overlay drawer instead of a static sidebar. */ mobile?: boolean; /** Close the mobile drawer. */ onClose?: () => void; /** Fixed width in px when not mobile (overrides default 260). */ width?: number; /** Called when the user deletes a session from the sidebar menu. */ onDeleteSession?: (sessionId: string) => void; /** Called when the user renames a session from the sidebar menu. */ onRenameSession?: (sessionId: string, newTitle: string) => void; /** Called when the user clicks the collapse/hide sidebar button. */ onCollapse?: () => void; /** When true, show a loader instead of empty state (e.g. initial sessions fetch). */ loading?: boolean; }; /** Format a timestamp into a human-readable relative time string. */ function timeAgo(ts: number): string { const now = Date.now(); const diff = now - ts; const seconds = Math.floor(diff / 1000); if (seconds < 60) {return "just now";} const minutes = Math.floor(seconds / 60); if (minutes < 60) {return `${minutes}m ago`;} const hours = Math.floor(minutes / 60); if (hours < 24) {return `${hours}h ago`;} const days = Math.floor(hours / 24); if (days < 30) {return `${days}d ago`;} const months = Math.floor(days / 30); if (months < 12) {return `${months}mo ago`;} return `${Math.floor(months / 12)}y ago`; } function PlusIcon() { return ( ); } function SubagentIcon() { return ( ); } function ChatBubbleIcon() { return ( ); } function MoreHorizontalIcon() { return ( ); } export function ChatSessionsSidebar({ sessions, activeSessionId, activeSessionTitle: _activeSessionTitle, streamingSessionIds, subagents, activeSubagentKey, onSelectSession, onNewSession, onSelectSubagent, onDeleteSession, onRenameSession, onCollapse, mobile, onClose, width: widthProp, loading = false, }: ChatSessionsSidebarProps) { const [hoveredId, setHoveredId] = useState(null); const [renamingId, setRenamingId] = useState(null); const [renameValue, setRenameValue] = useState(""); const handleSelect = useCallback( (id: string) => { onSelectSession(id); onClose?.(); }, [onSelectSession, onClose], ); const handleSelectSubagentItem = useCallback( (sessionKey: string) => { onSelectSubagent?.(sessionKey); onClose?.(); }, [onSelectSubagent, onClose], ); const handleDeleteSession = useCallback( (sessionId: string) => { onDeleteSession?.(sessionId); }, [onDeleteSession], ); const handleStartRename = useCallback((sessionId: string, currentTitle: string) => { setRenamingId(sessionId); setRenameValue(currentTitle || ""); }, []); const handleCommitRename = useCallback(() => { if (renamingId && renameValue.trim()) { onRenameSession?.(renamingId, renameValue.trim()); } setRenamingId(null); setRenameValue(""); }, [renamingId, renameValue, onRenameSession]); // Index subagents by parent session ID const subagentsByParent = useMemo(() => { const map = new Map(); if (!subagents) {return map;} for (const sa of subagents) { let list = map.get(sa.parentSessionId); if (!list) { list = []; map.set(sa.parentSessionId, list); } list.push(sa); } return map; }, [subagents]); // Group sessions: today, yesterday, this week, this month, older const grouped = groupSessions(sessions); const width = mobile ? "280px" : (widthProp ?? 260); const headerHeight = 40; // px — match padding so list content clears the overlay const sidebar = ( ); if (!mobile) { return sidebar; } return (
void onClose?.()}> {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
e.stopPropagation()} className="fixed inset-y-0 right-0 z-50"> {sidebar}
); } // ── Grouping helpers ── type SessionGroup = { label: string; sessions: WebSession[]; }; function groupSessions(sessions: WebSession[]): SessionGroup[] { const now = new Date(); const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); const yesterdayStart = todayStart - 86400000; const weekStart = todayStart - 7 * 86400000; const monthStart = todayStart - 30 * 86400000; const today: WebSession[] = []; const yesterday: WebSession[] = []; const thisWeek: WebSession[] = []; const thisMonth: WebSession[] = []; const older: WebSession[] = []; for (const s of sessions) { const t = s.updatedAt; if (t >= todayStart) {today.push(s);} else if (t >= yesterdayStart) {yesterday.push(s);} else if (t >= weekStart) {thisWeek.push(s);} else if (t >= monthStart) {thisMonth.push(s);} else {older.push(s);} } const groups: SessionGroup[] = []; if (today.length > 0) {groups.push({ label: "Today", sessions: today });} if (yesterday.length > 0) {groups.push({ label: "Yesterday", sessions: yesterday });} if (thisWeek.length > 0) {groups.push({ label: "This Week", sessions: thisWeek });} if (thisMonth.length > 0) {groups.push({ label: "This Month", sessions: thisMonth });} if (older.length > 0) {groups.push({ label: "Older", sessions: older });} return groups; }