Files
SuperCharged-Claude-Code-Up…/public/claude-ide/ide.js
uroma 0dd2083556 Initial commit: Obsidian Web Interface for Claude Code
- Full IDE with terminal integration using xterm.js
- Session management with local and web sessions
- HTML preview functionality
- Multi-terminal support with session picker

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 16:29:44 +00:00

835 lines
27 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 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 => `
<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 {
activeSessionsEl.innerHTML = '<p class="placeholder">No active sessions</p>';
}
// 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 => `
<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 {
projectsEl.innerHTML = '<p class="placeholder">No projects yet</p>';
}
} catch (error) {
console.error('Error loading dashboard:', error);
}
}
function refreshSessions() {
loadDashboard();
}
// Sessions
async function loadSessions() {
try {
const res = await fetch('/claude/api/claude/sessions');
const data = await res.json();
const sessionsListEl = document.getElementById('sessions-list');
const allSessions = [
...(data.active || []),
...(data.historical || [])
];
if (allSessions.length > 0) {
sessionsListEl.innerHTML = allSessions.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">
${session.workingDir}<br>
${new Date(session.createdAt).toLocaleString()}
</div>
</div>
`).join('');
} else {
sessionsListEl.innerHTML = '<p class="placeholder">No sessions</p>';
}
} catch (error) {
console.error('Error loading sessions:', error);
}
}
async function viewSession(sessionId) {
try {
const res = await fetch(`/claude/api/claude/sessions/${sessionId}`);
const data = await res.json();
currentSession = data.session;
const detailEl = document.getElementById('session-detail');
detailEl.innerHTML = `
<div class="session-header-info">
<h2>${data.session.id}</h2>
<p>Status: <span class="session-status ${data.session.status}">${data.session.status}</span></p>
<p>PID: ${data.session.pid || 'N/A'}</p>
<p>Working Directory: ${data.session.workingDir}</p>
<p>Created: ${new Date(data.session.createdAt).toLocaleString()}</p>
</div>
<h3>Context Usage</h3>
<div class="context-bar">
<div class="context-fill" style="width: ${data.session.context.totalTokens / data.session.context.maxTokens * 100}%"></div>
</div>
<div class="context-stats">
<span>${data.session.context.totalTokens.toLocaleString()} tokens</span>
<span>${Math.round(data.session.context.totalTokens / data.session.context.maxTokens * 100)}% used</span>
</div>
<h3>Session Output</h3>
<div class="session-output" id="session-output">
${data.session.outputBuffer.map(entry => `
<div class="output-line ${entry.type}">${escapeHtml(entry.content)}</div>
`).join('')}
</div>
${data.session.status === 'running' ? `
<div class="command-input-container">
<input type="text" id="command-input" class="command-input" placeholder="Enter command..." onkeypress="handleCommandKeypress(event)">
<button class="btn-primary" onclick="sendCommand()">Send</button>
</div>
` : ''}
`;
// Switch to sessions view
switchView('sessions');
} catch (error) {
console.error('Error viewing session:', error);
alert('Failed to load session');
}
}
function handleCommandKeypress(event) {
if (event.key === 'Enter') {
sendCommand();
}
}
async function sendCommand() {
const input = document.getElementById('command-input');
const command = input.value.trim();
if (!command || !currentSession) return;
try {
await fetch(`/claude/api/claude/sessions/${currentSession.id}/command`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command })
});
input.value = '';
// Append command to output
appendOutput({
type: 'command',
content: `$ ${command}\n`
});
} catch (error) {
console.error('Error sending command:', error);
alert('Failed to send command');
}
}
function appendOutput(data) {
const outputEl = document.getElementById('session-output');
if (outputEl) {
const line = document.createElement('div');
line.className = `output-line ${data.type}`;
line.textContent = data.content;
outputEl.appendChild(line);
outputEl.scrollTop = outputEl.scrollHeight;
}
}
// 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 => `
<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 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 `
<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 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 = `
<div class="file-header">
<h2>${filePath}</h2>
<div class="file-actions">
<button class="btn-secondary btn-sm" onclick="editFile('${filePath}')">Edit</button>
<button class="btn-primary 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 as before
editorEl.innerHTML = `
<div class="file-header">
<h2>${filePath}</h2>
<div class="file-actions">
<button class="btn-secondary btn-sm" onclick="editFile('${filePath}')">Edit</button>
</div>
</div>
<div class="file-content">
<div class="markdown-body">${data.html}</div>
</div>
`;
}
} 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 = `
<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;
}
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);
}
});