Files
SuperCharged-Claude-Code-Up…/public/claude-ide/components/approval-card.js
uroma a45b71e1e4 Implement terminal approval UI system
Phase 1: Backend approval tracking
- Add PendingApprovalsManager class to track pending approvals
- Add approval-request, approval-response, approval-expired WebSocket handlers
- Add requestApproval() method to ClaudeCodeService
- Add event forwarding for approval requests

Phase 2: Frontend approval card component
- Create approval-card.js with interactive UI
- Create approval-card.css with styled component
- Add Approve, Custom Instructions, Reject buttons
- Add expandable custom command input

Phase 3: Wire up approval flow end-to-end
- Add handleApprovalRequest, handleApprovalConfirmed, handleApprovalExpired handlers
- Add detectApprovalRequest() to parse AI approval request patterns
- Integrate approval card into WebSocket message flow
- Route approval responses based on source (server vs AI conversational)

This allows the AI agent to request command approval through a clean
UI instead of confusing conversational text responses.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-21 14:24:13 +00:00

268 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
/**
* 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 = `
<div class="approval-card-header">
<span class="approval-icon">🤖</span>
<span class="approval-label">Executing:</span>
<code class="approval-command">${escapeHtml(approvalData.command)}</code>
</div>
${approvalData.explanation ? `
<div class="approval-explanation">
<span class="explanation-icon"></span>
<span class="explanation-text">${escapeHtml(approvalData.explanation)}</span>
</div>
` : ''}
<div class="approval-buttons">
<button class="btn-approve" onclick="ApprovalCard.handleApprove('${approvalData.id}')">Approve</button>
<button class="btn-custom" onclick="ApprovalCard.handleCustom('${approvalData.id}')" ${approvalData.explanation ? '' : 'style="display:none"'}>Custom Instructions</button>
<button class="btn-reject" onclick="ApprovalCard.handleReject('${approvalData.id}')">Reject</button>
</div>
<div class="approval-custom" style="display:none;">
<label class="custom-label">Custom command:</label>
<input type="text" class="custom-input" id="${cardId}-custom-input" placeholder="Enter modified command..." />
<div class="custom-buttons">
<button class="btn-approve-small" onclick="ApprovalCard.executeCustom('${approvalData.id}')">Execute Custom</button>
<button class="btn-cancel-small" onclick="ApprovalCard.closeCustom('${approvalData.id}')">Cancel</button>
</div>
</div>
`;
// 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 = `
<span class="approval-icon" style="color: #ff6b6b;">⏱️</span>
<span class="approval-label">Expired:</span>
<span class="approval-command" style="color: #ff6b6b;">This approval request has expired</span>
`;
}
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');
})();