/**
* 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 =
'
';
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.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 += '| ' + (i + 1) +
' | ' +
(this._escapeHtml(line) || ' ') + ' |
';
});
html += '
';
return html;
}
_renderGeneric(input, output) {
let html = '';
if (input && Object.keys(input).length) {
html += '';
}
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 });
})();