diff --git a/public/claude-ide/sessions-landing.css b/public/claude-ide/sessions-landing.css index cae2a69f..67b081bd 100644 --- a/public/claude-ide/sessions-landing.css +++ b/public/claude-ide/sessions-landing.css @@ -473,3 +473,138 @@ body.sessions-page { max-width: none; } } + +/** + * Project Section Styles + */ +.project-section { + border-bottom: 1px solid #333; +} + +.project-section:last-child { + border-bottom: none; +} + +/* Project Section Header */ +.project-section-header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + background: #151515; + cursor: pointer; + user-select: none; + transition: background 0.2s ease; +} + +.project-section-header:hover { + background: #1a1a1a; +} + +.project-section-toggle { + font-size: 12px; + color: #888; + transition: transform 0.2s ease; + flex-shrink: 0; +} + +.project-section-icon { + font-size: 20px; + flex-shrink: 0; +} + +.project-section-name { + font-size: 15px; + font-weight: 600; + color: #e0e0e0; + flex: 1; +} + +.project-section-count { + font-size: 13px; + color: #888; + font-weight: 500; + flex-shrink: 0; +} + +/* Project Section Sessions Container */ +.project-section-sessions { + display: block; + transition: all 0.3s ease; +} + +.project-section.collapsed .project-section-sessions { + display: none; +} + +/* Session Rows within Project Sections */ +.session-row { + border-bottom: 1px solid #252525; + transition: background 0.2s ease; + cursor: pointer; +} + +.session-row:last-child { + border-bottom: none; +} + +.session-row:hover { + background: #252525; +} + +/* Session Project Badge */ +.session-project-badge { + display: inline-block; + margin-top: 4px; + padding: 3px 8px; + background: rgba(74, 158, 255, 0.15); + color: #4a9eff; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Unassigned Section Special Styling */ +.unassigned-section .project-section-header { + background: #1a1a1a; + border-top: 1px solid #333; +} + +.unassigned-section .project-section-header:hover { + background: #222; +} + +/* Collapsed state */ +.project-section.collapsed .project-section-toggle { + transform: rotate(-90deg); +} + +/* Responsive Design for Project Sections */ +@media (max-width: 768px) { + .project-section-header { + padding: 14px 16px; + } + + .project-section-toggle { + font-size: 11px; + } + + .project-section-icon { + font-size: 18px; + } + + .project-section-name { + font-size: 14px; + } + + .project-section-count { + font-size: 12px; + } + + .session-project-badge { + font-size: 10px; + padding: 2px 6px; + } +} diff --git a/public/claude-ide/sessions-landing.js b/public/claude-ide/sessions-landing.js index d508834b..6175f407 100644 --- a/public/claude-ide/sessions-landing.js +++ b/public/claude-ide/sessions-landing.js @@ -6,7 +6,7 @@ document.addEventListener('DOMContentLoaded', () => { checkAuth(); initializeProjectInput(); - loadSessions(); + loadSessionsAndProjects(); }); // Check authentication @@ -100,9 +100,9 @@ async function createProject(projectName) { } /** - * Load all sessions and render in table + * Load all sessions and projects, then render grouped by project */ -async function loadSessions() { +async function loadSessionsAndProjects() { const tbody = document.getElementById('projects-tbody'); const emptyState = document.getElementById('projects-empty'); const table = document.getElementById('projects-table'); @@ -112,42 +112,54 @@ async function loadSessions() { tbody.innerHTML = 'Loading...'; try { - const res = await fetch('/claude/api/claude/sessions'); + // Fetch both sessions and projects in parallel + const [sessionsRes, projectsRes] = await Promise.all([ + fetch('/claude/api/claude/sessions'), + fetch('/api/projects') + ]); - if (!res.ok) throw new Error('Request failed'); + if (!sessionsRes.ok) throw new Error('Failed to load sessions'); + if (!projectsRes.ok) throw new Error('Failed to load projects'); - const data = await res.json(); + const sessionsData = await sessionsRes.json(); + const projectsData = await projectsRes.json(); + // Combine active and historical sessions const allSessions = [ - ...(data.active || []).map(s => ({...s, type: 'active'})), - ...(data.historical || []).map(s => ({...s, type: 'historical'})) + ...(sessionsData.active || []).map(s => ({...s, type: 'active'})), + ...(sessionsData.historical || []).map(s => ({...s, type: 'historical'})) ]; - renderProjectsTable(allSessions); + // Store projects in global map for quick lookup + window.projectsMap = new Map( + (projectsData.projects || []).map(p => [p.id, p]) + ); + + renderSessionsGroupedByProject(allSessions); } catch (error) { - console.error('Error loading sessions:', error); + console.error('Error loading sessions and projects:', error); tbody.innerHTML = 'Failed to load'; } } /** - * Render projects table with session data + * Load all sessions and render in table (legacy function for backward compatibility) */ -function renderProjectsTable(sessions) { +async function loadSessions() { + await loadSessionsAndProjects(); +} + +/** + * Render sessions grouped by project + */ +function renderSessionsGroupedByProject(sessions) { const tbody = document.getElementById('projects-tbody'); const emptyState = document.getElementById('projects-empty'); const table = document.getElementById('projects-table'); if (!tbody) return; - // Sort by last activity (newest first) - sessions.sort((a, b) => { - const dateA = new Date(a.lastActivity || a.createdAt || a.created_at); - const dateB = new Date(b.lastActivity || b.createdAt || b.created_at); - return dateB - dateA; - }); - tbody.innerHTML = ''; if (sessions.length === 0) { @@ -159,17 +171,148 @@ function renderProjectsTable(sessions) { if (table) table.style.display = 'table'; if (emptyState) emptyState.style.display = 'none'; + // Group sessions by projectId + const grouped = { + byProject: new Map(), // projectId -> {project, sessions: []} + unassigned: [] // sessions without projectId + }; + sessions.forEach(session => { - const row = createProjectRow(session); - tbody.appendChild(row); + const projectId = session.metadata?.projectId; + + if (projectId && window.projectsMap?.has(projectId)) { + const project = window.projectsMap.get(projectId); + + if (!grouped.byProject.has(projectId)) { + grouped.byProject.set(projectId, { + project: project, + sessions: [] + }); + } + + grouped.byProject.get(projectId).sessions.push(session); + } else { + grouped.unassigned.push(session); + } }); + + // Sort sessions within each group by last activity + grouped.byProject.forEach(group => { + group.sessions.sort((a, b) => { + const dateA = new Date(a.lastActivity || a.createdAt || a.created_at); + const dateB = new Date(b.lastActivity || b.createdAt || b.created_at); + return dateB - dateA; + }); + }); + + grouped.unassigned.sort((a, b) => { + const dateA = new Date(a.lastActivity || a.createdAt || a.created_at); + const dateB = new Date(b.lastActivity || b.createdAt || b.created_at); + return dateB - dateA; + }); + + // Render project sections + grouped.byProject.forEach((group, projectId) => { + const section = createProjectSection(group.project, group.sessions); + tbody.appendChild(section); + }); + + // Render unassigned sessions if any + if (grouped.unassigned.length > 0) { + const section = createUnassignedSection(grouped.unassigned); + tbody.appendChild(section); + } } /** - * Create a single project row for the table + * Render projects table with session data (legacy function for backward compatibility) */ -function createProjectRow(session) { +function renderProjectsTable(sessions) { + renderSessionsGroupedByProject(sessions); +} + +/** + * Create a collapsible project section + */ +function createProjectSection(project, sessions) { + const container = document.createElement('div'); + container.className = 'project-section'; + container.dataset.projectId = project.id; + + // Create section header + const header = document.createElement('div'); + header.className = 'project-section-header'; + header.onclick = () => toggleProjectSection(project.id); + + const icon = project.icon || '📁'; + const sessionCount = sessions.length; + + header.innerHTML = ` + + ${escapeHtml(icon)} + ${escapeHtml(project.name)} + ${sessionCount} session${sessionCount !== 1 ? 's' : ''} + `; + + // Create sessions container + const sessionsContainer = document.createElement('div'); + sessionsContainer.className = 'project-section-sessions'; + + // Render sessions within this project + sessions.forEach(session => { + const row = createSessionRow(session); + sessionsContainer.appendChild(row); + }); + + container.appendChild(header); + container.appendChild(sessionsContainer); + + return container; +} + +/** + * Create unassigned sessions section + */ +function createUnassignedSection(sessions) { + const container = document.createElement('div'); + container.className = 'project-section unassigned-section'; + + // Create section header + const header = document.createElement('div'); + header.className = 'project-section-header'; + header.onclick = () => toggleProjectSection('unassigned'); + + const sessionCount = sessions.length; + + header.innerHTML = ` + + 📄 + Unassigned Sessions + ${sessionCount} session${sessionCount !== 1 ? 's' : ''} + `; + + // Create sessions container + const sessionsContainer = document.createElement('div'); + sessionsContainer.className = 'project-section-sessions'; + + // Render unassigned sessions + sessions.forEach(session => { + const row = createSessionRow(session); + sessionsContainer.appendChild(row); + }); + + container.appendChild(header); + container.appendChild(sessionsContainer); + + return container; +} + +/** + * Create a single session row + */ +function createSessionRow(session) { const tr = document.createElement('tr'); + tr.className = 'session-row'; tr.dataset.sessionId = session.id; const projectName = getProjectName(session); @@ -177,9 +320,16 @@ function createProjectRow(session) { const status = getStatus(session); const statusClass = getStatusClass(session); + // Check if session has a project assigned + const projectId = session.metadata?.projectId; + const hasProject = projectId && window.projectsMap?.has(projectId); + const projectBadge = hasProject ? + `${escapeHtml(window.projectsMap.get(projectId).name)}` : ''; + tr.innerHTML = `

${escapeHtml(projectName)}

+ ${projectBadge} ${relativeTime} @@ -212,6 +362,39 @@ function createProjectRow(session) { return tr; } +/** + * Toggle project section collapse/expand + */ +function toggleProjectSection(projectId) { + const section = projectId === 'unassigned' ? + document.querySelector('.unassigned-section') : + document.querySelector(`.project-section[data-project-id="${projectId}"]`); + + if (!section) return; + + const toggle = section.querySelector('.project-section-toggle'); + const sessionsContainer = section.querySelector('.project-section-sessions'); + + if (section.classList.contains('collapsed')) { + // Expand + section.classList.remove('collapsed'); + toggle.textContent = '▼'; + sessionsContainer.style.display = ''; + } else { + // Collapse + section.classList.add('collapsed'); + toggle.textContent = '▶'; + sessionsContainer.style.display = 'none'; + } +} + +/** + * Create a single project row for the table (legacy function for backward compatibility) + */ +function createProjectRow(session) { + return createSessionRow(session); +} + /** * Extract project name from session metadata */ @@ -295,13 +478,13 @@ function showProjectMenu(event, session) { menu.querySelector('[data-action="duplicate"]').addEventListener('click', async () => { closeProjectMenu(); await duplicateProject(session); - loadSessions(); + loadSessionsAndProjects(); }); menu.querySelector('[data-action="delete"]').addEventListener('click', async () => { closeProjectMenu(); await deleteProject(session); - loadSessions(); + loadSessionsAndProjects(); }); setTimeout(() => { @@ -494,7 +677,7 @@ async function deleteProject(session) { // Refresh sessions function refreshSessions() { - loadSessions(); + loadSessionsAndProjects(); } /**