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();
}
/**
|