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>
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
/**
|
||||
* Session Picker Component
|
||||
* Show modal on startup to select existing session or create new
|
||||
*
|
||||
* Features:
|
||||
* - Session picker modal on startup
|
||||
* - Recent sessions list
|
||||
* - Sessions grouped by project
|
||||
* - Create new session
|
||||
* - Session forking support
|
||||
*/
|
||||
|
||||
class SessionPicker {
|
||||
constructor() {
|
||||
this.modal = null;
|
||||
this.sessions = [];
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
if (this.initialized) return;
|
||||
|
||||
// Check URL params first
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const sessionId = urlParams.get('session');
|
||||
const project = urlParams.get('project');
|
||||
|
||||
if (sessionId) {
|
||||
// Load specific session
|
||||
console.log('[SessionPicker] Loading session from URL:', sessionId);
|
||||
await this.loadSession(sessionId);
|
||||
this.initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (project) {
|
||||
// Create or load session for project
|
||||
console.log('[SessionPicker] Project context:', project);
|
||||
await this.ensureSessionForProject(project);
|
||||
this.initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// No session or project - show picker
|
||||
await this.showPicker();
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
async showPicker() {
|
||||
// Create modal
|
||||
this.modal = document.createElement('div');
|
||||
this.modal.className = 'session-picker-modal';
|
||||
this.modal.innerHTML = `
|
||||
<div class="session-picker-content">
|
||||
<div class="picker-header">
|
||||
<h2>Select a Session</h2>
|
||||
<button class="btn-close" onclick="window.sessionPicker.close()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="picker-tabs">
|
||||
<button class="picker-tab active" data-tab="recent" onclick="window.sessionPicker.switchTab('recent')">
|
||||
<span class="tab-icon">🕐</span>
|
||||
<span class="tab-label">Recent</span>
|
||||
</button>
|
||||
<button class="picker-tab" data-tab="projects" onclick="window.sessionPicker.switchTab('projects')">
|
||||
<span class="tab-icon">📁</span>
|
||||
<span class="tab-label">Projects</span>
|
||||
</button>
|
||||
<button class="picker-tab" data-tab="new" onclick="window.sessionPicker.switchTab('new')">
|
||||
<span class="tab-icon">➕</span>
|
||||
<span class="tab-label">New Session</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="picker-body">
|
||||
<div id="picker-recent" class="picker-tab-content active">
|
||||
<div class="loading">Loading recent sessions...</div>
|
||||
</div>
|
||||
<div id="picker-projects" class="picker-tab-content">
|
||||
<div class="loading">Loading projects...</div>
|
||||
</div>
|
||||
<div id="picker-new" class="picker-tab-content">
|
||||
<div class="new-session-form">
|
||||
<div class="form-group">
|
||||
<label>Session Name</label>
|
||||
<input type="text" id="new-session-name" placeholder="My Session" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Project (optional)</label>
|
||||
<input type="text" id="new-session-project" placeholder="my-project" />
|
||||
</div>
|
||||
<button class="btn-primary btn-block" onclick="window.sessionPicker.createNewSession()">
|
||||
Create Session
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(this.modal);
|
||||
document.body.style.overflow = 'hidden'; // Prevent scrolling
|
||||
|
||||
// Load recent sessions
|
||||
await this.loadRecentSessions();
|
||||
await this.loadProjects();
|
||||
}
|
||||
|
||||
async loadRecentSessions() {
|
||||
const container = document.getElementById('picker-recent');
|
||||
|
||||
try {
|
||||
const response = await fetch('/claude/api/claude/sessions');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.sessions = data.sessions || [];
|
||||
|
||||
if (this.sessions.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">💬</div>
|
||||
<h3>No sessions yet</h3>
|
||||
<p>Create a new session to get started</p>
|
||||
<button class="btn-primary" onclick="window.sessionPicker.switchTab('new')">
|
||||
Create Session
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by last modified
|
||||
this.sessions.sort((a, b) => {
|
||||
const dateA = new Date(a.modified || a.created);
|
||||
const dateB = new Date(b.modified || b.created);
|
||||
return dateB - dateA;
|
||||
});
|
||||
|
||||
// Show last 10 sessions
|
||||
const recentSessions = this.sessions.slice(0, 10);
|
||||
|
||||
container.innerHTML = recentSessions.map(session => {
|
||||
const date = new Date(session.modified || session.created);
|
||||
const timeAgo = this.formatTimeAgo(date);
|
||||
const title = session.title || session.id;
|
||||
const project = session.project || 'General';
|
||||
|
||||
return `
|
||||
<div class="session-item" onclick="window.sessionPicker.selectSession('${session.id}')">
|
||||
<div class="session-icon">💬</div>
|
||||
<div class="session-info">
|
||||
<div class="session-title">${this.escapeHtml(title)}</div>
|
||||
<div class="session-meta">
|
||||
<span class="session-project">${this.escapeHtml(project)}</span>
|
||||
<span class="session-time">${timeAgo}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-arrow">→</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[SessionPicker] Failed to load sessions:', error);
|
||||
container.innerHTML = `
|
||||
<div class="error-state">
|
||||
<h3>Failed to load sessions</h3>
|
||||
<p>${error.message}</p>
|
||||
<button class="btn-secondary" onclick="window.sessionPicker.loadRecentSessions()">
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async loadProjects() {
|
||||
const container = document.getElementById('picker-projects');
|
||||
|
||||
try {
|
||||
// Use the sessions endpoint to get projects
|
||||
const response = await fetch('/claude/api/claude/sessions');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Group sessions by project
|
||||
const projectMap = new Map();
|
||||
const allSessions = [
|
||||
...(data.active || []),
|
||||
...(data.historical || [])
|
||||
];
|
||||
|
||||
allSessions.forEach(session => {
|
||||
const projectName = session.metadata?.project || session.workingDir?.split('/').pop() || 'Untitled';
|
||||
if (!projectMap.has(projectName)) {
|
||||
projectMap.set(projectName, {
|
||||
name: projectName,
|
||||
sessionCount: 0,
|
||||
lastSession: session
|
||||
});
|
||||
}
|
||||
const project = projectMap.get(projectName);
|
||||
project.sessionCount++;
|
||||
});
|
||||
|
||||
const projects = Array.from(projectMap.values());
|
||||
|
||||
if (projects.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">📁</div>
|
||||
<h3>No projects yet</h3>
|
||||
<p>Create a new project to organize your sessions</p>
|
||||
<button class="btn-primary" onclick="window.sessionPicker.switchTab('new')">
|
||||
New Session
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by session count (most used first)
|
||||
projects.sort((a, b) => b.sessionCount - a.sessionCount);
|
||||
|
||||
container.innerHTML = projects.map(project => {
|
||||
const sessionCount = project.sessionCount || 0;
|
||||
return `
|
||||
<div class="project-item" onclick="window.sessionPicker.selectProject('${this.escapeHtml(project.name)}')">
|
||||
<div class="project-icon">📁</div>
|
||||
<div class="project-info">
|
||||
<div class="project-name">${this.escapeHtml(project.name)}</div>
|
||||
<div class="project-meta">${sessionCount} session${sessionCount !== 1 ? 's' : ''}</div>
|
||||
</div>
|
||||
<div class="project-arrow">→</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[SessionPicker] Failed to load projects:', error);
|
||||
container.innerHTML = `
|
||||
<div class="error-state">
|
||||
<h3>Failed to load projects</h3>
|
||||
<p>${error.message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async selectSession(sessionId) {
|
||||
await this.loadSession(sessionId);
|
||||
this.close();
|
||||
}
|
||||
|
||||
async selectProject(projectName) {
|
||||
await this.ensureSessionForProject(projectName);
|
||||
this.close();
|
||||
}
|
||||
|
||||
async loadSession(sessionId) {
|
||||
try {
|
||||
const response = await fetch(`/claude/api/claude/sessions/${sessionId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const session = await response.json();
|
||||
|
||||
// Attach to session
|
||||
if (typeof attachToSession === 'function') {
|
||||
attachToSession(sessionId);
|
||||
}
|
||||
|
||||
console.log('[SessionPicker] Loaded session:', sessionId);
|
||||
return session;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[SessionPicker] Failed to load session:', error);
|
||||
if (typeof showToast === 'function') {
|
||||
showToast(`Failed to load session: ${error.message}`, 'error', 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async ensureSessionForProject(projectName) {
|
||||
try {
|
||||
// Check if session exists for this project
|
||||
const response = await fetch('/claude/api/claude/sessions');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const sessions = data.sessions || [];
|
||||
|
||||
const projectSession = sessions.find(s => s.project === projectName);
|
||||
|
||||
if (projectSession) {
|
||||
return await this.loadSession(projectSession.id);
|
||||
}
|
||||
|
||||
// Create new session for project
|
||||
return await this.createNewSession(projectName);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[SessionPicker] Failed to ensure session:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async createNewSession(projectName = null) {
|
||||
const nameInput = document.getElementById('new-session-name');
|
||||
const projectInput = document.getElementById('new-session-project');
|
||||
|
||||
const name = nameInput?.value || projectName || 'Untitled Session';
|
||||
const project = projectInput?.value || projectName || '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/claude/api/claude/sessions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: name,
|
||||
project: project,
|
||||
source: 'web-ide'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const session = await response.json();
|
||||
|
||||
// Attach to new session
|
||||
if (typeof attachToSession === 'function') {
|
||||
attachToSession(session.id);
|
||||
}
|
||||
|
||||
console.log('[SessionPicker] Created session:', session.id);
|
||||
this.close();
|
||||
return session;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[SessionPicker] Failed to create session:', error);
|
||||
if (typeof showToast === 'function') {
|
||||
showToast(`Failed to create session: ${error.message}`, 'error', 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switchTab(tabName) {
|
||||
// Update tab buttons
|
||||
this.modal.querySelectorAll('.picker-tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
if (tab.dataset.tab === tabName) {
|
||||
tab.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Update tab content
|
||||
this.modal.querySelectorAll('.picker-tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
|
||||
const activeContent = document.getElementById(`picker-${tabName}`);
|
||||
if (activeContent) {
|
||||
activeContent.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.modal) {
|
||||
this.modal.remove();
|
||||
this.modal = null;
|
||||
}
|
||||
document.body.style.overflow = ''; // Restore scrolling
|
||||
}
|
||||
|
||||
formatTimeAgo(date) {
|
||||
const seconds = Math.floor((new Date() - date) / 1000);
|
||||
|
||||
if (seconds < 60) {
|
||||
return 'Just now';
|
||||
} else if (seconds < 3600) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
return `${minutes}m ago`;
|
||||
} else if (seconds < 86400) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
return `${hours}h ago`;
|
||||
} else if (seconds < 604800) {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
return `${days}d ago`;
|
||||
} else {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
// Global instance
|
||||
let sessionPicker = null;
|
||||
|
||||
// Auto-initialize
|
||||
if (typeof window !== 'undefined') {
|
||||
window.SessionPicker = SessionPicker;
|
||||
|
||||
// Create instance
|
||||
sessionPicker = new SessionPicker();
|
||||
window.sessionPicker = sessionPicker;
|
||||
|
||||
// Initialize on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
sessionPicker.initialize();
|
||||
});
|
||||
} else {
|
||||
sessionPicker.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other scripts
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { SessionPicker };
|
||||
}
|
||||
Reference in New Issue
Block a user