Files
SuperCharged-Claude-Code-Up…/docs/plans/2025-01-19-project-session-organization.md
uroma 1fbc565a66 feat: add session move endpoint and project-session cascading delete
- Add sessions table to database with projectId and deletedAt columns
- Create POST /api/sessions/:id/move endpoint to reassign sessions
- Update DELETE /api/projects/:id to cascade soft-delete to sessions
- Support moving sessions between projects or to unassigned state
- Handle both active (in-memory) and historical sessions

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 16:55:15 +00:00

57 KiB
Raw Blame History

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.

// 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

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

// 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

// 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

// 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

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)

// 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

// 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

// 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

// 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

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

// 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

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

// 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

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

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Projects - Claude Code Web Interface</title>
  <link rel="stylesheet" href="/css/style.css">
  <link rel="stylesheet" href="/claude-ide/projects.css">
</head>
<body class="projects-page">
  <div class="projects-container">
    <!-- Header -->
    <header class="projects-header">
      <div class="header-left">
        <h1>Projects</h1>
        <a href="/claude/" class="back-link">← Back to Sessions</a>
      </div>
      <div class="header-right">
        <div class="search-box">
          <input type="text" id="projectSearch" placeholder="Search projects...">
        </div>
        <button id="createProjectBtn" class="btn btn-primary">
          <span class="icon">+</span> New Project
        </button>
        <button id="recycleBinBtn" class="btn btn-secondary">
          <span class="icon">🗑️</span> Recycle Bin
        </button>
      </div>
    </header>

    <!-- Projects Grid -->
    <div id="projectsGrid" class="projects-grid">
      <!-- Projects will be rendered here -->
    </div>

    <!-- Empty State -->
    <div id="emptyState" class="empty-state" style="display: none;">
      <div class="empty-icon">📁</div>
      <h2>No projects yet</h2>
      <p>Create your first project to organize your sessions</p>
      <button class="btn btn-primary" onclick="document.getElementById('createProjectBtn').click()">
        Create Project
      </button>
    </div>
  </div>

  <!-- Create/Edit Project Modal -->
  <div id="projectModal" class="modal" style="display: none;">
    <div class="modal-content">
      <div class="modal-header">
        <h2 id="modalTitle">Create New Project</h2>
        <button class="modal-close" onclick="closeProjectModal()">×</button>
      </div>
      <form id="projectForm">
        <div class="form-group">
          <label for="projectName">Project Name *</label>
          <input type="text" id="projectName" required placeholder="My API Project">
        </div>
        <div class="form-group">
          <label for="projectPath">Working Directory *</label>
          <input type="text" id="projectPath" required placeholder="/home/uroma/my-project">
        </div>
        <div class="form-group">
          <label for="projectDescription">Description</label>
          <textarea id="projectDescription" rows="3" placeholder="What is this project about?"></textarea>
        </div>
        <div class="form-row">
          <div class="form-group">
            <label for="projectIcon">Icon</label>
            <input type="text" id="projectIcon" placeholder="📁" maxlength="2">
          </div>
          <div class="form-group">
            <label for="projectColor">Color</label>
            <input type="color" id="projectColor" value="#4a9eff">
          </div>
        </div>
        <div class="form-actions">
          <button type="button" class="btn btn-secondary" onclick="closeProjectModal()">Cancel</button>
          <button type="submit" class="btn btn-primary">Save Project</button>
        </div>
      </form>
    </div>
  </div>

  <!-- Recycle Bin Modal -->
  <div id="recycleBinModal" class="modal" style="display: none;">
    <div class="modal-content modal-large">
      <div class="modal-header">
        <h2>Recycle Bin</h2>
        <button class="modal-close" onclick="closeRecycleBinModal()">×</button>
      </div>
      <div id="recycleBinItems" class="recycle-bin-items">
        <!-- Deleted items will be rendered here -->
      </div>
    </div>
  </div>

  <!-- Context Menu -->
  <div id="contextMenu" class="context-menu" style="display: none;">
    <!-- Menu items will be added dynamically -->
  </div>

  <script src="/claude-ide/projects.js"></script>
</body>
</html>

Step 2: Commit

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

/* 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

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

// 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 => `
    <div class="project-card" data-id="${project.id}" onclick="openProject('${project.id}')">
      <div class="project-card-header">
        <div class="project-icon">${project.icon}</div>
        <div class="project-info">
          <h3 class="project-name">${escapeHtml(project.name)}</h3>
          <p class="project-description">${escapeHtml(project.description || 'No description')}</p>
        </div>
        <button class="project-menu-btn" onclick="event.stopPropagation(); showProjectMenu('${project.id}', event)">
        </button>
      </div>
      <div class="project-meta">
        <div class="project-path">${escapeHtml(project.path)}</div>
        <div class="project-stats">
          <div class="session-count">
            <span>📄</span>
            <span>${project.sessionCount} session${project.sessionCount !== 1 ? 's' : ''}</span>
          </div>
          <div class="last-activity">${formatDate(project.lastActivity)}</div>
        </div>
      </div>
    </div>
  `).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 = `
    <div class="context-menu-item" onclick="event.stopPropagation(); openProjectModalById('${projectId}')">
      ✏️ Edit
    </div>
    <div class="context-menu-item" onclick="event.stopPropagation(); deleteProject('${projectId}')">
      🗑️ Move to Recycle Bin
    </div>
  `;

  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 = '<p class="text-center">Recycle bin is empty 🎉</p>';
      } else {
        container.innerHTML = data.items.map(item => `
          <div class="recycle-bin-item">
            <div class="recycle-bin-item-header">
              <div>
                <strong>${item.icon} ${escapeHtml(item.name)}</strong>
                <div style="font-size: 0.875rem; color: var(--text-secondary);">
                  ${item.sessionCount} session${item.sessionCount !== 1 ? 's' : ''} • Deleted ${formatDate(item.deletedAt)}
                </div>
              </div>
              <div class="recycle-bin-actions">
                <button class="btn btn-sm btn-success" onclick="restoreProject('${item.id}')">Restore</button>
                <button class="btn btn-sm btn-danger" onclick="permanentDeleteProject('${item.id}')">Delete</button>
              </div>
            </div>
          </div>
        `).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

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:

// 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:

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 += `
      <div class="project-section">
        <div class="project-section-header" onclick="toggleProjectSection('${projectId}')">
          <span class="project-icon">${project.icon}</span>
          <span class="project-name">${escapeHtml(project.name)}</span>
          <span class="session-count">${projectSessions.length}</span>
          <span class="toggle-icon">▼</span>
        </div>
        <div class="project-sessions" id="project-${projectId}">
          ${projectSessions.map(session => renderSessionCard(session)).join('')}
        </div>
      </div>
    `;
  }

  // Render unassigned sessions
  if (grouped.unassigned.length > 0) {
    html += `
      <div class="project-section">
        <div class="project-section-header unassigned" onclick="toggleProjectSection('unassigned')">
          <span class="project-icon">📄</span>
          <span class="project-name">Unassigned Sessions</span>
          <span class="session-count">${grouped.unassigned.length}</span>
          <span class="toggle-icon">▼</span>
        </div>
        <div class="project-sessions" id="project-unassigned">
          ${grouped.unassigned.map(session => renderSessionCard(session)).join('')}
        </div>
      </div>
    `;
  }

  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:

function renderSessionCard(session) {
  const project = session.projectId ? window.projectsMap?.get(session.projectId.toString()) : null;

  return `
    <div class="session-card" data-id="${session.id}" oncontextmenu="showSessionContextMenu(event, '${session.id}')">
      <!-- existing card content -->
      ${project ? `<div class="session-project-badge">${project.icon} ${escapeHtml(project.name)}</div>` : ''}
      <!-- rest of card -->
    </div>
  `;
}

Step 3: Commit

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

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 = `
    <div class="context-menu-item" onclick="openSession('${sessionId}')">
      🔗 Open in IDE
    </div>
    <div class="context-menu-divider"></div>
    <div class="context-menu-label">Move to Project</div>
  `;

  // Add suggestions
  if (suggestionsData.success && suggestionsData.suggestions.length > 0) {
    suggestionsData.suggestions.forEach(suggestion => {
      const icon = getMatchIcon(suggestion.score);
      const reasons = suggestion.reasons.join(', ');
      menuHtml += `
        <div class="context-menu-item" onclick="moveSessionToProject('${sessionId}', '${suggestion.id}')">
          ${icon} ${escapeHtml(suggestion.name)}
          <div class="suggestion-reason">${reasons}</div>
        </div>
      `;
    });

    menuHtml += `<div class="context-menu-divider"></div>`;
  }

  // Add "Show All Projects" option
  menuHtml += `
    <div class="context-menu-item" onclick="showAllProjectsForMove('${sessionId}')">
      📂 Show All Projects...
    </div>
    <div class="context-menu-divider"></div>
    <div class="context-menu-item" onclick="moveSessionToProject('${sessionId}', null)">
      📄 Move to Unassigned
    </div>
  `;

  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

#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

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

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:

"scripts": {
  "migrate:projects": "node scripts/migrate-to-projects.js"
}

Step 3: Commit

git add scripts/migrate-to-projects.js package.json
git commit -m "feat: add database migration script for projects"

Step 4: Run migration

npm run migrate:projects

Step 5: Commit migration results

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:

// 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:

// In the landing page HTML or navigation component
// Add: <a href="/projects" class="nav-link">Projects</a>

Step 3: Commit

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:

// 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:

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

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

# 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:

# 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":"<id>"}'

# 4. Get suggestions
curl "http://localhost:3000/api/projects/suggestions?sessionId=<id>"

# 5. Move session
curl -X POST http://localhost:3000/api/sessions/<id>/move \
  -H "Content-Type: application/json" \
  -d '{"projectId":"<new-project-id>"}'

# 6. Delete project
curl -X DELETE http://localhost:3000/api/projects/<id>

# 7. Restore project
curl -X POST http://localhost:3000/api/projects/<id>/restore

Step 3: Commit

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:

const { ObjectId } = require('mongodb');

Step 2: Add error handling for missing collections

Add safety check in case projects collection doesn't exist yet:

// 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

# 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

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

git push origin feature/project-organization

Completion Criteria

  • Projects collection created with proper indexes
  • All CRUD endpoints working (create, read, update, delete)
  • Soft delete implemented (deletedAt timestamp)
  • Recycle bin UI functional
  • Session reassignment via context menu
  • Smart suggestions based on directory/recency/name
  • Auto-assignment when creating sessions in project context
  • Migration script successfully backfills existing data
  • All tests passing
  • Documentation complete

Ready for PR when all tasks complete!