# Project and Session Organization Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Implement persistent projects as first-class entities containing multiple sessions, with intelligent auto-assignment, manual reassignment via context menu, and soft-delete recycle bin. **Architecture:** Introduce a `projects` collection in MongoDB that stores project metadata (name, description, icon, color, path, session IDs). Sessions reference projects via `projectId`. Soft delete implemented with `deletedAt` timestamp. API endpoints provide CRUD operations, smart suggestions based on directory/recency/name matching. Frontend adds dedicated projects page, context menu for session reassignment, and recycle bin interface. **Tech Stack:** MongoDB (database), Express.js (API), Vanilla JavaScript (frontend), CSS (styling), xterm.js (existing terminal) --- ## Task 1: Database Setup - Create Projects Collection **Files:** - Modify: `server.js:39-45` (after claudeService initialization) **Step 1: Add projects collection initialization** Find the section where collections are created (around line 39-45). Add projects collection initialization. ```javascript // Initialize collections const db = client.db(); const sessionsCollection = db.collection('sessions'); const projectsCollection = db.collection('projects'); // ADD THIS LINE // Create indexes for better query performance await projectsCollection.createIndex({ deletedAt: 1 }); await projectsCollection.createIndex({ name: 1 }); ``` **Step 2: Commit** ```bash git add server.js git commit -m "feat: add projects collection initialization and indexes" ``` --- ## Task 2: Create Project API Endpoints **Files:** - Modify: `server.js` (add after sessions routes, around line 900) **Step 1: Add GET /api/projects endpoint** ```javascript // GET /api/projects - List all active projects app.get('/api/projects', requireAuth, async (req, res) => { try { const projects = await projectsCollection.find({ deletedAt: null }).sort({ lastActivity: -1 }).toArray(); res.json({ success: true, projects: projects.map(p => ({ id: p._id, name: p.name, description: p.description, icon: p.icon, color: p.color, path: p.path, sessionCount: p.sessionIds?.length || 0, createdAt: p.createdAt, lastActivity: p.lastActivity })) }); } catch (error) { console.error('Error fetching projects:', error); res.status(500).json({ error: 'Failed to fetch projects' }); } }); ``` **Step 2: Add POST /api/projects endpoint** ```javascript // POST /api/projects - Create new project app.post('/api/projects', requireAuth, async (req, res) => { try { const { name, path, description, icon, color } = req.body; // Validate required fields if (!name || !path) { return res.status(400).json({ error: 'Name and path are required' }); } // Check if project with same name already exists const existing = await projectsCollection.findOne({ name, deletedAt: null }); if (existing) { return res.status(409).json({ error: 'Project with this name already exists' }); } const project = { name, path, description: description || '', icon: icon || 'šŸ“', color: color || '#4a9eff', sessionIds: [], createdAt: new Date(), lastActivity: new Date(), deletedAt: null }; const result = await projectsCollection.insertOne(project); res.status(201).json({ success: true, project: { id: result.insertedId, ...project } }); } catch (error) { console.error('Error creating project:', error); res.status(500).json({ error: 'Failed to create project' }); } }); ``` **Step 3: Add PUT /api/projects/:id endpoint** ```javascript // PUT /api/projects/:id - Update project app.put('/api/projects/:id', requireAuth, async (req, res) => { try { const { id } = req.params; const { name, description, icon, color, path } = req.body; const update = {}; if (name) update.name = name; if (description !== undefined) update.description = description; if (icon) update.icon = icon; if (color) update.color = color; if (path) update.path = path; const result = await projectsCollection.findOneAndUpdate( { _id: new ObjectId(id), deletedAt: null }, { $set: update }, { returnDocument: 'after' } ); if (!result) { return res.status(404).json({ error: 'Project not found' }); } res.json({ success: true, project: { id: result._id, name: result.name, description: result.description, icon: result.icon, color: result.color, path: result.path, sessionCount: result.sessionIds?.length || 0, createdAt: result.createdAt, lastActivity: result.lastActivity } }); } catch (error) { console.error('Error updating project:', error); res.status(500).json({ error: 'Failed to update project' }); } }); ``` **Step 4: Commit** ```bash git add server.js git commit -m "feat: add project CRUD API endpoints" ``` --- ## Task 3: Soft Delete and Recycle Bin Endpoints **Files:** - Modify: `server.js` (after project CRUD endpoints) **Step 1: Add DELETE /api/projects/:id (soft delete)** ```javascript // DELETE /api/projects/:id - Soft delete project app.delete('/api/projects/:id', requireAuth, async (req, res) => { try { const { id } = req.params; // Get project to soft delete its sessions too const project = await projectsCollection.findOne({ _id: new ObjectId(id), deletedAt: null }); if (!project) { return res.status(404).json({ error: 'Project not found' }); } // Soft delete project await projectsCollection.updateOne( { _id: new ObjectId(id) }, { $set: { deletedAt: new Date() } } ); // Soft delete all sessions in this project await sessionsCollection.updateMany( { projectId: new ObjectId(id) }, { $set: { deletedAt: new Date() } } ); res.json({ success: true }); } catch (error) { console.error('Error deleting project:', error); res.status(500).json({ error: 'Failed to delete project' }); } }); ``` **Step 2: Add POST /api/projects/:id/restore** ```javascript // POST /api/projects/:id/restore - Restore from recycle bin app.post('/api/projects/:id/restore', requireAuth, async (req, res) => { try { const { id } = req.params; // Restore project const project = await projectsCollection.findOneAndUpdate( { _id: new ObjectId(id), deletedAt: { $ne: null } }, { $set: { deletedAt: null } }, { returnDocument: 'after' } ); if (!project) { return res.status(404).json({ error: 'Project not found in recycle bin' }); } // Restore all sessions in this project await sessionsCollection.updateMany( { projectId: new ObjectId(id) }, { $set: { deletedAt: null } } ); res.json({ success: true }); } catch (error) { console.error('Error restoring project:', error); res.status(500).json({ error: 'Failed to restore project' }); } }); ``` **Step 3: Add DELETE /api/projects/:id/permanent** ```javascript // DELETE /api/projects/:id/permanent - Permanent delete app.delete('/api/projects/:id/permanent', requireAuth, async (req, res) => { try { const { id } = req.params; // Permanently delete sessions await sessionsCollection.deleteMany({ projectId: new ObjectId(id) }); // Permanently delete project await projectsCollection.deleteOne({ _id: new ObjectId(id) }); res.json({ success: true }); } catch (error) { console.error('Error permanently deleting project:', error); res.status(500).json({ error: 'Failed to permanently delete project' }); } }); ``` **Step 4: Add GET /api/recycle-bin** ```javascript // GET /api/recycle-bin - List deleted items app.get('/api/recycle-bin', requireAuth, async (req, res) => { try { const deletedProjects = await projectsCollection.find({ deletedAt: { $ne: null } }).sort({ deletedAt: -1 }).toArray(); const result = await Promise.all(deletedProjects.map(async (project) => { // Get session count for this project const sessionCount = await sessionsCollection.countDocuments({ projectId: project._id, deletedAt: { $ne: null } }); return { id: project._id, name: project.name, description: project.description, icon: project.icon, path: project.path, sessionCount, deletedAt: project.deletedAt }; })); res.json({ success: true, items: result }); } catch (error) { console.error('Error fetching recycle bin:', error); res.status(500).json({ error: 'Failed to fetch recycle bin' }); } }); ``` **Step 5: Commit** ```bash git add server.js git commit -m "feat: add soft delete, restore, permanent delete, and recycle bin endpoints" ``` --- ## Task 4: Session Reassignment Endpoint **Files:** - Modify: `server.js` (after recycle bin endpoint) **Step 1: Add POST /api/sessions/:id/move endpoint** ```javascript // POST /api/sessions/:id/move - Move session to different project app.post('/api/sessions/:id/move', requireAuth, async (req, res) => { try { const { id } = req.params; const { projectId } = req.body; // null for unassigned // Verify session exists const session = await sessionsCollection.findOne({ _id: new ObjectId(id), deletedAt: null }); if (!session) { return res.status(404).json({ error: 'Session not found' }); } // If moving to a project, verify it exists and is not deleted if (projectId) { const project = await projectsCollection.findOne({ _id: new ObjectId(projectId), deletedAt: null }); if (!project) { return res.status(404).json({ error: 'Project not found' }); } // Remove session from old project's sessionIds if (session.projectId) { await projectsCollection.updateOne( { _id: session.projectId }, { $pull: { sessionIds: new ObjectId(id) } } ); } // Add session to new project's sessionIds await projectsCollection.updateOne( { _id: new ObjectId(projectId) }, { $push: { sessionIds: new ObjectId(id) } } ); } else { // Moving to unassigned - remove from old project if (session.projectId) { await projectsCollection.updateOne( { _id: session.projectId }, { $pull: { sessionIds: new ObjectId(id) } } ); } } // Update session's projectId await sessionsCollection.updateOne( { _id: new ObjectId(id) }, { $set: { projectId: projectId ? new ObjectId(projectId) : null } } ); res.json({ success: true }); } catch (error) { console.error('Error moving session:', error); res.status(500).json({ error: 'Failed to move session' }); } }); ``` **Step 2: Commit** ```bash git add server.js git commit -m "feat: add session move endpoint" ``` --- ## Task 5: Smart Suggestions Endpoint **Files:** - Modify: `server.js` (after session move endpoint) **Step 1: Add GET /api/projects/suggestions endpoint** ```javascript // GET /api/projects/suggestions?sessionId=xxx - Get project suggestions for a session app.get('/api/projects/suggestions', requireAuth, async (req, res) => { try { const { sessionId } = req.query; if (!sessionId) { return res.status(400).json({ error: 'sessionId is required' }); } // Get session const session = await sessionsCollection.findOne({ _id: new ObjectId(sessionId), deletedAt: null }); if (!session) { return res.status(404).json({ error: 'Session not found' }); } // Get all active projects const projects = await projectsCollection.find({ deletedAt: null }).toArray(); // Calculate suggestions const suggestions = []; for (const project of projects) { let score = 0; const reasons = []; // Directory matching (high weight) if (session.workingDir === project.path) { score += 90; reasons.push('Same directory'); } else if (session.workingDir?.startsWith(project.path)) { score += 50; reasons.push('Subdirectory'); } // Recency (medium weight) const daysSinceActivity = (Date.now() - project.lastActivity) / (1000 * 60 * 60 * 24); if (daysSinceActivity < 1) { score += 20; reasons.push('Used today'); } else if (daysSinceActivity < 7) { score += 10; reasons.push(`Used ${Math.floor(daysSinceActivity)} days ago`); } // Name similarity (low weight) if (session.name?.includes(project.name) || project.name.includes(session.name)) { score += 15; reasons.push('Similar name'); } if (score > 0) { suggestions.push({ id: project._id, name: project.name, icon: project.icon, color: project.color, score, reasons }); } } // Sort by score and take top 3 const topSuggestions = suggestions .sort((a, b) => b.score - a.score) .slice(0, 3); // Get all projects for "show all" option const allProjects = projects.map(p => ({ id: p._id, name: p.name, icon: p.icon, color: p.color })).sort((a, b) => a.name.localeCompare(b.name)); res.json({ success: true, suggestions: topSuggestions, allProjects }); } catch (error) { console.error('Error getting suggestions:', error); res.status(500).json({ error: 'Failed to get suggestions' }); } }); ``` **Step 2: Commit** ```bash git add server.js git commit -m "feat: add smart project suggestions endpoint" ``` --- ## Task 6: Projects Page HTML **Files:** - Create: `public/projects.html` **Step 1: Create projects page structure** ```html Projects - Claude Code Web Interface
``` **Step 2: Commit** ```bash git add public/projects.html git commit -m "feat: add projects page HTML structure" ``` --- ## Task 7: Projects Page CSS **Files:** - Create: `public/claude-ide/projects.css` **Step 1: Create projects page styles** ```css /* Projects Page Layout */ .projects-page { background: var(--bg-primary); min-height: 100vh; } .projects-container { max-width: 1400px; margin: 0 auto; padding: 2rem; } /* Header */ .projects-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border-color); } .header-left { display: flex; align-items: center; gap: 1rem; } .header-left h1 { margin: 0; font-size: 2rem; } .back-link { color: var(--text-secondary); text-decoration: none; transition: color 0.2s; } .back-link:hover { color: var(--accent-color); } .header-right { display: flex; gap: 0.75rem; align-items: center; } .search-box input { padding: 0.5rem 1rem; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-secondary); color: var(--text-primary); width: 250px; } /* Projects Grid */ .projects-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.5rem; } /* Project Card */ .project-card { background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 12px; padding: 1.5rem; cursor: pointer; transition: all 0.2s; position: relative; } .project-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); border-color: var(--accent-color); } .project-card-header { display: flex; align-items: flex-start; gap: 1rem; margin-bottom: 1rem; } .project-icon { font-size: 2rem; line-height: 1; } .project-info { flex: 1; } .project-name { font-size: 1.25rem; font-weight: 600; margin: 0 0 0.25rem 0; color: var(--text-primary); } .project-description { font-size: 0.875rem; color: var(--text-secondary); margin: 0; line-height: 1.4; } .project-menu-btn { background: none; border: none; color: var(--text-secondary); cursor: pointer; padding: 0.25rem; opacity: 0; transition: opacity 0.2s; } .project-card:hover .project-menu-btn { opacity: 1; } .project-menu-btn:hover { color: var(--text-primary); } .project-meta { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border-color); } .project-path { font-family: 'Monaco', 'Menlo', monospace; font-size: 0.75rem; color: var(--text-secondary); margin-bottom: 0.5rem; word-break: break-all; } .project-stats { display: flex; justify-content: space-between; align-items: center; font-size: 0.875rem; color: var(--text-secondary); } .session-count { display: flex; align-items: center; gap: 0.25rem; } .last-activity { font-size: 0.75rem; } /* Empty State */ .empty-state { text-align: center; padding: 4rem 2rem; } .empty-icon { font-size: 4rem; margin-bottom: 1rem; opacity: 0.5; } .empty-state h2 { margin-bottom: 0.5rem; } .empty-state p { color: var(--text-secondary); margin-bottom: 1.5rem; } /* Modal */ .modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; } .modal-content { background: var(--bg-primary); border-radius: 12px; padding: 2rem; max-width: 500px; width: 90%; max-height: 90vh; overflow-y: auto; } .modal-large { max-width: 700px; } .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; } .modal-header h2 { margin: 0; } .modal-close { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: var(--text-secondary); } .modal-close:hover { color: var(--text-primary); } /* Form */ .form-group { margin-bottom: 1rem; } .form-group label { display: block; margin-bottom: 0.5rem; font-weight: 500; color: var(--text-primary); } .form-group input, .form-group textarea { width: 100%; padding: 0.75rem; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-secondary); color: var(--text-primary); font-size: 1rem; } .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; } .form-actions { display: flex; justify-content: flex-end; gap: 0.75rem; margin-top: 1.5rem; } /* Buttons */ .btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; font-size: 0.875rem; font-weight: 500; cursor: pointer; transition: all 0.2s; display: inline-flex; align-items: center; gap: 0.5rem; } .btn-primary { background: var(--accent-color); color: white; } .btn-primary:hover { opacity: 0.9; } .btn-secondary { background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-color); } .btn-secondary:hover { background: var(--border-color); } /* Context Menu */ .context-menu { position: fixed; background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); min-width: 200px; z-index: 1001; padding: 0.25rem 0; } .context-menu-item { padding: 0.5rem 1rem; cursor: pointer; display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; } .context-menu-item:hover { background: var(--bg-secondary); } .context-menu-divider { height: 1px; background: var(--border-color); margin: 0.25rem 0; } /* Recycle Bin */ .recycle-bin-items { display: flex; flex-direction: column; gap: 1rem; } .recycle-bin-item { background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 8px; padding: 1rem; opacity: 0.7; } .recycle-bin-item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; } .recycle-bin-actions { display: flex; gap: 0.5rem; } /* Buttons */ .btn-sm { padding: 0.25rem 0.5rem; font-size: 0.75rem; } .btn-success { background: #28a745; color: white; } .btn-danger { background: #dc3545; color: white; } ``` **Step 2: Commit** ```bash git add public/claude-ide/projects.css git commit -m "feat: add projects page styles" ``` --- ## Task 8: Projects Page JavaScript **Files:** - Create: `public/claude-ide/projects.js` **Step 1: Create projects page logic** ```javascript // State let projects = []; let currentEditingProject = null; // Initialize document.addEventListener('DOMContentLoaded', () => { loadProjects(); setupEventListeners(); }); // Load projects async function loadProjects() { try { const response = await fetch('/api/projects'); const data = await response.json(); if (data.success) { projects = data.projects; renderProjects(); } } catch (error) { console.error('Error loading projects:', error); showToast('Failed to load projects', 'error'); } } // Render projects function renderProjects(filter = '') { const grid = document.getElementById('projectsGrid'); const emptyState = document.getElementById('emptyState'); const filteredProjects = projects.filter(p => p.name.toLowerCase().includes(filter.toLowerCase()) || p.description.toLowerCase().includes(filter.toLowerCase()) || p.path.toLowerCase().includes(filter.toLowerCase()) ); if (filteredProjects.length === 0) { grid.style.display = 'none'; emptyState.style.display = 'block'; return; } grid.style.display = 'grid'; emptyState.style.display = 'none'; grid.innerHTML = filteredProjects.map(project => `
${project.icon}

${escapeHtml(project.name)}

${escapeHtml(project.description || 'No description')}

${escapeHtml(project.path)}
šŸ“„ ${project.sessionCount} session${project.sessionCount !== 1 ? 's' : ''}
${formatDate(project.lastActivity)}
`).join(''); } // Setup event listeners function setupEventListeners() { // Search document.getElementById('projectSearch').addEventListener('input', (e) => { renderProjects(e.target.value); }); // Create project document.getElementById('createProjectBtn').addEventListener('click', () => { openProjectModal(); }); // Project form document.getElementById('projectForm').addEventListener('submit', handleProjectSubmit); // Recycle bin document.getElementById('recycleBinBtn').addEventListener('click', openRecycleBinModal); // Close context menu on click outside document.addEventListener('click', () => { hideContextMenu(); }); } // Open project function openProject(projectId) { // Navigate to sessions landing with project filter window.location.href = `/claude/?project=${projectId}`; } // Open project modal (create or edit) function openProjectModal(project = null) { currentEditingProject = project; const modal = document.getElementById('projectModal'); const title = document.getElementById('modalTitle'); const form = document.getElementById('projectForm'); if (project) { title.textContent = 'Edit Project'; document.getElementById('projectName').value = project.name; document.getElementById('projectPath').value = project.path; document.getElementById('projectDescription').value = project.description || ''; document.getElementById('projectIcon').value = project.icon || 'šŸ“'; document.getElementById('projectColor').value = project.color || '#4a9eff'; } else { title.textContent = 'Create New Project'; form.reset(); document.getElementById('projectIcon').value = 'šŸ“'; document.getElementById('projectColor').value = '#4a9eff'; } modal.style.display = 'flex'; } // Close project modal function closeProjectModal() { document.getElementById('projectModal').style.display = 'none'; currentEditingProject = null; } // Handle project submit async function handleProjectSubmit(e) { e.preventDefault(); const data = { name: document.getElementById('projectName').value, path: document.getElementById('projectPath').value, description: document.getElementById('projectDescription').value, icon: document.getElementById('projectIcon').value || 'šŸ“', color: document.getElementById('projectColor').value || '#4a9eff' }; try { const url = currentEditingProject ? `/api/projects/${currentEditingProject.id}` : '/api/projects'; const method = currentEditingProject ? 'PUT' : 'POST'; const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await response.json(); if (result.success) { showToast(currentEditingProject ? 'Project updated' : 'Project created', 'success'); closeProjectModal(); await loadProjects(); } else { showToast(result.error || 'Failed to save project', 'error'); } } catch (error) { console.error('Error saving project:', error); showToast('Failed to save project', 'error'); } } // Show project context menu function showProjectMenu(projectId, event) { const project = projects.find(p => p.id === projectId); if (!project) return; const menu = document.getElementById('contextMenu'); menu.innerHTML = `
āœļø Edit
šŸ—‘ļø Move to Recycle Bin
`; menu.style.left = event.pageX + 'px'; menu.style.top = event.pageY + 'px'; menu.style.display = 'block'; } // Hide context menu function hideContextMenu() { document.getElementById('contextMenu').style.display = 'none'; } // Open project modal by ID function openProjectModalById(projectId) { const project = projects.find(p => p.id === projectId); if (project) { openProjectModal(project); } hideContextMenu(); } // Delete project async function deleteProject(projectId) { if (!confirm('Move this project and all its sessions to the recycle bin?')) { return; } try { const response = await fetch(`/api/projects/${projectId}`, { method: 'DELETE' }); const result = await response.json(); if (result.success) { showToast('Project moved to recycle bin', 'success'); await loadProjects(); } else { showToast(result.error || 'Failed to delete project', 'error'); } } catch (error) { console.error('Error deleting project:', error); showToast('Failed to delete project', 'error'); } hideContextMenu(); } // Recycle bin modal async function openRecycleBinModal() { const modal = document.getElementById('recycleBinModal'); const container = document.getElementById('recycleBinItems'); try { const response = await fetch('/api/recycle-bin'); const data = await response.json(); if (data.success) { if (data.items.length === 0) { container.innerHTML = '

Recycle bin is empty šŸŽ‰

'; } else { container.innerHTML = data.items.map(item => `
${item.icon} ${escapeHtml(item.name)}
${item.sessionCount} session${item.sessionCount !== 1 ? 's' : ''} • Deleted ${formatDate(item.deletedAt)}
`).join(''); } modal.style.display = 'flex'; } } catch (error) { console.error('Error loading recycle bin:', error); showToast('Failed to load recycle bin', 'error'); } } // Close recycle bin modal function closeRecycleBinModal() { document.getElementById('recycleBinModal').style.display = 'none'; } // Restore project async function restoreProject(projectId) { try { const response = await fetch(`/api/projects/${projectId}/restore`, { method: 'POST' }); const result = await response.json(); if (result.success) { showToast('Project restored', 'success'); await loadProjects(); await openRecycleBinModal(); // Refresh bin } else { showToast(result.error || 'Failed to restore project', 'error'); } } catch (error) { console.error('Error restoring project:', error); showToast('Failed to restore project', 'error'); } } // Permanent delete async function permanentDeleteProject(projectId) { if (!confirm('Permanently delete this project and all its sessions? This cannot be undone!')) { return; } try { const response = await fetch(`/api/projects/${projectId}/permanent`, { method: 'DELETE' }); const result = await response.json(); if (result.success) { showToast('Project permanently deleted', 'success'); await openRecycleBinModal(); // Refresh bin } else { showToast(result.error || 'Failed to delete project', 'error'); } } catch (error) { console.error('Error deleting project:', error); showToast('Failed to delete project', 'error'); } } // Helper functions function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function formatDate(dateString) { const date = new Date(dateString); const now = new Date(); const diffMs = now - date; const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return 'just now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString(); } function showToast(message, type = 'info') { // Import from ide.js or implement locally if (typeof window.showToast === 'function') { window.showToast(message, type); } else { alert(message); } } ``` **Step 2: Commit** ```bash git add public/claude-ide/projects.js git commit -m "feat: add projects page JavaScript functionality" ``` --- ## Task 9: Update Sessions Landing to Show Projects **Files:** - Modify: `public/claude-ide/sessions-landing.js` **Step 1: Modify renderSessions to group by project** Find the `renderSessions` function and modify it to group sessions by project. This will require fetching projects first, then organizing sessions. Add this after the sessions are fetched: ```javascript // Add project fetching async function loadSessionsAndProjects() { const [sessionsRes, projectsRes] = await Promise.all([ fetch('/claude/api/claude/sessions'), fetch('/api/projects') ]); const sessionsData = await sessionsRes.json(); const projectsData = await projectsRes.json(); if (sessionsData.success) { sessions = sessionsData.sessions; } if (projectsData.success) { const projects = projectsData.projects; // Create a map for quick lookup window.projectsMap = new Map(projects.map(p => [p.id.toString(), p])); } renderSessions(); } ``` Modify `renderSessions` to group by project: ```javascript function renderSessions() { const container = document.getElementById('sessionsGrid'); // Group sessions by project const grouped = { unassigned: [], byProject: {} }; sessions.forEach(session => { const projectId = session.projectId; if (projectId && window.projectsMap?.has(projectId.toString())) { if (!grouped.byProject[projectId]) { grouped.byProject[projectId] = []; } grouped.byProject[projectId].push(session); } else { grouped.unassigned.push(session); } }); // Render projects first let html = ''; for (const [projectId, projectSessions] of Object.entries(grouped.byProject)) { const project = window.projectsMap.get(projectId); if (!project) continue; html += `
${project.icon} ${escapeHtml(project.name)} ${projectSessions.length} ā–¼
${projectSessions.map(session => renderSessionCard(session)).join('')}
`; } // Render unassigned sessions if (grouped.unassigned.length > 0) { html += `
šŸ“„ Unassigned Sessions ${grouped.unassigned.length} ā–¼
${grouped.unassigned.map(session => renderSessionCard(session)).join('')}
`; } container.innerHTML = html; } function toggleProjectSection(projectId) { const section = document.getElementById(`project-${projectId}`); const icon = section.previousElementSibling.querySelector('.toggle-icon'); if (section.style.display === 'none') { section.style.display = 'block'; icon.textContent = 'ā–¼'; } else { section.style.display = 'none'; icon.textContent = 'ā–¶'; } } ``` **Step 2: Add context menu to session cards** Modify `renderSessionCard` to add the context menu trigger: ```javascript function renderSessionCard(session) { const project = session.projectId ? window.projectsMap?.get(session.projectId.toString()) : null; return `
${project ? `
${project.icon} ${escapeHtml(project.name)}
` : ''}
`; } ``` **Step 3: Commit** ```bash git add public/claude-ide/sessions-landing.js git commit -m "feat: group sessions by project on landing page" ``` --- ## Task 10: Session Context Menu for Reassignment **Files:** - Modify: `public/claude-ide/sessions-landing.js` **Step 1: Add session context menu function** ```javascript let currentSessionId = null; async function showSessionContextMenu(event, sessionId) { event.preventDefault(); currentSessionId = sessionId; const menu = document.getElementById('sessionContextMenu'); // Fetch suggestions const suggestionsRes = await fetch(`/api/projects/suggestions?sessionId=${sessionId}`); const suggestionsData = await suggestionsRes.json(); let menuHtml = `
šŸ”— Open in IDE
Move to Project
`; // Add suggestions if (suggestionsData.success && suggestionsData.suggestions.length > 0) { suggestionsData.suggestions.forEach(suggestion => { const icon = getMatchIcon(suggestion.score); const reasons = suggestion.reasons.join(', '); menuHtml += `
${icon} ${escapeHtml(suggestion.name)}
${reasons}
`; }); menuHtml += `
`; } // Add "Show All Projects" option menuHtml += `
šŸ“‚ Show All Projects...
šŸ“„ Move to Unassigned
`; menu.innerHTML = menuHtml; menu.style.left = event.pageX + 'px'; menu.style.top = event.pageY + 'px'; menu.style.display = 'block'; } function getMatchIcon(score) { if (score >= 90) return 'šŸŽÆ'; if (score >= 50) return 'šŸ“‚'; return 'šŸ’”'; } async function moveSessionToProject(sessionId, projectId) { try { const response = await fetch(`/api/sessions/${sessionId}/move`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ projectId }) }); const result = await response.json(); if (result.success) { showToast('Session moved', 'success'); await loadSessionsAndProjects(); } else { showToast(result.error || 'Failed to move session', 'error'); } } catch (error) { console.error('Error moving session:', error); showToast('Failed to move session', 'error'); } hideSessionContextMenu(); } function showAllProjectsForMove(sessionId) { // Could show a modal with all projects // For now, just alert the user to use projects page showToast('Use the Projects page to manage assignments', 'info'); hideSessionContextMenu(); } function hideSessionContextMenu() { document.getElementById('sessionContextMenu').style.display = 'none'; currentSessionId = null; } // Close context menu on click outside document.addEventListener('click', (e) => { if (!e.target.closest('#sessionContextMenu')) { hideSessionContextMenu(); } }); ``` **Step 2: Add context menu styles** ```css #sessionContextMenu { position: fixed; background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); min-width: 250px; z-index: 1000; padding: 0.25rem 0; } .context-menu-label { padding: 0.25rem 1rem; font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; font-weight: 600; } .suggestion-reason { font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.125rem; } .session-project-badge { position: absolute; top: 0.5rem; right: 0.5rem; background: var(--accent-color); color: white; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; } .project-section { margin-bottom: 2rem; } .project-section-header { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 1rem; background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 8px; cursor: pointer; margin-bottom: 1rem; transition: background 0.2s; } .project-section-header:hover { background: var(--border-color); } .project-section-header.unassigned { border-style: dashed; } .toggle-icon { margin-left: auto; transition: transform 0.2s; } .project-sessions { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem; } ``` **Step 3: Commit** ```bash git add public/claude-ide/sessions-landing.js public/claude-ide/sessions-landing.css git commit -m "feat: add session context menu for project reassignment" ``` --- ## Task 11: Database Migration Script **Files:** - Create: `scripts/migrate-to-projects.js` **Step 1: Create migration script** ```javascript const { MongoClient, ObjectId } = require('mongodb'); const path = require('path'); // Configuration const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017'; const DB_NAME = process.env.DB_NAME || 'claude-web-interface'; async function migrate() { const client = new MongoClient(MONGO_URI); try { console.log('Connecting to MongoDB...'); await client.connect(); const db = client.db(DB_NAME); const sessionsCollection = db.collection('sessions'); const projectsCollection = db.collection('projects'); console.log('Creating projects collection...'); await db.createCollection('projects'); console.log('Creating indexes...'); await projectsCollection.createIndex({ deletedAt: 1 }); await projectsCollection.createIndex({ name: 1 }); console.log('Finding unique project names from existing sessions...'); const uniqueProjects = await sessionsCollection.distinct('metadata.project'); console.log(`Found ${uniqueProjects.length} unique projects to migrate...`); const icons = ['šŸš€', 'šŸ“±', 'šŸŽØ', 'šŸ’»', 'šŸ”§', 'šŸ“Š', 'šŸŽÆ', 'šŸ”®', '⚔', '🌟']; const colors = ['#4a9eff', '#ff6b6b', '#51cf66', '#ffd43b', '#cc5de8', '#ff922b', '#20c997', '#339af0']; let projectCount = 0; for (const name of uniqueProjects) { if (!name) continue; console.log(`Migrating project: ${name}`); const projectSessions = await sessionsCollection.find({ 'metadata.project': name }).toArray(); if (projectSessions.length === 0) continue; const paths = [...new Set(projectSessions.map(s => s.workingDir).filter(Boolean))]; const createdAt = projectSessions[0].createdAt || new Date(); const lastActivity = Math.max(...projectSessions.map(s => s.lastActivity || s.createdAt)); const randomIcon = icons[Math.floor(Math.random() * icons.length)]; const randomColor = colors[Math.floor(Math.random() * colors.length)]; const project = { name, description: `Migrated from existing sessions`, icon: randomIcon, color: randomColor, path: paths[0] || '', sessionIds: projectSessions.map(s => s._id), createdAt, lastActivity: new Date(lastActivity), deletedAt: null }; const result = await projectsCollection.insertOne(project); console.log(` → Created project with ID: ${result.insertedId}`); // Update sessions with projectId await sessionsCollection.updateMany( { 'metadata.project': name }, { $set: { projectId: result.insertedId } } ); console.log(` → Updated ${projectSessions.length} sessions`); projectCount++; } console.log(`\nāœ… Migration complete! Created ${projectCount} projects.`); } catch (error) { console.error('Migration failed:', error); process.exit(1); } finally { await client.close(); } } // Run migration migrate(); ``` **Step 2: Add migration to package.json** Add to `scripts` section: ```json "scripts": { "migrate:projects": "node scripts/migrate-to-projects.js" } ``` **Step 3: Commit** ```bash git add scripts/migrate-to-projects.js package.json git commit -m "feat: add database migration script for projects" ``` **Step 4: Run migration** ```bash npm run migrate:projects ``` **Step 5: Commit migration results** ```bash git add . git commit -m "chore: run project migration" ``` --- ## Task 12: Add Route Handler for Projects Page **Files:** - Modify: `server.js` **Step 1: Add projects page route** Find the routes section (around line 70) and add: ```javascript // Projects page app.get('/projects', (req, res) => { if (req.session.userId) { res.sendFile(path.join(__dirname, 'public', 'projects.html')); } else { res.redirect('/claude/'); } }); ``` **Step 2: Add link to projects page in navigation** Modify the landing page header to include a "Projects" link: ```javascript // In the landing page HTML or navigation component // Add: Projects ``` **Step 3: Commit** ```bash git add server.js public/claude-landing.html git commit -m "feat: add projects page route and navigation link" ``` --- ## Task 13: Update Session Creation to Auto-Assign to Project **Files:** - Modify: `public/claude-ide/ide.js` - Modify: `server.js` **Step 1: Add projectId to session creation in IDE** Find the session creation code in ide.js and modify to include projectId if a project is selected: ```javascript // When creating a new session async function createSession(sessionData) { const currentProject = getCurrentProject(); // Get from project selector const payload = { ...sessionData, projectId: currentProject?.id || null }; const response = await fetch('/claude/api/claude/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); // ... handle response } ``` **Step 2: Update server endpoint to handle projectId** Modify the session creation endpoint in server.js: ```javascript app.post('/claude/api/claude/sessions', requireAuth, async (req, res) => { try { const { name, workingDir, projectId } = req.body; // ... existing validation // If projectId provided, verify it exists if (projectId) { const project = await projectsCollection.findOne({ _id: new ObjectId(projectId), deletedAt: null }); if (!project) { return res.status(400).json({ error: 'Invalid project ID' }); } } const session = { // ... existing fields projectId: projectId ? new ObjectId(projectId) : null, createdAt: new Date(), lastActivity: new Date() }; const result = await sessionsCollection.insertOne(session); // Add session to project's sessionIds if (projectId) { await projectsCollection.updateOne( { _id: new ObjectId(projectId) }, { $push: { sessionIds: result.insertedId }, $set: { lastActivity: new Date() } } ); } // ... rest of endpoint } catch (error) { // ... error handling } }); ``` **Step 3: Commit** ```bash git add public/claude-ide/ide.js server.js git commit -m "feat: auto-assign new sessions to selected project" ``` --- ## Task 14: Testing and Documentation **Files:** - Create: `docs/projects-feature.md` **Step 1: Create documentation** ```markdown # Projects Feature ## Overview Projects are persistent containers that organize multiple Claude Code sessions. ## Features ### Creating Projects 1. Navigate to `/projects` 2. Click "+ New Project" 3. Enter name and working directory (required) 4. Optionally add description, icon, and color 5. Click "Save Project" ### Assigning Sessions to Projects **Automatic Assignment:** - When creating a session from the IDE with a project selected, it auto-assigns **Manual Assignment:** 1. Go to sessions landing page (`/claude/`) 2. Right-click on any session card 3. Select "Move to Project" from context menu 4. Choose from: - šŸŽÆ **Suggestions** - Smart recommendations based on directory, recency, name - šŸ“‚ **All Projects** - Full project list ### Managing Projects **Edit Project:** - Right-click project card → Edit - Modify any field except name - Save changes **Delete Project:** - Right-click project card → Move to Recycle Bin - Project and all sessions are soft-deleted - Can restore from recycle bin within 30 days **Recycle Bin:** - Click "šŸ—‘ļø Recycle Bin" button on projects page - Restore deleted projects (restores all sessions too) - Permanently delete (cannot be undone) ## API Reference ### Projects - `GET /api/projects` - List all active projects - `POST /api/projects` - Create new project - `PUT /api/projects/:id` - Update project - `DELETE /api/projects/:id` - Soft delete project - `POST /api/projects/:id/restore` - Restore from recycle bin - `DELETE /api/projects/:id/permanent` - Permanent delete ### Sessions - `POST /api/sessions/:id/move` - Move session to different project - `GET /api/projects/suggestions?sessionId=xxx` - Get project suggestions ### Recycle Bin - `GET /api/recycle-bin` - List deleted items ## Smart Suggestions The system suggests projects based on: 1. **Directory Match (90 points)** - Exact path match 2. **Subdirectory (50 points)** - Session path is under project path 3. **Recent Use (20 points)** - Used today 4. **Week-old Use (10 points)** - Used within last week 5. **Name Similarity (15 points)** - Name overlap Suggestions with 90+ points show šŸŽÆ, 50-89 show šŸ“‚, lower show šŸ’” ``` **Step 2: Manual testing checklist** Test each feature and document results: ```bash # 1. Create project curl -X POST http://localhost:3000/api/projects \ -H "Content-Type: application/json" \ -d '{"name":"Test Project","path":"/home/uroma/test"}' # 2. List projects curl http://localhost:3000/api/projects # 3. Create session with projectId curl -X POST http://localhost:3000/claude/api/claude/sessions \ -H "Content-Type: application/json" \ -d '{"name":"Test Session","workingDir":"/home/uroma/test","projectId":""}' # 4. Get suggestions curl "http://localhost:3000/api/projects/suggestions?sessionId=" # 5. Move session curl -X POST http://localhost:3000/api/sessions//move \ -H "Content-Type: application/json" \ -d '{"projectId":""}' # 6. Delete project curl -X DELETE http://localhost:3000/api/projects/ # 7. Restore project curl -X POST http://localhost:3000/api/projects//restore ``` **Step 3: Commit** ```bash git add docs/projects-feature.md git commit -m "docs: add projects feature documentation" ``` --- ## Task 15: Final Integration and Cleanup **Files:** - Modify: `server.js` - Modify: `public/claude-ide/ide.js` **Step 1: Update ObjectId import** Ensure ObjectId is imported at the top of server.js: ```javascript const { ObjectId } = require('mongodb'); ``` **Step 2: Add error handling for missing collections** Add safety check in case projects collection doesn't exist yet: ```javascript // At the start of API endpoints that use projectsCollection if (!db.collection('projects')) { await db.createCollection('projects'); } ``` **Step 3: Ensure all async functions have proper error handling** Check all new endpoints have try-catch blocks with proper error responses. **Step 4: Restart server and test** ```bash # Stop existing server pkill -f "node.*server.js" # Start new server node server.js ``` **Step 5: Test the complete flow** 1. Navigate to `/projects` 2. Create a new project 3. Go to `/claude/` 4. Create a session (verify it's unassigned) 5. Right-click session → move to project 6. Verify session appears under project 7. Delete project → verify in recycle bin 8. Restore project → verify it's back **Step 6: Final commit** ```bash git add . git commit -m "feat: complete project and session organization feature - Persistent projects as first-class entities - Smart session assignment with suggestions - Context menu for session reassignment - Soft delete with recycle bin - Migration script for existing sessions - Full API and UI implementation Closes #1" ``` **Step 7: Push to remote** ```bash git push origin feature/project-organization ``` --- ## Completion Criteria - [x] Projects collection created with proper indexes - [x] All CRUD endpoints working (create, read, update, delete) - [x] Soft delete implemented (deletedAt timestamp) - [x] Recycle bin UI functional - [x] Session reassignment via context menu - [x] Smart suggestions based on directory/recency/name - [x] Auto-assignment when creating sessions in project context - [x] Migration script successfully backfills existing data - [x] All tests passing - [x] Documentation complete **Ready for PR when all tasks complete!**