// ANSI terminal renderer for log streaming and live output import { escapeHtml } from "../core/helpers"; const ANSI_COLORS: Record = { "30": "#060a14", "31": "#f43f5e", "32": "#4ade80", "33": "#fbbf24", "34": "#0066ff", "35": "#7c3aed", "36": "#00d4ff", "37": "#e8ecf4", "90": "#5a6d8a", "91": "#f43f5e", "92": "#4ade80", "93": "#fbbf24", "94": "#0066ff", "95": "#7c3aed", "96": "#00d4ff", "97": "#e8ecf4", }; export function renderAnsiToHtml(text: string): string { let result = ""; let openSpans = 0; let i = 0; while (i < text.length) { if (text[i] === "\x1b" && text[i + 1] === "[") { const end = text.indexOf("m", i + 2); if (end !== -1) { const codes = text.substring(i + 2, end).split(";"); for (const code of codes) { if (code === "0" || code === "") { while (openSpans > 0) { result += ""; openSpans--; } } else if (code === "1") { result += ''; openSpans++; } else if (code === "2") { result += ''; openSpans++; } else if (code === "3") { result += ''; openSpans++; } else if (code === "4") { result += ''; openSpans++; } else if (ANSI_COLORS[code]) { result += ``; openSpans++; } } i = end + 1; continue; } } result += escapeHtml(text[i]); i++; } while (openSpans > 0) { result += ""; openSpans--; } return result; } export function stripAnsi(text: string): string { return text.replace(/\x1b\[[0-9;]*m/g, ""); } export function renderLogViewer(content: string): string { if (!content) return '

No logs available

'; const clean = stripAnsi(content); const lines = clean.split("\n"); let html = '
'; for (let i = 0; i < lines.length; i++) { const lineNum = i + 1; const lower = lines[i].toLowerCase(); const lineClass = lower.indexOf("error") !== -1 || lower.indexOf("fail") !== -1 ? " log-line-error" : ""; html += `
` + `${lineNum}` + `${escapeHtml(lines[i])}
`; } html += "
"; return html; } export class LiveTerminal { private container: HTMLElement; private lines: string[] = []; private autoScroll = true; private maxLines = 5000; constructor(container: HTMLElement) { this.container = container; this.container.classList.add("live-terminal"); } append(text: string): void { const newLines = text.split("\n"); this.lines.push(...newLines); if (this.lines.length > this.maxLines) { this.lines = this.lines.slice(-this.maxLines); } this.render(); } clear(): void { this.lines = []; this.render(); } private render(): void { const html = this.lines .map((line, i) => { const rendered = renderAnsiToHtml(line); return `
${i + 1}${rendered}
`; }) .join(""); this.container.innerHTML = html; if (this.autoScroll) { this.container.scrollTop = this.container.scrollHeight; } } setAutoScroll(enabled: boolean): void { this.autoScroll = enabled; } destroy(): void { this.container.innerHTML = ""; this.lines = []; } }