// Claude Code IDE JavaScript
let currentSession = null;
let ws = null;
// 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
document.addEventListener('DOMContentLoaded', () => {
initNavigation();
connectWebSocket();
// Check URL params for session and prompt
const urlParams = new URLSearchParams(window.location.search);
const sessionId = urlParams.get('session');
const prompt = urlParams.get('prompt');
if (sessionId || prompt) {
// Switch to chat view first
switchView('chat');
// Wait for chat to load, then handle session/prompt
setTimeout(() => {
if (sessionId) {
attachToSession(sessionId);
}
if (prompt) {
setTimeout(() => {
const input = document.getElementById('chat-input');
if (input) {
input.value = decodeURIComponent(prompt);
sendChatMessage();
}
}, 1000);
}
}, 500);
} else {
// Default to chat view
switchView('chat');
}
});
// 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) {
// 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);
window.ws.onopen = () => {
console.log('WebSocket connected, readyState:', window.ws.readyState);
// Send a test message to verify connection
try {
window.ws.send(JSON.stringify({ type: 'ping' }));
} catch (error) {
console.error('Error sending ping:', error);
}
};
window.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('WebSocket message received:', data.type);
handleWebSocketMessage(data);
};
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
});
// Clear the ws reference
window.ws = null;
// Attempt to reconnect after 5 seconds
setTimeout(() => {
console.log('Attempting to reconnect...');
connectWebSocket();
}, 5000);
};
}
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 '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);
}
}
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
if (typeof hideStreamingIndicator === 'function') {
hideStreamingIndicator();
}
// Append output as assistant message
if (typeof appendMessage === 'function') {
appendMessage('assistant', data.data.content, true);
}
}
}
// Dashboard
async function loadDashboard() {
try {
// Load stats
const [sessionsRes, projectsRes] = await Promise.all([
fetch('/claude/api/claude/sessions'),
fetch('/claude/api/claude/projects')
]);
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
const activeSessionsEl = document.getElementById('active-sessions-list');
if (sessionsData.active && sessionsData.active.length > 0) {
activeSessionsEl.innerHTML = sessionsData.active.map(session => `
Working: ${session.workingDir}
Created: ${new Date(session.createdAt).toLocaleString()}
`).join('');
} else {
activeSessionsEl.innerHTML = 'No active sessions
';
}
// Update projects list
const projectsEl = document.getElementById('recent-projects-list');
if (projectsData.projects && projectsData.projects.length > 0) {
projectsEl.innerHTML = projectsData.projects.slice(0, 5).map(project => `
${project.name}
Modified: ${new Date(project.modified).toLocaleDateString()}
`).join('');
} else {
projectsEl.innerHTML = 'No projects yet
';
}
} catch (error) {
console.error('Error loading dashboard:', error);
}
}
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 = 'Loading sessions...
';
const res = await fetch(apiUrl);
// Handle HTTP errors
if (!res.ok) {
if (res.status === 401) {
sessionsListEl.innerHTML = `
β οΈ Session expired
`;
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
if (allSessions.length === 0) {
const projectName = projectPath ? projectPath.split('/').pop() : 'this project';
sessionsListEl.innerHTML = `
π
No sessions found for ${escapeHtml(projectName)}
`;
return;
}
// Render session list
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 `
`;
}).join('');
} catch (error) {
console.error('[loadSessions] Error:', error);
sessionsListEl.innerHTML = `
β οΈ
Failed to load sessions
${escapeHtml(error.message)}
`;
}
}
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;
}
async function viewSessionDetails(sessionId) {
const detailEl = document.getElementById('session-detail');
try {
// Show loading state
detailEl.innerHTML = 'Loading session details...
';
const res = await fetch(`/claude/api/claude/sessions/${sessionId}`);
// Handle 404 - session not found
if (res.status === 404) {
detailEl.innerHTML = `
π
Session Not Found
The session ${escapeHtml(sessionId)} could not be found.
`;
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
detailEl.innerHTML = `
Token Usage
${(session.context?.totalTokens || 0).toLocaleString()} / ${(session.context?.maxTokens || 200000).toLocaleString()} tokens
${Math.round((session.context?.totalTokens || 0) / (session.context?.maxTokens || 200000) * 100)}% used
Session Output (${messageCount} entries)
`;
currentSession = session;
} catch (error) {
console.error('[viewSessionDetails] Error:', error);
detailEl.innerHTML = `
β οΈ
Failed to Load Session
${escapeHtml(error.message)}
`;
}
}
async function continueSessionInChat(sessionId) {
console.log('[Sessions] Continuing session in Chat:', sessionId);
try {
showLoadingOverlay('Loading session...');
const res = await fetch(`/claude/api/claude/sessions/${sessionId}`);
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;
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 fetch(`/claude/api/claude/sessions/${sessionId}`);
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 fetch('/claude/api/claude/sessions', {
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 fetch(`/claude/api/claude/sessions/${sessionId}`, {
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 = `
Session Terminated
Select another session from the sidebar
`;
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 fetch('/claude/api/claude/projects');
const data = await res.json();
const gridEl = document.getElementById('projects-grid');
if (data.projects && data.projects.length > 0) {
gridEl.innerHTML = data.projects.map(project => `
${project.name}
Modified: ${new Date(project.modified).toLocaleString()}
Click to view project details
`).join('');
} else {
gridEl.innerHTML = 'No projects yet. Create your first project!
';
}
} 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 fetch('/claude/api/files');
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 `
${icon}
${item.name}
${renderFileTree(item.children, level + 1)}
`;
} else {
return `
`;
}
}).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 fetch(`/claude/api/file/${encodeURIComponent(filePath)}`);
const data = await res.json();
const isHtmlFile = filePath.toLowerCase().endsWith('.html') || filePath.toLowerCase().endsWith('.htm');
const editorEl = document.getElementById('file-editor');
if (isHtmlFile) {
// HTML file - show with preview option
editorEl.innerHTML = `
${escapeHtml(data.content)}
`;
// 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 as before
editorEl.innerHTML = `
`;
}
} catch (error) {
console.error('Error loading file:', error);
}
}
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 fetch('/claude/api/claude/sessions', {
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 fetch('/claude/api/claude/projects', {
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 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 = `
${getToastIcon(type)}
${escapeHtml(message)}
`;
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;
}
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);
}
});