Files
SuperCharged-Claude-Code-Up…/public/claude-ide/project-manager.js
uroma efb506009a Add close button functionality to project tabs in Claude IDE
Fix missing 'x' close buttons on project tabs (Level 1). Session tabs
( Level 2) already had close buttons implemented.

Changes:
- project-manager.js: Added close button element to renderProjectTab()
- Added closeProject() method with confirmation dialog
- Added getSessionName() helper for session name display
- Auto-switches to next project when active project is closed
- project-tabs.css: Added .tab-close styling for project tabs
- Close button shows on hover, red highlight on hover
- Mobile responsive: always visible on small screens

The close button removes project tab from view but sessions remain
accessible via API/reload. Confirmation shows session count and list
for non-empty projects.

Resolves: https://rommark.dev/claude/ide/session/session-1769083280612-mdof554ot

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 12:28:18 +00:00

538 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Project Manager - Organizes sessions by project/folder
* Inspired by CodeNomad's two-level tab system
* https://github.com/NeuralNomadsAI/CodeNomad
*
* Provides:
* - Project-level organization (top tabs)
* - Session-level organization (second tabs)
* - Easy switching between projects and sessions
* - Project creation and management
*/
'use strict';
// ============================================================
// Project Manager Class
// ============================================================
class ProjectManager {
constructor() {
this.projects = new Map(); // Map<projectId, Project>
this.activeProjectId = null;
this.activeSessionId = null;
this.initialized = false;
}
/**
* Initialize the project manager
*/
async initialize() {
if (this.initialized) return;
console.log('[ProjectManager] Initializing...');
await this.loadProjects();
this.renderProjectTabs();
this.initialized = true;
// Auto-select first project if available
if (this.projects.size > 0 && !this.activeProjectId) {
const firstProject = this.projects.values().next().value;
this.switchProject(firstProject.id);
}
console.log('[ProjectManager] Initialized with', this.projects.size, 'projects');
}
/**
* Load all sessions and organize them by project
*/
async loadProjects() {
try {
const res = await fetch('/claude/api/claude/sessions');
if (!res.ok) throw new Error('Failed to fetch sessions');
const data = await res.json();
// Combine active and historical sessions
const allSessions = [
...(data.active || []).map(s => ({...s, status: 'active'})),
...(data.historical || []).map(s => ({...s, status: 'historical'}))
];
// Group by working directory
const grouped = new Map();
allSessions.forEach(session => {
const dir = session.workingDir || 'default';
const projectKey = dir.replace(/\//g, '-').replace(/^-/, '') || 'default';
if (!grouped.has(projectKey)) {
const projectName = dir.split('/').pop() || 'Default';
const project = {
id: `project-${projectKey}`,
name: this.deduplicateProjectName(projectName, grouped),
workingDir: dir,
sessions: [],
activeSessionId: null,
createdAt: this.getOldestSessionTime(allSessions.filter(s => s.workingDir === dir))
};
grouped.set(projectKey, project);
}
grouped.get(projectKey).sessions.push(session);
});
// Sort sessions by last activity within each project
grouped.forEach(project => {
project.sessions.sort((a, b) => {
const dateA = new Date(a.lastActivity || a.createdAt || a.created_at || 0);
const dateB = new Date(b.lastActivity || b.createdAt || b.created_at || 0);
return dateB - dateA;
});
// Set active session to most recent
if (project.sessions.length > 0) {
project.activeSessionId = project.sessions[0].id;
}
});
this.projects = grouped;
console.log('[ProjectManager] Loaded', this.projects.size, 'projects');
} catch (error) {
console.error('[ProjectManager] Error loading projects:', error);
// Create default project on error
this.projects.set('default', {
id: 'project-default',
name: 'Default',
workingDir: '',
sessions: [],
activeSessionId: null,
createdAt: Date.now()
});
}
}
/**
* Deduplicate project names by adding counter
*/
deduplicateProjectName(name, existingProjects) {
const names = Array.from(existingProjects.values()).map(p => p.name);
let finalName = name;
let counter = 2;
while (names.includes(finalName)) {
finalName = `${name} (${counter})`;
counter++;
}
return finalName;
}
/**
* Get oldest session time for a project
*/
getOldestSessionTime(sessions) {
if (sessions.length === 0) return Date.now();
return sessions.reduce((oldest, session) => {
const time = new Date(session.createdAt || session.created_at || 0).getTime();
return time < oldest ? time : oldest;
}, Infinity);
}
/**
* Render project tabs
*/
renderProjectTabs() {
const container = document.getElementById('project-tabs');
if (!container) {
console.warn('[ProjectManager] Project tabs container not found');
return;
}
const projectsArray = Array.from(this.projects.values());
if (projectsArray.length === 0) {
container.innerHTML = `
<div class="project-tabs-empty">
<span>No projects yet</span>
</div>
`;
return;
}
container.innerHTML = `
<div class="project-tabs">
${projectsArray.map(project => this.renderProjectTab(project)).join('')}
<button class="project-tab project-tab-new" onclick="window.projectManager.createNewProject()">
<span class="tab-icon">+</span>
<span class="tab-label">New Project</span>
</button>
</div>
`;
}
/**
* Render a single project tab
*/
renderProjectTab(project) {
const isActive = project.id === this.activeProjectId;
const sessionCount = project.sessions.length;
return `
<button class="project-tab ${isActive ? 'active' : ''}"
data-project-id="${project.id}"
onclick="window.projectManager.switchProject('${project.id}')"
title="${escapeHtml(project.workingDir || 'Default project')}">
<span class="tab-icon">📁</span>
<span class="tab-label">${escapeHtml(project.name)}</span>
${sessionCount > 0 ? `<span class="tab-count">${sessionCount}</span>` : ''}
<span class="tab-close" onclick="event.stopPropagation(); window.projectManager.closeProject('${project.id}')">×</span>
</button>
`;
}
/**
* Switch to a different project
*/
async switchProject(projectId) {
const project = this.projects.get(projectId.replace('project-', ''));
if (!project) {
console.warn('[ProjectManager] Project not found:', projectId);
return;
}
console.log('[ProjectManager] Switching to project:', project.name);
this.activeProjectId = project.id;
// Re-render project tabs to update active state
this.renderProjectTabs();
// Update session tabs for this project
if (window.sessionTabs) {
window.sessionTabs.setSessions(project.sessions);
window.sessionTabs.setActiveSession(project.activeSessionId);
window.sessionTabs.render();
}
// Attach to active session if exists
if (project.activeSessionId && typeof attachToSession === 'function') {
await attachToSession(project.activeSessionId);
} else {
// Show empty state
this.showEmptyProjectState(project);
}
}
/**
* Show empty project state
*/
showEmptyProjectState(project) {
const messagesContainer = document.getElementById('chat-messages');
if (!messagesContainer) return;
messagesContainer.innerHTML = `
<div class="empty-project-state">
<div class="empty-icon">📁</div>
<h3>${escapeHtml(project.name)}</h3>
<p>No sessions yet in this project</p>
<button class="btn-primary" onclick="window.projectManager.createNewSessionInProject('${project.id}')">
Create Session
</button>
</div>
`;
}
/**
* Create a new project (select folder)
*/
async createNewProject() {
// Trigger folder picker if available
if (window.folderPicker && typeof window.folderPicker.pick === 'function') {
try {
const folder = await window.folderPicker.pick();
if (folder) {
await this.createSessionInFolder(folder);
}
} catch (error) {
console.error('[ProjectManager] Error picking folder:', error);
this.showError('Failed to select folder');
}
} else {
// Fallback: prompt for folder or create default session
await this.createNewSessionInProject('default');
}
}
/**
* Create a new session in a specific folder
*/
async createSessionInFolder(workingDir) {
try {
if (typeof showLoadingOverlay === 'function') {
showLoadingOverlay('Creating session...');
}
const res = await fetch('/claude/api/claude/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
workingDir,
metadata: {
type: 'chat',
source: 'web-ide'
}
})
});
if (!res.ok) throw new Error('Failed to create session');
const data = await res.json();
if (data.success) {
// Reload projects and switch to new session
await this.loadProjects();
await this.initialize();
// Find the new session and switch to it
for (const project of this.projects.values()) {
const session = project.sessions.find(s => s.id === data.session.id);
if (session) {
this.switchProject(project.id);
break;
}
}
if (typeof hideLoadingOverlay === 'function') {
hideLoadingOverlay();
}
}
} catch (error) {
console.error('[ProjectManager] Error creating session:', error);
if (typeof hideLoadingOverlay === 'function') {
hideLoadingOverlay();
}
this.showError('Failed to create session');
}
}
/**
* Create a new session in the current project
*/
async createNewSessionInProject(projectId) {
const project = this.projects.get(projectId.replace('project-', ''));
if (!project) return;
await this.createSessionInFolder(project.workingDir);
}
/**
* Get project for a session ID
*/
getProjectForSession(sessionId) {
for (const project of this.projects.values()) {
if (project.sessions.find(s => s.id === sessionId)) {
return project;
}
}
return null;
}
/**
* Add a session to its project
*/
addSessionToProject(session) {
const dir = session.workingDir || 'default';
const projectKey = dir.replace(/\//g, '-').replace(/^-/, '') || 'default';
if (!this.projects.has(projectKey)) {
const projectName = dir.split('/').pop() || 'Default';
this.projects.set(projectKey, {
id: `project-${projectKey}`,
name: this.deduplicateProjectName(projectName, this.projects),
workingDir: dir,
sessions: [],
activeSessionId: null,
createdAt: Date.now()
});
}
const project = this.projects.get(projectKey);
project.sessions.unshift(session); // Add to beginning
project.activeSessionId = session.id;
// Re-render if this is the active project
if (this.activeProjectId === project.id) {
this.renderProjectTabs();
if (window.sessionTabs) {
window.sessionTabs.setSessions(project.sessions);
window.sessionTabs.render();
}
}
}
/**
* Update session in its project
*/
updateSessionInProject(session) {
const project = this.getProjectForSession(session.id);
if (project) {
const index = project.sessions.findIndex(s => s.id === session.id);
if (index !== -1) {
project.sessions[index] = session;
// Move to top
project.sessions.splice(index, 1);
project.sessions.unshift(session);
}
}
}
/**
* Remove session from project
*/
removeSessionFromProject(sessionId) {
const project = this.getProjectForSession(sessionId);
if (project) {
project.sessions = project.sessions.filter(s => s.id !== sessionId);
if (project.activeSessionId === sessionId) {
project.activeSessionId = project.sessions.length > 0 ? project.sessions[0].id : null;
}
}
}
/**
* Show error message
*/
showError(message) {
if (typeof showToast === 'function') {
showToast(message, 'error');
} else {
alert(message);
}
}
/**
* Close a project (with confirmation)
*/
async closeProject(projectId) {
const projectKey = projectId.replace('project-', '');
const project = this.projects.get(projectKey);
if (!project) return;
// Check if project has sessions
if (project.sessions.length > 0) {
const sessionList = project.sessions.map(s => this.getSessionName(s)).join(', ');
if (!confirm(`Close project "${project.name}"?\n\nThis project contains ${project.sessions.length} session(s):\n${sessionList}\n\nThe sessions will remain accessible but the project tab will be removed.`)) {
return;
}
} else {
if (!confirm(`Close project "${project.name}"?`)) {
return;
}
}
console.log('[ProjectManager] Closing project:', projectId);
// Remove project from map
this.projects.delete(projectKey);
// If we closed the active project, switch to another
if (this.activeProjectId === projectId) {
const remainingProjects = Array.from(this.projects.values());
if (remainingProjects.length > 0) {
// Switch to the next available project
await this.switchProject(remainingProjects[0].id);
} else {
// No projects left
this.activeProjectId = null;
if (window.sessionTabs) {
window.sessionTabs.setSessions([]);
window.sessionTabs.render();
}
// Show empty state
const messagesContainer = document.getElementById('chat-messages');
if (messagesContainer) {
messagesContainer.innerHTML = `
<div class="empty-project-state">
<div class="empty-icon">📁</div>
<h3>No Projects</h3>
<p>Create a new project to get started</p>
<button class="btn-primary" onclick="window.projectManager.createNewProject()">
Create Project
</button>
</div>
`;
}
}
}
this.renderProjectTabs();
}
/**
* Get display name for a session
*/
getSessionName(session) {
// Try to get name from metadata
if (session.metadata) {
if (session.metadata.project) return session.metadata.project;
if (session.metadata.title) return session.metadata.title;
if (session.metadata.name) return session.metadata.name;
}
// Use working directory
if (session.workingDir) {
return session.workingDir.split('/').pop();
}
// Fallback to session ID
return session.id.substring(0, 8);
}
/**
* Refresh all projects
*/
async refresh() {
await this.loadProjects();
this.renderProjectTabs();
if (this.activeProjectId) {
const project = this.projects.get(this.activeProjectId.replace('project-', ''));
if (project && window.sessionTabs) {
window.sessionTabs.setSessions(project.sessions);
window.sessionTabs.render();
}
}
}
}
// ============================================================
// Helper Functions
// ============================================================
/**
* Escape HTML to prevent XSS
*/
function escapeHtml(text) {
if (text === null || text === undefined) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ============================================================
// Initialize Globally
// ============================================================
window.projectManager = new ProjectManager();
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => window.projectManager.initialize());
} else {
window.projectManager.initialize();
}
console.log('[ProjectManager] Module loaded');