Files
SuperCharged-Claude-Code-Up…/public/claude-ide/project-manager.js
uroma 55aafbae9a Fix project isolation: Make loadChatHistory respect active project sessions
- Modified loadChatHistory() to check for active project before fetching all sessions
- When active project exists, use project.sessions instead of fetching from API
- Added detailed console logging to debug session filtering
- This prevents ALL sessions from appearing in every project's sidebar

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 14:43:05 +00:00

779 lines
30 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
}
/**
* 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
*/
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();
// 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');