/** * 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 = `
🤖 Executing: ${escapeHtml(approvalData.command)}
${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]; // Get the session ID const sessionId = window.attachedSessionId || window.chatSessionId || (pendingApproval && pendingApproval.sessionId); if (pendingApproval) { // AI-conversational approval - send as chat message to Claude // This is the Kimi-style flow: approval responses are sent as chat messages // Claude will continue execution upon receiving "yes" console.log('[ApprovalCard] Sending AI-conversational approval as chat message'); let responseMessage; if (approved) { if (customCommand) { responseMessage = customCommand; } else { responseMessage = 'yes'; } } else { responseMessage = 'no'; } // Send directly via WebSocket as a chat command if (window.ws && window.ws.readyState === WebSocket.OPEN && sessionId) { window.ws.send(JSON.stringify({ type: 'command', sessionId: sessionId, command: responseMessage, metadata: { isApprovalResponse: true, approvalId: approvalId, originalCommand: pendingApproval.command || null } })); console.log('[ApprovalCard] Sent approval response via WebSocket:', responseMessage); } else { console.error('[ApprovalCard] WebSocket not connected for approval response'); if (typeof appendSystemMessage === 'function') { appendSystemMessage('❌ Failed to send approval: WebSocket not connected'); } return; } // Clean up pending approval delete window._pendingApprovals[approvalId]; // Show feedback if (typeof appendSystemMessage === 'function' && approved) { appendSystemMessage('✅ Approval sent - continuing execution...'); } } else { // Server-initiated approval - send via WebSocket console.log('[ApprovalCard] Sending server-initiated approval 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: sessionId })); } 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'); })();