- Full IDE with terminal integration using xterm.js - Session management with local and web sessions - HTML preview functionality - Multi-terminal support with session picker Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
258 lines
7.3 KiB
Markdown
258 lines
7.3 KiB
Markdown
# Project and Session Organization Design
|
|
|
|
**Date:** 2025-01-19
|
|
**Status:** Approved
|
|
**Author:** Claude (with user collaboration)
|
|
|
|
## Overview
|
|
|
|
Introduce persistent projects as first-class entities that contain multiple sessions, with intelligent assignment, reorganization, and soft-delete capabilities.
|
|
|
|
## Architecture
|
|
|
|
### Data Model
|
|
|
|
**Projects Collection**
|
|
```javascript
|
|
{
|
|
_id: ObjectId,
|
|
name: "My API Project",
|
|
description: "REST API development",
|
|
icon: "🚀",
|
|
color: "#4a9eff",
|
|
path: "/home/uroma/api",
|
|
sessionIds: [ObjectId, ...],
|
|
createdAt: Date,
|
|
lastActivity: Date,
|
|
deletedAt: Date | null // null = active, Date = in recycle bin
|
|
}
|
|
```
|
|
|
|
**Sessions Collection (Updated)**
|
|
```javascript
|
|
{
|
|
...
|
|
projectId: ObjectId | null, // null = unassigned
|
|
deletedAt: Date | null
|
|
}
|
|
```
|
|
|
|
### API Endpoints
|
|
|
|
| Method | Endpoint | Description |
|
|
|--------|----------|-------------|
|
|
| GET | `/api/projects` | List active projects |
|
|
| POST | `/api/projects` | Create project |
|
|
| PUT | `/api/projects/:id` | Update project |
|
|
| DELETE | `/api/projects/:id` | Soft delete (move to recycle bin) |
|
|
| POST | `/api/projects/:id/restore` | Restore from bin |
|
|
| DELETE | `/api/projects/:id/permanent` | Permanent delete |
|
|
| GET | `/api/recycle-bin` | List deleted items |
|
|
| POST | `/api/sessions/:id/move` | Move session to different project |
|
|
|
|
## UI Components
|
|
|
|
### Projects Page (`/projects`)
|
|
|
|
- **Header**: Title, "+ New Project" button, search bar
|
|
- **Project Grid**: Cards with icon, name, description, path, session count, last activity
|
|
- **Context Menu**: Edit, move to recycle bin
|
|
- **Empty State**: "No projects yet" with CTA
|
|
|
|
### Enhanced Landing Page (`/claude/`)
|
|
|
|
Projects as top-level, sessions nested inside:
|
|
- Collapsible project sections
|
|
- "Unassigned Sessions" section at bottom
|
|
- Session cards show project badge
|
|
- Right-click for context menu
|
|
|
|
### Session Context Menu
|
|
|
|
```
|
|
┌─────────────────────────┐
|
|
│ Open in IDE │
|
|
│ ─────────────────────── │
|
|
│ Move to Project ▶ │
|
|
│ ├── 🚀 My API (🎯 95%)│
|
|
│ ├── 📱 Mobile App │
|
|
│ ├── ──────────────────│
|
|
│ └── Show All Projects │
|
|
│ ─────────────────────── │
|
|
│ Duplicate │
|
|
│ Delete │
|
|
└─────────────────────────┘
|
|
```
|
|
|
|
### Recycle Bin
|
|
|
|
- Accessible from projects page header
|
|
- Shows deleted projects with faded sessions
|
|
- "Restore" and "Delete Permanently" buttons per item
|
|
|
|
## Smart Assignment Algorithm
|
|
|
|
### Auto-Assignment
|
|
|
|
When creating a session from the IDE project selector:
|
|
1. User selects project → `projectId` stored
|
|
2. Session added to project's `sessionIds` array
|
|
3. Project `lastActivity` updated
|
|
|
|
### Smart Suggestions
|
|
|
|
Calculated when moving sessions:
|
|
|
|
```javascript
|
|
function getSuggestions(session, allProjects) {
|
|
const suggestions = [];
|
|
|
|
for (const project of allProjects) {
|
|
let score = 0;
|
|
let 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({ project, score, reasons });
|
|
}
|
|
}
|
|
|
|
return suggestions.sort((a, b) => b.score - a.score).slice(0, 3);
|
|
}
|
|
```
|
|
|
|
**Visual Indicators:**
|
|
- 🎯 90%+ match → Same directory
|
|
- 📂 50-89% match → Subdirectory or recent use
|
|
- 💡 10-49% match → Similar name or recently used
|
|
|
|
## Recycle Bin System
|
|
|
|
### Soft Delete Flow
|
|
|
|
**Delete:**
|
|
```javascript
|
|
// Mark project as deleted
|
|
await db.projects.updateOne(
|
|
{ _id: projectId },
|
|
{ $set: { deletedAt: new Date() } }
|
|
);
|
|
|
|
// Soft delete all sessions in project
|
|
await db.sessions.updateMany(
|
|
{ projectId },
|
|
{ $set: { deletedAt: new Date() } }
|
|
);
|
|
```
|
|
|
|
**Restore:**
|
|
```javascript
|
|
await db.projects.updateOne({ _id }, { $set: { deletedAt: null } });
|
|
await db.sessions.updateMany({ projectId }, { $set: { deletedAt: null } });
|
|
```
|
|
|
|
**Permanent Delete:**
|
|
```javascript
|
|
await db.projects.deleteOne({ _id });
|
|
await db.sessions.deleteMany({ projectId });
|
|
```
|
|
|
|
## Edge Cases
|
|
|
|
| Scenario | Behavior |
|
|
|----------|----------|
|
|
| Reassigning from deleted project | Session becomes unassigned (projectId = null) |
|
|
| Deleting last session in project | Project persists with 0 sessions |
|
|
| Two projects with same path | Both shown as suggestions with equal score |
|
|
| Local CLI sessions | Can be assigned, path read from info.json |
|
|
| Moving to deleted project | Blocked - deleted projects excluded from menu |
|
|
| Concurrent edits | Last write wins (MongoDB atomic updates) |
|
|
|
|
## Implementation
|
|
|
|
### File Structure
|
|
|
|
```
|
|
server.js (add endpoints)
|
|
public/claude-ide/
|
|
├── projects.html (new)
|
|
├── projects.js (new)
|
|
├── projects.css (new)
|
|
└── sessions-landing.js (modify)
|
|
context-menu.js (new shared component)
|
|
```
|
|
|
|
### Migration Script
|
|
|
|
```javascript
|
|
// 1. Create projects collection
|
|
db.createCollection('projects');
|
|
|
|
// 2. Find unique project names from existing sessions
|
|
const uniqueProjects = await db.sessions.distinct('metadata.project');
|
|
|
|
// 3. Create project for each unique name
|
|
for (const name of uniqueProjects) {
|
|
const projectSessions = await db.sessions.find({ 'metadata.project': name }).toArray();
|
|
const paths = [...new Set(projectSessions.map(s => s.workingDir).filter(Boolean))];
|
|
|
|
await db.projects.insertOne({
|
|
name,
|
|
description: '',
|
|
icon: getRandomIcon(),
|
|
color: getRandomColor(),
|
|
path: paths[0] || '',
|
|
sessionIds: projectSessions.map(s => s._id),
|
|
createdAt: projectSessions[0].createdAt,
|
|
lastActivity: Math.max(...projectSessions.map(s => s.lastActivity)),
|
|
deletedAt: null
|
|
});
|
|
}
|
|
|
|
// 4. Update sessions with projectId
|
|
for (const project of await db.projects.find().toArray()) {
|
|
await db.sessions.updateMany(
|
|
{ 'metadata.project': project.name },
|
|
{ $set: { projectId: project._id } }
|
|
);
|
|
}
|
|
```
|
|
|
|
## Testing Checklist
|
|
|
|
- [ ] Create project via inline form
|
|
- [ ] Auto-assign session on creation
|
|
- [ ] Smart suggestions calculate correctly
|
|
- [ ] Move session via context menu
|
|
- [ ] Delete project → goes to recycle bin
|
|
- [ ] Restore project from bin
|
|
- [ ] Permanent delete works
|
|
- [ ] Unassigned sessions display correctly
|
|
- [ ] Migration backfills existing data
|
|
- [ ] Icon/color randomization works
|
|
- [ ] Path validation on project creation
|