+ ${projectsArray.map(project => this.renderProjectTab(project)).join('')}
+
+
+ `;
+ }
+
+ /**
+ * Render a single project tab
+ */
+ renderProjectTab(project) {
+ const isActive = project.id === this.activeProjectId;
+ const sessionCount = project.sessions.length;
+
+ return `
+
+
📁
+
${escapeHtml(project.name)}
+
No sessions yet in this project
+
+
+ `;
+ }
+
+ /**
+ * Create a new project (select folder)
+ */
+ async createNewProject() {
+ // Trigger folder picker if available
+ if (window.folderPicker && typeof window.folderPicker.pick === 'function') {
+ try {
+ const folder = await window.folderPicker.pick();
+ if (folder) {
+ await this.createSessionInFolder(folder);
+ }
+ } catch (error) {
+ console.error('[ProjectManager] Error picking folder:', error);
+ this.showError('Failed to select folder');
+ }
+ } else {
+ // Fallback: prompt for folder or create default session
+ await this.createNewSessionInProject('default');
+ }
+ }
+
+ /**
+ * Create a new session in a specific folder
+ */
+ async createSessionInFolder(workingDir) {
+ try {
+ if (typeof showLoadingOverlay === 'function') {
+ showLoadingOverlay('Creating session...');
+ }
+
+ const res = await fetch('/claude/api/claude/sessions', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ workingDir,
+ metadata: {
+ type: 'chat',
+ source: 'web-ide'
+ }
+ })
+ });
+
+ if (!res.ok) throw new Error('Failed to create session');
+
+ const data = await res.json();
+ if (data.success) {
+ // Reload projects and switch to new session
+ await this.loadProjects();
+ await this.initialize();
+
+ // Find the new session and switch to it
+ for (const project of this.projects.values()) {
+ const session = project.sessions.find(s => s.id === data.session.id);
+ if (session) {
+ this.switchProject(project.id);
+ break;
+ }
+ }
+
+ if (typeof hideLoadingOverlay === 'function') {
+ hideLoadingOverlay();
+ }
+ }
+ } catch (error) {
+ console.error('[ProjectManager] Error creating session:', error);
+ if (typeof hideLoadingOverlay === 'function') {
+ hideLoadingOverlay();
+ }
+ this.showError('Failed to create session');
+ }
+ }
+
+ /**
+ * Create a new session in the current project
+ */
+ async createNewSessionInProject(projectId) {
+ const project = this.projects.get(projectId.replace('project-', ''));
+ if (!project) return;
+
+ await this.createSessionInFolder(project.workingDir);
+ }
+
+ /**
+ * Get project for a session ID
+ */
+ getProjectForSession(sessionId) {
+ for (const project of this.projects.values()) {
+ if (project.sessions.find(s => s.id === sessionId)) {
+ return project;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Add a session to its project
+ */
+ addSessionToProject(session) {
+ const dir = session.workingDir || 'default';
+ const projectKey = dir.replace(/\//g, '-').replace(/^-/, '') || 'default';
+
+ if (!this.projects.has(projectKey)) {
+ const projectName = dir.split('/').pop() || 'Default';
+ this.projects.set(projectKey, {
+ id: `project-${projectKey}`,
+ name: this.deduplicateProjectName(projectName, this.projects),
+ workingDir: dir,
+ sessions: [],
+ activeSessionId: null,
+ createdAt: Date.now()
+ });
+ }
+
+ const project = this.projects.get(projectKey);
+ project.sessions.unshift(session); // Add to beginning
+ project.activeSessionId = session.id;
+
+ // Re-render if this is the active project
+ if (this.activeProjectId === project.id) {
+ this.renderProjectTabs();
+ if (window.sessionTabs) {
+ window.sessionTabs.setSessions(project.sessions);
+ window.sessionTabs.render();
+ }
+ }
+ }
+
+ /**
+ * Update session in its project
+ */
+ updateSessionInProject(session) {
+ const project = this.getProjectForSession(session.id);
+ if (project) {
+ const index = project.sessions.findIndex(s => s.id === session.id);
+ if (index !== -1) {
+ project.sessions[index] = session;
+ // Move to top
+ project.sessions.splice(index, 1);
+ project.sessions.unshift(session);
+ }
+ }
+ }
+
+ /**
+ * Remove session from project
+ */
+ removeSessionFromProject(sessionId) {
+ const project = this.getProjectForSession(sessionId);
+ if (project) {
+ project.sessions = project.sessions.filter(s => s.id !== sessionId);
+ if (project.activeSessionId === sessionId) {
+ project.activeSessionId = project.sessions.length > 0 ? project.sessions[0].id : null;
+ }
+ }
+ }
+
+ /**
+ * Show error message
+ */
+ showError(message) {
+ if (typeof showToast === 'function') {
+ showToast(message, 'error');
+ } else {
+ alert(message);
+ }
+ }
+
+ /**
+ * Refresh all projects
+ */
+ async refresh() {
+ await this.loadProjects();
+ this.renderProjectTabs();
+
+ if (this.activeProjectId) {
+ const project = this.projects.get(this.activeProjectId.replace('project-', ''));
+ if (project && window.sessionTabs) {
+ window.sessionTabs.setSessions(project.sessions);
+ window.sessionTabs.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.projectManager = new ProjectManager();
+
+// Auto-initialize when DOM is ready
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', () => window.projectManager.initialize());
+} else {
+ window.projectManager.initialize();
+}
+
+console.log('[ProjectManager] Module loaded');
diff --git a/public/claude-ide/project-tabs.css b/public/claude-ide/project-tabs.css
new file mode 100644
index 00000000..4f35073c
--- /dev/null
+++ b/public/claude-ide/project-tabs.css
@@ -0,0 +1,471 @@
+/**
+ * Project and Session Tabs Styling
+ * Inspired by CodeNomad's two-level tab system
+ * https://github.com/NeuralNomadsAI/CodeNomad
+ */
+
+/* ============================================================
+ Project Tabs (Level 1)
+ ============================================================ */
+
+#project-tabs {
+ background: #1a1a1a;
+ border-bottom: 1px solid #333;
+ padding: 0;
+}
+
+.project-tabs {
+ display: flex;
+ align-items: center;
+ padding: 0 8px;
+ gap: 4px;
+ overflow-x: auto;
+ overflow-y: hidden;
+ scrollbar-width: thin;
+ scrollbar-color: #444 #1a1a1a;
+}
+
+.project-tabs::-webkit-scrollbar {
+ height: 6px;
+}
+
+.project-tabs::-webkit-scrollbar-track {
+ background: #1a1a1a;
+}
+
+.project-tabs::-webkit-scrollbar-thumb {
+ background: #444;
+ border-radius: 3px;
+}
+
+.project-tabs::-webkit-scrollbar-thumb:hover {
+ background: #555;
+}
+
+/* Project Tab Button */
+.project-tab {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 10px 14px;
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid transparent;
+ border-radius: 6px 6px 0 0;
+ color: #888;
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ white-space: nowrap;
+ min-width: 0;
+}
+
+.project-tab:hover {
+ background: #252525;
+ color: #e0e0e0;
+}
+
+.project-tab.active {
+ background: #222;
+ color: #4a9eff;
+ border-bottom-color: #4a9eff;
+}
+
+.project-tab .tab-icon {
+ font-size: 14px;
+ flex-shrink: 0;
+}
+
+.project-tab .tab-label {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.project-tab .tab-count {
+ font-size: 11px;
+ background: #333;
+ color: #888;
+ padding: 2px 6px;
+ border-radius: 10px;
+ flex-shrink: 0;
+}
+
+.project-tab.active .tab-count {
+ background: rgba(74, 158, 255, 0.2);
+ color: #4a9eff;
+}
+
+/* New Project Tab */
+.project-tab-new {
+ color: #51cf66;
+}
+
+.project-tab-new:hover {
+ background: rgba(81, 207, 102, 0.1);
+ color: #51cf66;
+}
+
+/* Empty State */
+.project-tabs-empty {
+ padding: 12px 16px;
+ color: #666;
+ font-size: 13px;
+ font-style: italic;
+}
+
+/* ============================================================
+ Session Tabs (Level 2)
+ ============================================================ */
+
+#session-tabs {
+ background: #151515;
+ border-bottom: 1px solid #333;
+ padding: 0;
+}
+
+.session-tabs {
+ display: flex;
+ align-items: center;
+ padding: 0 8px;
+ gap: 2px;
+ overflow-x: auto;
+ overflow-y: hidden;
+ scrollbar-width: thin;
+ scrollbar-color: #444 #151515;
+}
+
+.session-tabs::-webkit-scrollbar {
+ height: 4px;
+}
+
+.session-tabs::-webkit-scrollbar-track {
+ background: #151515;
+}
+
+.session-tabs::-webkit-scrollbar-thumb {
+ background: #444;
+ border-radius: 2px;
+}
+
+.session-tabs::-webkit-scrollbar-thumb:hover {
+ background: #555;
+}
+
+/* Session Tab Button */
+.session-tab {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 12px;
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid transparent;
+ border-radius: 6px 6px 0 0;
+ color: #888;
+ font-size: 12px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ white-space: nowrap;
+ min-width: 0;
+ position: relative;
+}
+
+.session-tab:hover {
+ background: #222;
+ color: #e0e0e0;
+}
+
+.session-tab:hover .tab-close {
+ opacity: 1;
+}
+
+.session-tab.active {
+ background: #1a1a1a;
+ color: #e0e0e0;
+ border-bottom-color: #51cf66;
+}
+
+.session-tab.running {
+ color: #4a9eff;
+}
+
+.session-tab.running .tab-indicator {
+ display: block;
+}
+
+.session-tab .tab-icon {
+ font-size: 12px;
+ flex-shrink: 0;
+}
+
+.session-tab .tab-label {
+ flex: 1;
+ min-width: 0;
+ max-width: 150px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.session-tab .tab-indicator {
+ display: none;
+ width: 6px;
+ height: 6px;
+ background: #4a9eff;
+ border-radius: 50%;
+ flex-shrink: 0;
+ animation: pulse 1.5s ease-in-out infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
+ 50% {
+ opacity: 0.5;
+ transform: scale(0.8);
+ }
+}
+
+.session-tab .tab-close {
+ opacity: 0;
+ font-size: 16px;
+ line-height: 1;
+ width: 18px;
+ height: 18px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 4px;
+ flex-shrink: 0;
+ transition: all 0.2s ease;
+}
+
+.session-tab .tab-close:hover {
+ background: rgba(255, 107, 107, 0.2);
+ color: #ff6b6b;
+}
+
+/* New Session Tab */
+.session-tab-new {
+ color: #51cf66;
+ padding: 8px 12px;
+}
+
+.session-tab-new:hover {
+ background: rgba(81, 207, 102, 0.1);
+ color: #51cf66;
+}
+
+/* Empty State */
+.session-tabs-empty {
+ padding: 10px 16px;
+ color: #666;
+ font-size: 12px;
+ font-style: italic;
+}
+
+/* ============================================================
+ Context Menu
+ ============================================================ */
+
+.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: 180px;
+ padding: 4px 0;
+}
+
+.context-menu-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 10px 16px;
+ background: none;
+ border: none;
+ color: #e0e0e0;
+ font-size: 13px;
+ cursor: pointer;
+ transition: background 0.2s ease;
+ width: 100%;
+ text-align: left;
+}
+
+.context-menu-item:hover {
+ background: #3a3a3a;
+}
+
+.context-menu-item.danger {
+ color: #ff6b6b;
+}
+
+.context-menu-item.danger:hover {
+ background: rgba(255, 107, 107, 0.1);
+}
+
+.context-menu-divider {
+ height: 1px;
+ background: #444;
+ margin: 4px 0;
+}
+
+.menu-icon {
+ font-size: 14px;
+ width: 20px;
+ text-align: center;
+}
+
+/* ============================================================
+ Empty Project State
+ ============================================================ */
+
+.empty-project-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 60px 20px;
+ text-align: center;
+}
+
+.empty-project-state .empty-icon {
+ font-size: 48px;
+ margin-bottom: 16px;
+}
+
+.empty-project-state h3 {
+ color: #e0e0e0;
+ font-size: 18px;
+ font-weight: 600;
+ margin: 0 0 8px 0;
+}
+
+.empty-project-state p {
+ color: #888;
+ font-size: 14px;
+ margin: 0 0 24px 0;
+}
+
+/* ============================================================
+ Responsive Design
+ ============================================================ */
+
+@media (max-width: 768px) {
+ .project-tabs,
+ .session-tabs {
+ padding: 0 4px;
+ }
+
+ .project-tab {
+ padding: 8px 10px;
+ font-size: 12px;
+ }
+
+ .project-tab .tab-label {
+ max-width: 80px;
+ }
+
+ .session-tab {
+ padding: 6px 10px;
+ font-size: 11px;
+ }
+
+ .session-tab .tab-label {
+ max-width: 100px;
+ }
+
+ .context-menu {
+ min-width: 160px;
+ }
+}
+
+@media (max-width: 480px) {
+ .project-tab .tab-count {
+ display: none;
+ }
+
+ .session-tab .tab-close {
+ opacity: 1;
+ }
+}
+
+/* ============================================================
+ Animations
+ ============================================================ */
+
+@keyframes tabFadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(-4px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.project-tab,
+.session-tab {
+ animation: tabFadeIn 0.15s ease-out;
+}
+
+/* ============================================================
+ Drag and Drop (Future Enhancement)
+ ============================================================ */
+
+.project-tab.dragging,
+.session-tab.dragging {
+ opacity: 0.5;
+}
+
+.project-tab.drag-over,
+.session-tab.drag-over {
+ background: rgba(74, 158, 255, 0.1);
+ border-bottom-color: #4a9eff;
+}
+
+/* ============================================================
+ Tab Close Button Animation
+ ============================================================ */
+
+.tab-close {
+ transform-origin: center;
+ transition: transform 0.2s ease;
+}
+
+.tab-close:hover {
+ transform: rotate(90deg);
+}
+
+/* ============================================================
+ Scroll Buttons for Overflow (Optional Enhancement)
+ ============================================================ */
+
+.tabs-scroll-button {
+ display: none; /* Show only when needed via JS */
+ width: 32px;
+ height: 100%;
+ background: #1a1a1a;
+ border: none;
+ color: #888;
+ font-size: 18px;
+ cursor: pointer;
+ flex-shrink: 0;
+}
+
+.tabs-scroll-button:hover {
+ background: #252525;
+ color: #e0e0e0;
+}
+
+.tabs-scroll-button:disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+}
diff --git a/public/claude-ide/session-tabs.js b/public/claude-ide/session-tabs.js
new file mode 100644
index 00000000..6f7e5dac
--- /dev/null
+++ b/public/claude-ide/session-tabs.js
@@ -0,0 +1,427 @@
+/**
+ * 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 = `
+
+ ${this.sessions.map(session => this.renderSessionTab(session)).join('')}
+
+
+ `;
+ }
+
+ /**
+ * 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 `
+