Files
SuperCharged-Claude-Code-Up…/public/claude-ide/session-tabs.js
uroma 36f00d270e Add CodeNomad-inspired two-level tab system (Phase 2)
Phase 2 of enhancement plan:
- Created project-manager.js for project-level organization
- Created session-tabs.js for session-level organization
- Created project-tabs.css with responsive design
- Added tab structure to index.html
- Cache bust: v1769083200000

Features:
- Project tabs organize sessions by working directory
- Session tabs show all sessions within active project
- Context menu for rename/duplicate/delete/close
- Visual indicators for active/running sessions
- Responsive design for mobile

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 12:03:42 +00:00

428 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Session Tabs - Manages session tabs within a project
* Inspired by CodeNomad's two-level tab system
* https://github.com/NeuralNomadsAI/CodeNomad
*
* Provides:
* - Session tabs for the active project
* - Session switching
* - Session creation, renaming, deletion
* - Visual indicators for active sessions
*/
'use strict';
// ============================================================
// Session Tabs Class
// ============================================================
class SessionTabs {
constructor() {
this.sessions = [];
this.activeSessionId = null;
this.initialized = false;
}
/**
* Initialize session tabs
*/
initialize() {
if (this.initialized) return;
console.log('[SessionTabs] Initializing...');
this.render();
this.initialized = true;
}
/**
* Set sessions for current project
*/
setSessions(sessions) {
this.sessions = sessions || [];
console.log('[SessionTabs] Set', this.sessions.length, 'sessions');
}
/**
* Set active session
*/
setActiveSession(sessionId) {
this.activeSessionId = sessionId;
console.log('[SessionTabs] Active session:', sessionId);
}
/**
* Render session tabs
*/
render() {
const container = document.getElementById('session-tabs');
if (!container) {
console.warn('[SessionTabs] Session tabs container not found');
return;
}
if (this.sessions.length === 0) {
container.innerHTML = `
<div class="session-tabs-empty">
<span>No sessions</span>
</div>
`;
return;
}
container.innerHTML = `
<div class="session-tabs">
${this.sessions.map(session => this.renderSessionTab(session)).join('')}
<button class="session-tab session-tab-new" onclick="window.sessionTabs.createNewSession()">
<span class="tab-icon">+</span>
</button>
</div>
`;
}
/**
* Render a single session tab
*/
renderSessionTab(session) {
const isActive = session.id === this.activeSessionId;
const isRunning = session.status === 'running';
const sessionName = this.getSessionName(session);
const relativeTime = this.getRelativeTime(session);
return `
<button class="session-tab ${isActive ? 'active' : ''} ${isRunning ? 'running' : ''}"
data-session-id="${session.id}"
onclick="window.sessionTabs.switchSession('${session.id}')"
oncontextmenu="window.sessionTabs.showContextMenu(event, '${session.id}')"
title="${escapeHtml(sessionName)}\nLast activity: ${relativeTime}">
<span class="tab-icon">${isRunning ? '💬' : '💭'}</span>
<span class="tab-label">${escapeHtml(sessionName)}</span>
${isRunning ? '<span class="tab-indicator"></span>' : ''}
<span class="tab-close" onclick="event.stopPropagation(); window.sessionTabs.closeSession('${session.id}')">×</span>
</button>
`;
}
/**
* Get display name for a session
*/
getSessionName(session) {
// Try to get name from metadata
if (session.metadata) {
if (session.metadata.project) return session.metadata.project;
if (session.metadata.title) return session.metadata.title;
if (session.metadata.name) return session.metadata.name;
}
// Use working directory
if (session.workingDir) {
return session.workingDir.split('/').pop();
}
// Fallback to session ID
return session.id.substring(0, 8);
}
/**
* Get relative time string
*/
getRelativeTime(session) {
const date = new Date(session.lastActivity || session.createdAt || session.created_at || Date.now());
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 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();
}
/**
* Switch to a different session
*/
async switchSession(sessionId) {
console.log('[SessionTabs] Switching to session:', sessionId);
this.activeSessionId = sessionId;
this.render();
// Attach to the session
if (typeof attachToSession === 'function') {
await attachToSession(sessionId);
}
}
/**
* Create a new session
*/
async createNewSession() {
console.log('[SessionTabs] Creating new session');
// Use project manager to create session in current project
if (window.projectManager && window.projectManager.activeProjectId) {
await window.projectManager.createNewSessionInProject(window.projectManager.activeProjectId);
} else {
// Create in default location
await window.projectManager.createNewSessionInProject('default');
}
}
/**
* Close a session (with confirmation)
*/
async closeSession(sessionId) {
const session = this.sessions.find(s => s.id === sessionId);
if (!session) return;
const sessionName = this.getSessionName(session);
// Confirm before closing
if (!confirm(`Close session "${sessionName}"?`)) {
return;
}
console.log('[SessionTabs] Closing session:', sessionId);
// Note: This just removes the tab from view
// The session still exists on the server
this.sessions = this.sessions.filter(s => s.id !== sessionId);
if (this.activeSessionId === sessionId) {
this.activeSessionId = this.sessions.length > 0 ? this.sessions[0].id : null;
if (this.activeSessionId) {
await this.switchSession(this.activeSessionId);
} else {
// Show empty state
if (window.projectManager) {
const project = window.projectManager.getProjectForSession(sessionId);
if (project) {
window.projectManager.showEmptyProjectState(project);
}
}
}
}
this.render();
}
/**
* Show context menu for session
*/
showContextMenu(event, sessionId) {
event.preventDefault();
const session = this.sessions.find(s => s.id === sessionId);
if (!session) return;
// Remove existing menu
const existingMenu = document.getElementById('session-context-menu');
if (existingMenu) existingMenu.remove();
// Create context menu
const menu = document.createElement('div');
menu.id = 'session-context-menu';
menu.className = 'context-menu';
menu.innerHTML = `
<div class="context-menu-item" data-action="rename">
<span class="menu-icon">✏️</span>
<span>Rename</span>
</div>
<div class="context-menu-item" data-action="duplicate">
<span class="menu-icon">📋</span>
<span>Duplicate</span>
</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item danger" data-action="delete">
<span class="menu-icon">🗑️</span>
<span>Delete</span>
</div>
<div class="context-menu-item danger" data-action="close">
<span class="menu-icon">✕</span>
<span>Close</span>
</div>
`;
// Position menu
const rect = event.target.getBoundingClientRect();
menu.style.position = 'fixed';
menu.style.top = `${rect.bottom + 4}px`;
menu.style.left = `${rect.left}px`;
menu.style.minWidth = '150px';
document.body.appendChild(menu);
// Add event listeners
menu.querySelector('[data-action="rename"]').addEventListener('click', () => {
this.renameSession(session);
this.closeContextMenu();
});
menu.querySelector('[data-action="duplicate"]').addEventListener('click', () => {
this.duplicateSession(session);
this.closeContextMenu();
});
menu.querySelector('[data-action="delete"]').addEventListener('click', () => {
this.deleteSession(session);
this.closeContextMenu();
});
menu.querySelector('[data-action="close"]').addEventListener('click', () => {
this.closeSession(session.id);
this.closeContextMenu();
});
// Close menu on click outside
setTimeout(() => {
document.addEventListener('click', this.closeContextMenu, { once: true });
}, 10);
}
/**
* Close context menu
*/
closeContextMenu() {
document.getElementById('session-context-menu')?.remove();
}
/**
* Rename a session
*/
async renameSession(session) {
const currentName = this.getSessionName(session);
const newName = prompt('Enter new name:', currentName);
if (newName && newName !== currentName) {
console.log('[SessionTabs] Renaming session to:', newName);
// Update metadata
if (!session.metadata) {
session.metadata = {};
}
session.metadata.project = newName; // Use 'project' field as name
// Update on server
try {
await fetch(`/claude/api/claude/sessions/${session.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ metadata: session.metadata })
});
// Refresh tabs
this.render();
if (window.projectManager) {
await window.projectManager.refresh();
}
} catch (error) {
console.error('[SessionTabs] Error renaming session:', error);
}
}
}
/**
* Duplicate a session
*/
async duplicateSession(session) {
console.log('[SessionTabs] Duplicating session:', session.id);
// Create new session with same metadata
try {
const res = await fetch('/claude/api/claude/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
workingDir: session.workingDir,
metadata: {
...session.metadata,
project: `${this.getSessionName(session)} (copy)`,
duplicatedFrom: session.id
}
})
});
if (res.ok) {
const data = await res.json();
if (data.success && window.projectManager) {
await window.projectManager.refresh();
// Switch to new session
await this.switchSession(data.session.id);
}
}
} catch (error) {
console.error('[SessionTabs] Error duplicating session:', error);
}
}
/**
* Delete a session
*/
async deleteSession(session) {
const sessionName = this.getSessionName(session);
if (!confirm(`Permanently delete "${sessionName}"? This cannot be undone.`)) {
return;
}
console.log('[SessionTabs] Deleting session:', session.id);
try {
await fetch(`/claude/api/claude/sessions/${session.id}`, {
method: 'DELETE'
});
// Refresh
if (window.projectManager) {
await window.projectManager.refresh();
}
} catch (error) {
console.error('[SessionTabs] Error deleting session:', error);
}
}
/**
* Update session (e.g., when it receives new activity)
*/
updateSession(session) {
const index = this.sessions.findIndex(s => s.id === session.id);
if (index !== -1) {
this.sessions[index] = session;
// Move to top
this.sessions.splice(index, 1);
this.sessions.unshift(session);
this.render();
}
}
}
// ============================================================
// Helper Functions
// ============================================================
/**
* Escape HTML to prevent XSS
*/
function escapeHtml(text) {
if (text === null || text === undefined) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ============================================================
// Initialize Globally
// ============================================================
window.sessionTabs = new SessionTabs();
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => window.sessionTabs.initialize());
} else {
window.sessionTabs.initialize();
}
console.log('[SessionTabs] Module loaded');