diff --git a/public/claude-ide/sessions-landing.css b/public/claude-ide/sessions-landing.css index 67b081bd..5dd840d8 100644 --- a/public/claude-ide/sessions-landing.css +++ b/public/claude-ide/sessions-landing.css @@ -608,3 +608,302 @@ body.sessions-page { padding: 2px 6px; } } + +/** + * Session Context Menu + */ +.session-context-menu { + position: fixed; + background: #2a2a2a; + border: 1px solid #444; + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); + z-index: 10000; + min-width: 280px; + max-width: 400px; + overflow: hidden; + animation: contextMenuFadeIn 0.15s ease; +} + +@keyframes contextMenuFadeIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.context-menu-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + color: #e0e0e0; + font-size: 14px; + cursor: pointer; + transition: background 0.2s ease; + user-select: none; +} + +.context-menu-item:hover { + background: #3a3a3a; +} + +.context-menu-item:first-child { + border-radius: 8px 8px 0 0; +} + +.context-menu-item:last-child { + border-radius: 0 0 8px 8px; +} + +.context-menu-icon { + font-size: 18px; + flex-shrink: 0; + width: 24px; + text-align: center; +} + +.context-menu-text { + flex: 1; + font-weight: 500; +} + +.context-menu-divider { + height: 1px; + background: #444; + margin: 4px 0; +} + +.context-menu-label { + padding: 8px 16px 4px; + font-size: 11px; + font-weight: 600; + color: #888; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Context Menu Suggestions */ +.context-menu-suggestion { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 10px 16px; + cursor: pointer; +} + +.context-menu-suggestion-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.context-menu-suggestion .context-menu-text { + font-size: 14px; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.context-menu-reason { + font-size: 12px; + color: #888; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: 1.3; +} + +.context-menu-score { + font-size: 12px; + font-weight: 600; + color: #4a9eff; + flex-shrink: 0; + background: rgba(74, 158, 255, 0.15); + padding: 2px 6px; + border-radius: 4px; +} + +/* High score styling */ +.context-menu-score.high-score { + color: #51cf66; + background: rgba(81, 207, 102, 0.15); +} + +/* Medium score styling */ +.context-menu-score.medium-score { + color: #ffd43b; + background: rgba(255, 212, 59, 0.15); +} + +/** + * All Projects Modal + */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 10001; + opacity: 0; + transition: opacity 0.3s ease; +} + +.modal-overlay.visible { + opacity: 1; +} + +.all-projects-modal { + background: #1a1a1a; + border: 1px solid #333; + border-radius: 12px; + width: 90%; + max-width: 500px; + max-height: 80vh; + display: flex; + flex-direction: column; + overflow: hidden; + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.6); + transform: scale(0.95); + transition: transform 0.3s ease; +} + +.modal-overlay.visible .all-projects-modal { + transform: scale(1); +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px; + border-bottom: 1px solid #333; + background: #151515; +} + +.modal-header h2 { + font-size: 20px; + font-weight: 600; + color: #e0e0e0; + margin: 0; +} + +.modal-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: 6px; + transition: all 0.2s ease; + line-height: 1; +} + +.modal-close:hover { + background: #2a2a2a; + color: #e0e0e0; +} + +.modal-body { + padding: 20px 24px; + overflow-y: auto; + flex: 1; +} + +.modal-info { + font-size: 14px; + color: #888; + margin: 0 0 16px 0; + line-height: 1.5; +} + +.projects-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.project-option { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + background: #252525; + border: 1px solid #333; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; +} + +.project-option:hover { + background: #2a2a2a; + border-color: #4a9eff; + transform: translateX(4px); +} + +.project-option-icon { + font-size: 20px; + flex-shrink: 0; +} + +.project-option-name { + flex: 1; + font-size: 14px; + font-weight: 500; + color: #e0e0e0; +} + +.project-option-arrow { + font-size: 18px; + color: #888; + flex-shrink: 0; +} + +.no-projects { + text-align: center; + padding: 40px 20px; + color: #888; + font-size: 14px; +} + +/* Responsive Design for Modal */ +@media (max-width: 768px) { + .all-projects-modal { + width: 95%; + max-height: 85vh; + } + + .modal-header { + padding: 16px 20px; + } + + .modal-header h2 { + font-size: 18px; + } + + .modal-body { + padding: 16px 20px; + } + + .project-option { + padding: 10px 14px; + } +} diff --git a/public/claude-ide/sessions-landing.js b/public/claude-ide/sessions-landing.js index 6175f407..005b6e05 100644 --- a/public/claude-ide/sessions-landing.js +++ b/public/claude-ide/sessions-landing.js @@ -359,6 +359,12 @@ function createSessionRow(session) { showProjectMenu(e, session); }); + // Add right-click context menu + tr.addEventListener('contextmenu', (e) => { + e.preventDefault(); + showSessionContextMenu(e, session.id); + }); + return tr; } @@ -789,3 +795,260 @@ function escapeHtml(text) { div.textContent = text; return div.innerHTML; } + +/** + * Show session context menu for project reassignment + */ +async function showSessionContextMenu(event, sessionId) { + event.preventDefault(); + hideSessionContextMenu(); + + // Create context menu element + const menu = document.createElement('div'); + menu.id = 'sessionContextMenu'; + menu.className = 'session-context-menu'; + + // Fetch project suggestions + let suggestions = []; + try { + const res = await fetch(`/api/projects/suggestions?sessionId=${sessionId}`); + if (res.ok) { + const data = await res.json(); + suggestions = data.suggestions || []; + } + } catch (error) { + console.error('Error fetching suggestions:', error); + } + + // Build menu HTML + let menuHtml = ` +
+ + + `; + + // Add top 3 suggestions + const topSuggestions = suggestions.slice(0, 3); + topSuggestions.forEach(suggestion => { + const icon = getMatchIcon(suggestion.score); + const project = window.projectsMap?.get(suggestion.projectId); + if (project) { + menuHtml += ` + + `; + } + }); + + // Add "Show All Projects" option + menuHtml += ` + + + + `; + + menu.innerHTML = menuHtml; + document.body.appendChild(menu); + + // Position menu at mouse coordinates + const x = event.clientX; + const y = event.clientY; + + // Ensure menu doesn't go off screen + const menuRect = menu.getBoundingClientRect(); + const maxX = window.innerWidth - menuRect.width - 10; + const maxY = window.innerHeight - menuRect.height - 10; + + menu.style.left = `${Math.min(x, maxX)}px`; + menu.style.top = `${Math.min(y, maxY)}px`; + + // Store session ID for event handlers + menu.dataset.sessionId = sessionId; + + // Add event listeners + menu.addEventListener('click', handleContextMenuClick); + + // Close menu when clicking outside + setTimeout(() => { + document.addEventListener('click', hideSessionContextMenu, { once: true }); + }, 10); +} + +/** + * Handle context menu item clicks + */ +async function handleContextMenuClick(event) { + const menu = document.getElementById('sessionContextMenu'); + if (!menu) return; + + const target = event.target.closest('.context-menu-item'); + if (!target) return; + + const action = target.dataset.action; + const sessionId = menu.dataset.sessionId; + + hideSessionContextMenu(); + + switch (action) { + case 'open': + continueToSession(sessionId); + break; + case 'move': + const projectId = target.dataset.projectId; + await moveSessionToProject(sessionId, projectId); + break; + case 'show-all': + showAllProjectsModal(sessionId); + break; + case 'unassigned': + await moveSessionToProject(sessionId, null); + break; + } +} + +/** + * Get match icon based on score + */ +function getMatchIcon(score) { + if (score >= 90) return '🎯'; + if (score >= 50) return '📂'; + return '💡'; +} + +/** + * Move session to project + */ +async function moveSessionToProject(sessionId, projectId) { + try { + showLoadingOverlay('Moving session...'); + + const res = await fetch(`/api/projects/sessions/${sessionId}/move`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ projectId }) + }); + + if (!res.ok) throw new Error('Request failed'); + + const data = await res.json(); + + if (data.success) { + hideLoadingOverlay(); + showToast('Session moved successfully', 'success'); + await loadSessionsAndProjects(); + } else { + throw new Error(data.error || 'Failed to move session'); + } + } catch (error) { + console.error('Error moving session:', error); + hideLoadingOverlay(); + showToast(error.message || 'Failed to move session', 'error'); + } +} + +/** + * Show all projects modal + */ +function showAllProjectsModal(sessionId) { + // Create modal overlay + const overlay = document.createElement('div'); + overlay.className = 'modal-overlay'; + overlay.id = 'allProjectsModal'; + + // Create modal content + const modal = document.createElement('div'); + modal.className = 'all-projects-modal'; + + // Build projects list + const projects = Array.from(window.projectsMap?.values() || []); + let projectsHtml = ''; + + if (projects.length === 0) { + projectsHtml = 'Select a project to move this session to
+ ${projectsHtml} +