/** * Tag Renderer - Display and manage dyad-style operations in UI */ class TagRenderer { constructor() { this.pendingOperations = null; this.isExecuting = false; } /** * Parse tags from text response */ parseTags(response) { const tags = { writes: this.extractWriteTags(response), renames: this.extractRenameTags(response), deletes: this.extractDeleteTags(response), dependencies: this.extractDependencyTags(response), commands: this.extractCommandTags(response) }; return tags; } /** * Extract dyad-write tags */ extractWriteTags(response) { const regex = /([\s\S]*?)<\/dyad-write>/g; const tags = []; let match; while ((match = regex.exec(response)) !== null) { tags.push({ type: 'write', path: match[1], content: match[2].trim() }); } return tags; } /** * Extract dyad-rename tags */ extractRenameTags(response) { const regex = //g; const tags = []; let match; while ((match = regex.exec(response)) !== null) { tags.push({ type: 'rename', from: match[1], to: match[2] }); } return tags; } /** * Extract dyad-delete tags */ extractDeleteTags(response) { const regex = //g; const tags = []; let match; while ((match = regex.exec(response)) !== null) { tags.push({ type: 'delete', path: match[1] }); } return tags; } /** * Extract dyad-add-dependency tags */ extractDependencyTags(response) { const regex = //g; const tags = []; let match; while ((match = regex.exec(response)) !== null) { tags.push({ type: 'install', packages: match[1].split(' ').filter(p => p.trim()) }); } return tags; } /** * Extract dyad-command tags */ extractCommandTags(response) { const regex = //g; const tags = []; let match; while ((match = regex.exec(response)) !== null) { tags.push({ type: 'command', command: match[1] }); } return tags; } /** * Generate operation summary for display */ generateOperationsSummary(tags) { const operations = []; tags.deletes.forEach(tag => { operations.push({ type: 'delete', description: `Delete ${tag.path}`, tag, status: 'pending' }); }); tags.renames.forEach(tag => { operations.push({ type: 'rename', description: `Rename ${tag.from} → ${tag.to}`, tag, status: 'pending' }); }); tags.writes.forEach(tag => { operations.push({ type: 'write', description: `Create/update ${tag.path}`, tag, status: 'pending' }); }); tags.dependencies.forEach(tag => { operations.push({ type: 'install', description: `Install packages: ${tag.packages.join(', ')}`, tag, status: 'pending' }); }); tags.commands.forEach(tag => { operations.push({ type: 'command', description: `Execute command: ${tag.command}`, tag, status: 'pending' }); }); return operations; } /** * Strip tags from response for clean display */ stripTags(response) { let stripped = response; stripped = stripped.replace(/[\s\S]*?<\/dyad-write>/g, '[File: code]'); stripped = stripped.replace(//g, '[Rename]'); stripped = stripped.replace(//g, '[Delete]'); stripped = stripped.replace(//g, '[Install]'); stripped = stripped.replace(//g, '[Command]'); return stripped; } /** * Render operations panel HTML */ renderOperationsPanel(operations) { const panel = document.createElement('div'); panel.className = 'operations-panel'; panel.innerHTML = `

Pending Operations (${operations.length})

${operations.map((op, index) => this.renderOperationItem(op, index)).join('')}
`; return panel; } /** * Render single operation item */ renderOperationItem(operation, index) { const icon = this.getOperationIcon(operation.type); const colorClass = this.getOperationColorClass(operation.type); return `
${icon}
${operation.description}
${operation.tag.content ? `` : ''}
Pending
`; } /** * Get icon for operation type */ getOperationIcon(type) { const icons = { write: '📄', rename: '✏️', delete: '🗑️', install: '📦', command: '⚡' }; return icons[type] || '⚙️'; } /** * Get color class for operation type */ getOperationColorClass(type) { const classes = { write: 'op-write', rename: 'op-rename', delete: 'op-delete', install: 'op-install', command: 'op-command' }; return classes[type] || ''; } /** * Show operations panel */ showOperationsPanel(operations, response) { this.pendingOperations = operations; window.currentOperationsResponse = response; // Remove existing panel const existing = document.querySelector('.operations-panel'); if (existing) { existing.remove(); } // Add new panel const chatContainer = document.getElementById('chat-messages'); const panel = this.renderOperationsPanel(operations); chatContainer.appendChild(panel); // Make it visible panel.style.display = 'block'; } /** * Update operation status */ updateOperationStatus(index, status) { const operationItem = document.querySelector(`.operation-item[data-index="${index}"]`); if (!operationItem) return; const statusElement = operationItem.querySelector('.operation-status'); statusElement.innerHTML = `${status.charAt(0).toUpperCase() + status.slice(1)}`; if (status === 'approved' || status === 'rejected') { // Disable buttons const approveBtn = operationItem.querySelector('.btn-approve'); const rejectBtn = operationItem.querySelector('.btn-reject'); approveBtn.disabled = true; rejectBtn.disabled = true; } } /** * Update all operations status */ updateAllOperationsStatus(status) { if (!this.pendingOperations) return; this.pendingOperations.forEach((_, index) => { this.updateOperationStatus(index, status); }); } /** * Hide operations panel */ hideOperationsPanel() { const panel = document.querySelector('.operations-panel'); if (panel) { panel.remove(); } this.pendingOperations = null; } /** * Show progress indicator */ showProgress(message) { const chatContainer = document.getElementById('chat-messages'); const progress = document.createElement('div'); progress.className = 'operation-progress'; progress.innerHTML = `
${message}
`; chatContainer.appendChild(progress); progress.style.display = 'block'; } /** * Update progress message */ updateProgress(message) { const progressElement = document.querySelector('.operation-progress'); if (progressElement) { const messageElement = progressElement.querySelector('.progress-message'); if (messageElement) { messageElement.textContent = message; } } } /** * Hide progress indicator */ hideProgress() { const progressElement = document.querySelector('.operation-progress'); if (progressElement) { progressElement.remove(); } } /** * Show completion message */ showCompletion(results) { const chatContainer = document.querySelector('.chat-container'); const completion = document.createElement('div'); completion.className = 'operation-completion'; const successCount = results.operations.filter(op => op.success).length; const errorCount = results.errors.length; completion.innerHTML = `

Operations Complete

${successCount} succeeded ${errorCount > 0 ? `${errorCount} failed` : ''}
`; chatContainer.appendChild(completion); completion.style.display = 'block'; // Auto-hide after 3 seconds setTimeout(() => { completion.remove(); }, 3000); } /** * Show error message */ showError(message) { const chatContainer = document.querySelector('.chat-container'); const error = document.createElement('div'); error.className = 'operation-error'; error.innerHTML = `
⚠️

Operation Failed

${message}
`; chatContainer.appendChild(error); error.style.display = 'block'; } } // Global instance const tagRenderer = new TagRenderer(); // Store current response for execution window.currentOperationsResponse = null; // Global functions for onclick handlers window.approveOperation = async function(index) { if (!tagRenderer.pendingOperations) return; tagRenderer.updateOperationStatus(index, 'approved'); }; window.rejectOperation = function(index) { if (!tagRenderer.pendingOperations) return; tagRenderer.updateOperationStatus(index, 'rejected'); }; window.approveAllOperations = async function() { if (!tagRenderer.pendingOperations) return; tagRenderer.updateAllOperationsStatus('approved'); // Execute operations await executeApprovedOperations(); }; window.rejectAllOperations = function() { if (!tagRenderer.pendingOperations) return; tagRenderer.hideOperationsPanel(); window.currentOperationsResponse = null; }; window.viewCode = function(index) { if (!tagRenderer.pendingOperations) return; const operation = tagRenderer.pendingOperations[index]; if (operation.tag && operation.tag.content) { // Show code in modal const modal = document.createElement('div'); modal.className = 'code-modal'; modal.innerHTML = ` `; document.body.appendChild(modal); modal.style.display = 'block'; } }; function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Execute approved operations */ async function executeApprovedOperations() { if (!window.currentOperationsResponse) { console.error('No response to execute'); tagRenderer.showError('No operations to execute'); return; } // Use attachedSessionId (from chat-functions.js) instead of chatSessionId const sessionId = window.attachedSessionId || window.chatSessionId; if (!sessionId) { console.error('No session ID - not attached to a session'); tagRenderer.showError('Not attached to a session. Please start or attach to a session first.'); return; } const response = window.currentOperationsResponse; console.log('Executing operations for session:', sessionId); try { // Show progress tagRenderer.showProgress('Executing operations...'); // Call execute API with credentials const res = await fetch(`/claude/api/claude/sessions/${sessionId}/operations/execute`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ response }) }); const data = await res.json(); if (!res.ok) { console.error('Execute API error:', data); throw new Error(data.error || 'Failed to execute operations'); } console.log('Operations executed successfully:', data.results); // The WebSocket will handle showing completion // when it receives the operations-executed event } catch (error) { console.error('Error executing operations:', error); tagRenderer.hideProgress(); tagRenderer.showError(error.message); } } // Export for use in other files if (typeof module !== 'undefined' && module.exports) { module.exports = TagRenderer; }