import React, { useState, useEffect } from 'react'; import { ScrollArea } from './ui/scroll-area'; import { Button } from './ui/button'; import { Badge } from './ui/badge'; import { Input } from './ui/input'; import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw, Sparkles, Edit2, Star, Search } from 'lucide-react'; import { cn } from '../lib/utils'; import ClaudeLogo from './ClaudeLogo'; import { api } from '../utils/api'; // Move formatTimeAgo outside component to avoid recreation on every render const formatTimeAgo = (dateString, currentTime) => { const date = new Date(dateString); const now = currentTime; // Check if date is valid if (isNaN(date.getTime())) { return 'Unknown'; } const diffInMs = now - date; const diffInSeconds = Math.floor(diffInMs / 1000); const diffInMinutes = Math.floor(diffInMs / (1000 * 60)); const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60)); const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24)); if (diffInSeconds < 60) return 'Just now'; if (diffInMinutes === 1) return '1 min ago'; if (diffInMinutes < 60) return `${diffInMinutes} mins ago`; if (diffInHours === 1) return '1 hour ago'; if (diffInHours < 24) return `${diffInHours} hours ago`; if (diffInDays === 1) return '1 day ago'; if (diffInDays < 7) return `${diffInDays} days ago`; return date.toLocaleDateString(); }; function Sidebar({ projects, selectedProject, selectedSession, onProjectSelect, onSessionSelect, onNewSession, onSessionDelete, onProjectDelete, isLoading, onRefresh, onShowSettings, updateAvailable, latestVersion, currentVersion, onShowVersionModal }) { const [expandedProjects, setExpandedProjects] = useState(new Set()); const [editingProject, setEditingProject] = useState(null); const [showNewProject, setShowNewProject] = useState(false); const [editingName, setEditingName] = useState(''); const [newProjectPath, setNewProjectPath] = useState(''); const [creatingProject, setCreatingProject] = useState(false); const [loadingSessions, setLoadingSessions] = useState({}); const [additionalSessions, setAdditionalSessions] = useState({}); const [initialSessionsLoaded, setInitialSessionsLoaded] = useState(new Set()); const [currentTime, setCurrentTime] = useState(new Date()); const [projectSortOrder, setProjectSortOrder] = useState('name'); const [isRefreshing, setIsRefreshing] = useState(false); const [editingSession, setEditingSession] = useState(null); const [editingSessionName, setEditingSessionName] = useState(''); const [generatingSummary, setGeneratingSummary] = useState({}); const [searchFilter, setSearchFilter] = useState(''); // Starred projects state - persisted in localStorage const [starredProjects, setStarredProjects] = useState(() => { try { const saved = localStorage.getItem('starredProjects'); return saved ? new Set(JSON.parse(saved)) : new Set(); } catch (error) { console.error('Error loading starred projects:', error); return new Set(); } }); // Touch handler to prevent double-tap issues on iPad (only for buttons, not scroll areas) const handleTouchClick = (callback) => { return (e) => { // Only prevent default for buttons/clickable elements, not scrollable areas if (e.target.closest('.overflow-y-auto') || e.target.closest('[data-scroll-container]')) { return; } e.preventDefault(); e.stopPropagation(); callback(); }; }; // Auto-update timestamps every minute useEffect(() => { const timer = setInterval(() => { setCurrentTime(new Date()); }, 60000); // Update every 60 seconds return () => clearInterval(timer); }, []); // Clear additional sessions when projects list changes (e.g., after refresh) useEffect(() => { setAdditionalSessions({}); setInitialSessionsLoaded(new Set()); }, [projects]); // Auto-expand project folder when a session is selected useEffect(() => { if (selectedSession && selectedProject) { setExpandedProjects(prev => new Set([...prev, selectedProject.name])); } }, [selectedSession, selectedProject]); // Mark sessions as loaded when projects come in useEffect(() => { if (projects.length > 0 && !isLoading) { const newLoaded = new Set(); projects.forEach(project => { if (project.sessions && project.sessions.length >= 0) { newLoaded.add(project.name); } }); setInitialSessionsLoaded(newLoaded); } }, [projects, isLoading]); // Load project sort order from settings useEffect(() => { const loadSortOrder = () => { try { const savedSettings = localStorage.getItem('claude-tools-settings'); if (savedSettings) { const settings = JSON.parse(savedSettings); setProjectSortOrder(settings.projectSortOrder || 'name'); } } catch (error) { console.error('Error loading sort order:', error); } }; // Load initially loadSortOrder(); // Listen for storage changes const handleStorageChange = (e) => { if (e.key === 'claude-tools-settings') { loadSortOrder(); } }; window.addEventListener('storage', handleStorageChange); // Also check periodically when component is focused (for same-tab changes) const checkInterval = setInterval(() => { if (document.hasFocus()) { loadSortOrder(); } }, 1000); return () => { window.removeEventListener('storage', handleStorageChange); clearInterval(checkInterval); }; }, []); const toggleProject = (projectName) => { const newExpanded = new Set(expandedProjects); if (newExpanded.has(projectName)) { newExpanded.delete(projectName); } else { newExpanded.add(projectName); } setExpandedProjects(newExpanded); }; // Starred projects utility functions const toggleStarProject = (projectName) => { const newStarred = new Set(starredProjects); if (newStarred.has(projectName)) { newStarred.delete(projectName); } else { newStarred.add(projectName); } setStarredProjects(newStarred); // Persist to localStorage try { localStorage.setItem('starredProjects', JSON.stringify([...newStarred])); } catch (error) { console.error('Error saving starred projects:', error); } }; const isProjectStarred = (projectName) => { return starredProjects.has(projectName); }; // Helper function to get all sessions for a project (initial + additional) const getAllSessions = (project) => { const initialSessions = project.sessions || []; const additional = additionalSessions[project.name] || []; return [...initialSessions, ...additional]; }; // Helper function to get the last activity date for a project const getProjectLastActivity = (project) => { const allSessions = getAllSessions(project); if (allSessions.length === 0) { return new Date(0); // Return epoch date for projects with no sessions } // Find the most recent session activity const mostRecentDate = allSessions.reduce((latest, session) => { const sessionDate = new Date(session.lastActivity); return sessionDate > latest ? sessionDate : latest; }, new Date(0)); return mostRecentDate; }; // Combined sorting: starred projects first, then by selected order const sortedProjects = [...projects].sort((a, b) => { const aStarred = isProjectStarred(a.name); const bStarred = isProjectStarred(b.name); // First, sort by starred status if (aStarred && !bStarred) return -1; if (!aStarred && bStarred) return 1; // For projects with same starred status, sort by selected order if (projectSortOrder === 'date') { // Sort by most recent activity (descending) return getProjectLastActivity(b) - getProjectLastActivity(a); } else { // Sort by display name (user-defined) or fallback to name (ascending) const nameA = a.displayName || a.name; const nameB = b.displayName || b.name; return nameA.localeCompare(nameB); } }); const startEditing = (project) => { setEditingProject(project.name); setEditingName(project.displayName); }; const cancelEditing = () => { setEditingProject(null); setEditingName(''); }; const saveProjectName = async (projectName) => { try { const response = await api.renameProject(projectName, editingName); if (response.ok) { // Refresh projects to get updated data if (window.refreshProjects) { window.refreshProjects(); } else { window.location.reload(); } } else { console.error('Failed to rename project'); } } catch (error) { console.error('Error renaming project:', error); } setEditingProject(null); setEditingName(''); }; const deleteSession = async (projectName, sessionId) => { if (!confirm('Are you sure you want to delete this session? This action cannot be undone.')) { return; } try { const response = await api.deleteSession(projectName, sessionId); if (response.ok) { // Call parent callback if provided if (onSessionDelete) { onSessionDelete(sessionId); } } else { console.error('Failed to delete session'); alert('Failed to delete session. Please try again.'); } } catch (error) { console.error('Error deleting session:', error); alert('Error deleting session. Please try again.'); } }; const deleteProject = async (projectName) => { if (!confirm('Are you sure you want to delete this empty project? This action cannot be undone.')) { return; } try { const response = await api.deleteProject(projectName); if (response.ok) { // Call parent callback if provided if (onProjectDelete) { onProjectDelete(projectName); } } else { const error = await response.json(); console.error('Failed to delete project'); alert(error.error || 'Failed to delete project. Please try again.'); } } catch (error) { console.error('Error deleting project:', error); alert('Error deleting project. Please try again.'); } }; const createNewProject = async () => { if (!newProjectPath.trim()) { alert('Please enter a project path'); return; } setCreatingProject(true); try { const response = await api.createProject(newProjectPath.trim()); if (response.ok) { const result = await response.json(); setShowNewProject(false); setNewProjectPath(''); // Refresh projects to show the new one if (window.refreshProjects) { window.refreshProjects(); } else { window.location.reload(); } } else { const error = await response.json(); alert(error.error || 'Failed to create project. Please try again.'); } } catch (error) { console.error('Error creating project:', error); alert('Error creating project. Please try again.'); } finally { setCreatingProject(false); } }; const cancelNewProject = () => { setShowNewProject(false); setNewProjectPath(''); }; const loadMoreSessions = async (project) => { // Check if we can load more sessions const canLoadMore = project.sessionMeta?.hasMore !== false; if (!canLoadMore || loadingSessions[project.name]) { return; } setLoadingSessions(prev => ({ ...prev, [project.name]: true })); try { const currentSessionCount = (project.sessions?.length || 0) + (additionalSessions[project.name]?.length || 0); const response = await api.sessions(project.name, 5, currentSessionCount); if (response.ok) { const result = await response.json(); // Store additional sessions locally setAdditionalSessions(prev => ({ ...prev, [project.name]: [ ...(prev[project.name] || []), ...result.sessions ] })); // Update project metadata if needed if (result.hasMore === false) { // Mark that there are no more sessions to load project.sessionMeta = { ...project.sessionMeta, hasMore: false }; } } } catch (error) { console.error('Error loading more sessions:', error); } finally { setLoadingSessions(prev => ({ ...prev, [project.name]: false })); } }; // Filter projects based on search input const filteredProjects = sortedProjects.filter(project => { if (!searchFilter.trim()) return true; const searchLower = searchFilter.toLowerCase(); const displayName = (project.displayName || project.name).toLowerCase(); const projectName = project.name.toLowerCase(); // Search in both display name and actual project name/path return displayName.includes(searchLower) || projectName.includes(searchLower); }); return (
AI coding assistant interface
Projects
Fetching your Claude projects and sessions
Run Claude CLI in a project directory to get started
Try adjusting your search term
{(() => { const sessionCount = getAllSessions(project).length; const hasMore = project.sessionMeta?.hasMore !== false; const count = hasMore && sessionCount >= 5 ? `${sessionCount}+` : sessionCount; return `${count} session${count === 1 ? '' : 's'}`; })()}
> )}No sessions yet