- Modified addSessionToProject to correctly extract projectKey from virtual workingDirs
- Virtual workingDir format: /virtual/projects/{projectKey}
- Previously was converting slashes to dashes, causing mismatch
- Added console logging to track session-to-project assignment
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
793 lines
31 KiB
JavaScript
793 lines
31 KiB
JavaScript
/**
|
||
* 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
|
||
}
|
||
|
||
/**
|
||
* 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
|
||
*/
|
||
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-', '');
|
||
this.projects.set(projectKey, projectData);
|
||
console.log('[ProjectManager] Loaded project:', projectData.name, 'with', projectData.sessions.length, 'sessions');
|
||
});
|
||
} 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
|
||
*/
|
||
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
|
||
// 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');
|
||
|
||
allSessions.forEach(session => {
|
||
const dir = session.workingDir || 'default';
|
||
const projectKey = dir.replace(/\//g, '-').replace(/^-/, '') || 'default';
|
||
|
||
// Check if this is a virtual workingDir
|
||
if (dir.startsWith('/virtual/projects/')) {
|
||
virtualSessions.push(session);
|
||
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);
|
||
}
|
||
|
||
grouped.get(projectKey).sessions.push(session);
|
||
});
|
||
|
||
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);
|
||
|
||
manuallyCreated.forEach(([key, manualProject]) => {
|
||
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);
|
||
}
|
||
}
|
||
|
||
// Add virtual sessions that belong to this project
|
||
const projectVirtualSessions = virtualSessions.filter(s => {
|
||
const sessionProjectKey = s.workingDir?.replace('/virtual/projects/', '') || '';
|
||
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, '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;
|
||
}
|
||
}
|
||
});
|
||
|
||
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) {
|
||
// Reload projects and switch to new session
|
||
await this.loadProjects();
|
||
await this.initialize();
|
||
|
||
// Find the new session and switch to it
|
||
const session = data.session || data;
|
||
for (const project of this.projects.values()) {
|
||
const foundSession = project.sessions.find(s => s.id === session.id);
|
||
if (foundSession) {
|
||
this.switchProject(project.id);
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (typeof hideLoadingOverlay === 'function') {
|
||
hideLoadingOverlay();
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('[ProjectManager] Error creating session:', error);
|
||
if (typeof hideLoadingOverlay === 'function') {
|
||
hideLoadingOverlay();
|
||
}
|
||
|
||
// 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);
|
||
project.sessions.unshift(session); // Add to beginning
|
||
project.activeSessionId = session.id;
|
||
|
||
console.log('[ProjectManager] Added session', session.id.substring(0, 8), 'to project:', project.name, 'key:', projectKey, 'total sessions:', project.sessions.length);
|
||
|
||
// 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
|
||
if (typeof loadChatHistory === 'function') {
|
||
loadChatHistory(project.sessions);
|
||
}
|
||
|
||
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');
|