/** * Live Coding Agent Viewer * * SSE consumer that renders coding agent tool calls in real-time, * reusing CSS classes from CodingTraceDisplay (ct-turn, ct-tool-call, etc.) */ (function () { 'use strict'; class LiveCodingAgentViewer { constructor(container) { this.container = container; this.fieldKey = container.dataset.fieldKey; this.sessionId = null; this.eventSource = null; this.turnCount = 0; this.currentTurnEl = null; this.state = 'idle'; this._init(); } _init() { // Start button const startBtn = this.container.querySelector('[data-action="start"]'); if (startBtn) { startBtn.addEventListener('click', () => this._startSession()); } // Control buttons this.container.querySelectorAll('[data-action]').forEach(btn => { const action = btn.dataset.action; if (action === 'pause') btn.addEventListener('click', () => this._pause()); if (action === 'resume') btn.addEventListener('click', () => this._resume()); if (action === 'stop') btn.addEventListener('click', () => this._stop()); if (action === 'instruct') btn.addEventListener('click', () => this._sendInstruction()); }); // Enter key on instruction input const instrInput = this.container.querySelector('.lca-instruction-input'); if (instrInput) { instrInput.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this._sendInstruction(); } }); } } async _startSession() { const taskInput = this.container.querySelector('.lca-task-input'); const task = taskInput ? taskInput.value.trim() : ''; if (!task) { taskInput && taskInput.focus(); return; } try { const resp = await fetch('/api/live_coding_agent/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ task_description: task, instance_id: this.fieldKey, }), }); const data = await resp.json(); if (data.error) { this._showError(data.error); return; } this.sessionId = data.session_id; // Switch to session view const startForm = this.container.querySelector('.lca-start-form'); const sessionView = this.container.querySelector('.lca-session'); if (startForm) startForm.style.display = 'none'; if (sessionView) sessionView.style.display = 'block'; this._connectSSE(); } catch (e) { this._showError('Failed to start session: ' + e.message); } } _connectSSE() { if (!this.sessionId) return; this.eventSource = new EventSource( '/api/live_coding_agent/stream/' + this.sessionId ); this.eventSource.addEventListener('connected', e => { const data = JSON.parse(e.data); this._updateStatus('running'); }); this.eventSource.addEventListener('thinking', e => { const data = JSON.parse(e.data); this._showThinking(data.text); }); this.eventSource.addEventListener('tool_call_start', e => { const data = JSON.parse(e.data); this._hideThinking(); this._renderToolCallStart(data); }); this.eventSource.addEventListener('tool_call', e => { const data = JSON.parse(e.data); this._renderToolCallEnd(data); }); this.eventSource.addEventListener('turn_end', e => { const data = JSON.parse(e.data); this._finalizeTurn(data); }); this.eventSource.addEventListener('state_change', e => { const data = JSON.parse(e.data); this._updateStatus(data.new_state); }); this.eventSource.addEventListener('instruction_received', e => { const data = JSON.parse(e.data); this._showInstructionConfirm(data.instruction); }); this.eventSource.addEventListener('error', e => { const data = JSON.parse(e.data); this._showError(data.message); this._updateStatus('error'); }); this.eventSource.addEventListener('complete', e => { this._hideThinking(); this._updateStatus('completed'); this._disconnectSSE(); }); this.eventSource.onerror = () => { // Reconnect after brief delay setTimeout(() => { if (this.state === 'running') this._connectSSE(); }, 3000); }; } _disconnectSSE() { if (this.eventSource) { this.eventSource.close(); this.eventSource = null; } } _showThinking(text) { const el = document.getElementById('lca-thinking-' + this.fieldKey); const textEl = document.getElementById('lca-thinking-text-' + this.fieldKey); if (el) el.style.display = 'flex'; if (textEl) textEl.textContent = text || 'Thinking...'; } _hideThinking() { const el = document.getElementById('lca-thinking-' + this.fieldKey); if (el) el.style.display = 'none'; } _renderToolCallStart(data) { const turnsEl = document.getElementById('lca-turns-' + this.fieldKey); if (!turnsEl) return; // Create turn container if needed if (!this.currentTurnEl) { this.currentTurnEl = document.createElement('div'); this.currentTurnEl.className = 'ct-turn ct-turn-assistant'; this.currentTurnEl.dataset.turnIndex = data.turn_index; // Step number this.turnCount++; this.currentTurnEl.innerHTML = '
' + 'Step ' + this.turnCount + '' + '
'; turnsEl.appendChild(this.currentTurnEl); } // Add tool call card (in-progress state) const toolType = this._classifyTool(data.tool); const colors = this._getToolColors(toolType); const filePath = (data.input || {}).file_path || (data.input || {}).path || ''; const card = document.createElement('div'); card.className = 'ct-tool-call ct-tool-' + toolType + ' lca-tool-pending'; card.dataset.toolId = data.turn_index + '-' + (data.tool_index || 0); card.innerHTML = '
' + '' + this._escapeHtml(data.tool) + '' + (filePath ? '' + this._escapeHtml(filePath) + '' : '') + 'Running...' + '
' + '
'; this.currentTurnEl.appendChild(card); // Scroll to bottom turnsEl.scrollTop = turnsEl.scrollHeight; } _renderToolCallEnd(data) { const toolId = data.turn_index + '-' + (data.tool_index || 0); const card = this.currentTurnEl ? this.currentTurnEl.querySelector('[data-tool-id="' + toolId + '"]') : null; if (!card) return; card.classList.remove('lca-tool-pending'); const spinner = card.querySelector('.lca-tool-spinner'); if (spinner) spinner.remove(); // Render the tool output const body = card.querySelector('.ct-tool-body'); if (body) { body.innerHTML = this._renderToolOutput(data); } // Scroll const turnsEl = document.getElementById('lca-turns-' + this.fieldKey); if (turnsEl) turnsEl.scrollTop = turnsEl.scrollHeight; } _renderToolOutput(data) { const tool = data.tool || ''; const input = data.input || {}; const output = data.output || ''; const outputType = data.output_type || 'generic'; if (outputType === 'diff' || tool === 'Edit') { return this._renderDiff(input, output); } else if (outputType === 'terminal' || tool === 'Bash') { return this._renderTerminal(input, output); } else if (outputType === 'code') { return this._renderCode(output); } else { return this._renderGeneric(input, output); } } _renderDiff(input, output) { const oldStr = input.old_string || ''; const newStr = input.new_string || ''; let html = '
'; oldStr.split('\n').forEach(line => { html += '
' + '-' + '' + this._escapeHtml(line) + '
'; }); newStr.split('\n').forEach(line => { html += '
' + '+' + '' + this._escapeHtml(line) + '
'; }); html += '
'; if (output) { html += '
' + this._escapeHtml(output) + '
'; } return html; } _renderTerminal(input, output) { const cmd = input.command || input.cmd || ''; let html = '
'; if (cmd) { html += '
$ ' + this._escapeHtml(cmd) + '
'; } if (output) { html += '
' + this._escapeHtml(output) + '
'; } html += '
'; return html; } _renderCode(output) { if (!output) return '
No output
'; const lines = output.split('\n'); let html = '
'; lines.forEach((line, i) => { html += ''; }); html += '
' + (i + 1) + '' + (this._escapeHtml(line) || ' ') + '
'; return html; } _renderGeneric(input, output) { let html = ''; if (input && Object.keys(input).length) { html += '
Input
' + '
' + this._escapeHtml(JSON.stringify(input, null, 2)) + '
'; } if (output) { html += '
Output
' + '
' + this._escapeHtml(output) + '
'; } return html || '
No data
'; } _finalizeTurn(data) { // Add reasoning text if present if (data.content && this.currentTurnEl) { const header = this.currentTurnEl.querySelector('.ct-turn-header'); if (header) { const reasoning = document.createElement('div'); reasoning.className = 'ct-reasoning'; reasoning.textContent = data.content; header.after(reasoning); } } // Update counter const counterEl = document.getElementById('lca-counter-' + this.fieldKey); if (counterEl) { counterEl.textContent = (data.turn_index + 1) + ' turns'; } // Reset for next turn this.currentTurnEl = null; } _updateStatus(state) { this.state = state; const indicator = document.getElementById('lca-status-' + this.fieldKey); const text = document.getElementById('lca-status-text-' + this.fieldKey); if (indicator) { indicator.className = 'lca-status-indicator lca-status-' + state; } if (text) { const labels = { idle: 'Idle', running: 'Running', paused: 'Paused', completed: 'Completed', error: 'Error', }; text.textContent = labels[state] || state; } // Toggle pause/resume buttons const pauseBtn = this.container.querySelector('[data-action="pause"]'); const resumeBtn = this.container.querySelector('[data-action="resume"]'); if (pauseBtn) pauseBtn.style.display = state === 'running' ? '' : 'none'; if (resumeBtn) resumeBtn.style.display = state === 'paused' ? '' : 'none'; } async _pause() { if (!this.sessionId) return; await fetch('/api/live_coding_agent/pause/' + this.sessionId, { method: 'POST' }); } async _resume() { if (!this.sessionId) return; await fetch('/api/live_coding_agent/resume/' + this.sessionId, { method: 'POST' }); } async _stop() { if (!this.sessionId) return; await fetch('/api/live_coding_agent/stop/' + this.sessionId, { method: 'POST' }); this._disconnectSSE(); } async _sendInstruction() { if (!this.sessionId) return; const input = document.getElementById('lca-instruction-input-' + this.fieldKey); if (!input || !input.value.trim()) return; await fetch('/api/live_coding_agent/instruct/' + this.sessionId, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ instruction: input.value.trim() }), }); input.value = ''; } _showInstructionConfirm(instruction) { // Brief toast showing the instruction was received const turnsEl = document.getElementById('lca-turns-' + this.fieldKey); if (!turnsEl) return; const toast = document.createElement('div'); toast.className = 'ct-turn ct-turn-user'; toast.innerHTML = '
Instruction
' + '
' + this._escapeHtml(instruction) + '
'; turnsEl.appendChild(toast); turnsEl.scrollTop = turnsEl.scrollHeight; } _showError(message) { const turnsEl = document.getElementById('lca-turns-' + this.fieldKey); if (!turnsEl) return; const errDiv = document.createElement('div'); errDiv.className = 'lca-error-msg'; errDiv.textContent = 'Error: ' + message; turnsEl.appendChild(errDiv); } _classifyTool(name) { const n = (name || '').toLowerCase(); if (['grep', 'glob', 'search', 'find'].includes(n)) return 'search'; if (['read'].includes(n)) return 'read'; if (['edit', 'replace'].includes(n)) return 'edit'; if (['write', 'create'].includes(n)) return 'write'; if (['bash', 'terminal', 'shell', 'run'].includes(n)) return 'bash'; return 'generic'; } _getToolColors(type) { const colors = { read: ['#e3f2fd', '#1565c0', '#1976d2'], edit: ['#fff3e0', '#e65100', '#ef6c00'], write: ['#e8f5e9', '#2e7d32', '#388e3c'], bash: ['#263238', '#b0bec5', '#78909c'], search: ['#f3e5f5', '#6a1b9a', '#7b1fa2'], generic: ['#f5f5f5', '#424242', '#616161'], }; return colors[type] || colors.generic; } _escapeHtml(text) { const div = document.createElement('div'); div.textContent = text || ''; return div.innerHTML; } } // Auto-initialize viewers function initViewers() { document.querySelectorAll('.live-coding-agent-viewer').forEach(el => { if (!el._lcaViewer) { el._lcaViewer = new LiveCodingAgentViewer(el); } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initViewers); } else { initViewers(); } // Watch for dynamically added viewers const observer = new MutationObserver(initViewers); observer.observe(document.body, { childList: true, subtree: true }); })();