- 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>
471 lines
18 KiB
JavaScript
471 lines
18 KiB
JavaScript
/**
|
||
* 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;
|
||
|
||
console.log('[SessionPicker] initialize() called');
|
||
if (window.traceExecution) {
|
||
window.traceExecution('session-picker', 'initialize() called', { pathname: window.location.pathname });
|
||
}
|
||
|
||
// ============================================================
|
||
// FIRST: Check URL path for session ID (route-based: /claude/ide/session/XXX)
|
||
// This is the PRIMARY method for session attachment
|
||
// ============================================================
|
||
const pathname = window.location.pathname;
|
||
const pathMatch = pathname.match(/\/claude\/ide\/session\/([^\/]+)$/);
|
||
|
||
if (pathMatch && pathMatch[1]) {
|
||
const sessionId = pathMatch[1];
|
||
console.log('[SessionPicker] Session ID in URL path, NOT showing picker:', sessionId);
|
||
console.log('[SessionPicker] ide.js will handle attachment');
|
||
if (window.traceExecution) {
|
||
window.traceExecution('session-picker', 'URL path has session ID, NOT showing picker', { sessionId, pathname });
|
||
}
|
||
this.initialized = true;
|
||
return; // Don't show picker, let ide.js handle it
|
||
}
|
||
|
||
// ============================================================
|
||
// SECOND: Check URL params (legacy format: ?session=XXX)
|
||
// ============================================================
|
||
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);
|
||
if (window.traceExecution) {
|
||
window.traceExecution('session-picker', 'Loading session from query param', { sessionId });
|
||
}
|
||
await this.loadSession(sessionId);
|
||
this.initialized = true;
|
||
return;
|
||
}
|
||
|
||
if (project) {
|
||
// Create or load session for project
|
||
console.log('[SessionPicker] Project context:', project);
|
||
if (window.traceExecution) {
|
||
window.traceExecution('session-picker', 'Project context', { project });
|
||
}
|
||
await this.ensureSessionForProject(project);
|
||
this.initialized = true;
|
||
return;
|
||
}
|
||
|
||
// No session or project - show picker
|
||
console.log('[SessionPicker] No session found, showing picker modal');
|
||
if (window.traceExecution) {
|
||
window.traceExecution('session-picker', 'SHOWING PICKER MODAL', { pathname, search: window.location.search });
|
||
}
|
||
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 };
|
||
}
|