# 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
š
No projects yet
Create your first project to organize your sessions
```
**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 => `
`).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 = `
`;
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 => `
`).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 += `
${projectSessions.map(session => renderSessionCard(session)).join('')}
`;
}
// Render unassigned sessions
if (grouped.unassigned.length > 0) {
html += `
${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 = `
`;
// Add suggestions
if (suggestionsData.success && suggestionsData.suggestions.length > 0) {
suggestionsData.suggestions.forEach(suggestion => {
const icon = getMatchIcon(suggestion.score);
const reasons = suggestion.reasons.join(', ');
menuHtml += `
`;
});
menuHtml += ``;
}
// Add "Show All Projects" option
menuHtml += `
`;
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!**