/** * Session Picker Modal * Shows when user clicks a project - allows resuming or creating sessions * Following CodeNomad's design pattern */ (function() { 'use strict'; let currentModal = null; let currentProject = null; /** * Show session picker modal for a project * @param {Object} project - Project object */ async function showSessionPicker(project) { // Close existing modal if open closeSessionPicker(); currentProject = project; // Create modal overlay const overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.id = 'session-picker-overlay'; // Create modal content const modal = document.createElement('div'); modal.className = 'session-picker-modal'; modal.id = 'session-picker-modal'; modal.innerHTML = `

Claude Code • ${escapeHtml(project.name)}

Loading sessions...
`; overlay.appendChild(modal); document.body.appendChild(overlay); // Prevent body scroll document.body.style.overflow = 'hidden'; // Trigger animation setTimeout(() => { overlay.classList.add('visible'); modal.classList.add('visible'); }, 10); // Load sessions await loadSessionsForProject(project.id); // Close on overlay click overlay.addEventListener('click', (e) => { if (e.target === overlay) { closeSessionPicker(); } }); // Close on Escape key const escapeHandler = (e) => { if (e.key === 'Escape') { closeSessionPicker(); document.removeEventListener('keydown', escapeHandler); } }; document.addEventListener('keydown', escapeHandler); currentModal = { overlay, escapeHandler }; } /** * Load sessions for a project */ async function loadSessionsForProject(projectId) { const content = document.getElementById('session-picker-content'); try { console.log('[SessionPicker] Loading sessions for project:', projectId); const res = await fetch(`/api/projects/${projectId}/sessions`, { credentials: 'same-origin' }); console.log('[SessionPicker] Response status:', res.status); if (!res.ok) { throw new Error(`Failed to load sessions (HTTP ${res.status})`); } const data = await res.json(); console.log('[SessionPicker] Response data:', data); if (data.sessions && data.sessions.length > 0) { renderSessionList(data.sessions); } else { renderEmptyState(); } } catch (error) { console.error('[SessionPicker] Error loading sessions:', error); console.error('[SessionPicker] Error details:', { message: error.message, stack: error.stack, projectId: projectId }); content.innerHTML = `
Failed to load sessions. Please try again.
`; } } /** * Render list of sessions */ function renderSessionList(sessions) { const content = document.getElementById('session-picker-content'); const sessionsHtml = sessions.map(session => { const title = session.title || session.metadata?.project || 'Untitled Session'; const relativeTime = getRelativeTime(session.updatedAt || session.created_at || session.lastActivity); const agent = session.agent || session.metadata?.agent || 'claude'; return ` `; }).join(''); content.innerHTML = `

Resume a session (${sessions.length}):

${sessionsHtml}
or

Start new session:

`; } /** * Render empty state (no existing sessions) */ function renderEmptyState() { const content = document.getElementById('session-picker-content'); content.innerHTML = `
💬

No previous sessions

Start a new conversation in this project

or

Start new session:

`; } /** * Resume an existing session */ async function resumeSession(sessionId) { if (!currentProject) return; try { showLoadingOverlay('Opening workspace...'); // Navigate to IDE with session await new Promise(resolve => setTimeout(resolve, 300)); window.location.href = `/claude/ide?session=${sessionId}`; } catch (error) { console.error('Error resuming session:', error); hideLoadingOverlay(); showToast('Failed to open session', 'error'); } } /** * Create a new session in the project */ async function createNewSession() { if (!currentProject) return; try { showLoadingOverlay('Creating session...'); const res = await fetch(`/api/projects/${currentProject.id}/sessions`, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ metadata: { type: 'chat', source: 'web-ide', project: currentProject.name } }) }); if (!res.ok) { throw new Error('Failed to create session'); } const data = await res.json(); if (data.success && data.session) { await new Promise(resolve => setTimeout(resolve, 300)); window.location.href = `/claude/ide?session=${data.session.id}`; } else { throw new Error(data.error || 'Failed to create session'); } } catch (error) { console.error('Error creating session:', error); hideLoadingOverlay(); showToast('Failed to create session', 'error'); } } /** * Close the modal */ function closeSessionPicker() { const overlay = document.getElementById('session-picker-overlay'); if (!overlay) return; overlay.classList.remove('visible'); const modal = document.getElementById('session-picker-modal'); if (modal) modal.classList.remove('visible'); setTimeout(() => { if (currentModal && currentModal.escapeHandler) { document.removeEventListener('keydown', currentModal.escapeHandler); } overlay.remove(); document.body.style.overflow = ''; currentModal = null; currentProject = null; }, 300); } /** * Get relative time string */ function getRelativeTime(timestamp) { const date = new Date(timestamp); const now = new Date(); const diffMins = Math.floor((now - date) / 60000); const diffHours = Math.floor((now - date) / 3600000); const diffDays = Math.floor((now - date) / 86400000); if (diffMins < 1) return 'just now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString(); } /** * Escape HTML to prevent XSS */ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Export to global scope window.SessionPicker = { show: showSessionPicker, close: closeSessionPicker, resumeSession, createNewSession }; // Add CSS styles const style = document.createElement('style'); style.textContent = ` /* Modal Overlay */ .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(4px); z-index: 10000; display: flex; align-items: center; justify-content: center; padding: 20px; opacity: 0; transition: opacity 0.2s ease; } .modal-overlay.visible { opacity: 1; } /* Session Picker Modal */ .session-picker-modal { background: #1a1a1a; border: 1px solid #333; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); width: 100%; max-width: 600px; max-height: 80vh; overflow: hidden; display: flex; flex-direction: column; transform: scale(0.95); transition: transform 0.2s ease; } .session-picker-modal.visible { transform: scale(1); } /* Header */ .session-picker-header { padding: 24px; border-bottom: 1px solid #333; display: flex; justify-content: space-between; align-items: center; } .session-picker-title { font-size: 20px; font-weight: 600; color: #e0e0e0; margin: 0; } .session-picker-close { background: none; border: none; color: #888; font-size: 28px; cursor: pointer; padding: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 8px; transition: all 0.2s ease; } .session-picker-close:hover { background: #252525; color: #e0e0e0; } /* Content */ .session-picker-content { padding: 24px; overflow-y: auto; flex: 1; } /* Session Section */ .session-section { margin-bottom: 24px; } .session-section-title { font-size: 14px; font-weight: 600; color: #888; text-transform: uppercase; letter-spacing: 0.5px; margin: 0 0 12px 0; } /* Session List */ .session-list { display: flex; flex-direction: column; gap: 8px; max-height: 400px; overflow-y: auto; } .session-item { width: 100%; text-align: left; padding: 16px; background: #1a1a1a; border: 1px solid #333; border-radius: 12px; cursor: pointer; transition: all 0.2s ease; } .session-item:hover { background: #252525; border-color: #4a9eff; } .session-item-content { display: flex; justify-content: space-between; align-items: center; gap: 12px; } .session-item-title { font-size: 15px; font-weight: 500; color: #e0e0e0; flex: 1; } .session-item-meta { display: flex; align-items: center; gap: 8px; flex-shrink: 0; } .session-agent { font-size: 12px; color: #888; background: #252525; padding: 4px 8px; border-radius: 6px; } .session-time { font-size: 12px; color: #888; } /* Divider */ .session-divider { position: relative; margin: 24px 0; } .session-divider::before { content: ''; position: absolute; top: 50%; left: 0; right: 0; height: 1px; background: #333; } .session-divider-text { position: relative; display: block; text-align: center; font-size: 14px; color: #888; background: #1a1a1a; padding: 0 12px; } /* New Session Form */ .new-session-form { display: flex; flex-direction: column; gap: 12px; } /* Buttons */ .btn-primary { width: 100%; padding: 14px 20px; background: linear-gradient(135deg, #4a9eff 0%, #a78bfa 100%); border: none; border-radius: 12px; color: white; font-size: 15px; font-weight: 600; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; transition: all 0.2s ease; } .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(74, 158, 255, 0.4); } .btn-primary:active { transform: translateY(0); } .btn-icon { font-size: 18px; line-height: 1; } .btn-secondary { padding: 12px 24px; background: transparent; border: 1px solid #333; border-radius: 8px; color: #e0e0e0; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; } .btn-secondary:hover { background: #252525; border-color: #4a9eff; } /* Keyboard Hint */ .kbd { background: #252525; border: 1px solid #444; border-radius: 6px; padding: 4px 8px; font-size: 12px; font-family: monospace; color: #888; margin-left: auto; } /* Empty State */ .session-empty-state { text-align: center; padding: 40px 20px; } .empty-state-icon { font-size: 48px; margin-bottom: 16px; } .empty-state-title { font-size: 18px; font-weight: 600; color: #e0e0e0; margin: 0 0 8px 0; } .empty-state-subtitle { font-size: 14px; color: #888; margin: 0; } /* Loading State */ .session-picker-loading { text-align: center; padding: 40px 20px; color: #888; } /* Error State */ .session-picker-error { text-align: center; padding: 40px 20px; color: #ff6b6b; } /* Footer */ .session-picker-footer { padding: 16px 24px; border-top: 1px solid #333; display: flex; justify-content: flex-end; } /* Responsive */ @media (max-width: 640px) { .session-picker-modal { max-height: 90vh; } .session-picker-header, .session-picker-content, .session-picker-footer { padding: 16px; } .session-item { padding: 12px; } .session-item-content { flex-direction: column; align-items: flex-start; } .session-item-meta { width: 100%; justify-content: space-between; } } `; document.head.appendChild(style); console.log('[SessionPicker] Module loaded'); })();