/** * 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 = `

Select a Session

Loading recent sessions...
Loading projects...
`; 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 = `
💬

No sessions yet

Create a new session to get started

`; 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 `
💬
${this.escapeHtml(title)}
${this.escapeHtml(project)} ${timeAgo}
`; }).join(''); } catch (error) { console.error('[SessionPicker] Failed to load sessions:', error); container.innerHTML = `

Failed to load sessions

${error.message}

`; } } 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 = `
📁

No projects yet

Create a new project to organize your sessions

`; 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 `
📁
${this.escapeHtml(project.name)}
${sessionCount} session${sessionCount !== 1 ? 's' : ''}
`; }).join(''); } catch (error) { console.error('[SessionPicker] Failed to load projects:', error); container.innerHTML = `

Failed to load projects

${error.message}

`; } } 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 }; }