/**
* 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 = `
Loading recent sessions...
`;
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 };
}