diff --git a/server/index.js b/server/index.js index b3c72b3..2f19dcd 100755 --- a/server/index.js +++ b/server/index.js @@ -455,11 +455,12 @@ app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken, } }); -// Delete project endpoint (only if empty) +// Delete project endpoint (force=true to delete with sessions) app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => { try { const { projectName } = req.params; - await deleteProject(projectName); + const force = req.query.force === 'true'; + await deleteProject(projectName, force); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); diff --git a/server/projects.js b/server/projects.js index c6deeac..b4606f8 100755 --- a/server/projects.js +++ b/server/projects.js @@ -1026,25 +1026,56 @@ async function isProjectEmpty(projectName) { } } -// Delete an empty project -async function deleteProject(projectName) { +// Delete a project (force=true to delete even with sessions) +async function deleteProject(projectName, force = false) { const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName); - + try { - // First check if the project is empty const isEmpty = await isProjectEmpty(projectName); - if (!isEmpty) { + if (!isEmpty && !force) { throw new Error('Cannot delete project with existing sessions'); } - - // Remove the project directory - await fs.rm(projectDir, { recursive: true, force: true }); - - // Remove from project config + const config = await loadProjectConfig(); + let projectPath = config[projectName]?.path || config[projectName]?.originalPath; + + // Fallback to extractProjectDirectory if projectPath is not in config + if (!projectPath) { + projectPath = await extractProjectDirectory(projectName); + } + + // Remove the project directory (includes all Claude sessions) + await fs.rm(projectDir, { recursive: true, force: true }); + + // Delete all Codex sessions associated with this project + if (projectPath) { + try { + const codexSessions = await getCodexSessions(projectPath, { limit: 0 }); + for (const session of codexSessions) { + try { + await deleteCodexSession(session.id); + } catch (err) { + console.warn(`Failed to delete Codex session ${session.id}:`, err.message); + } + } + } catch (err) { + console.warn('Failed to delete Codex sessions:', err.message); + } + + // Delete Cursor sessions directory if it exists + try { + const hash = crypto.createHash('md5').update(projectPath).digest('hex'); + const cursorProjectDir = path.join(os.homedir(), '.cursor', 'chats', hash); + await fs.rm(cursorProjectDir, { recursive: true, force: true }); + } catch (err) { + // Cursor dir may not exist, ignore + } + } + + // Remove from project config delete config[projectName]; await saveProjectConfig(config); - + return true; } catch (error) { console.error(`Error deleting project ${projectName}:`, error); @@ -1055,17 +1086,17 @@ async function deleteProject(projectName) { // Add a project manually to the config (without creating folders) async function addProjectManually(projectPath, displayName = null) { const absolutePath = path.resolve(projectPath); - + try { // Check if the path exists await fs.access(absolutePath); } catch (error) { throw new Error(`Path does not exist: ${absolutePath}`); } - + // Generate project name (encode path for use as directory name) const projectName = absolutePath.replace(/\//g, '-'); - + // Check if project already exists in config const config = await loadProjectConfig(); const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName); @@ -1076,13 +1107,13 @@ async function addProjectManually(projectPath, displayName = null) { // Allow adding projects even if the directory exists - this enables tracking // existing Claude Code or Cursor projects in the UI - + // Add to config as manually added project config[projectName] = { manuallyAdded: true, originalPath: absolutePath }; - + if (displayName) { config[projectName].displayName = displayName; } @@ -1214,7 +1245,8 @@ async function getCursorSessions(projectPath) { // Fetch Codex sessions for a given project path -async function getCodexSessions(projectPath) { +async function getCodexSessions(projectPath, options = {}) { + const { limit = 5 } = options; try { const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions'); const sessions = []; @@ -1279,8 +1311,8 @@ async function getCodexSessions(projectPath) { // Sort sessions by last activity (newest first) sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity)); - // Return only the first 5 sessions for performance - return sessions.slice(0, 5); + // Return limited sessions for performance (0 = unlimited for deletion) + return limit > 0 ? sessions.slice(0, limit) : sessions; } catch (error) { console.error('Error fetching Codex sessions:', error); diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 35a56a2..e558a9f 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -6,7 +6,7 @@ import { Badge } from './ui/badge'; import { Input } from './ui/input'; import { useTranslation } from 'react-i18next'; -import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw, Sparkles, Edit2, Star, Search } from 'lucide-react'; +import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw, Sparkles, Edit2, Star, Search, AlertTriangle } from 'lucide-react'; import { cn } from '../lib/utils'; import ClaudeLogo from './ClaudeLogo'; import CursorLogo from './CursorLogo.jsx'; @@ -80,6 +80,9 @@ function Sidebar({ const [editingSessionName, setEditingSessionName] = useState(''); const [generatingSummary, setGeneratingSummary] = useState({}); const [searchFilter, setSearchFilter] = useState(''); + const [deletingProjects, setDeletingProjects] = useState(new Set()); + const [deleteConfirmation, setDeleteConfirmation] = useState(null); // { project, sessionCount } + const [sessionDeleteConfirmation, setSessionDeleteConfirmation] = useState(null); // { projectName, sessionId, sessionTitle, provider } // TaskMaster context const { setCurrentProject, mcpServerStatus } = useTaskMaster(); @@ -306,10 +309,15 @@ function Sidebar({ setEditingName(''); }; - const deleteSession = async (projectName, sessionId, provider = 'claude') => { - if (!confirm(t('messages.deleteSessionConfirm'))) { - return; - } + const showDeleteSessionConfirmation = (projectName, sessionId, sessionTitle, provider = 'claude') => { + setSessionDeleteConfirmation({ projectName, sessionId, sessionTitle, provider }); + }; + + const confirmDeleteSession = async () => { + if (!sessionDeleteConfirmation) return; + + const { projectName, sessionId, provider } = sessionDeleteConfirmation; + setSessionDeleteConfirmation(null); try { console.log('[Sidebar] Deleting session:', { projectName, sessionId, provider }); @@ -343,18 +351,26 @@ function Sidebar({ } }; - const deleteProject = async (projectName) => { - if (!confirm(t('messages.deleteProjectConfirm'))) { - return; - } + const deleteProject = (project) => { + const sessionCount = getAllSessions(project).length; + setDeleteConfirmation({ project, sessionCount }); + }; + + const confirmDeleteProject = async () => { + if (!deleteConfirmation) return; + + const { project, sessionCount } = deleteConfirmation; + const isEmpty = sessionCount === 0; + + setDeleteConfirmation(null); + setDeletingProjects(prev => new Set([...prev, project.name])); try { - const response = await api.deleteProject(projectName); + const response = await api.deleteProject(project.name, !isEmpty); if (response.ok) { - // Call parent callback if provided if (onProjectDelete) { - onProjectDelete(projectName); + onProjectDelete(project.name); } } else { const error = await response.json(); @@ -364,6 +380,12 @@ function Sidebar({ } catch (error) { console.error('Error deleting project:', error); alert(t('messages.deleteProjectError')); + } finally { + setDeletingProjects(prev => { + const next = new Set(prev); + next.delete(project.name); + return next; + }); } }; @@ -488,6 +510,110 @@ function Sidebar({ document.body )} + {/* Delete Confirmation Modal */} + {deleteConfirmation && ReactDOM.createPortal( +
+ {t('deleteConfirmation.confirmDelete')}{' '} + + {deleteConfirmation.project.displayName || deleteConfirmation.project.name} + ? +
+ {deleteConfirmation.sessionCount > 0 && ( ++ {t('deleteConfirmation.sessionCount', { count: deleteConfirmation.sessionCount })} +
++ {t('deleteConfirmation.allConversationsDeleted')} +
++ {t('deleteConfirmation.cannotUndo')} +
++ {t('deleteConfirmation.confirmDelete')}{' '} + + {sessionDeleteConfirmation.sessionTitle || t('sessions.unnamed')} + ? +
++ {t('deleteConfirmation.cannotUndo')} +
+