Files
SuperCharged-Claude-Code-Up…/public/claude-ide/ide.js
uroma 55aafbae9a Fix project isolation: Make loadChatHistory respect active project sessions
- Modified loadChatHistory() to check for active project before fetching all sessions
- When active project exists, use project.sessions instead of fetching from API
- Added detailed console logging to debug session filtering
- This prevents ALL sessions from appearing in every project's sidebar

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 14:43:05 +00:00

2138 lines
76 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.
// Claude Code IDE JavaScript
let currentSession = null;
let currentProjectName = null;
let ws = null;
// Streaming message state for accumulating response chunks
// MUST be declared at module scope BEFORE any functions that use them
let streamingMessageElement = null;
let streamingMessageContent = '';
let streamingTimeout = null;
// ============================================================
// REAL-TIME LOGGER WITH AUTO-FIX
// ============================================================
// Tier 1: Client-side auto-fix (instant)
// Tier 2: Escalation to AI agents (systematic-debugging + brainstorming)
window.AutoFixLogger = (function() {
const logs = [];
const maxLogs = 100;
let panel = null;
// Create the logger panel
function createPanel() {
if (panel) return;
panel = document.createElement('div');
panel.id = 'autofix-logger-panel';
panel.innerHTML = `
<div class="autofix-logger-header">
<span>🔧 Auto-Fix Logger</span>
<button onclick="AutoFixLogger.clear()">Clear</button>
<button onclick="AutoFixLogger.export()">Export</button>
<button onclick="AutoFixLogger.toggle()"></button>
</div>
<div class="autofix-logger-content" id="autofix-logger-content"></div>
`;
// Add styles
const style = document.createElement('style');
style.textContent = `
#autofix-logger-panel {
position: fixed;
bottom: 20px;
right: 20px;
width: 400px;
max-height: 300px;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
z-index: 99999;
font-family: 'SF Mono', Monaco, monospace;
font-size: 12px;
overflow: hidden;
}
#autofix-logger-panel.minimized {
height: 40px;
}
.autofix-logger-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 15px;
background: #252525;
border-bottom: 1px solid #333;
font-weight: 600;
}
.autofix-logger-header button {
background: #333;
border: 1px solid #444;
color: #e0e0e0;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
margin-left: 5px;
}
.autofix-logger-header button:hover {
background: #444;
}
.autofix-logger-content {
padding: 10px;
overflow-y: auto;
max-height: 250px;
}
.autofix-log-entry {
margin-bottom: 8px;
padding: 8px;
border-radius: 6px;
border-left: 3px solid #666;
}
.autofix-log-entry.success {
background: rgba(34, 197, 94, 0.1);
border-left-color: #22c55e;
}
.autofix-log-entry.error {
background: rgba(239, 68, 68, 0.1);
border-left-color: #ef4444;
}
.autofix-log-entry.warning {
background: rgba(234, 179, 8, 0.1);
border-left-color: #eab308;
}
.autofix-log-entry.info {
background: rgba(74, 158, 255, 0.1);
border-left-color: #4a9eff;
}
.autofix-log-time {
color: #888;
font-size: 10px;
}
.autofix-log-message {
margin-top: 4px;
color: #e0e0e0;
}
.autofix-log-detail {
margin-top: 4px;
padding: 4px 8px;
background: #0d0d0d;
border-radius: 4px;
font-size: 10px;
color: #888;
}
`;
document.head.appendChild(style);
document.body.appendChild(panel);
}
function addLog(type, message, detail = null) {
const timestamp = new Date().toLocaleTimeString();
const log = { timestamp, type, message, detail };
logs.push(log);
if (logs.length > maxLogs) logs.shift();
if (!panel) createPanel();
const content = document.getElementById('autofix-logger-content');
const entry = document.createElement('div');
entry.className = `autofix-log-entry ${type}`;
entry.innerHTML = `
<div class="autofix-log-time">${timestamp}</div>
<div class="autofix-log-message">${message}</div>
${detail ? `<div class="autofix-log-detail">${typeof detail === 'object' ? JSON.stringify(detail, null, 2) : detail}</div>` : ''}
`;
content.appendChild(entry);
content.scrollTop = content.scrollHeight;
// Auto-fix triggers
checkAndAutoFix();
}
function checkAndAutoFix() {
const recentLogs = logs.slice(-10);
// Detect: Session ID in URL but not attached
const hasSessionInUrl = window.PRELOAD_SESSION_ID || window.location.pathname.match(/\/claude\/ide\/session\/([^\/]+)$/);
const showsNoSessions = document.body.textContent.includes('No sessions yet');
if (hasSessionInUrl && showsNoSessions) {
const sessionId = window.PRELOAD_SESSION_ID || window.location.pathname.match(/\/claude\/ide\/session\/([^\/]+)$/)[1];
// Check if already attached
if (typeof attachedSessionId !== 'undefined' && attachedSessionId === sessionId) {
return; // Already attached, no fix needed
}
addLog('warning', '⚠️ Detected: Session in URL but not attached. Attempting auto-fix...');
// Tier 1 Auto-fix: Force attach
if (typeof attachToSession === 'function') {
setTimeout(() => {
attachToSession(sessionId);
addLog('success', '✅ Auto-fix applied: Force attached to session', { sessionId });
}, 100);
}
}
// Detect: API errors
const apiErrors = recentLogs.filter(l => l.type === 'error' && l.message.includes('API'));
if (apiErrors.length >= 3) {
addLog('warning', '⚠️ Multiple API errors detected. Consider reloading page.');
}
}
function escalateToAgents(issue) {
addLog('warning', '🤖 Tier 1 auto-fix failed. Escalating to AI agents...');
const diagnosticReport = {
url: window.location.href,
sessionId: window.PRELOAD_SESSION_ID,
attachedSessionId: typeof attachedSessionId !== 'undefined' ? attachedSessionId : null,
timestamp: new Date().toISOString(),
logs: logs.slice(-20),
userAgent: navigator.userAgent,
sessionStorage: { ...sessionStorage },
localStorage: { ...localStorage }
};
addLog('info', '📋 Diagnostic report generated', { issue, reportKeys: Object.keys(diagnosticReport) });
// Store report for agent retrieval
sessionStorage.setItem('AUTOFIX_DIAGNOSTIC_REPORT', JSON.stringify(diagnosticReport));
sessionStorage.setItem('AUTOFIX_ISSUE', JSON.stringify(issue));
// The actual agent escalation happens server-side via the skill system
console.log('[AUTOFIX] Diagnostic report ready for agent retrieval');
console.log('[AUTOFIX] Report:', diagnosticReport);
addLog('info', '💡 Tip: Share this diagnostic report with Claude for agent-assisted fix');
}
return {
init: function() {
createPanel();
addLog('info', '🔧 Auto-Fix Logger initialized');
addLog('info', '✅ PRELOAD_SESSION_ID:', window.PRELOAD_SESSION_ID || 'none');
// Start monitoring
setInterval(checkAndAutoFix, 5000);
},
log: function(message, detail = null) {
addLog('info', message, detail);
},
success: function(message, detail = null) {
addLog('success', '✅ ' + message, detail);
},
error: function(message, detail = null) {
addLog('error', '❌ ' + message, detail);
},
warning: function(message, detail = null) {
addLog('warning', '⚠️ ' + message, detail);
},
clear: function() {
logs.length = 0;
if (panel) {
document.getElementById('autofix-logger-content').innerHTML = '';
}
},
export: function() {
const report = {
timestamp: new Date().toISOString(),
url: window.location.href,
sessionId: window.PRELOAD_SESSION_ID,
logs: logs
};
const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `autofix-log-${Date.now()}.json`;
a.click();
},
toggle: function() {
if (panel) {
panel.classList.toggle('minimized');
}
},
escalate: escalateToAgents
};
})();
// Make ws globally accessible for other scripts
Object.defineProperty(window, 'ws', {
get: function() { return ws; },
set: function(value) { ws = value; },
enumerable: true,
configurable: true
});
// Initialize - Use same pattern as session-picker (run immediately if DOM ready, otherwise wait for DOMContentLoaded)
function ideInit() {
// ============================================================
// TRACE: ide.js initialized
// ============================================================
if (window.traceExecution) {
window.traceExecution('ide.js', 'ideInit called', { pathname: window.location.pathname, readyState: document.readyState });
}
// ============================================================
// Initialize Auto-Fix Logger FIRST
// ============================================================
if (window.AutoFixLogger) {
window.AutoFixLogger.init();
}
initNavigation();
connectWebSocket();
// ============================================================
// Session ID Extraction - Use PRELOAD_SESSION_ID first
// ============================================================
// PRELOAD_SESSION_ID is set by inline script BEFORE any other JS
// This guarantees it's available when loadChatView() runs
let sessionId = window.PRELOAD_SESSION_ID || null;
if (sessionId) {
console.log('[Init] Using PRELOAD_SESSION_ID:', sessionId);
if (window.AutoFixLogger) {
window.AutoFixLogger.success('Session ID from PRELOAD', { sessionId });
}
if (window.traceExecution) {
window.traceExecution('ide.js', 'Using PRELOAD_SESSION_ID', { sessionId });
}
}
// Fallback: Extract from route-based URL if PRELOAD didn't work
if (!sessionId) {
const pathname = window.location.pathname;
const sessionMatch = pathname.match(/\/claude\/ide\/session\/([^\/]+)$/);
if (sessionMatch && sessionMatch[1]) {
sessionId = sessionMatch[1];
console.log('[Init] Extracted sessionId from route:', sessionId);
if (window.traceExecution) {
window.traceExecution('ide.js', 'Extracted sessionId from URL path', { sessionId, pathname });
}
}
}
// Check URL params for session (legacy format), prompt, project, and view
const urlParams = new URLSearchParams(window.location.search);
const legacySessionId = urlParams.get('session');
const prompt = urlParams.get('prompt');
const project = urlParams.get('project');
const view = urlParams.get('view');
// Use route-based sessionId if available, otherwise fall back to legacy query param
if (!sessionId && legacySessionId) {
sessionId = legacySessionId;
console.log('[Init] Using legacy sessionId from query param:', sessionId);
}
// Parse project parameter if present
if (project) {
window.currentProjectDir = decodeURIComponent(project);
currentProjectName = project.split('/').filter(Boolean).pop() || 'Project';
console.log('[Init] Project context loaded:', currentProjectName, window.currentProjectDir);
}
if (sessionId || prompt) {
// CRITICAL: Set pending session attachment flag BEFORE switching view
// This allows loadChatView() to know a session is about to be attached
// and show appropriate loading state instead of "No sessions yet"
if (sessionId) {
window.pendingSessionAttach = sessionId;
console.log('[Init] Set pendingSessionAttach:', sessionId);
if (window.AutoFixLogger) {
window.AutoFixLogger.log('Set pendingSessionAttach flag', { sessionId });
}
if (window.traceExecution) {
window.traceExecution('ide.js', 'Set pendingSessionAttach flag', { sessionId });
}
}
// Switch to chat view first
switchView('chat');
// Wait for chat to load, then handle session/prompt
setTimeout(() => {
if (sessionId) {
if (window.AutoFixLogger) {
window.AutoFixLogger.log('Calling attachToSession...', { sessionId });
}
if (window.traceExecution) {
window.traceExecution('ide.js', 'Calling attachToSession', { sessionId });
}
attachToSession(sessionId);
}
if (prompt) {
setTimeout(() => {
const input = document.getElementById('chat-input');
if (input) {
input.value = decodeURIComponent(prompt);
sendChatMessage();
}
}, 1000);
}
}, 500);
} else if (view) {
// Switch to the specified view
switchView('chat');
} else {
// Default to chat view
switchView('chat');
}
}
// Auto-initialize using same pattern as session-picker
// Check if DOM is already loaded, if so run immediately, otherwise wait for DOMContentLoaded
if (typeof window !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', ideInit);
} else {
// DOM already loaded, run immediately
ideInit();
}
}
// Navigation
function initNavigation() {
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
item.addEventListener('click', () => {
const view = item.dataset.view;
switchView(view);
});
});
}
function switchView(viewName) {
if (window.traceExecution) {
window.traceExecution('ide.js', 'switchView called', { viewName });
}
// Update nav items
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
if (item.dataset.view === viewName) {
item.classList.add('active');
}
});
// Update views
document.querySelectorAll('.view').forEach(view => {
view.classList.remove('active');
});
document.getElementById(`${viewName}-view`).classList.add('active');
// Load content for the view
switch(viewName) {
case 'dashboard':
loadDashboard();
break;
case 'chat':
loadChatView();
break;
case 'sessions':
loadSessions();
break;
case 'projects':
loadProjects();
break;
case 'files':
loadFiles();
break;
case 'terminal':
loadTerminal();
break;
}
}
// WebSocket Connection
function connectWebSocket() {
const wsUrl = `wss://${window.location.host}/claude/api/claude/chat`;
console.log('Connecting to WebSocket:', wsUrl);
window.ws = new WebSocket(wsUrl);
// Set ready state to connecting
window.wsReady = false;
window.ws.onopen = () => {
console.log('WebSocket connected, readyState:', window.ws.readyState);
window.wsReady = true;
// Send a test message to verify connection
try {
window.ws.send(JSON.stringify({ type: 'ping' }));
} catch (error) {
console.error('Error sending ping:', error);
}
// Flush any queued messages
flushMessageQueue();
};
window.ws.onmessage = (event) => {
// Use requestIdleCallback or setTimeout to prevent blocking
// Priority: requestIdleCallback > setTimeout(0) > setTimeout(4ms)
if (window.requestIdleCallback) {
window.requestIdleCallback(() => {
processWebSocketMessage(event.data);
}, { timeout: 1000 });
} else {
setTimeout(() => {
processWebSocketMessage(event.data);
}, 0);
}
};
window.ws.onerror = (error) => {
console.error('WebSocket error:', error);
console.log('WebSocket error details:', {
type: error.type,
target: error.target,
readyState: window.ws?.readyState
});
};
window.ws.onclose = (event) => {
console.log('WebSocket disconnected:', {
code: event.code,
reason: event.reason,
wasClean: event.wasClean
});
window.wsReady = false;
window.ws = null;
// Attempt to reconnect with exponential backoff
scheduleReconnect();
};
}
/**
* Process WebSocket message with error handling and timeout protection
* @param {string} rawData - Raw message data from WebSocket
*/
function processWebSocketMessage(rawData) {
const startTime = performance.now();
const MESSAGE_PROCESSING_TIMEOUT = 100; // 100ms max per message
try {
// Add timeout protection for message processing
const timeoutId = setTimeout(() => {
console.warn('[WebSocket] Message processing timeout, blocking detected:', {
dataLength: rawData?.length || 0,
elapsed: performance.now() - startTime
});
}, MESSAGE_PROCESSING_TIMEOUT);
const data = JSON.parse(rawData);
console.log('WebSocket message received:', data.type);
// Clear timeout if processing completed in time
clearTimeout(timeoutId);
// Use defer for heavy message handlers
if (data.type === 'output' && data.data?.content?.length > 10000) {
// Large message - defer processing
setTimeout(() => handleWebSocketMessage(data), 0);
} else {
handleWebSocketMessage(data);
}
} catch (error) {
console.error('[WebSocket] Failed to parse message:', error, 'Raw data length:', rawData?.length);
}
}
// Exponential backoff for reconnection
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 10;
const BASE_RECONNECT_DELAY = 1000; // 1 second
function scheduleReconnect() {
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
console.error('[WebSocket] Max reconnection attempts reached');
if (typeof appendSystemMessage === 'function') {
appendSystemMessage('⚠️ WebSocket connection lost. Please refresh the page.');
}
return;
}
// Exponential backoff with jitter
const delay = Math.min(
BASE_RECONNECT_DELAY * Math.pow(2, reconnectAttempts) + Math.random() * 1000,
30000 // Max 30 seconds
);
reconnectAttempts++;
console.log(`[WebSocket] Scheduling reconnect attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS} in ${Math.round(delay)}ms`);
setTimeout(() => {
if (!window.ws || window.ws.readyState === WebSocket.CLOSED) {
console.log('[WebSocket] Attempting to reconnect...');
connectWebSocket();
}
}, delay);
}
// Reset reconnect attempts on successful connection
window.ws.onopen = () => {
reconnectAttempts = 0;
console.log('WebSocket connected, readyState:', window.ws.readyState);
window.wsReady = true;
// Send a test message to verify connection
try {
window.ws.send(JSON.stringify({ type: 'ping' }));
} catch (error) {
console.error('Error sending ping:', error);
}
// Flush any queued messages
flushMessageQueue();
};
// === WebSocket State Management ===
// Message queue for messages sent before WebSocket is ready
window.messageQueue = [];
window.wsReady = false;
/**
* Queue a message to be sent when WebSocket is ready
* @param {Object} message - Message to queue
*/
function queueMessage(message) {
window.messageQueue.push({
message: message,
timestamp: Date.now()
});
console.log(`[WebSocket] Message queued (${window.messageQueue.length} in queue):`, {
type: message.type,
sessionId: message.sessionId
});
showQueuedMessageIndicator();
// Try to flush immediately
console.log('[WebSocket] Attempting immediate flush...');
flushMessageQueue();
}
/**
* Flush all queued messages to WebSocket
*/
function flushMessageQueue() {
console.log('[WebSocket] flushMessageQueue called:', {
wsReady: window.wsReady,
wsExists: !!window.ws,
wsReadyState: window.ws?.readyState,
queueLength: window.messageQueue.length
});
if (!window.wsReady || !window.ws) {
console.log('[WebSocket] Not ready, keeping messages in queue');
return;
}
if (window.messageQueue.length === 0) {
console.log('[WebSocket] Queue is empty, nothing to flush');
return;
}
console.log(`[WebSocket] Flushing ${window.messageQueue.length} queued messages`);
// Send all queued messages in batches to prevent blocking
const BATCH_SIZE = 10;
const batches = [];
for (let i = 0; i < window.messageQueue.length; i += BATCH_SIZE) {
batches.push(window.messageQueue.slice(i, i + BATCH_SIZE));
}
window.messageQueue = [];
// Send batches with defer to prevent blocking
let batchIndex = 0;
function sendNextBatch() {
if (batchIndex >= batches.length) {
hideQueuedMessageIndicator();
return;
}
const batch = batches[batchIndex];
for (const item of batch) {
try {
const payloadStr = JSON.stringify(item.message);
console.log('[WebSocket] Sending queued message:', {
type: item.message.type,
sessionId: item.message.sessionId,
payloadLength: payloadStr.length
});
window.ws.send(payloadStr);
console.log('[WebSocket] ✓ Sent queued message:', item.message.type);
} catch (error) {
console.error('[WebSocket] ✗ Failed to send queued message:', error);
// Put it back in the queue
window.messageQueue.push(item);
}
}
batchIndex++;
// Defer next batch to prevent blocking
setTimeout(sendNextBatch, 0);
}
sendNextBatch();
}
/**
* Show indicator that messages are queued
*/
function showQueuedMessageIndicator() {
let indicator = document.getElementById('queued-message-indicator');
if (!indicator) {
indicator = document.createElement('div');
indicator.id = 'queued-message-indicator';
indicator.className = 'queued-message-indicator';
indicator.innerHTML = `
<span class="indicator-icon">⏳</span>
<span class="indicator-text">Message queued...</span>
`;
// Add to chat input area
const chatContainer = document.getElementById('chat-input-container');
if (chatContainer) {
chatContainer.appendChild(indicator);
}
}
indicator.style.display = 'flex';
}
/**
* Hide queued message indicator
*/
function hideQueuedMessageIndicator() {
const indicator = document.getElementById('queued-message-indicator');
if (indicator && window.messageQueue.length === 0) {
indicator.style.display = 'none';
}
}
function handleWebSocketMessage(data) {
switch(data.type) {
case 'connected':
console.log(data.message);
break;
case 'output':
handleSessionOutput(data);
break;
case 'operations-detected':
handleOperationsDetected(data);
break;
case 'operations-executed':
handleOperationsExecuted(data);
break;
case 'operations-error':
handleOperationsError(data);
break;
case 'operation-progress':
handleOperationProgress(data);
break;
case 'approval-request':
handleApprovalRequest(data);
break;
case 'approval-confirmed':
handleApprovalConfirmed(data);
break;
case 'approval-expired':
handleApprovalExpired(data);
break;
case 'error':
console.error('WebSocket error:', data.error);
// Show error in chat if attached
if (typeof appendSystemMessage === 'function') {
appendSystemMessage('Error: ' + data.error);
}
break;
}
}
/**
* Handle operations detected event
*/
function handleOperationsDetected(data) {
console.log('Operations detected:', data.operations.length);
// Only show if we're attached to this session
if (data.sessionId !== attachedSessionId) return;
// Store response for execution
window.currentOperationsResponse = data.response;
// Use tag renderer to show operations panel
if (typeof tagRenderer !== 'undefined') {
tagRenderer.showOperationsPanel(data.operations, data.response);
}
}
/**
* Handle operations executed event
*/
function handleOperationsExecuted(data) {
console.log('Operations executed:', data.results);
// Only handle if we're attached to this session
if (data.sessionId !== attachedSessionId) return;
// Hide progress and show completion
if (typeof tagRenderer !== 'undefined') {
tagRenderer.hideProgress();
tagRenderer.hideOperationsPanel();
tagRenderer.showCompletion(data.results);
}
}
/**
* Handle operations error event
*/
function handleOperationsError(data) {
console.error('Operations error:', data.error);
// Only handle if we're attached to this session
if (data.sessionId !== attachedSessionId) return;
// Show error
if (typeof tagRenderer !== 'undefined') {
tagRenderer.hideProgress();
tagRenderer.showError(data.error);
}
}
/**
* Handle operation progress event
*/
function handleOperationProgress(data) {
console.log('Operation progress:', data.progress);
// Only handle if we're attached to this session
if (data.sessionId !== attachedSessionId) return;
// Update progress
if (typeof tagRenderer !== 'undefined') {
const progress = data.progress;
let message = '';
switch(progress.type) {
case 'write':
message = `Creating ${progress.path}...`;
break;
case 'rename':
message = `Renaming ${progress.from} to ${progress.to}...`;
break;
case 'delete':
message = `Deleting ${progress.path}...`;
break;
case 'install':
message = `Installing packages: ${progress.packages.join(', ')}...`;
break;
case 'command':
message = `Executing command: ${progress.command}...`;
break;
default:
message = 'Processing...';
}
tagRenderer.updateProgress(message);
}
}
/**
* Handle approval request event
*/
function handleApprovalRequest(data) {
console.log('Approval request received:', data);
// Only handle if we're attached to this session
if (data.sessionId !== attachedSessionId) return;
// Render approval card in chat
if (typeof ApprovalCard !== 'undefined' && ApprovalCard.render) {
const card = ApprovalCard.render(data);
// Add to chat messages container
const messagesContainer = document.getElementById('chat-messages');
if (messagesContainer) {
messagesContainer.appendChild(card);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
} else {
console.error('[ApprovalCard] Component not loaded');
}
}
/**
* Handle approval confirmed event
*/
function handleApprovalConfirmed(data) {
console.log('Approval confirmed:', data);
// Only handle if we're attached to this session
if (data.sessionId !== attachedSessionId) return;
// Show confirmation message
const message = data.approved
? (data.customCommand
? `✅ Approved with custom command: ${data.customCommand}`
: '✅ Command approved')
: '❌ Command rejected';
if (typeof appendSystemMessage === 'function') {
appendSystemMessage(message);
}
}
/**
* Handle approval expired event
*/
function handleApprovalExpired(data) {
console.log('Approval expired:', data);
// Update the approval card to show expired state
if (typeof ApprovalCard !== 'undefined' && ApprovalCard.handleExpired) {
ApprovalCard.handleExpired(data.id);
}
}
/**
* Detect approval request in AI response
* @param {string} content - AI response content
* @returns {Object|null} - Approval request data or null
*/
function detectApprovalRequest(content) {
if (!content || typeof content !== 'string') return null;
const lower = content.toLowerCase();
// Common approval request patterns
const approvalPatterns = [
// "Would you like me to proceed with X"
/would\s+(?:you\s+)?like\s+me\s+to\s+proceed\s+(?:with\s+)?(?:executing\s+)?(?:running\s+)?["']?([a-z0-9._\s\-\/]+)["']?/i,
/should\s+i\s+(?:run|execute)\s+["']?([a-z0-9._\s\-\/]+)["']?/i,
/do\s+(?:you\s+)?want\s+me\s+to\s+(?:run|execute)\s+["']?([a-z0-9._\s\-\/]+)["']?/i,
/shall\s+i\s+(?:run|execute)\s+["']?([a-z0-9._\s\-\/]+)["']?/i,
// "Proceed with X?"
/proceed\s+(?:with\s+)?["']?([a-z0-9._\s\-\/]+)["']?\?/i,
// "Execute X?"
/execute\s+["']?([a-z0-9._\s\-\/]+)["']?\?/i,
// "Run X?"
/run\s+["']?([a-z0-9._\s\-\/]+)["']?\?/i
];
for (const pattern of approvalPatterns) {
const match = content.match(pattern);
if (match && match[1]) {
let command = match[1].trim();
// Clean up the command
command = command.replace(/[?\\s.]+$/, '').trim();
// Extract explanation from the content
let explanation = '';
// Pattern 1: "this will/does X" followed by period, newline, or end
const thisPattern = content.match(/this\s+(?:will|is)\s+([^.]+?)(?:\.|\n|$)/i);
// Pattern 2: "operation: X" followed by period, newline, or end
const operationPattern = content.match(/(?:network\s+operation|file\s+operation|system\s+operation)\s*[:\-]\s*([^.]+?)(?:\.|\n|$)/i);
if (thisPattern) {
explanation = thisPattern[1].trim();
} else if (operationPattern) {
explanation = operationPattern[1].trim();
}
// Generate a reasonable explanation if not found
if (!explanation) {
if (command.includes('ping')) {
explanation = 'Network operation - will send ICMP packets to the target host';
} else if (command.includes('rm') || command.includes('delete')) {
explanation = 'File deletion operation - files will be permanently removed';
} else if (command.includes('curl') || command.includes('wget') || command.includes('http')) {
explanation = 'Network operation - will make HTTP request(s)';
} else if (command.includes('ssh') || command.includes('scp')) {
explanation = 'Remote connection operation - will connect to external host';
} else {
explanation = `Execute command: ${command}`;
}
}
return {
command,
explanation,
originalText: content
};
}
}
return null;
}
/**
* Escape HTML to prevent XSS
* @param {string} text - Text to escape
* @returns {string} - Escaped text
*/
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function handleSessionOutput(data) {
// Handle output for sessions view
if (currentSession && data.sessionId === currentSession.id) {
appendOutput(data.data);
}
// Handle output for chat view
if (typeof attachedSessionId !== 'undefined' && data.sessionId === attachedSessionId) {
// Hide streaming indicator on first chunk
if (typeof hideStreamingIndicator === 'function') {
hideStreamingIndicator();
}
const content = data.data.content || '';
// ============================================================
// SEMANTIC VALIDATION - Detect confusing UX messages
// ============================================================
if (window.semanticValidator && content) {
const confusingOutput = window.semanticValidator.detectConfusingOutput(content);
if (confusingOutput) {
// Report to bug tracker
window.semanticValidator.reportSemanticError(confusingOutput);
// Log for debugging
console.warn('[SemanticValidator] Confusing UX message detected:', confusingOutput);
}
}
// ============================================================
// APPROVAL REQUEST DETECTION - Intercept AI approval requests
// ============================================================
// Check if AI is asking for approval
const approvalRequest = detectApprovalRequest(content);
if (approvalRequest && window.ApprovalCard) {
console.log('[ApprovalRequest] Detected:', approvalRequest);
// Don't show the raw AI message - show approval card instead
// But we need to still show the explanation part
// Check if we already have a pending approval for this command
const existingCard = document.querySelector(`[data-pending-command="${escapeHtml(approvalRequest.command)}"]`);
if (!existingCard) {
// Generate a unique ID for this approval
const approvalId = `approval-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
// Render approval card
const card = window.ApprovalCard.render({
id: approvalId,
sessionId: data.sessionId,
command: approvalRequest.command,
explanation: approvalRequest.explanation || 'AI agent requests approval to execute this command'
});
// Add tracking attribute
card.dataset.pendingCommand = approvalRequest.command;
// Add to chat
const messagesContainer = document.getElementById('chat-messages');
if (messagesContainer) {
messagesContainer.appendChild(card);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
// Store the approval for later response
window._pendingApprovals = window._pendingApprovals || {};
window._pendingApprovals[approvalId] = {
command: approvalRequest.command,
sessionId: data.sessionId,
originalMessage: content
};
// Don't add the AI's raw text to the chat
// Just skip to next iteration
return;
}
}
// Accumulate streaming content
if (streamingMessageElement && streamingMessageElement.isConnected) {
// Append to existing message
streamingMessageContent += content;
const bubble = streamingMessageElement.querySelector('.chat-message-bubble');
if (bubble && typeof formatMessage === 'function') {
bubble.innerHTML = formatMessage(streamingMessageContent);
}
} else {
// Start new streaming message
streamingMessageContent = content;
if (typeof appendMessage === 'function') {
appendMessage('assistant', content, true);
// Get the message element we just created
streamingMessageElement = document.querySelector('.chat-message.assistant:last-child');
}
}
// Reset streaming timeout - if no new chunks for 1 second, consider stream complete
clearTimeout(streamingTimeout);
streamingTimeout = setTimeout(() => {
streamingMessageElement = null;
streamingMessageContent = '';
if (typeof setGeneratingState === 'function') {
setGeneratingState(false);
}
// ============================================================
// COMMAND TRACKER - Complete command execution
// ============================================================
if (window.commandTracker && window._pendingCommandId) {
// Try to extract exit code from the last output
const exitCode = extractExitCode(streamingMessageContent);
// Complete the command tracking
window.commandTracker.completeCommand(
window._pendingCommandId,
exitCode,
streamingMessageContent
);
// Clear pending command ID
window._pendingCommandId = null;
console.log('[CommandTracker] Command completed via stream timeout');
}
}, 1000);
}
/**
* Extract exit code from command output
* @param {string} output - Command output
* @returns {number|null} - Exit code or null
*/
function extractExitCode(output) {
if (!output) return null;
// Look for exit code patterns
const exitCodeMatch = output.match(/exited with code (\d+|undefined|null)/i);
if (exitCodeMatch) {
const code = exitCodeMatch[1];
if (code === 'undefined' || code === 'null') {
return null; // Undefined/null exit code
}
return parseInt(code, 10);
}
// If no explicit exit code found, assume success (0)
return 0;
}
}
// Dashboard
async function loadDashboard() {
try {
// Load stats with timeout protection
const [sessionsRes, projectsRes] = await Promise.all([
fetchWithTimeout('/claude/api/claude/sessions', 5000),
fetchWithTimeout('/claude/api/claude/projects', 5000)
]);
const sessionsData = await sessionsRes.json();
const projectsData = await projectsRes.json();
// Update stats
document.getElementById('active-sessions-count').textContent =
sessionsData.active?.length || 0;
document.getElementById('historical-sessions-count').textContent =
sessionsData.historical?.length || 0;
document.getElementById('total-projects-count').textContent =
projectsData.projects?.length || 0;
// Update active sessions list - CLEAR LOADING STATE
const activeSessionsEl = document.getElementById('active-sessions-list');
if (sessionsData.active && sessionsData.active.length > 0) {
activeSessionsEl.innerHTML = sessionsData.active.map(session => `
<div class="session-item" onclick="viewSession('${session.id}')">
<div class="session-header">
<span class="session-id">${session.id.substring(0, 20)}...</span>
<span class="session-status ${session.status}">${session.status}</span>
</div>
<div class="session-meta">
Working: ${session.workingDir}<br>
Created: ${new Date(session.createdAt).toLocaleString()}
</div>
</div>
`).join('');
} else {
// Clear loading state, show empty state
activeSessionsEl.innerHTML = '<p class="placeholder">No active sessions</p>';
}
// Update projects list - CLEAR LOADING STATE
const projectsEl = document.getElementById('recent-projects-list');
if (projectsData.projects && projectsData.projects.length > 0) {
projectsEl.innerHTML = projectsData.projects.slice(0, 5).map(project => `
<div class="project-card" onclick="viewProject('${project.name}')">
<h3>${project.name}</h3>
<p class="project-meta">
Modified: ${new Date(project.modified).toLocaleDateString()}
</p>
</div>
`).join('');
} else {
// Clear loading state, show empty state
projectsEl.innerHTML = '<p class="placeholder">No projects yet</p>';
}
} catch (error) {
console.error('Error loading dashboard:', error);
// Clear loading states on error
const activeSessionsEl = document.getElementById('active-sessions-list');
const projectsEl = document.getElementById('recent-projects-list');
if (activeSessionsEl) activeSessionsEl.innerHTML = '<p class="placeholder">Error loading sessions</p>';
if (projectsEl) projectsEl.innerHTML = '<p class="placeholder">Error loading projects</p>';
}
}
function refreshSessions() {
loadDashboard();
}
// Sessions
async function loadSessions() {
const sessionsListEl = document.getElementById('sessions-list');
try {
// Get current project from URL
const urlParams = new URLSearchParams(window.location.search);
const projectPath = urlParams.get('project');
// Build API URL with project filter
let apiUrl = '/claude/api/claude/sessions';
if (projectPath) {
apiUrl += `?project=${encodeURIComponent(projectPath)}`;
console.log('[Sessions] Loading sessions for project:', projectPath);
}
// Show loading state
sessionsListEl.innerHTML = '<div class="loading">Loading sessions...</div>';
const res = await fetchWithTimeout(apiUrl, 5000);
// Handle HTTP errors
if (!res.ok) {
if (res.status === 401) {
sessionsListEl.innerHTML = `
<div class="error-state">
<p>⚠️ Session expired</p>
<button class="btn-primary" onclick="location.reload()">Login Again</button>
</div>
`;
return;
}
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const data = await res.json();
// Handle API errors
if (data.error) {
throw new Error(data.error);
}
const allSessions = [
...(data.active || []).map(s => ({...s, type: 'active'})),
...(data.historical || []).map(s => ({...s, type: 'historical'}))
];
// Sort by last activity (newest first)
allSessions.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;
});
// Empty state - CLEAR LOADING STATE
if (allSessions.length === 0) {
const projectName = projectPath ? projectPath.split('/').pop() : 'this project';
sessionsListEl.innerHTML = `
<div class="empty-state">
<div class="empty-icon">📂</div>
<p>No sessions found for <strong>${escapeHtml(projectName)}</strong></p>
<button class="btn-primary" onclick="startNewChat()">Create New Session</button>
</div>
`;
return;
}
// Render session list - CLEAR LOADING STATE
sessionsListEl.innerHTML = allSessions.map(session => {
const isRunning = session.status === 'running' && session.type === 'active';
const relativeTime = getRelativeTime(session);
const messageCount = session.messageCount || session.metadata?.messageCount || 0;
return `
<div class="session-item ${session.type}" onclick="viewSessionDetails('${session.id}')">
<button class="session-close-btn" onclick="deleteSession('${session.id}', event)" title="Delete session">×</button>
<div class="session-header">
<div class="session-info">
<span class="session-id">${session.id.substring(0, 12)}...</span>
<span class="session-status ${isRunning ? 'running' : 'stopped'}">
${isRunning ? '🟢 Running' : '⏸️ ' + (session.type === 'historical' ? 'Historical' : 'Stopped')}
</span>
</div>
<div class="session-time">${relativeTime}</div>
</div>
<div class="session-meta">
<div class="session-path">📁 ${escapeHtml(session.workingDir)}</div>
<div class="session-stats">
<span>💬 ${messageCount} messages</span>
</div>
</div>
</div>
`;
}).join('');
// CRITICAL FIX: Also update session tabs with the same sessions
if (window.sessionTabs && typeof window.sessionTabs.setSessions === 'function') {
window.sessionTabs.setSessions(allSessions);
console.log('[loadSessions] Updated session tabs with', allSessions.length, 'sessions');
}
} catch (error) {
console.error('[loadSessions] Error:', error);
sessionsListEl.innerHTML = `
<div class="error-state">
<div class="error-icon">⚠️</div>
<p>Failed to load sessions</p>
<p class="error-message">${escapeHtml(error.message)}</p>
<button class="btn-secondary" onclick="loadSessions()">Try Again</button>
</div>
`;
}
}
function getRelativeTime(session) {
const date = new Date(session.lastActivity || session.createdAt || session.created_at);
const now = new Date();
const diffMins = Math.floor((now - date) / 60000);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffMins < 1440) return `${Math.floor(diffMins/60)}h ago`;
return `${Math.floor(diffMins/1440)}d ago`;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Delete Session
async function deleteSession(sessionId, event) {
// Prevent triggering the session item click
if (event) {
event.stopPropagation();
event.preventDefault();
}
// Confirm deletion
const shortId = sessionId.substring(0, 12);
if (!confirm(`Delete session ${shortId}...?\n\nThis action cannot be undone.`)) {
return;
}
try {
console.log('[deleteSession] Deleting session:', sessionId);
const res = await fetch(`/claude/api/claude/sessions/${sessionId}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
credentials: 'include'
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const data = await res.json();
if (data.success || data.deleted) {
console.log('[deleteSession] Session deleted successfully');
// If the deleted session was the current one, clear current session state
if (attachedSessionId === sessionId || chatSessionId === sessionId) {
console.log('[deleteSession] Deleted current session, clearing state');
attachedSessionId = null;
chatSessionId = null;
// Clear UI
document.getElementById('current-session-id').textContent = 'None';
document.getElementById('chat-title').textContent = 'Claude Code IDE';
clearChatDisplay();
}
// Refresh the session list
await loadSessions();
// Show success message
if (typeof appendSystemMessage === 'function') {
appendSystemMessage(`✅ Session ${shortId}... deleted`);
}
} else {
throw new Error(data.error || 'Failed to delete session');
}
} catch (error) {
console.error('[deleteSession] Error:', error);
if (typeof appendSystemMessage === 'function') {
appendSystemMessage(`❌ Failed to delete session: ${error.message}`);
} else {
alert(`Failed to delete session: ${error.message}`);
}
}
}
async function viewSessionDetails(sessionId) {
const detailEl = document.getElementById('session-detail');
try {
// Show loading state
detailEl.innerHTML = '<div class="loading">Loading session details...</div>';
const res = await fetchWithTimeout(`/claude/api/claude/sessions/${sessionId}`, 5000);
// Handle 404 - session not found
if (res.status === 404) {
detailEl.innerHTML = `
<div class="error-state">
<div class="error-icon">🔍</div>
<h3>Session Not Found</h3>
<p>The session <code>${escapeHtml(sessionId)}</code> could not be found.</p>
<button class="btn-primary" onclick="loadSessions()">Back to Sessions</button>
</div>
`;
return;
}
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const data = await res.json();
if (data.error) {
throw new Error(data.error);
}
if (!data.session) {
throw new Error('No session data in response');
}
const session = data.session;
const isRunning = session.status === 'running' && session.pid;
const messageCount = session.outputBuffer?.length || 0;
// Render session detail card - CLEAR LOADING STATE
detailEl.innerHTML = `
<div class="session-detail-card">
<div class="session-detail-header">
<div class="session-title">
<h2>Session ${session.id.substring(0, 12)}...</h2>
<span class="session-status-badge ${isRunning ? 'running' : 'stopped'}">
${isRunning ? '🟢 Running' : '⏸️ Stopped'}
</span>
</div>
<div class="session-detail-actions">
<button class="btn-primary" onclick="continueSessionInChat('${session.id}')">
💬 Continue in Chat
</button>
<button class="btn-secondary" onclick="duplicateSession('${session.id}')">
📋 Duplicate
</button>
${isRunning ? `
<button class="btn-danger" onclick="terminateSession('${session.id}')">
⏹️ Terminate
</button>
` : ''}
</div>
</div>
<div class="session-detail-meta">
<div class="meta-row">
<span class="meta-label">Working Directory:</span>
<span class="meta-value">${escapeHtml(session.workingDir)}</span>
</div>
<div class="meta-row">
<span class="meta-label">Created:</span>
<span class="meta-value">${new Date(session.createdAt).toLocaleString()}</span>
</div>
<div class="meta-row">
<span class="meta-label">Last Activity:</span>
<span class="meta-value">${new Date(session.lastActivity).toLocaleString()}</span>
</div>
<div class="meta-row">
<span class="meta-label">Messages:</span>
<span class="meta-value">${messageCount}</span>
</div>
${session.pid ? `
<div class="meta-row">
<span class="meta-label">PID:</span>
<span class="meta-value">${session.pid}</span>
</div>
` : ''}
</div>
<div class="session-context">
<h3>Token Usage</h3>
<div class="context-bar">
<div class="context-fill" style="width: ${Math.min(100, (session.context?.totalTokens || 0) / (session.context?.maxTokens || 200000) * 100)}%"></div>
</div>
<div class="context-stats">
<span>${(session.context?.totalTokens || 0).toLocaleString()} / ${(session.context?.maxTokens || 200000).toLocaleString()} tokens</span>
<span>${Math.round((session.context?.totalTokens || 0) / (session.context?.maxTokens || 200000) * 100)}% used</span>
</div>
</div>
<div class="session-output-preview">
<h3>Session Output (${messageCount} entries)</h3>
<div class="output-scroll-area">
${session.outputBuffer?.slice(0, 50).map(entry => `
<div class="output-entry ${entry.type}">
<div class="output-header">
<span class="output-type">${entry.type}</span>
<span class="output-time">${new Date(entry.timestamp).toLocaleTimeString()}</span>
</div>
<div class="output-content">${escapeHtml(entry.content.substring(0, 500))}${entry.content.length > 500 ? '...' : ''}</div>
</div>
`).join('') || '<p class="no-output">No output yet</p>'}
${session.outputBuffer?.length > 50 ? `<p class="output-truncated">...and ${session.outputBuffer.length - 50} more entries</p>` : ''}
</div>
</div>
</div>
`;
currentSession = session;
} catch (error) {
console.error('[viewSessionDetails] Error:', error);
detailEl.innerHTML = `
<div class="error-state">
<div class="error-icon">⚠️</div>
<h3>Failed to Load Session</h3>
<p class="error-message">${escapeHtml(error.message)}</p>
<button class="btn-primary" onclick="loadSessions()">Back to Sessions</button>
</div>
`;
}
}
async function continueSessionInChat(sessionId) {
console.log('[Sessions] Continuing session in Chat:', sessionId);
try {
showLoadingOverlay('Loading session...');
const res = await fetchWithTimeout(`/claude/api/claude/sessions/${sessionId}`, 5000);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = await res.json();
if (!data.session) {
throw new Error('Session not found');
}
const session = data.session;
// Check if session is runnable
if (session.status === 'terminated' || session.status === 'stopped') {
hideLoadingOverlay();
if (confirm('This session has ended. Do you want to create a new session with the same working directory?')) {
await duplicateSession(sessionId);
}
return;
}
// Store pending session and switch views
window.pendingSessionId = sessionId;
window.pendingSessionData = session;
hideLoadingOverlay();
switchView('chat');
} catch (error) {
console.error('[continueSessionInChat] Error:', error);
hideLoadingOverlay();
showToast('❌ Failed to load session: ' + error.message, 'error');
}
}
async function duplicateSession(sessionId) {
try {
const res = await fetchWithTimeout(`/claude/api/claude/sessions/${sessionId}`, 5000);
const data = await res.json();
if (!data.session) {
throw new Error('Session not found');
}
const workingDir = data.session.workingDir;
const projectName = workingDir.split('/').pop();
showLoadingOverlay('Duplicating session...');
const createRes = await fetchWithTimeout('/claude/api/claude/sessions', 5000, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
workingDir,
metadata: {
type: 'chat',
source: 'web-ide',
project: projectName,
duplicatedFrom: sessionId
}
})
});
if (!createRes.ok) {
throw new Error(`HTTP ${createRes.status}`);
}
const createData = await createRes.json();
hideLoadingOverlay();
showToast('✅ Session duplicated!', 'success');
loadSessions();
setTimeout(() => {
if (confirm('Start chatting in the duplicated session?')) {
continueSessionInChat(createData.session.id);
}
}, 500);
} catch (error) {
console.error('[duplicateSession] Error:', error);
hideLoadingOverlay();
showToast('Failed to duplicate session: ' + error.message, 'error');
}
}
async function terminateSession(sessionId) {
if (!confirm('Are you sure you want to terminate this session?')) {
return;
}
try {
showLoadingOverlay('Terminating session...');
const res = await fetchWithTimeout(`/claude/api/claude/sessions/${sessionId}`, 5000, {
method: 'DELETE'
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
hideLoadingOverlay();
showToast('✅ Session terminated', 'success');
loadSessions();
if (currentSession && currentSession.id === sessionId) {
document.getElementById('session-detail').innerHTML = `
<div class="placeholder">
<h2>Session Terminated</h2>
<p>Select another session from the sidebar</p>
</div>
`;
currentSession = null;
}
} catch (error) {
console.error('[terminateSession] Error:', error);
hideLoadingOverlay();
showToast('Failed to terminate session: ' + error.message, 'error');
}
}
// Projects
async function loadProjects() {
try {
const res = await fetchWithTimeout('/claude/api/claude/projects', 5000);
const data = await res.json();
const gridEl = document.getElementById('projects-grid');
if (data.projects && data.projects.length > 0) {
gridEl.innerHTML = data.projects.map(project => `
<div class="project-card" onclick="viewProject('${project.name}')">
<h3>${project.name}</h3>
<p class="project-meta">
Modified: ${new Date(project.modified).toLocaleString()}
</p>
<p class="project-description">Click to view project details</p>
</div>
`).join('');
} else {
gridEl.innerHTML = '<p class="placeholder">No projects yet. Create your first project!</p>';
}
} catch (error) {
console.error('Error loading projects:', error);
}
}
async function viewProject(projectName) {
// Open the project file in the files view
const path = `Claude Projects/${projectName}.md`;
loadFileContent(path);
switchView('files');
}
// Files
async function loadFiles() {
try {
const res = await fetchWithTimeout('/claude/api/files', 5000);
const data = await res.json();
const treeEl = document.getElementById('file-tree');
treeEl.innerHTML = renderFileTree(data.tree);
} catch (error) {
console.error('Error loading files:', error);
}
}
async function loadTerminal() {
// Initialize terminal manager if not already done
if (!window.terminalManager) {
window.terminalManager = new TerminalManager();
await window.terminalManager.initialize();
}
// Set up new terminal button
const btnNewTerminal = document.getElementById('btn-new-terminal');
if (btnNewTerminal) {
btnNewTerminal.onclick = () => {
window.terminalManager.createTerminal();
};
}
}
function renderFileTree(tree, level = 0) {
return tree.map(item => {
const padding = level * 1 + 0.5;
const icon = item.type === 'folder' ? '📁' : '📄';
if (item.type === 'folder' && item.children) {
return `
<div style="padding-left: ${padding}rem">
<div class="tree-item folder" onclick="toggleFolder(this)">
<span>${icon}</span>
<span>${item.name}</span>
</div>
<div class="tree-children" style="display: none;">
${renderFileTree(item.children, level + 1)}
</div>
</div>
`;
} else {
return `
<div style="padding-left: ${padding}rem">
<div class="tree-item file" onclick="loadFile('${item.path}')">
<span>${icon}</span>
<span>${item.name}</span>
</div>
</div>
`;
}
}).join('');
}
function toggleFolder(element) {
const children = element.parentElement.querySelector('.tree-children');
const icon = element.querySelector('span:first-child');
if (children.style.display === 'none') {
children.style.display = 'block';
icon.textContent = '📂';
} else {
children.style.display = 'none';
icon.textContent = '📁';
}
}
async function loadFile(filePath) {
try {
const res = await fetchWithTimeout(`/claude/api/file/${encodeURIComponent(filePath)}`, 5000);
const data = await res.json();
// Check if Monaco Editor component is available
if (window.monacoEditor) {
// Use the Monaco-based editor
await window.monacoEditor.openFile(filePath, data.content || '');
} else {
// Fallback to simple view
console.warn('[loadFile] Monaco Editor not available, using fallback');
const editorEl = document.getElementById('file-editor');
const isHtmlFile = filePath.toLowerCase().endsWith('.html') || filePath.toLowerCase().endsWith('.htm');
if (isHtmlFile) {
// HTML file - show with preview option
editorEl.innerHTML = `
<div class="file-header">
<h2>${filePath}</h2>
<div class="file-actions">
<button class="btn-secondary btn-sm" onclick="showHtmlPreview('${filePath}')">👁️ Preview</button>
</div>
</div>
<div class="file-content" id="file-content-view">
<div class="view-toggle">
<button class="toggle-btn active" data-view="code" onclick="switchFileView('code')">Code</button>
<button class="toggle-btn" data-view="preview" onclick="switchFileView('preview')">Preview</button>
</div>
<div class="code-view">
<pre><code class="language-html">${escapeHtml(data.content || '')}</code></pre>
</div>
<div class="preview-view" style="display: none;">
<iframe id="html-preview-frame" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
</div>
</div>
`;
// Store file content for preview
window.currentFileContent = data.content || '';
window.currentFilePath = filePath;
// Highlight code
if (window.hljs) {
document.querySelectorAll('#file-content-view pre code').forEach((block) => {
hljs.highlightElement(block);
});
}
} else {
// Non-HTML file - show content
const language = getLanguageFromFile(filePath);
editorEl.innerHTML = `
<div class="file-header">
<h2>${filePath}</h2>
<span class="language-badge">${language}</span>
</div>
<div class="file-content">
<pre class="code-content"><code>${escapeHtml(data.content || '')}</code></pre>
</div>
`;
}
}
} catch (error) {
console.error('Error loading file:', error);
const editorEl = document.getElementById('file-editor');
if (editorEl) {
editorEl.innerHTML = `
<div class="file-error">
<h2>Error loading file</h2>
<p>${error.message}</p>
</div>
`;
}
}
}
// Helper function to get language from file path
function getLanguageFromFile(filePath) {
const ext = filePath.split('.').pop().toLowerCase();
const languageMap = {
'js': 'JavaScript',
'jsx': 'JavaScript JSX',
'ts': 'TypeScript',
'tsx': 'TypeScript JSX',
'py': 'Python',
'html': 'HTML',
'css': 'CSS',
'json': 'JSON',
'md': 'Markdown',
'txt': 'Plain Text'
};
return languageMap[ext] || 'Plain Text';
}
async function loadFileContent(filePath) {
await loadFile(filePath);
switchView('files');
}
// HTML Preview Functions
function showHtmlPreview(filePath) {
switchFileView('preview');
}
function switchFileView(view) {
const codeView = document.querySelector('.code-view');
const previewView = document.querySelector('.preview-view');
const toggleBtns = document.querySelectorAll('.toggle-btn');
// Update buttons
toggleBtns.forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.view === view) {
btn.classList.add('active');
}
});
// Show/hide views
if (view === 'code') {
codeView.style.display = 'block';
previewView.style.display = 'none';
} else if (view === 'preview') {
codeView.style.display = 'none';
previewView.style.display = 'block';
// Load HTML into iframe using blob URL
const iframe = document.getElementById('html-preview-frame');
if (iframe && window.currentFileContent) {
// Create blob URL from HTML content
const blob = new Blob([window.currentFileContent], { type: 'text/html' });
const blobUrl = URL.createObjectURL(blob);
// Load blob URL in iframe
iframe.src = blobUrl;
// Clean up blob URL when iframe is unloaded
iframe.onload = () => {
// Keep the blob URL active while preview is shown
};
}
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Modals
function createNewSession() {
document.getElementById('modal-overlay').classList.remove('hidden');
document.getElementById('new-session-modal').classList.remove('hidden');
}
function createNewProject() {
document.getElementById('modal-overlay').classList.remove('hidden');
document.getElementById('new-project-modal').classList.remove('hidden');
}
function closeModal() {
document.getElementById('modal-overlay').classList.add('hidden');
document.querySelectorAll('.modal').forEach(modal => {
modal.classList.add('hidden');
});
}
async function submitNewSession() {
const workingDir = document.getElementById('session-working-dir').value;
const project = document.getElementById('session-project').value;
try {
const res = await fetchWithTimeout('/claude/api/claude/sessions', 5000, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
workingDir,
metadata: { project }
})
});
const data = await res.json();
if (data.success) {
closeModal();
viewSession(data.session.id);
}
} catch (error) {
console.error('Error creating session:', error);
alert('Failed to create session');
}
}
async function submitNewProject() {
const name = document.getElementById('project-name').value;
const description = document.getElementById('project-description').value;
const type = document.getElementById('project-type').value;
if (!name) {
alert('Please enter a project name');
return;
}
try {
const res = await fetchWithTimeout('/claude/api/claude/projects', 5000, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description, type })
});
const data = await res.json();
if (data.success) {
closeModal();
loadProjects();
viewProject(name);
}
} catch (error) {
console.error('Error creating project:', error);
alert('Failed to create project');
}
}
// Utility functions
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Show loading overlay
* @param {string} message - The message to display
*/
function showLoadingOverlay(message = 'Loading...') {
let overlay = document.getElementById('loading-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'loading-overlay';
overlay.className = 'loading-overlay';
overlay.innerHTML = `
<div class="loading-spinner"></div>
<p class="loading-text">${escapeHtml(message)}</p>
`;
document.body.appendChild(overlay);
} else {
// Update message if provided
const textElement = overlay.querySelector('.loading-text');
if (textElement) {
textElement.textContent = message;
}
}
overlay.classList.remove('hidden');
setTimeout(() => {
overlay.classList.add('visible');
}, 10);
}
/**
* Hide loading overlay
*/
function hideLoadingOverlay() {
const overlay = document.getElementById('loading-overlay');
if (overlay) {
overlay.classList.remove('visible');
setTimeout(() => {
overlay.classList.add('hidden');
}, 300);
}
}
/**
* Show toast notification
* @param {string} message - The message to display
* @param {string} type - The type of toast: 'success', 'error', 'info'
* @param {number} duration - Duration in milliseconds (default: 3000)
*/
function showToast(message, type = 'info', duration = 3000) {
// Remove existing toasts
const existingToasts = document.querySelectorAll('.toast-notification');
existingToasts.forEach(toast => toast.remove());
// Create toast element
const toast = document.createElement('div');
toast.className = `toast-notification toast-${type}`;
toast.innerHTML = `
<span class="toast-icon">${getToastIcon(type)}</span>
<span class="toast-message">${escapeHtml(message)}</span>
`;
document.body.appendChild(toast);
// Trigger animation
setTimeout(() => {
toast.classList.add('visible');
}, 10);
// Auto remove after duration
setTimeout(() => {
toast.classList.remove('visible');
setTimeout(() => {
toast.remove();
}, 300);
}, duration);
}
/**
* Get toast icon based on type
*/
function getToastIcon(type) {
const icons = {
success: '✓',
error: '✕',
info: '',
warning: '⚠'
};
return icons[type] || icons.info;
}
/**
* Fetch with timeout protection to prevent hanging requests
* @param {string} url - The URL to fetch
* @param {number} timeout - Timeout in milliseconds
* @param {object} options - Fetch options
* @returns {Promise<Response>}
*/
async function fetchWithTimeout(url, timeout = 5000, options = {}) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timeout after ${timeout}ms`);
}
throw error;
}
}
function showProjects() {
switchView('projects');
}
// Logout
document.getElementById('logout-btn')?.addEventListener('click', async () => {
try {
await fetch('/claude/api/logout', { method: 'POST' });
window.location.reload();
} catch (error) {
console.error('Error logging out:', error);
}
});