Files
SuperCharged-Claude-Code-Up…/public/claude-ide/project-manager.js
uroma ea7f90519f Add Project Roman session fix analysis and design documentation
This commit includes comprehensive analysis and design documentation
for fixing critical session management issues in manually created projects.

Phase 1 Complete:
- Identified 4 critical errors (SSE null reference, array access,
  race conditions, virtual workingDir mismatch)
- Created detailed root cause analysis
- Designed comprehensive solution with 5 components
- Complete implementation plan with testing strategy

Files added:
- ROMAN_SESSION_ISSUE_ANALYSIS.md - Detailed root cause analysis
- ROMAN_SESSION_FIX_DESIGN.md - Complete solution design
- ROMAN_IMPLEMENTATION_SUMMARY.md - Quick reference guide
- PHASE_1_COMPLETE_REPORT.md - Executive summary

Next: Awaiting AI Engineer review before implementation

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 15:19:25 +00:00

881 lines
36 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;
this.closedProjects = new Set(); // Track closed project IDs
this.STORAGE_KEY = 'claude_ide_closed_projects';
this.PROJECTS_STORAGE_KEY = 'claude_ide_projects'; // Store manually created projects
this.pendingSessionAdd = null; // Track newly created session to preserve it during loadProjects
}
/**
* Initialize the project manager
*/
async initialize() {
if (this.initialized) return;
console.log('[ProjectManager] Initializing...');
this.loadClosedProjects();
this.loadManuallyCreatedProjects(); // Load manually created projects first
await this.loadProjects(); // Then load from sessions
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 manually created projects from localStorage
* CRITICAL FIX: Reset sessions array to avoid stale data from localStorage
* Sessions will be populated by loadProjects() from the API
*/
loadManuallyCreatedProjects() {
try {
const stored = localStorage.getItem(this.PROJECTS_STORAGE_KEY);
console.log('[ProjectManager] Checking localStorage for projects...');
console.log('[ProjectManager] Storage key:', this.PROJECTS_STORAGE_KEY);
console.log('[ProjectManager] Found data:', stored ? 'YES' : 'NO');
if (stored) {
const projectsData = JSON.parse(stored);
console.log('[ProjectManager] Loading', projectsData.length, 'manually created projects from storage');
console.log('[ProjectManager] Projects data:', projectsData);
projectsData.forEach(projectData => {
const projectKey = projectData.id.replace('project-', '');
// CRITICAL FIX: Reset sessions to empty array to avoid stale localStorage data
// Sessions will be populated from API by loadProjects()
const sanitizedProject = {
...projectData,
sessions: [], // Reset sessions - will be loaded from API
activeSessionId: null // Reset active session
};
this.projects.set(projectKey, sanitizedProject);
console.log('[ProjectManager] Loaded project:', projectData.name, 'sessions reset to empty (will load from API)');
});
} else {
console.log('[ProjectManager] No manually created projects found in storage');
}
} catch (error) {
console.error('[ProjectManager] Error loading manually created projects:', error);
}
}
/**
* Save manually created projects to localStorage
*/
saveManuallyCreatedProjects() {
try {
// Only save projects that were manually created (not auto-generated from workingDir)
const manuallyCreatedProjects = Array.from(this.projects.values())
.filter(p => p.manuallyCreated === true);
const dataToStore = JSON.stringify(manuallyCreatedProjects);
localStorage.setItem(this.PROJECTS_STORAGE_KEY, dataToStore);
console.log('[ProjectManager] Saved', manuallyCreatedProjects.length, 'manually created projects to storage');
console.log('[ProjectManager] Stored data:', dataToStore);
} catch (error) {
console.error('[ProjectManager] Error saving manually created projects:', error);
console.error('[ProjectManager] localStorage available:', typeof localStorage !== 'undefined');
console.error('[ProjectManager] Storage key:', this.PROJECTS_STORAGE_KEY);
}
}
/**
* Load closed projects from localStorage
*/
loadClosedProjects() {
try {
const stored = localStorage.getItem(this.STORAGE_KEY);
if (stored) {
const closedIds = JSON.parse(stored);
this.closedProjects = new Set(closedIds);
console.log('[ProjectManager] Loaded', this.closedProjects.size, 'closed projects from storage');
}
} catch (error) {
console.error('[ProjectManager] Error loading closed projects:', error);
this.closedProjects = new Set();
}
}
/**
* Save closed projects to localStorage
*/
saveClosedProjects() {
try {
const closedIds = Array.from(this.closedProjects);
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(closedIds));
console.log('[ProjectManager] Saved', closedIds.length, 'closed projects to storage');
} catch (error) {
console.error('[ProjectManager] Error saving closed projects:', error);
}
}
/**
* Load all sessions and organize them by project
* CRITICAL FIX: Preserve pending session addition to avoid race condition
*/
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'}))
];
// CRITICAL FIX: Preserve pending session if it exists
// This ensures a newly created session is not lost if API doesn't return it yet
if (this.pendingSessionAdd) {
console.log('[ProjectManager] Preserving pending session:', this.pendingSessionAdd.id.substring(0, 8));
// Check if session is already in the API response
const sessionExists = allSessions.some(s => s.id === this.pendingSessionAdd.id);
if (!sessionExists) {
// Add to allSessions so it gets included in the load
allSessions.unshift(this.pendingSessionAdd);
console.log('[ProjectManager] Added pending session to allSessions:', this.pendingSessionAdd.id.substring(0, 8));
}
}
// Group by working directory
// CRITICAL FIX: Handle virtual projects by adding sessions directly to manually created projects
const virtualSessions = []; // Store sessions with virtual workingDirs
const grouped = new Map();
console.log('[ProjectManager] Processing', allSessions.length, 'total sessions');
console.log('[ProjectManager] Existing manually created projects:', Array.from(this.projects.entries()).filter(([k, p]) => p.manuallyCreated).map(([k, p]) => ({key: k, name: p.name, workingDir: p.workingDir})));
allSessions.forEach(session => {
const dir = session.workingDir || 'default';
const projectKey = dir.replace(/\//g, '-').replace(/^-/, '') || 'default';
console.log('[ProjectManager] Processing session:', {
id: session.id.substring(0, 8),
workingDir: dir,
projectKey: projectKey,
project: session.metadata?.project
});
// Check if this is a virtual workingDir
if (dir.startsWith('/virtual/projects/')) {
virtualSessions.push(session);
console.log('[ProjectManager] -> Virtual session, will add to manually created project');
return; // Don't add to grouped, will handle in manually created projects
}
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);
console.log('[ProjectManager] -> Created new project group:', projectKey, projectName);
}
grouped.get(projectKey).sessions.push(session);
console.log('[ProjectManager] -> Added to project group:', projectKey, 'total sessions:', grouped.get(projectKey).sessions.length);
});
console.log('[ProjectManager] Separated', virtualSessions.length, 'virtual sessions and', grouped.size, 'real projects');
// 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;
}
});
// Filter out closed projects
const filtered = new Map();
grouped.forEach((project, key) => {
if (!this.closedProjects.has(project.id)) {
filtered.set(key, project);
}
});
// CRITICAL FIX: Merge with existing manually created projects
// Add virtual sessions to their corresponding manually created projects
const manuallyCreated = Array.from(this.projects.entries())
.filter(([key, p]) => p.manuallyCreated === true);
console.log('[ProjectManager] Processing', manuallyCreated.length, 'manually created projects');
console.log('[ProjectManager] Virtual sessions to assign:', virtualSessions.map(s => ({id: s.id.substring(0, 8), workingDir: s.workingDir, project: s.metadata?.project})));
manuallyCreated.forEach(([key, manualProject]) => {
console.log('[ProjectManager] Processing manually created project:', key, manualProject.name, 'workingDir:', manualProject.workingDir);
if (!filtered.has(key)) {
// Project doesn't exist in filtered, just add it
filtered.set(key, manualProject);
console.log('[ProjectManager] Preserving manually created project:', manualProject.name);
} else {
// Project exists in filtered - for virtual projects, prefer manually created version
if (manualProject.isVirtual) {
// Replace with manually created version (which has correct name, etc.)
filtered.set(key, manualProject);
console.log('[ProjectManager] Replaced with manually created version:', manualProject.name);
}
}
// Add virtual sessions that belong to this project
const projectVirtualSessions = virtualSessions.filter(s => {
const sessionProjectKey = s.workingDir?.replace('/virtual/projects/', '') || '';
console.log('[ProjectManager] Checking if session', s.id.substring(0, 8), 'belongs to project', key, ':', sessionProjectKey, '===', key, '?', sessionProjectKey === key);
return sessionProjectKey === key;
});
if (projectVirtualSessions.length > 0) {
console.log('[ProjectManager] Found', projectVirtualSessions.length, 'virtual sessions for project:', manualProject.name, 'key:', key);
const existingSessionIds = new Set(manualProject.sessions.map(s => s.id));
projectVirtualSessions.forEach(session => {
if (!existingSessionIds.has(session.id)) {
manualProject.sessions.push(session);
console.log('[ProjectManager] Added session', session.id.substring(0, 8), 'to virtual project:', manualProject.name);
}
});
// Sort sessions
manualProject.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;
});
// Update active session
if (manualProject.sessions.length > 0) {
manualProject.activeSessionId = manualProject.sessions[0].id;
}
} else {
console.log('[ProjectManager] No virtual sessions found for project:', manualProject.name);
}
});
this.projects = filtered;
console.log('[ProjectManager] Loaded', this.projects.size, 'projects (filtered out', grouped.size - this.projects.size, 'closed)');
} 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, 'with', project.sessions.length, 'sessions');
this.activeProjectId = project.id;
// Re-render project tabs to update active state
this.renderProjectTabs();
// CRITICAL FIX: Update left sidebar chat history with this project's sessions
if (typeof loadChatHistory === 'function') {
await loadChatHistory(project.sessions);
}
// 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() {
console.log('[ProjectManager] Creating new project...');
// Prompt user for project name
const projectName = prompt('Enter project name:', 'My Project');
if (!projectName || projectName.trim() === '') {
console.log('[ProjectManager] Project creation cancelled');
return;
}
try {
// Create a new session with the project name
// This will automatically create a new project if needed
const workingDir = this.projects.size > 0 ?
Array.from(this.projects.values())[0].workingDir :
'/home/uroma/obsidian-vault';
// Create a unique project key from the project name
const projectKey = projectName.trim().replace(/\s+/g, '-').toLowerCase();
const newProjectId = `project-${projectKey}`;
// CRITICAL FIX: Give each manually created project a unique virtual workingDir
// This prevents sessions from other projects leaking into this project
const virtualWorkingDir = `/virtual/projects/${projectKey}`;
console.log('[ProjectManager] Creating project:', projectName, 'with key:', projectKey, 'and virtual workingDir:', virtualWorkingDir);
// Create the project in memory
if (!this.projects.has(projectKey)) {
this.projects.set(projectKey, {
id: newProjectId,
name: this.deduplicateProjectName(projectName, this.projects),
workingDir: virtualWorkingDir, // Use unique virtual workingDir
sessions: [],
activeSessionId: null,
createdAt: Date.now(),
manuallyCreated: true, // Mark as manually created for persistence
isVirtual: true // Flag to identify virtual projects
});
// CRITICAL FIX: Save to localStorage
this.saveManuallyCreatedProjects();
// Re-render project tabs
this.renderProjectTabs();
// Switch to the new project
await this.switchProject(newProjectId);
// Show success message
if (typeof appendSystemMessage === 'function') {
appendSystemMessage(`✅ Created project "${projectName}"`);
}
console.log('[ProjectManager] Project created successfully:', newProjectId);
} else {
this.showError('Project already exists');
}
} catch (error) {
console.error('[ProjectManager] Error creating project:', error);
this.showError('Failed to create project: ' + error.message);
}
}
/**
* Create a new session in a specific folder
* CRITICAL FIX: Added projectId parameter to associate sessions with their project
*/
async createSessionInFolder(workingDir, projectId = null, projectName = null) {
try {
if (typeof showLoadingOverlay === 'function') {
showLoadingOverlay('Creating session...');
}
// CRITICAL FIX: Add timeout to prevent hanging
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
// CRITICAL FIX: Include project metadata to properly associate session with project
const sessionMetadata = {
type: 'chat',
source: 'web-ide'
};
// Add project info to metadata if provided
if (projectId && projectName) {
sessionMetadata.projectId = projectId;
sessionMetadata.project = projectName;
console.log('[ProjectManager] Creating session in project:', projectName, 'with ID:', projectId);
}
const res = await fetch('/claude/api/claude/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: controller.signal,
body: JSON.stringify({
workingDir,
metadata: sessionMetadata
})
});
clearTimeout(timeoutId); // Clear timeout if request completes
if (!res.ok) {
const errorText = await res.text();
throw new Error(`HTTP ${res.status}: ${errorText}`);
}
const data = await res.json();
if (data.success || data.id) {
// CRITICAL FIX: Extract the session object from response
const session = data.session || data;
// CRITICAL FIX: Normalize status to 'active' for consistency
// Backend returns 'running' but frontend expects 'active' or 'historical'
const normalizedSession = {
...session,
status: session.status === 'running' ? 'active' : session.status
};
// CRITICAL FIX: Store pending session to preserve it during loadProjects
this.pendingSessionAdd = normalizedSession;
console.log('[ProjectManager] Session created successfully, adding to project:', normalizedSession.id);
console.log('[ProjectManager] Session data:', {
id: normalizedSession.id,
workingDir: normalizedSession.workingDir,
metadata: normalizedSession.metadata,
status: normalizedSession.status
});
// Add session to project immediately
this.addSessionToProject(normalizedSession);
// CRITICAL FIX: Refresh from API to ensure consistency, but the pending session
// will be preserved if not in API response yet
await this.loadProjects();
this.renderProjectTabs();
// Clear pending session after loadProjects completes
this.pendingSessionAdd = null;
// Find the project containing this session and switch to it
const targetProject = this.getProjectForSession(normalizedSession.id);
if (targetProject) {
console.log('[ProjectManager] Found session in project:', targetProject.name);
await this.switchProject(targetProject.id);
} else {
console.warn('[ProjectManager] Could not find project for session:', normalizedSession.id);
}
if (typeof hideLoadingOverlay === 'function') {
hideLoadingOverlay();
}
}
} catch (error) {
console.error('[ProjectManager] Error creating session:', error);
if (typeof hideLoadingOverlay === 'function') {
hideLoadingOverlay();
}
// Clear pending session on error
this.pendingSessionAdd = null;
// Special handling for timeout/abort errors
if (error.name === 'AbortError') {
this.showError('Request timed out. The server took too long to respond. Please try again.');
} else {
this.showError('Failed to create session: ' + error.message);
}
}
}
/**
* Create a new session in the current project
* CRITICAL FIX: Pass project info to ensure sessions stay in their project
*/
async createNewSessionInProject(projectId) {
const project = this.projects.get(projectId.replace('project-', ''));
if (!project) return;
// CRITICAL FIX: Pass project ID and name to associate session with this project
await this.createSessionInFolder(project.workingDir, project.id, project.name);
}
/**
* 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
* CRITICAL FIX: Handle virtual working directories correctly
*/
addSessionToProject(session) {
const dir = session.workingDir || 'default';
let projectKey;
// CRITICAL FIX: Handle virtual working directories specially
// Virtual workingDir format: /virtual/projects/{projectKey}
if (dir.startsWith('/virtual/projects/')) {
// Extract the projectKey from the virtual workingDir
projectKey = dir.replace('/virtual/projects/', '');
console.log('[ProjectManager] Session has virtual workingDir, extracted projectKey:', projectKey, 'from', dir);
} else {
// Standard workingDir - convert path to projectKey
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);
// Check if session already exists in the project
const existingIndex = project.sessions.findIndex(s => s.id === session.id);
if (existingIndex === -1) {
// Add to beginning (newest first)
project.sessions.unshift(session);
console.log('[ProjectManager] Added session', session.id.substring(0, 8), 'to project:', project.name, 'key:', projectKey, 'total sessions:', project.sessions.length);
} else {
// Move to beginning if already exists
project.sessions.splice(existingIndex, 1);
project.sessions.unshift(session);
console.log('[ProjectManager] Moved existing session', session.id.substring(0, 8), 'to top of project:', project.name);
}
project.activeSessionId = session.id;
// Re-render if this is the active project
if (this.activeProjectId === project.id) {
this.renderProjectTabs();
// CRITICAL FIX: Update left sidebar chat history with this project's sessions
// CRITICAL FIX: Use await to ensure the UI updates before continuing
if (typeof loadChatHistory === 'function') {
loadChatHistory(project.sessions).catch(err => {
console.error('[ProjectManager] Error loading chat history:', err);
});
}
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);
// Add to closed projects set and persist
this.closedProjects.add(projectId);
this.saveClosedProjects();
// 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');