diff --git a/public/claude-ide/bug-tracker.js b/public/claude-ide/bug-tracker.js
index 60dfd95c..be345d81 100644
--- a/public/claude-ide/bug-tracker.js
+++ b/public/claude-ide/bug-tracker.js
@@ -14,6 +14,12 @@
activityLog: [], // New: stores AI activity stream
addError(error) {
+ // Skip 'info' type errors - they're for learning, not bugs
+ if (error.type === 'info') {
+ console.log('[BugTracker] Skipping info-type error:', error.message);
+ return null;
+ }
+
const errorId = this.generateErrorId(error);
const existingError = this.errors.find(e => e.id === errorId);
@@ -221,17 +227,43 @@
triggerManualFix(errorId) {
const error = this.errors.find(e => e.id === errorId);
- if (error) {
- // Report to server to trigger fix
- fetch('/claude/api/log-error', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- ...error,
- manualTrigger: true
- })
- });
+ if (!error) {
+ console.error('[BugTracker] Error not found:', errorId);
+ return;
}
+
+ // Report to server to trigger fix
+ fetch('/claude/api/log-error', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ ...error,
+ manualTrigger: true
+ })
+ })
+ .then(res => {
+ if (!res.ok) {
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
+ }
+ return res.json();
+ })
+ .then(data => {
+ console.log('[BugTracker] Manual fix triggered:', data);
+
+ // Update error status to 'fixing'
+ this.startFix(errorId);
+
+ // Show success feedback
+ if (typeof showToast === 'function') {
+ showToast('Auto-fix agent triggered', 'success');
+ }
+ })
+ .catch(err => {
+ console.error('[BugTracker] Failed to trigger fix:', err);
+ if (typeof showToast === 'function') {
+ showToast('Failed to trigger auto-fix', 'error');
+ }
+ });
},
getTimeAgo(timestamp) {
diff --git a/public/claude-ide/components/approval-card.css b/public/claude-ide/components/approval-card.css
new file mode 100644
index 00000000..14c7d360
--- /dev/null
+++ b/public/claude-ide/components/approval-card.css
@@ -0,0 +1,265 @@
+/* ============================================================
+ Approval Card Component Styles
+ ============================================================ */
+
+.approval-card {
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
+ border: 1px solid #4a9eff;
+ border-radius: 12px;
+ padding: 16px;
+ margin: 12px 0;
+ box-shadow: 0 4px 20px rgba(74, 158, 255, 0.2);
+ animation: approvalCardSlideIn 0.3s ease-out;
+}
+
+@keyframes approvalCardSlideIn {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* Header Section */
+.approval-card-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding-bottom: 12px;
+ border-bottom: 1px solid rgba(74, 158, 255, 0.3);
+ margin-bottom: 12px;
+}
+
+.approval-icon {
+ font-size: 20px;
+ animation: approvalIconPulse 2s infinite;
+}
+
+@keyframes approvalIconPulse {
+ 0%, 100% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.1);
+ }
+}
+
+.approval-label {
+ font-weight: 600;
+ color: #4a9eff;
+ font-size: 13px;
+}
+
+.approval-command {
+ background: rgba(0, 0, 0, 0.3);
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: 12px;
+ color: #7dd3fc;
+ flex: 1;
+ overflow-x: auto;
+}
+
+/* Explanation Section */
+.approval-explanation {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ padding: 10px;
+ background: rgba(74, 158, 255, 0.1);
+ border-radius: 8px;
+ margin-bottom: 12px;
+ font-size: 13px;
+ line-height: 1.4;
+}
+
+.explanation-icon {
+ font-size: 14px;
+ flex-shrink: 0;
+}
+
+.explanation-text {
+ color: #e0e0e0;
+ flex: 1;
+}
+
+/* Buttons Section */
+.approval-buttons {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.approval-buttons button {
+ flex: 1;
+ min-width: 100px;
+ padding: 10px 16px;
+ border: none;
+ border-radius: 8px;
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.btn-approve {
+ background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
+ color: white;
+}
+
+.btn-approve:hover {
+ background: linear-gradient(135deg, #16a34a 0%, #15803d 100%);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(34, 197, 94, 0.3);
+}
+
+.btn-custom {
+ background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
+ color: white;
+}
+
+.btn-custom:hover {
+ background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
+}
+
+.btn-reject {
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
+ color: white;
+}
+
+.btn-reject:hover {
+ background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
+}
+
+/* Custom Instructions Section */
+.approval-custom {
+ margin-top: 12px;
+ padding: 12px;
+ background: rgba(59, 130, 246, 0.1);
+ border-radius: 8px;
+ border: 1px solid rgba(59, 130, 246, 0.3);
+}
+
+.custom-label {
+ display: block;
+ font-size: 12px;
+ color: #94a3b8;
+ margin-bottom: 6px;
+}
+
+.custom-input {
+ width: 100%;
+ padding: 10px 12px;
+ background: rgba(0, 0, 0, 0.3);
+ border: 1px solid rgba(74, 158, 255, 0.3);
+ border-radius: 6px;
+ color: #e0e0e0;
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: 13px;
+ outline: none;
+ transition: all 0.2s ease;
+}
+
+.custom-input:focus {
+ border-color: #4a9eff;
+ box-shadow: 0 0 0 3px rgba(74, 158, 255, 0.1);
+}
+
+.custom-input::placeholder {
+ color: #64748b;
+}
+
+.custom-buttons {
+ display: flex;
+ gap: 8px;
+ margin-top: 10px;
+}
+
+.btn-approve-small,
+.btn-cancel-small {
+ flex: 1;
+ padding: 8px 12px;
+ border: none;
+ border-radius: 6px;
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.btn-approve-small {
+ background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
+ color: white;
+}
+
+.btn-approve-small:hover {
+ background: linear-gradient(135deg, #16a34a 0%, #15803d 100%);
+}
+
+.btn-cancel-small {
+ background: rgba(100, 116, 139, 0.2);
+ color: #94a3b8;
+}
+
+.btn-cancel-small:hover {
+ background: rgba(100, 116, 139, 0.3);
+}
+
+/* Responsive Design */
+@media (max-width: 600px) {
+ .approval-buttons {
+ flex-direction: column;
+ }
+
+ .approval-buttons button {
+ width: 100%;
+ }
+}
+
+/* Loading/Disabled States */
+.approval-card.loading {
+ opacity: 0.6;
+ pointer-events: none;
+}
+
+.approval-card.loading .approval-icon {
+ animation: approvalIconSpin 1s infinite linear;
+}
+
+@keyframes approvalIconSpin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+/* Expired State */
+.approval-card.expired {
+ opacity: 0.5;
+ border-color: #ff6b6b;
+}
+
+/* Success State */
+.approval-card.success {
+ border-color: #22c55e;
+ animation: approvalCardSuccess 0.5s ease-out;
+}
+
+@keyframes approvalCardSuccess {
+ 0% {
+ background: rgba(34, 197, 94, 0.1);
+ }
+ 100% {
+ background: transparent;
+ }
+}
diff --git a/public/claude-ide/components/approval-card.js b/public/claude-ide/components/approval-card.js
new file mode 100644
index 00000000..a2060b61
--- /dev/null
+++ b/public/claude-ide/components/approval-card.js
@@ -0,0 +1,267 @@
+/**
+ * Approval Card Component
+ * Interactive UI for approving/rejecting commands
+ */
+
+(function() {
+ 'use strict';
+
+ // Approval card instance tracking
+ let activeCards = new Map();
+
+ /**
+ * Render approval card
+ * @param {Object} approvalData - Approval request data
+ * @returns {HTMLElement} - The approval card element
+ */
+ function renderApprovalCard(approvalData) {
+ // Check if card already exists
+ if (activeCards.has(approvalData.id)) {
+ const existingCard = activeCards.get(approvalData.id);
+ if (existingCard && existingCard.isConnected) {
+ return existingCard;
+ }
+ }
+
+ const cardId = `approval-card-${approvalData.id}`;
+
+ // Create card container
+ const card = document.createElement('div');
+ card.className = 'approval-card';
+ card.id = cardId;
+ card.dataset.approvalId = approvalData.id;
+
+ // Generate HTML
+ card.innerHTML = `
+
+ ${approvalData.explanation ? `
+
+ âšī¸
+ ${escapeHtml(approvalData.explanation)}
+
+ ` : ''}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ // Store in active cards
+ activeCards.set(approvalData.id, card);
+
+ return card;
+ }
+
+ /**
+ * Handle approve button click
+ * @param {string} approvalId - Approval ID
+ */
+ function handleApprove(approvalId) {
+ sendApprovalResponse(approvalId, true, null);
+ }
+
+ /**
+ * Handle reject button click
+ * @param {string} approvalId - Approval ID
+ */
+ function handleReject(approvalId) {
+ sendApprovalResponse(approvalId, false, null);
+ }
+
+ /**
+ * Handle custom instructions click
+ * @param {string} approvalId - Approval ID
+ */
+ function handleCustom(approvalId) {
+ const card = activeCards.get(approvalId);
+ if (!card) return;
+
+ const customSection = card.querySelector('.approval-custom');
+ const customButton = card.querySelector('.btn-custom');
+
+ if (customSection.style.display === 'none') {
+ // Show custom input
+ customSection.style.display = 'block';
+ const input = card.querySelector(`#${approvalId}-custom-input`);
+ if (input) {
+ input.focus();
+ }
+ if (customButton) {
+ customButton.textContent = 'Close';
+ customButton.onclick = () => ApprovalCard.closeCustom(approvalId);
+ }
+ } else {
+ // Close custom input
+ closeCustom(approvalId);
+ }
+ }
+
+ /**
+ * Execute custom command
+ * @param {string} approvalId - Approval ID
+ */
+ function executeCustom(approvalId) {
+ const card = activeCards.get(approvalId);
+ if (!card) return;
+
+ const input = card.querySelector(`#${approvalId}-custom-input`);
+ const customCommand = input ? input.value.trim() : '';
+
+ if (!customCommand) {
+ // Show error
+ const existingError = card.querySelector('.approval-custom-error');
+ if (!existingError) {
+ const errorDiv = document.createElement('div');
+ errorDiv.className = 'approval-custom-error';
+ errorDiv.textContent = 'Please enter a command';
+ errorDiv.style.color = '#ff6b6b';
+ errorDiv.style.marginTop = '5px';
+ errorDiv.style.fontSize = '12px';
+ card.querySelector('.approval-custom-buttons').insertBefore(errorDiv, card.querySelector('.approval-custom-buttons').firstChild);
+ }
+ input.focus();
+ return;
+ }
+
+ sendApprovalResponse(approvalId, true, customCommand);
+ }
+
+ /**
+ * Close custom input
+ * @param {string} approvalId - Approval ID
+ */
+ function closeCustom(approvalId) {
+ const card = activeCards.get(approvalId);
+ if (!card) return;
+
+ const customSection = card.querySelector('.approval-custom');
+ const customButton = card.querySelector('.btn-custom');
+
+ customSection.style.display = 'none';
+ customButton.textContent = 'Custom Instructions';
+ customButton.onclick = () => ApprovalCard.handleCustom(approvalId);
+ }
+
+ /**
+ * Send approval response to server
+ * @param {string} approvalId - Approval ID
+ * @param {boolean} approved - Whether user approved
+ * @param {string|null} customCommand - Custom command if provided
+ */
+ function sendApprovalResponse(approvalId, approved, customCommand) {
+ // Remove card from UI
+ const card = activeCards.get(approvalId);
+ if (card && card.isConnected) {
+ card.remove();
+ }
+ activeCards.delete(approvalId);
+
+ // Check if this is a server-initiated approval or AI-conversational approval
+ const pendingApproval = window._pendingApprovals && window._pendingApprovals[approvalId];
+
+ if (pendingApproval) {
+ // AI-conversational approval - send as chat message
+ let responseMessage;
+
+ if (approved) {
+ if (customCommand) {
+ responseMessage = `Execute: ${customCommand}`;
+ } else {
+ responseMessage = 'yes';
+ }
+ } else {
+ responseMessage = 'no';
+ }
+
+ // Send as chat message
+ if (typeof sendChatMessage === 'function') {
+ sendChatMessage(responseMessage, 'webcontainer');
+ } else if (window.sendMessageToSession) {
+ window.sendMessageToSession(responseMessage);
+ }
+
+ // Clean up pending approval
+ delete window._pendingApprovals[approvalId];
+ } else {
+ // Server-initiated approval - send via WebSocket
+ if (window.ws && window.ws.readyState === WebSocket.OPEN) {
+ window.ws.send(JSON.stringify({
+ type: 'approval-response',
+ id: approvalId,
+ approved: approved,
+ customCommand: customCommand,
+ sessionId: window.attachedSessionId || window.chatSessionId
+ }));
+ } else {
+ console.error('[ApprovalCard] WebSocket not connected');
+ }
+ }
+ }
+
+ /**
+ * Handle approval expired event
+ * @param {string} approvalId - Approval ID
+ */
+ function handleExpired(approvalId) {
+ const card = activeCards.get(approvalId);
+ if (card && card.isConnected) {
+ const header = card.querySelector('.approval-card-header');
+ if (header) {
+ header.innerHTML = `
+ âąī¸
+ Expired:
+ This approval request has expired
+ `;
+ }
+
+ const buttons = card.querySelector('.approval-buttons');
+ if (buttons) {
+ buttons.style.display = 'none';
+ }
+
+ const custom = card.querySelector('.approval-custom');
+ if (custom) {
+ custom.style.display = 'none';
+ }
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ // Export public API
+ window.ApprovalCard = {
+ render: renderApprovalCard,
+ handleApprove,
+ handleReject,
+ handleCustom,
+ executeCustom,
+ closeCustom,
+ sendApprovalResponse,
+ handleExpired
+ };
+
+ console.log('[ApprovalCard] Component loaded');
+})();
diff --git a/public/claude-ide/ide.js b/public/claude-ide/ide.js
index 80b3a5ea..d9c63fab 100644
--- a/public/claude-ide/ide.js
+++ b/public/claude-ide/ide.js
@@ -291,6 +291,15 @@ function handleWebSocketMessage(data) {
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
@@ -390,6 +399,141 @@ function handleOperationProgress(data) {
}
}
+/**
+ * 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 = '';
+ const explanationMatch = content.match(/(?:this\s+(?:will|is)\s+(.+?)(?:\.|\n|$))|(?:network\s+operation|file\s+operation|system\s+operation)\s*[:\-]\s*(.+?)(?:\.|\n|$))/i);
+ if (explanationMatch) {
+ explanation = (explanationMatch[1] || explanationMatch[2] || '').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;
+}
+
// Streaming message state for accumulating response chunks
let streamingMessageElement = null;
let streamingMessageContent = '';
@@ -410,6 +554,69 @@ function handleSessionOutput(data) {
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
@@ -436,8 +643,50 @@ function handleSessionOutput(data) {
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
diff --git a/public/claude-ide/index.html b/public/claude-ide/index.html
index 323b3c1e..195bbd0f 100644
--- a/public/claude-ide/index.html
+++ b/public/claude-ide/index.html
@@ -13,6 +13,7 @@
+
@@ -345,6 +346,9 @@
+
+
+
diff --git a/server.js b/server.js
index 3694d31e..059501b2 100644
--- a/server.js
+++ b/server.js
@@ -3,6 +3,7 @@ const session = require('express-session');
const bcrypt = require('bcryptjs');
const path = require('path');
const fs = require('fs');
+const os = require('os');
const MarkdownIt = require('markdown-it');
const hljs = require('highlight.js');
const WebSocket = require('ws');
@@ -11,6 +12,10 @@ const terminalService = require('./services/terminal-service');
const { db } = require('./services/database');
const app = express();
+
+// Store recent browser errors for debugging
+const recentErrors = [];
+
const md = new MarkdownIt({
html: true,
linkify: true,
@@ -40,6 +45,162 @@ const users = {
// Initialize Claude Code Service
const claudeService = new ClaudeCodeService(VAULT_PATH);
+// ============================================================
+// Pending Approvals Manager
+// ============================================================
+
+class PendingApprovalsManager {
+ constructor() {
+ this.approvals = new Map();
+ this.cleanupInterval = setInterval(() => this.cleanup(), 60000); // Cleanup every minute
+ }
+
+ /**
+ * Create a new pending approval
+ * @param {string} sessionId - Session ID requesting approval
+ * @param {string} command - Command awaiting approval
+ * @param {string} explanation - Human-readable explanation
+ * @returns {string} Approval ID
+ */
+ createApproval(sessionId, command, explanation) {
+ const id = `approval-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+
+ const approval = {
+ id,
+ sessionId,
+ command,
+ explanation,
+ createdAt: Date.now(),
+ expiresAt: Date.now() + (5 * 60 * 1000) // 5 minutes
+ };
+
+ this.approvals.set(id, approval);
+
+ console.log(`[ApprovalManager] Created approval ${id} for session ${sessionId}`);
+
+ // Auto-expire after 5 minutes
+ setTimeout(() => {
+ this.expire(id);
+ }, 5 * 60 * 1000);
+
+ return id;
+ }
+
+ /**
+ * Get approval by ID
+ * @param {string} id - Approval ID
+ * @returns {object|null} Approval object or null if not found/expired
+ */
+ getApproval(id) {
+ const approval = this.approvals.get(id);
+
+ if (!approval) {
+ return null;
+ }
+
+ // Check if expired
+ if (Date.now() > approval.expiresAt) {
+ this.approvals.delete(id);
+ return null;
+ }
+
+ return approval;
+ }
+
+ /**
+ * Remove approval (approved/rejected/expired)
+ * @param {string} id - Approval ID
+ */
+ removeApproval(id) {
+ const removed = this.approvals.delete(id);
+ if (removed) {
+ console.log(`[ApprovalManager] Removed approval ${id}`);
+ }
+ return removed;
+ }
+
+ /**
+ * Mark approval as expired
+ * @param {string} id - Approval ID
+ */
+ expire(id) {
+ const approval = this.approvals.get(id);
+ if (approval && Date.now() < approval.expiresAt) {
+ // Not actually expired yet, don't remove
+ return;
+ }
+
+ this.removeApproval(id);
+ console.log(`[ApprovalManager] Approval ${id} expired`);
+
+ // Notify session about expiration
+ claudeService.emit('approval-expired', { id, sessionId: approval?.sessionId });
+ }
+
+ /**
+ * Clean up expired approvals
+ */
+ cleanup() {
+ const now = Date.now();
+ let expiredCount = 0;
+
+ for (const [id, approval] of this.approvals.entries()) {
+ if (now > approval.expiresAt) {
+ this.approvals.delete(id);
+ expiredCount++;
+ }
+ }
+
+ if (expiredCount > 0) {
+ console.log(`[ApprovalManager] Cleaned up ${expiredCount} expired approvals`);
+ }
+ }
+
+ /**
+ * Get all pending approvals for a session
+ * @param {string} sessionId - Session ID
+ * @returns {Array} Array of approval objects
+ */
+ getApprovalsForSession(sessionId) {
+ const sessionApprovals = [];
+
+ for (const [id, approval] of this.approvals.entries()) {
+ if (approval.sessionId === sessionId && Date.now() < approval.expiresAt) {
+ sessionApprovals.push(approval);
+ }
+ }
+
+ return sessionApprovals;
+ }
+
+ /**
+ * Check if session has any pending approvals
+ * @param {string} sessionId - Session ID
+ * @returns {boolean} True if session has pending approvals
+ */
+ hasPendingApproval(sessionId) {
+ for (const [id, approval] of this.approvals.entries()) {
+ if (approval.sessionId === sessionId && Date.now() < approval.expiresAt) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get statistics
+ */
+ getStats() {
+ return {
+ total: this.approvals.size,
+ pending: Array.from(this.approvals.values()).filter(a => Date.now() < a.expiresAt).length
+ };
+ }
+}
+
+// Initialize approval manager
+const approvalManager = new PendingApprovalsManager();
+
// Cleanup old sessions every hour
setInterval(() => {
claudeService.cleanup();
@@ -1193,6 +1354,15 @@ function validateProjectPath(projectPath) {
return { valid: true, path: resolvedPath };
}
+// GET /api/debug/errors - View recent browser errors (debug only)
+app.get('/api/debug/errors', (req, res) => {
+ const errors = recentErrors.slice(-20); // Last 20 errors
+ res.json({
+ total: recentErrors.length,
+ recent: errors
+ });
+});
+
// GET /api/projects - List all active projects
app.get('/api/projects', requireAuth, (req, res) => {
try {
@@ -1203,11 +1373,20 @@ app.get('/api/projects', requireAuth, (req, res) => {
ORDER BY lastActivity DESC
`).all();
- // Add sessionCount (0 for now, will be implemented in Task 3)
- const projectsWithSessionCount = projects.map(project => ({
- ...project,
- sessionCount: 0
- }));
+ // Add sessionCount for each project
+ const projectsWithSessionCount = projects.map(project => {
+ const sessionCount = db.prepare(`
+ SELECT COUNT(*) as count
+ FROM sessions
+ WHERE projectId = ? AND deletedAt IS NULL
+ `).get(project.id);
+
+ return {
+ ...project,
+ sessionCount: sessionCount?.count || 0,
+ sources: ['web'] // TODO: Track CLI vs Web sources
+ };
+ });
res.json({
success: true,
@@ -1496,6 +1675,133 @@ app.delete('/api/projects/:id/permanent', requireAuth, (req, res) => {
}
});
+// GET /api/projects/:id/sessions - Get all sessions for a project
+app.get('/api/projects/:id/sessions', requireAuth, (req, res) => {
+ try {
+ const { id } = req.params;
+
+ // Validate ID
+ const validatedId = validateProjectId(id);
+ if (!validatedId) {
+ return res.status(400).json({ error: 'Invalid project ID' });
+ }
+
+ // Check if project exists
+ const project = db.prepare(`
+ SELECT id, name FROM projects
+ WHERE id = ? AND deletedAt IS NULL
+ `).get(validatedId);
+
+ if (!project) {
+ return res.status(404).json({ error: 'Project not found' });
+ }
+
+ // Get sessions from in-memory Claude service and historical
+ const allSessions = [];
+
+ // Get active sessions
+ const activeSessions = Array.from(claudeService.sessions.values())
+ .filter(session => session.projectId == validatedId)
+ .map(session => ({
+ id: session.id,
+ title: session.title || session.metadata?.project || 'Untitled Session',
+ agent: session.metadata?.agent || 'claude',
+ createdAt: new Date(session.createdAt).toISOString(),
+ updatedAt: new Date(session.lastActivity || session.createdAt).toISOString(),
+ status: 'active',
+ metadata: session.metadata
+ }));
+
+ // Get historical sessions from DB
+ const historicalSessions = db.prepare(`
+ SELECT id, createdAt, metadata
+ FROM sessions
+ WHERE projectId = ? AND deletedAt IS NULL
+ ORDER BY createdAt DESC
+ `).all(validatedId).map(session => ({
+ id: session.id,
+ title: session.metadata?.project || 'Untitled Session',
+ agent: session.metadata?.agent || 'claude',
+ createdAt: session.createdAt,
+ updatedAt: session.createdAt,
+ status: 'historical',
+ metadata: typeof session.metadata === 'string' ? JSON.parse(session.metadata) : session.metadata
+ }));
+
+ // Merge and deduplicate
+ const sessionMap = new Map();
+ [...activeSessions, ...historicalSessions].forEach(session => {
+ if (!sessionMap.has(session.id)) {
+ sessionMap.set(session.id, session);
+ }
+ });
+
+ const sessions = Array.from(sessionMap.values())
+ .sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
+
+ res.json({
+ success: true,
+ sessions
+ });
+ } catch (error) {
+ console.error('Error listing project sessions:', error);
+ res.status(500).json({ error: 'Failed to list sessions' });
+ }
+});
+
+// POST /api/projects/:id/sessions - Create new session in project
+app.post('/api/projects/:id/sessions', requireAuth, async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { metadata = {} } = req.body;
+
+ // Validate ID
+ const validatedId = validateProjectId(id);
+ if (!validatedId) {
+ return res.status(400).json({ error: 'Invalid project ID' });
+ }
+
+ // Check if project exists
+ const project = db.prepare(`
+ SELECT id, name, path FROM projects
+ WHERE id = ? AND deletedAt IS NULL
+ `).get(validatedId);
+
+ if (!project) {
+ return res.status(404).json({ error: 'Project not found' });
+ }
+
+ // Create session using Claude service
+ const session = await claudeService.createSession(project.path, {
+ ...metadata,
+ project: project.name,
+ projectId: validatedId
+ });
+
+ // Update project lastActivity
+ db.prepare(`
+ UPDATE projects
+ SET lastActivity = ?
+ WHERE id = ?
+ `).run(new Date().toISOString(), validatedId);
+
+ res.json({
+ success: true,
+ session: {
+ id: session.id,
+ title: session.title || session.metadata?.project || 'New Session',
+ agent: session.metadata?.agent || 'claude',
+ createdAt: session.createdAt,
+ updatedAt: session.lastActivity || session.createdAt,
+ metadata: session.metadata
+ }
+ });
+ } catch (error) {
+ console.error('Error creating session:', error);
+ res.status(500).json({ error: 'Failed to create session' });
+ }
+});
+
// GET /api/recycle-bin - List deleted items
app.get('/api/recycle-bin', requireAuth, (req, res) => {
try {
@@ -1987,6 +2293,71 @@ wss.on('connection', (ws, req) => {
error: error.message
}));
}
+ } else if (data.type === 'approval-request') {
+ // AI agent requesting approval for a command
+ const { sessionId, command, explanation } = data;
+
+ console.log(`[WebSocket] Approval request for session ${sessionId}: ${command}`);
+
+ // Create pending approval
+ const approvalId = approvalManager.createApproval(sessionId, command, explanation);
+
+ // Forward approval request to all clients subscribed to this session
+ clients.forEach((client) => {
+ if (client.sessionId === sessionId && client.ws.readyState === WebSocket.OPEN) {
+ client.ws.send(JSON.stringify({
+ type: 'approval-request',
+ id: approvalId,
+ sessionId,
+ command,
+ explanation
+ }));
+ }
+ });
+
+ } else if (data.type === 'approval-response') {
+ // User responded to approval request
+ const { id, approved, customCommand } = data;
+
+ console.log(`[WebSocket] Approval response for ${id}: approved=${approved}`);
+
+ // Get the approval
+ const approval = approvalManager.getApproval(id);
+
+ if (!approval) {
+ console.error(`[WebSocket] Approval ${id} not found or expired`);
+ ws.send(JSON.stringify({
+ type: 'approval-expired',
+ id
+ }));
+ return;
+ }
+
+ // Remove from pending
+ approvalManager.removeApproval(id);
+
+ // Send approval as a message to the AI agent
+ const approvalMessage = approved
+ ? (customCommand
+ ? `[USER APPROVED WITH MODIFICATION: ${customCommand}]`
+ : `[USER APPROVED: ${approval.command}]`)
+ : `[USER REJECTED: ${approval.command}]`;
+
+ // Add to session context
+ claudeService.sendCommand(approval.sessionId, approvalMessage);
+
+ // Forward response confirmation to clients
+ clients.forEach((client) => {
+ if (client.sessionId === approval.sessionId && client.ws.readyState === WebSocket.OPEN) {
+ client.ws.send(JSON.stringify({
+ type: 'approval-confirmed',
+ id,
+ approved,
+ customCommand: customCommand || null
+ }));
+ }
+ });
+
} else if (data.type === 'subscribe') {
// Subscribe to a specific session's output
const { sessionId } = data;
@@ -2105,6 +2476,50 @@ claudeService.on('operations-error', (data) => {
});
});
+// Forward approval-request event
+claudeService.on('approval-request', (data) => {
+ console.log(`Approval request for session ${data.sessionId}:`, data.command);
+
+ // Create pending approval
+ const approvalId = approvalManager.createApproval(data.sessionId, data.command, data.explanation);
+
+ // Forward to all clients subscribed to this session
+ clients.forEach((client, clientId) => {
+ if (client.sessionId === data.sessionId && client.ws.readyState === WebSocket.OPEN) {
+ try {
+ client.ws.send(JSON.stringify({
+ type: 'approval-request',
+ id: approvalId,
+ sessionId: data.sessionId,
+ command: data.command,
+ explanation: data.explanation
+ }));
+ } catch (error) {
+ console.error(`Error sending approval request to client ${clientId}:`, error);
+ }
+ }
+ });
+});
+
+// Forward approval-expired event
+claudeService.on('approval-expired', (data) => {
+ console.log(`Approval expired for session ${data.sessionId}:`, data.id);
+
+ // Forward to all clients subscribed to this session
+ clients.forEach((client, clientId) => {
+ if (client.sessionId === data.sessionId && client.ws.readyState === WebSocket.OPEN) {
+ try {
+ client.ws.send(JSON.stringify({
+ type: 'approval-expired',
+ id: data.id
+ }));
+ } catch (error) {
+ console.error(`Error sending approval-expired to client ${clientId}:`, error);
+ }
+ }
+ });
+});
+
console.log(`WebSocket server running on ws://localhost:${PORT}/claude/api/claude/chat`);
// Real-time error monitoring endpoint
@@ -2128,6 +2543,13 @@ app.post('/claude/api/log-error', express.json(), (req, res) => {
const errorEntry = JSON.stringify({ ...error, loggedAt: timestamp }) + '\n';
fs.appendFileSync(errorLogPath, errorEntry);
+ // Store in recent errors array for debug endpoint
+ recentErrors.push({ ...error, loggedAt: timestamp });
+ // Keep only last 100 errors
+ if (recentErrors.length > 100) {
+ recentErrors.shift();
+ }
+
// đ¤ AUTO-TRIGGER: Spawn auto-fix agent in background
const { spawn } = require('child_process');
const agentPath = '/home/uroma/obsidian-web-interface/scripts/auto-fix-agent.js';
@@ -2157,6 +2579,284 @@ app.post('/claude/api/log-error', express.json(), (req, res) => {
res.json({ received: true, autoFixTriggered: true });
});
+// ============================================================
+// FILESYSTEM API - Folder Explorer
+// ============================================================
+
+/**
+ * Expand ~ to home directory and validate path
+ */
+function expandPath(userPath) {
+ const homeDir = os.homedir();
+
+ // Expand ~ or ~user
+ let expanded = userPath.replace(/^~\/?/, homeDir + '/');
+
+ // Resolve to absolute path
+ expanded = path.resolve(expanded);
+
+ // Security: Must be under home directory
+ if (!expanded.startsWith(homeDir)) {
+ throw new Error('Access denied: Path outside home directory');
+ }
+
+ return expanded;
+}
+
+/**
+ * Get directory contents for folder tree
+ */
+app.get('/api/filesystem/list', requireAuth, async (req, res) => {
+ try {
+ const { path: userPath = '~' } = req.query;
+
+ // Expand and validate path
+ const expandedPath = expandPath(userPath);
+
+ // Check if path exists
+ try {
+ await fs.promises.access(expandedPath, fs.constants.R_OK);
+ } catch {
+ return res.json({
+ success: false,
+ error: 'Directory not found or inaccessible',
+ path: expandedPath
+ });
+ }
+
+ // Read directory
+ const entries = await fs.promises.readdir(expandedPath, { withFileTypes: true });
+
+ // Filter directories only, sort, and get metadata
+ const items = [];
+ for (const entry of entries) {
+ if (entry.isDirectory()) {
+ const fullPath = path.join(expandedPath, entry.name);
+
+ // Skip hidden directories (except .git)
+ if (entry.name.startsWith('.') && entry.name !== '.git') {
+ continue;
+ }
+
+ try {
+ const subEntries = await fs.promises.readdir(fullPath, { withFileTypes: true });
+ const subDirs = subEntries.filter(e => e.isDirectory()).length;
+
+ items.push({
+ name: entry.name,
+ type: 'directory',
+ path: fullPath,
+ children: subDirs,
+ hasChildren: subDirs > 0
+ });
+ } catch {
+ // Can't read subdirectory (permissions)
+ items.push({
+ name: entry.name,
+ type: 'directory',
+ path: fullPath,
+ children: 0,
+ hasChildren: false,
+ locked: true
+ });
+ }
+ }
+ }
+
+ // Sort alphabetically
+ items.sort((a, b) => a.name.localeCompare(b.name));
+
+ res.json({
+ success: true,
+ path: expandedPath,
+ displayPath: expandedPath.replace(os.homedir(), '~'),
+ items
+ });
+ } catch (error) {
+ console.error('Error listing directory:', error);
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+/**
+ * Validate if path exists and is accessible
+ */
+app.get('/api/filesystem/validate', requireAuth, async (req, res) => {
+ try {
+ const { path: userPath } = req.query;
+
+ if (!userPath) {
+ return res.status(400).json({
+ success: false,
+ error: 'Path parameter is required'
+ });
+ }
+
+ // Expand and validate path
+ const expandedPath = expandPath(userPath);
+
+ // Check if path exists
+ try {
+ await fs.promises.access(expandedPath, fs.constants.F_OK);
+ } catch {
+ return res.json({
+ success: true,
+ valid: false,
+ path: expandedPath,
+ displayPath: expandedPath.replace(os.homedir(), '~'),
+ exists: false
+ });
+ }
+
+ // Check read access
+ let readable = false;
+ try {
+ await fs.promises.access(expandedPath, fs.constants.R_OK);
+ readable = true;
+ } catch {}
+
+ // Check write access
+ let writable = false;
+ try {
+ await fs.promises.access(expandedPath, fs.constants.W_OK);
+ writable = true;
+ } catch {}
+
+ // Check if it's a directory
+ const stats = await fs.promises.stat(expandedPath);
+ const isDirectory = stats.isDirectory();
+
+ res.json({
+ success: true,
+ valid: true,
+ path: expandedPath,
+ displayPath: expandedPath.replace(os.homedir(), '~'),
+ exists: true,
+ readable,
+ writable,
+ isDirectory
+ });
+ } catch (error) {
+ // Path validation error
+ res.json({
+ success: true,
+ valid: false,
+ error: error.message,
+ path: userPath
+ });
+ }
+});
+
+/**
+ * Create new directory
+ */
+app.post('/api/filesystem/mkdir', requireAuth, async (req, res) => {
+ try {
+ const { path: userPath } = req.body;
+
+ if (!userPath) {
+ return res.status(400).json({
+ success: false,
+ error: 'Path parameter is required'
+ });
+ }
+
+ // Expand and validate path
+ const expandedPath = expandPath(userPath);
+
+ // Validate folder name (no special characters)
+ const folderName = path.basename(expandedPath);
+ if (!/^[a-zA-Z0-9._-]+$/.test(folderName)) {
+ return res.status(400).json({
+ success: false,
+ error: 'Invalid folder name. Use only letters, numbers, dots, dashes, and underscores.'
+ });
+ }
+
+ // Check if already exists
+ try {
+ await fs.access(expandedPath, fs.constants.F_OK);
+ return res.status(400).json({
+ success: false,
+ error: 'Directory already exists'
+ });
+ } catch {
+ // Doesn't exist, good to create
+ }
+
+ // Create directory
+ await fs.promises.mkdir(expandedPath, { mode: 0o755 });
+
+ res.json({
+ success: true,
+ path: expandedPath,
+ displayPath: expandedPath.replace(os.homedir(), '~')
+ });
+ } catch (error) {
+ console.error('Error creating directory:', error);
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+/**
+ * Get quick access locations
+ */
+app.get('/api/filesystem/quick-access', requireAuth, async (req, res) => {
+ try {
+ const homeDir = os.homedir();
+ const locations = [
+ { path: '~', name: 'Home', icon: 'đ ' },
+ { path: '~/projects', name: 'Projects', icon: 'đ' },
+ { path: '~/code', name: 'Code', icon: 'đģ' },
+ { path: '~/Documents', name: 'Documents', icon: 'đ' },
+ { path: '~/Downloads', name: 'Downloads', icon: 'đĨ' }
+ ];
+
+ // Check which locations exist and get item counts
+ const enrichedLocations = await Promise.all(locations.map(async (loc) => {
+ try {
+ const expandedPath = expandPath(loc.path);
+ await fs.promises.access(expandedPath, fs.constants.R_OK);
+
+ // Get item count
+ const entries = await fs.promises.readdir(expandedPath, { withFileTypes: true });
+ const count = entries.filter(e => e.isDirectory()).length;
+
+ return {
+ ...loc,
+ path: expandedPath,
+ displayPath: expandedPath.replace(homeDir, '~'),
+ exists: true,
+ count
+ };
+ } catch {
+ return {
+ ...loc,
+ exists: false,
+ count: 0
+ };
+ }
+ }));
+
+ res.json({
+ success: true,
+ locations: enrichedLocations
+ });
+ } catch (error) {
+ console.error('Error getting quick access locations:', error);
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
// Graceful shutdown
process.on('SIGINT', async () => {
console.log('\nShutting down gracefully...');
diff --git a/services/claude-service.js b/services/claude-service.js
index e7b0edaa..1c94eccc 100644
--- a/services/claude-service.js
+++ b/services/claude-service.js
@@ -293,6 +293,38 @@ class ClaudeCodeService extends EventEmitter {
return { success: true };
}
+ /**
+ * Request approval for a command (instead of executing directly)
+ * @param {string} sessionId - Session ID
+ * @param {string} command - Command requiring approval
+ * @param {string} explanation - Human-readable explanation of what will happen
+ */
+ requestApproval(sessionId, command, explanation) {
+ const session = this.sessions.get(sessionId);
+
+ if (!session) {
+ throw new Error(`Session ${sessionId} not found`);
+ }
+
+ console.log(`[ClaudeService] Requesting approval for session ${sessionId}:`, command);
+
+ // Track the pending request in session context
+ session.context.messages.push({
+ role: 'system',
+ content: `[AWAITING APPROVAL: ${command}]`,
+ timestamp: new Date().toISOString()
+ });
+
+ // Emit approval-request event for WebSocket to handle
+ this.emit('approval-request', {
+ sessionId,
+ command,
+ explanation
+ });
+
+ return { success: true, awaitingApproval: true };
+ }
+
/**
* Get session details (handles both active and historical sessions)
*/