Files
SuperCharged-Claude-Code-Up…/public/claude-ide/session-tabs.js
uroma fc76581337 Fix session close button with non-blocking confirmation modal
Replace blocking confirm() dialog with custom non-blocking modal to prevent
browser warning issues when users have "don't show warnings" enabled.

Changes:
- Add showConfirmModal() method with Promise-based async modal
- Update closeSession() to use non-blocking modal
- Update deleteSession() to use non-blocking modal
- Add complete CSS styling for confirmation modal
- Support keyboard (Escape key) and click-outside to close
- Responsive design for mobile devices
- Dark theme matching existing UI

Fixes issue where close button completely stopped working after browser
blocked confirm() dialog and user selected "don't show warnings".

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 12:35:52 +00:00

668 lines
21 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 non-blocking confirmation modal)
*/
async closeSession(sessionId) {
const session = this.sessions.find(s => s.id === sessionId);
if (!session) return;
const sessionName = this.getSessionName(session);
// Show non-blocking confirmation modal
const confirmed = await this.showConfirmModal(
'Close Session',
`Are you sure you want to close "${escapeHtml(sessionName)}"?`,
'Close Session'
);
if (!confirmed) {
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();
}
/**
* Show a non-blocking confirmation modal
* @param {string} title - Modal title
* @param {string} message - Confirmation message
* @param {string} confirmText - Text for confirm button (default: "Confirm")
* @returns {Promise<boolean>} - True if confirmed, false otherwise
*/
showConfirmModal(title, message, confirmText = 'Confirm') {
return new Promise((resolve) => {
// Remove existing modal if present
const existingModal = document.getElementById('confirm-modal-overlay');
if (existingModal) existingModal.remove();
// Create overlay
const overlay = document.createElement('div');
overlay.id = 'confirm-modal-overlay';
overlay.className = 'confirm-modal-overlay';
// Create modal
const modal = document.createElement('div');
modal.className = 'confirm-modal';
modal.innerHTML = `
<div class="confirm-modal-header">
<h3 class="confirm-modal-title">${escapeHtml(title)}</h3>
</div>
<div class="confirm-modal-body">
<p class="confirm-modal-message">${message}</p>
</div>
<div class="confirm-modal-footer">
<button class="btn-confirm-cancel" id="confirm-modal-cancel">Cancel</button>
<button class="btn-confirm-ok" id="confirm-modal-ok">${escapeHtml(confirmText)}</button>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Prevent body scroll
document.body.style.overflow = 'hidden';
// Trigger animation
setTimeout(() => {
overlay.classList.add('visible');
modal.classList.add('visible');
}, 10);
// Handle confirm button
document.getElementById('confirm-modal-ok').addEventListener('click', () => {
closeModal(true);
});
// Handle cancel button
document.getElementById('confirm-modal-cancel').addEventListener('click', () => {
closeModal(false);
});
// Close on overlay click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
closeModal(false);
}
});
// Close on Escape key
const escapeHandler = (e) => {
if (e.key === 'Escape') {
closeModal(false);
document.removeEventListener('keydown', escapeHandler);
}
};
document.addEventListener('keydown', escapeHandler);
// Function to close modal
function closeModal(result) {
overlay.classList.remove('visible');
modal.classList.remove('visible');
setTimeout(() => {
document.removeEventListener('keydown', escapeHandler);
overlay.remove();
document.body.style.overflow = '';
resolve(result);
}, 200);
}
});
}
/**
* 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);
const confirmed = await this.showConfirmModal(
'Delete Session',
`Are you sure you want to permanently delete "${escapeHtml(sessionName)}"? This action cannot be undone.`,
'Delete'
);
if (!confirmed) {
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();
}
// ============================================================
// Add CSS Styles
// ============================================================
(function() {
const style = document.createElement('style');
style.textContent = `
/* Confirm Modal Overlay */
.confirm-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
opacity: 0;
transition: opacity 0.2s ease;
}
.confirm-modal-overlay.visible {
opacity: 1;
}
/* Confirm Modal */
.confirm-modal {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
width: 100%;
max-width: 400px;
overflow: hidden;
transform: scale(0.95);
transition: transform 0.2s ease;
}
.confirm-modal.visible {
transform: scale(1);
}
/* Header */
.confirm-modal-header {
padding: 20px 20px 12px 20px;
border-bottom: 1px solid #333;
}
.confirm-modal-title {
font-size: 18px;
font-weight: 600;
color: #e0e0e0;
margin: 0;
}
/* Body */
.confirm-modal-body {
padding: 20px;
}
.confirm-modal-message {
font-size: 14px;
color: #b0b0b0;
margin: 0;
line-height: 1.5;
}
/* Footer */
.confirm-modal-footer {
padding: 12px 20px 20px 20px;
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* Buttons */
.btn-confirm-cancel,
.btn-confirm-ok {
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.btn-confirm-cancel {
background: transparent;
border: 1px solid #333;
color: #e0e0e0;
}
.btn-confirm-cancel:hover {
background: #252525;
border-color: #444;
}
.btn-confirm-ok {
background: linear-gradient(135deg, #4a9eff 0%, #a78bfa 100%);
color: white;
}
.btn-confirm-ok:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(74, 158, 255, 0.4);
}
.btn-confirm-ok:active {
transform: translateY(0);
}
/* Responsive */
@media (max-width: 640px) {
.confirm-modal {
max-width: 90%;
}
.confirm-modal-header,
.confirm-modal-body,
.confirm-modal-footer {
padding: 16px;
}
.confirm-modal-footer {
flex-direction: column-reverse;
}
.btn-confirm-cancel,
.btn-confirm-ok {
width: 100%;
}
}
`;
document.head.appendChild(style);
})();
console.log('[SessionTabs] Module loaded');