/** * Debug Dashboard - Multi-View Application * All panels visible simultaneously, focus on robustness and utility */ // ============================================================================= // Configuration & Constants // ============================================================================= const CONFIG = { MAX_LOGS: 500, MAX_LLM_TRACES: 50, MAX_REPL_RESULTS: 20, RECONNECT_MAX_ATTEMPTS: 10, RECONNECT_DELAY: 1000, PING_INTERVAL: 25000, UPDATE_THROTTLE: 100, // ms } const SYSTEM_STATE_MARKER = 'The following blackboard provides you with information about your current state:' // ============================================================================= // Utility Functions // ============================================================================= function escapeHtml(text) { if (text == null) return '' return String(text) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } function formatSystemMessageContent(content) { if (typeof content !== 'string') return content const idx = content.indexOf(SYSTEM_STATE_MARKER) if (idx === -1) return content return `===TRUNCATED===\n${content.slice(idx + SYSTEM_STATE_MARKER.length).trimStart()}` } function throttle(func, wait) { let timeout return function executedFunction(...args) { const later = () => { clearTimeout(timeout) func(...args) } clearTimeout(timeout) timeout = setTimeout(later, wait) } } // ============================================================================= // WebSocket Client // ============================================================================= class DebugClient { constructor() { this.ws = null this.reconnectAttempts = 0 this.pingInterval = null this.messageIdCounter = 0 this.eventHandlers = new Map() } on(eventType, handler) { if (!this.eventHandlers.has(eventType)) { this.eventHandlers.set(eventType, []) } this.eventHandlers.get(eventType).push(handler) } emit(eventType, data) { const handlers = this.eventHandlers.get(eventType) if (handlers) { handlers.forEach((handler) => { try { handler(data) } catch (err) { console.error(`Error in ${eventType} handler:`, err) } }) } } connect() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' const wsUrl = `${protocol}//${window.location.host}` this.ws = new WebSocket(wsUrl) this.ws.onopen = () => { this.reconnectAttempts = 0 this.updateConnectionStatus(true) this.startPing() this.emit('connected') } this.ws.onclose = () => { this.updateConnectionStatus(false) this.stopPing() this.scheduleReconnect() } this.ws.onerror = (err) => { console.error('WebSocket error:', err) } this.ws.onmessage = (event) => { this.handleMessage(event.data) } } disconnect() { if (this.ws) { this.ws.close() this.ws = null } this.stopPing() } reconnect() { this.disconnect() this.reconnectAttempts = 0 this.connect() } send(command) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { const message = { id: `${++this.messageIdCounter}`, data: command, timestamp: Date.now(), } this.ws.send(JSON.stringify(message)) } } handleMessage(data) { try { const message = JSON.parse(data) const event = message.data if (event.type === 'history') { for (const historyEvent of event.payload) { this.routeEvent(historyEvent) } return } if (event.type === 'pong') { return } this.routeEvent(event) } catch (err) { console.error('Failed to parse message:', err) } } routeEvent(event) { this.emit(event.type, event.payload) } updateConnectionStatus(connected) { const dot = document.getElementById('connection-status') const text = document.getElementById('status-text') if (connected) { dot.classList.add('connected') text.textContent = 'Connected' } else { dot.classList.remove('connected') text.textContent = 'Disconnected' } } scheduleReconnect() { if (this.reconnectAttempts >= CONFIG.RECONNECT_MAX_ATTEMPTS) { console.warn('Max reconnect attempts reached') return } this.reconnectAttempts++ const delay = CONFIG.RECONNECT_DELAY * Math.min(this.reconnectAttempts, 5) const text = document.getElementById('status-text') text.textContent = `Reconnecting (${this.reconnectAttempts})...` setTimeout(() => this.connect(), delay) } startPing() { this.pingInterval = setInterval(() => { this.send({ type: 'ping', payload: { timestamp: Date.now() } }) }, CONFIG.PING_INTERVAL) } stopPing() { if (this.pingInterval) { clearInterval(this.pingInterval) this.pingInterval = null } } } // ============================================================================= // Queue Panel // ============================================================================= class QueuePanel { constructor(client) { this.client = client this.data = { queue: [], processing: null } this.elements = { queueList: document.getElementById('queue-list'), queueCount: document.getElementById('queue-count'), processingContent: document.getElementById('processing-content'), statQueue: document.getElementById('stat-queue'), } } init() { this.client.on('queue', data => this.update(data)) this.client.on('connected', () => this.reset()) this.render() } update(data) { this.data = data this.render() } reset() { this.data = { queue: [], processing: null } this.render() } render() { const { queue, processing } = this.data // Queue count const queueSize = queue?.length || 0 this.elements.queueCount.textContent = queueSize this.elements.statQueue.textContent = queueSize // Queue list if (queue && queue.length > 0) { this.elements.queueList.innerHTML = queue.map((item, idx) => `
#${idx + 1} ${escapeHtml(item.type)} ${escapeHtml(item.source?.id || 'unknown')}
`).join('') } else { this.elements.queueList.innerHTML = '
Queue empty
' } // Processing if (processing) { this.elements.processingContent.innerHTML = `
${escapeHtml(processing.type)}
Source: ${escapeHtml(processing.source?.type)}/${escapeHtml(processing.source?.id)}
` } else { this.elements.processingContent.innerHTML = 'Idle' } } } // ============================================================================= // Reflex Panel // ============================================================================= class ReflexPanel { constructor(client) { this.client = client this.state = null this.elements = { mode: document.getElementById('reflex-mode'), statMode: document.getElementById('stat-reflex-mode'), activeBehavior: document.getElementById('reflex-active-behavior'), signalType: document.getElementById('reflex-signal-type'), signalSource: document.getElementById('reflex-signal-source'), socialSpeaker: document.getElementById('reflex-social-speaker'), } } init() { this.client.on('reflex', data => this.update(data)) this.client.on('connected', () => this.reset()) this.render() } update(data) { this.state = data this.render() } reset() { this.state = null this.render() } render() { if (!this.state) { this.elements.mode.textContent = 'unknown' this.elements.mode.className = 'panel-badge' if (this.elements.statMode) this.elements.statMode.textContent = 'unknown' this.elements.activeBehavior.textContent = 'None' this.elements.signalType.textContent = 'None' this.elements.signalSource.textContent = '-' this.elements.socialSpeaker.textContent = 'None' return } const { mode, activeBehaviorId, context } = this.state // Mode this.elements.mode.textContent = mode this.elements.mode.className = `panel-badge ${mode === 'alert' ? 'badge-error' : (mode === 'social' ? 'badge-success' : '')}` if (this.elements.statMode) this.elements.statMode.textContent = mode // Behavior this.elements.activeBehavior.textContent = activeBehaviorId ? escapeHtml(activeBehaviorId) : 'None' // Attention if (context.attention?.lastSignalType) { this.elements.signalType.textContent = escapeHtml(context.attention.lastSignalType) this.elements.signalSource.textContent = escapeHtml(context.attention.lastSignalSourceId || '-') } else { this.elements.signalType.textContent = 'None' } // Social if (context.social?.lastSpeaker) { this.elements.socialSpeaker.textContent = escapeHtml(context.social.lastSpeaker) } else { this.elements.socialSpeaker.textContent = 'None' } } } // ============================================================================= // Brain Panel // ============================================================================= class BrainPanel { constructor(client) { this.client = client this.state = null this.elements = { status: document.getElementById('brain-status'), queue: document.getElementById('brain-queue'), context: document.getElementById('brain-context'), } } init() { this.client.on('brain_state', data => this.update(data)) this.client.on('connected', () => this.reset()) } update(data) { this.state = data this.render() } reset() { this.state = null this.render() } render() { if (!this.state) { this.elements.status.textContent = 'Unknown' this.elements.queue.textContent = '-' this.elements.context.textContent = '' return } this.elements.status.textContent = this.state.status.toUpperCase() this.elements.status.className = `status-badge status-${this.state.status}` this.elements.queue.textContent = this.state.queueLength // Render context view (it's markdown/text) this.elements.context.textContent = this.state.lastContextView || '(No context yet)' } } // ============================================================================= // Logs Panel // ============================================================================= class LogsPanel { constructor(client) { this.client = client this.logs = [] this.autoScroll = true this.paused = false this.filter = { level: 'all', search: '' } this.currentFile = '' this.elements = { container: document.getElementById('logs-container'), search: document.getElementById('log-search'), levelFilter: document.getElementById('log-level-filter'), autoScroll: document.getElementById('auto-scroll'), fileSelect: document.getElementById('log-file-select'), loadFile: document.getElementById('load-log-file'), statEvents: document.getElementById('stat-events'), } } init() { this.client.on('log', data => this.addLog(data)) this.client.on('connected', () => this.reset()) this.elements.search.addEventListener('input', (e) => { this.filter.search = e.target.value.toLowerCase() this.renderThrottled() }) this.elements.levelFilter.addEventListener('change', (e) => { this.filter.level = e.target.value this.renderThrottled() }) this.elements.autoScroll.addEventListener('change', (e) => { this.autoScroll = e.target.checked }) this.elements.loadFile.addEventListener('click', () => { const file = this.elements.fileSelect.value if (!file) { this.currentFile = '' this.reset() return } this.loadPersistedLog(file) }) this.refreshFileList() this.renderThrottled = throttle(() => this.render(), CONFIG.UPDATE_THROTTLE) this.render() } addLog(entry) { if (this.paused) return this.logs.push(entry) if (this.logs.length > CONFIG.MAX_LOGS) { this.logs.shift() } this.elements.statEvents.textContent = this.logs.length this.renderThrottled() } reset() { this.logs = [] this.elements.statEvents.textContent = '0' this.render() } async refreshFileList() { try { const res = await fetch('/api/logs') const json = await res.json() const files = json.files || [] const select = this.elements.fileSelect if (!select) return const current = select.value select.innerHTML = `${files.map(f => ``).join('')}` if (files.includes(current)) select.value = current } catch (err) { console.error('Failed to load log files', err) } } async loadPersistedLog(file) { try { const res = await fetch(`/api/logs?file=${encodeURIComponent(file)}&limit=1000`) if (!res.ok) { console.error('Failed to fetch log file', await res.text()) return } const json = await res.json() const events = Array.isArray(json.events) ? json.events : [] const logEntries = events .filter(e => e.type === 'log') .map(e => e.payload) .filter(Boolean) this.logs = logEntries this.currentFile = file this.elements.statEvents.textContent = this.logs.length this.render() } catch (err) { console.error('Failed to load persisted log', err) } } clear() { this.logs = [] this.elements.statEvents.textContent = '0' this.render() } setPaused(paused) { this.paused = paused } render() { const filtered = this.logs.filter((log) => { if (this.filter.level !== 'all' && log.level !== this.filter.level) { return false } if (this.filter.search && !log.message.toLowerCase().includes(this.filter.search)) { return false } return true }) this.elements.container.innerHTML = filtered.map((log) => { const time = new Date(log.timestamp).toLocaleTimeString() const fieldsStr = log.fields ? JSON.stringify(log.fields) : '' return `
${time} [${log.level}] ${escapeHtml(log.message)} ${fieldsStr ? `
${escapeHtml(fieldsStr)}
` : ''}
` }).join('') if (this.autoScroll) { this.elements.container.scrollTop = this.elements.container.scrollHeight } } } // ============================================================================= // Conversation Panel (Live Chat View) // ============================================================================= // --- User message parser: extracts structured sections from brain's buildUserMessage output --- function parseUserMessage(content) { if (typeof content !== 'string') return { sections: [], raw: '' } const sections = [] // Known section tags in order they may appear const tagPattern = /^\[(EVENT|FEEDBACK|PERCEPTION|STATE|ERROR_BURST_GUARD|ERROR_BURST|MANDATORY|SCRIPT|ACTION_QUEUE|NO_ACTION_BUDGET|CONTEXT)\]\s*/ // Split on double-newline (the separator used by buildUserMessage) const blocks = content.split(/\n\n/) for (const block of blocks) { const trimmed = block.trim() if (!trimmed) continue const m = trimmed.match(tagPattern) if (m) { sections.push({ tag: m[1], text: trimmed.slice(m[0].length) }) } else { // Could be a continuation of PERCEPTION or unknown block sections.push({ tag: 'OTHER', text: trimmed }) } } return { sections, raw: content } } class ConversationPanel { constructor(client) { this.client = client this._mkSession = () => ({ messages: [], greyed: false, activeContext: null, archivedContexts: [], activeContextStartIndex: 0, contextHistoryMessage: null }) this.sessions = [this._mkSession()] this.isProcessing = false this.autoScroll = true this.turnCounter = 0 this.elements = { container: document.getElementById('conversation-container'), count: document.getElementById('conversation-count'), statLlm: document.getElementById('stat-llm'), processingBadge: document.getElementById('conversation-processing'), scroll: document.getElementById('conversation-container')?.closest('.panel-content') || document.getElementById('conversation-container'), } } init() { this.client.on('conversation_update', data => this.handleUpdate(data)) this.client.on('connected', () => { this.reset() this.client.send({ type: 'request_conversation' }) }) // Unified collapsible toggle handler (event delegation — attached once) if (this.elements.container) { this.elements.container.addEventListener('click', (e) => { const toggle = e.target.closest('[data-toggle]') if (!toggle) return const targetId = toggle.getAttribute('data-toggle') const body = document.getElementById(targetId) if (!body) return const isOpen = body.classList.toggle('cv-open') const arrow = toggle.querySelector('.cv-arrow') if (arrow) arrow.textContent = isOpen ? '\u25BC' : '\u25B6' }) } this.render() } handleUpdate(data) { if (data.sessionBoundary) { const cur = this.sessions.at(-1) if (cur) cur.greyed = true this.sessions.push(this._mkSession()) } else { const cur = this.sessions.at(-1) if (cur) { cur.messages = data.messages || [] cur.activeContext = data.activeContext || null cur.archivedContexts = data.archivedContexts || [] cur.activeContextStartIndex = data.activeContextStartIndex ?? 0 cur.contextHistoryMessage = data.contextHistoryMessage || null } } this.isProcessing = !!data.isProcessing this.updateStats() this.render() } reset() { this.sessions = [this._mkSession()] this.isProcessing = false this.turnCounter = 0 this.updateStats() this.render() } updateStats() { const total = this.sessions.reduce((s, sess) => s + sess.messages.length, 0) if (this.elements.count) this.elements.count.textContent = total if (this.elements.statLlm) this.elements.statLlm.textContent = total if (this.elements.processingBadge) this.elements.processingBadge.classList.toggle('hidden', !this.isProcessing) } render() { if (!this.elements.container) return this.turnCounter = 0 const html = this.sessions.map((session) => { const cls = session.greyed ? 'chat-session chat-session-greyed' : 'chat-session' const body = this.renderSession(session) const divider = session.greyed ? '
Session cleared
' : '' return `
${body}
${divider}` }).join('') const typing = this.isProcessing ? '
' : '' this.elements.container.innerHTML = html + typing if (this.autoScroll) { const el = this.elements.scroll if (el) requestAnimationFrame(() => { el.scrollTop = el.scrollHeight }) } } // --- Session rendering --- renderSession(session) { const { messages, activeContext, archivedContexts, contextHistoryMessage } = session if (!messages || messages.length === 0) return '
No messages yet
' const parts = [] // Context status bar if (archivedContexts?.length > 0 || activeContext) { parts.push(this.renderContextStatusBar(activeContext, archivedContexts, contextHistoryMessage)) } // Group messages into turns (user+assistant pairs) const turns = this.groupIntoTurns(messages) for (const turn of turns) { parts.push(this.renderTurn(turn)) } return parts.join('') } groupIntoTurns(messages) { const turns = [] let current = null for (const msg of messages) { if (msg.role === 'system') { turns.push({ type: 'system', system: msg }) } else if (msg.role === 'user') { current = { type: 'turn', user: msg, assistant: null } turns.push(current) } else if (msg.role === 'assistant') { if (current && !current.assistant) { current.assistant = msg } else { turns.push({ type: 'turn', user: null, assistant: msg }) } } } return turns } renderTurn(turn) { if (turn.type === 'system') return this.renderSystemMessage(turn.system) this.turnCounter++ const n = this.turnCounter const userParsed = turn.user ? parseUserMessage(turn.user.content || '') : null const eventSection = userParsed?.sections.find(s => s.tag === 'EVENT' || s.tag === 'FEEDBACK') const contextSection = userParsed?.sections.find(s => s.tag === 'CONTEXT') // Build a short summary for the turn header let summary = `Turn ${n}` if (eventSection) { summary = this.summarizeEvent(eventSection) } // Detect context label from the [CONTEXT] section let ctxBadge = '' if (contextSection) { const ctxMatch = contextSection.text.match(/active="([^"]+)"/) if (ctxMatch) { ctxBadge = `${escapeHtml(ctxMatch[1])}` } else if (contextSection.text.includes('no active context')) { ctxBadge = 'no ctx' } } const turnId = `cv-turn-${n}` const userHtml = turn.user ? this.renderParsedUserMessage(userParsed, n) : '' const assistantHtml = turn.assistant ? this.renderParsedAssistantMessage(turn.assistant, n) : '' return `
${userHtml} ${assistantHtml}
` } // --- Event summary helpers --- summarizeEvent(section) { const text = section.text // Chat messages: "Chat from X: "message"" const chatMatch = text.match(/^Chat from (\w+):\s*"(.+)"$/s) if (chatMatch) return `Chat from ${chatMatch[1]}: "${chatMatch[2].slice(0, 60)}${chatMatch[2].length > 60 ? '...' : ''}"` // Perception Signal if (text.startsWith('Perception Signal:')) return text.slice(0, 70) + (text.length > 70 ? '...' : '') // system_alert with JSON — extract reason + returnValue preview if (text.startsWith('system_alert:')) { try { const json = JSON.parse(text.slice('system_alert:'.length).trim()) const reason = json.reason || 'unknown' const rv = json.returnValue const rvPreview = typeof rv === 'string' ? rv.slice(0, 50) : '' return `system: ${reason}${rvPreview ? ` → ${rvPreview}${rv.length > 50 ? '...' : ''}` : ''}` } catch { /* fall through */ } } // FEEDBACK: "toolName: Success/Failed. details" if (section.tag === 'FEEDBACK') { // eslint-disable-next-line regexp/no-super-linear-backtracking const fbMatch = text.match(/^(\w+):\s*(Success|Failed)\.?\s*(.*)$/s) if (fbMatch) { const detail = fbMatch[3].slice(0, 50) return `${fbMatch[1]}: ${fbMatch[2]}${detail ? ` — ${detail}${fbMatch[3].length > 50 ? '...' : ''}` : ''}` } } // Fallback: truncate const flat = text.replace(/\n/g, ' ').slice(0, 70) return flat + (text.length > 70 ? '...' : '') } // --- Parsed user message rendering --- renderParsedUserMessage(parsed, turnNum) { if (!parsed) return '' const cards = [] for (const section of parsed.sections) { cards.push(this.renderUserSection(section, turnNum)) } return `
${cards.join('')}
` } renderUserSection(section, turnNum) { const id = `cv-s-${turnNum}-${section.tag}-${Math.random().toString(36).slice(2, 6)}` const tagColors = { EVENT: 'cv-tag-event', FEEDBACK: 'cv-tag-feedback', PERCEPTION: 'cv-tag-perception', SCRIPT: 'cv-tag-script', ACTION_QUEUE: 'cv-tag-queue', NO_ACTION_BUDGET: 'cv-tag-budget', CONTEXT: 'cv-tag-context', ERROR_BURST: 'cv-tag-error', ERROR_BURST_GUARD: 'cv-tag-error', MANDATORY: 'cv-tag-error', STATE: 'cv-tag-state', OTHER: 'cv-tag-other', } const colorCls = tagColors[section.tag] || 'cv-tag-other' // Some sections are compact enough to show inline if (['ACTION_QUEUE', 'NO_ACTION_BUDGET', 'CONTEXT', 'ERROR_BURST', 'STATE'].includes(section.tag)) { return `
${section.tag} ${escapeHtml(section.text)}
` } // EVENT / FEEDBACK — show prominently if (section.tag === 'EVENT' || section.tag === 'FEEDBACK') { return `
${section.tag} ${escapeHtml(section.text)}
` } // SCRIPT, PERCEPTION, OTHER — collapsible const preview = section.text.slice(0, 60).replace(/\n/g, ' ') return `
${escapeHtml(section.text)}
` } // --- Parsed assistant message rendering --- renderParsedAssistantMessage(msg, turnNum) { const reasoning = msg.reasoning || '' const code = msg.content || '' const parts = [] if (reasoning) { const rid = `cv-reason-${turnNum}` const preview = reasoning.slice(0, 80).replace(/\n/g, ' ') parts.push(`
${escapeHtml(reasoning)}
`) } if (code) { parts.push(`
Code
${escapeHtml(code)}
`) } return `
${parts.join('')}
` } // --- Context status bar --- renderContextStatusBar(activeContext, archivedContexts, contextHistoryMessage) { const parts = [] // Active context indicator if (activeContext?.label) { parts.push(` ${escapeHtml(activeContext.label)} (${activeContext.messageCount} msgs)`) } else { parts.push(' No active context') } // Archived count if (archivedContexts?.length > 0) { const archId = `cv-archived-${Math.random().toString(36).slice(2, 6)}` const items = archivedContexts.map((ctx, i) => { const time = new Date(ctx.archivedAt).toLocaleTimeString() return `
#${i + 1} ${escapeHtml(ctx.label || 'unnamed')} ${ctx.turns}t · ${time}
${escapeHtml(ctx.summary)}
` }).join('') parts.push(``) // Append the collapsible body after the status bar parts.push(`
${items}
`) } // Context history prefix if (contextHistoryMessage) { const chId = `cv-ctxhist-${Math.random().toString(36).slice(2, 6)}` parts.push(``) parts.push(`
${escapeHtml(contextHistoryMessage)}
`) } return `
${parts.join('')}
` } // --- System message --- renderSystemMessage(msg) { const id = `cv-sys-${Math.random().toString(36).slice(2, 6)}` const preview = formatSystemMessageContent(msg.content || '') return `
${escapeHtml(preview)}
` } } // ============================================================================= // Tools Panel // ============================================================================= class ToolsPanel { constructor(client) { this.client = client this.tools = [] this.filter = '' this.executingTools = new Set() this.elements = { grid: document.getElementById('tools-grid'), search: document.getElementById('tools-search'), } } init() { this.client.on('debug:tools_list', data => this.updateTools(data)) this.client.on('debug:tool_result', data => this.handleResult(data)) this.client.on('connected', () => this.requestTools()) this.elements.search?.addEventListener('input', (e) => { this.filter = e.target.value.toLowerCase() this.render() }) } requestTools() { // eslint-disable-next-line no-console console.log('[ToolsPanel] Requesting tools...') // Check if we already have tools to avoid re-rendering on reconnect if not needed // But re-requesting ensures we are in sync with server capabilities this.client.send({ type: 'request_tools' }) } updateTools(data) { if (data && data.tools) { this.tools = data.tools this.render() } } render() { if (!this.tools || this.tools.length === 0) { this.elements.grid.innerHTML = '
Loading tools...
' return } const filtered = this.tools.filter((tool) => { if (!this.filter) return true return tool.name.toLowerCase().includes(this.filter) || (tool.description && tool.description.toLowerCase().includes(this.filter)) }) if (filtered.length === 0) { this.elements.grid.innerHTML = '
No tools match filter
' return } // Don't nuke usage of existing DOM elements if possible to preserve form state? // For simplicity, re-render is fine for this debug tool. this.elements.grid.innerHTML = filtered.map(tool => this.renderCard(tool)).join('') // Attach event listeners filtered.forEach((tool) => { const card = document.getElementById(`tool-card-${tool.name}`) const executeBtn = card?.querySelector('.btn-execute') executeBtn?.addEventListener('click', () => this.executeTool(tool)) }) } renderCard(tool) { const isExecuting = this.executingTools.has(tool.name) const cardState = isExecuting ? 'executing' : '' const paramCount = tool.params.length return `
${escapeHtml(tool.name)} ${paramCount > 0 ? `${paramCount} param${paramCount > 1 ? 's' : ''}` : ''}
${escapeHtml(tool.description || '')}
${this.renderParams(tool)}
` } renderParams(tool) { if (tool.params.length === 0) return '' return `
${tool.params.map(param => `
${this.renderParamInput(param)}
`).join('')}
` } renderParamInput(param) { if (param.type === 'boolean') { const defaultValue = param.default === true ? 'true' : 'false' return ` ` } const defaultValue = param.default !== undefined ? `value="${escapeHtml(String(param.default))}"` : '' return ` ` } executeTool(tool) { const card = document.getElementById(`tool-card-${tool.name}`) if (!card) return // Collect parameter values const params = {} const inputs = card.querySelectorAll('.param-input') for (const input of inputs) { const paramName = input.dataset.param let value = input.value // Convert to appropriate type based on definition const paramDef = tool.params.find(p => p.name === paramName) if (paramDef) { if (paramDef.type === 'number') { if (value === '') { continue } value = Number.parseFloat(value) if (Number.isNaN(value)) { this.showResult(tool.name, { error: `Invalid number for ${paramName}` }, true) return } } else if (paramDef.type === 'boolean') { if (value === '') { continue } const normalized = value.trim().toLowerCase() if (normalized === 'true') { value = true } else if (normalized === 'false') { value = false } else { this.showResult(tool.name, { error: `Invalid boolean for ${paramName}` }, true) return } } } params[paramName] = value } // Mark as executing this.executingTools.add(tool.name) this.updateCardState(tool.name, 'executing') this.hideResult(tool.name) // Send command to server this.client.send({ type: 'execute_tool', payload: { toolName: tool.name, params, }, }) } handleResult(data) { const { toolName, error } = data this.executingTools.delete(toolName) this.updateCardState(toolName, error ? 'error' : 'success') this.showResult(toolName, data, !!error) if (!error) { setTimeout(() => { // Reset state visual but keep result visible for a bit? // Or remove success styling const card = document.getElementById(`tool-card-${toolName}`) if (card) { card.classList.remove('success') // Don't auto-hide result immediately, maybe user wants to read it } }, 3000) } } updateCardState(toolName, state) { const card = document.getElementById(`tool-card-${toolName}`) if (!card) return card.classList.remove('executing', 'success', 'error') if (state) card.classList.add(state) const btn = card.querySelector('.btn-execute') if (btn) { if (state === 'executing') { btn.disabled = true btn.textContent = 'Executing...' } else { btn.disabled = false btn.textContent = 'Execute' } } } showResult(toolName, data, isError) { const resultEl = document.getElementById(`result-${toolName}`) if (!resultEl) return resultEl.classList.remove('hidden') const label = isError ? 'Error' : 'Result' const content = isError ? data.error : data.result resultEl.innerHTML = ` ${label}
${escapeHtml(content || 'No result')}
` } hideResult(toolName) { const resultEl = document.getElementById(`result-${toolName}`) if (resultEl) { resultEl.classList.add('hidden') } } } class ReplPanel { constructor(client) { this.client = client this.variables = [] this.variableFilter = '' this.results = [] this.isRunning = false this.elements = { varsList: document.getElementById('repl-vars-list'), varsSearch: document.getElementById('repl-vars-search'), refreshBtn: document.getElementById('repl-refresh-state'), runBtn: document.getElementById('repl-run-btn'), codeInput: document.getElementById('repl-code-input'), resultList: document.getElementById('repl-result-list'), } } init() { this.client.on('debug:repl_state', data => this.updateState(data)) this.client.on('debug:repl_result', data => this.handleResult(data)) this.client.on('connected', () => this.requestState()) this.elements.refreshBtn?.addEventListener('click', () => this.requestState()) this.elements.varsSearch?.addEventListener('input', (event) => { this.variableFilter = (event.target?.value || '').toLowerCase() this.renderVariables() }) this.elements.runBtn?.addEventListener('click', () => this.execute()) this.elements.codeInput?.addEventListener('keydown', (event) => { const isEnter = event.key === 'Enter' const hasModifier = event.metaKey || event.ctrlKey if (!isEnter || !hasModifier) return event.preventDefault() this.execute() }) this.renderVariables() this.renderResults() } requestState() { this.client.send({ type: 'request_repl_state' }) } execute() { const code = this.elements.codeInput?.value ?? '' if (!code.trim()) { this.results.unshift({ source: 'manual', code: '', logs: [], actions: [], error: 'Code is empty', durationMs: 0, timestamp: Date.now(), }) this.results = this.results.slice(0, CONFIG.MAX_REPL_RESULTS) this.renderResults() return } this.isRunning = true this.renderRunState() this.client.send({ type: 'execute_repl', payload: { code }, }) } updateState(data) { if (!data || !Array.isArray(data.variables)) return this.variables = data.variables this.renderVariables() } handleResult(data) { this.isRunning = false this.renderRunState() this.results.unshift(data) if (this.results.length > CONFIG.MAX_REPL_RESULTS) { this.results = this.results.slice(0, CONFIG.MAX_REPL_RESULTS) } this.renderResults() } renderRunState() { if (!this.elements.runBtn) return this.elements.runBtn.disabled = this.isRunning this.elements.runBtn.textContent = this.isRunning ? 'Running...' : 'Run (Ctrl/Cmd+Enter)' } renderVariables() { if (!this.elements.varsList) return if (this.variables.length === 0) { this.elements.varsList.innerHTML = '
No variables loaded
' return } const filtered = this.variables.filter((variable) => { if (!this.variableFilter) return true const searchSpace = `${variable.name} ${variable.kind} ${variable.preview} ${variable.readonly ? 'readonly' : 'writable'}`.toLowerCase() return searchSpace.includes(this.variableFilter) }) if (filtered.length === 0) { this.elements.varsList.innerHTML = '
No variables match filter
' return } this.elements.varsList.innerHTML = filtered.map(v => `
${escapeHtml(v.name)}
${escapeHtml(v.kind)} · ${v.readonly ? 'readonly' : 'writable'}
${escapeHtml(v.preview || '')}
`).join('') } renderResults() { if (!this.elements.resultList) return if (this.results.length === 0) { this.elements.resultList.innerHTML = '
No REPL executions yet
' return } this.elements.resultList.innerHTML = this.results.map((result) => { const isError = !!result.error const source = result.source === 'llm' ? 'llm' : 'manual' const sourceLabel = source === 'llm' ? 'LLM' : 'MANUAL' const actionSummary = Array.isArray(result.actions) && result.actions.length > 0 ? result.actions.map((action) => { const status = action.ok ? 'ok' : 'error' return `${status} ${action.tool}(${JSON.stringify(action.params || {})})${action.error ? ` -> ${action.error}` : ''}${action.result ? ` -> ${action.result}` : ''}` }).join('\n') : '(none)' const logsSummary = Array.isArray(result.logs) && result.logs.length > 0 ? result.logs.join('\n') : '(none)' const returnValue = typeof result.returnValue === 'string' ? result.returnValue : '(undefined)' const time = new Date(result.timestamp || Date.now()).toLocaleTimeString() return `
${time} · ${sourceLabel} ${Number.isFinite(result.durationMs) ? `${result.durationMs}ms` : '-'}
Code
${escapeHtml(result.code || '')}
Return
${escapeHtml(returnValue)}
Actions
${escapeHtml(actionSummary)}
Logs
${escapeHtml(logsSummary)}
${isError ? `
Error
${escapeHtml(result.error)}
` : ''}
` }).join('') } } class TimelinePanel { constructor(client) { this.client = client this.events = [] this.filter = { type: 'all', search: '' } this.selectedTraceId = null this.elements = { list: document.getElementById('timeline-list'), container: document.getElementById('timeline-container'), search: document.getElementById('timeline-search'), typeFilter: document.getElementById('timeline-type-filter'), clearBtn: document.getElementById('timeline-clear-btn'), detail: document.getElementById('trace-detail'), detailTitle: document.getElementById('trace-detail-title'), detailContent: document.getElementById('trace-detail-content'), detailClose: document.getElementById('trace-detail-close'), } } init() { this.client.on('trace', data => this.addEvent(data)) this.client.on('trace_batch', (data) => { if (data.events) { data.events.forEach(e => this.addEvent(e)) } }) this.client.on('connected', () => this.reset()) this.elements.search?.addEventListener('input', (e) => { this.filter.search = e.target.value.toLowerCase() this.renderThrottled() }) this.elements.typeFilter?.addEventListener('change', (e) => { this.filter.type = e.target.value this.renderThrottled() }) this.elements.clearBtn?.addEventListener('click', () => this.clear()) this.elements.detailClose?.addEventListener('click', () => this.hideDetail()) this.renderThrottled = throttle(() => this.render(), CONFIG.UPDATE_THROTTLE) } addEvent(event) { this.events.push(event) if (this.events.length > 1000) { this.events.shift() } this.renderThrottled() } reset() { this.events = [] this.selectedTraceId = null this.hideDetail() this.render() } clear() { this.reset() } render() { const filtered = this.events.filter(e => this.matchesFilter(e)) const recent = filtered.slice(-200) // Show last 200 if (recent.length === 0) { this.elements.list.innerHTML = '
No events
' return } this.elements.list.innerHTML = recent.map(e => this.renderEvent(e)).join('') // Attach click handlers this.elements.list.querySelectorAll('.timeline-event').forEach((el) => { el.addEventListener('click', () => { const traceId = el.dataset.traceId this.showTraceDetail(traceId) }) }) // Auto-scroll this.elements.container.scrollTop = this.elements.container.scrollHeight } matchesFilter(event) { if (this.filter.type !== 'all' && !event.type.startsWith(this.filter.type)) { return false } if (this.filter.search) { const searchStr = `${event.type} ${JSON.stringify(event.payload)}`.toLowerCase() if (!searchStr.includes(this.filter.search)) { return false } } return true } renderEvent(event) { const time = new Date(event.timestamp).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3, }) const isSignal = event.type.startsWith('signal:') const isRaw = event.type.startsWith('raw:') const typeClass = isSignal ? 'type-signal' : (isRaw ? 'type-raw' : 'type-other') // Extract useful info from payload let info = '' if (event.payload) { const p = event.payload if (p.description) info = p.description else if (p.displayName) info = p.displayName else if (p.entityType) info = p.entityType } const hasParent = event.parentId ? 'has-parent' : '' return `
${time} ${escapeHtml(event.type)} ${info ? `${escapeHtml(info)}` : ''}
` } showTraceDetail(traceId) { this.selectedTraceId = traceId const traceEvents = this.events.filter(e => e.traceId === traceId) if (traceEvents.length === 0) { return } // Build event tree const tree = this.buildEventTree(traceEvents) this.elements.detailTitle.textContent = `Trace: ${traceId.slice(0, 8)}...` this.elements.detailContent.innerHTML = this.renderEventTree(tree) this.elements.detail.classList.remove('hidden') // Highlight in main list this.elements.list.querySelectorAll('.timeline-event').forEach((el) => { el.classList.toggle('selected', el.dataset.traceId === traceId) }) } hideDetail() { this.elements.detail?.classList.add('hidden') this.selectedTraceId = null this.elements.list?.querySelectorAll('.timeline-event.selected').forEach((el) => { el.classList.remove('selected') }) } buildEventTree(events) { const eventMap = new Map() const roots = [] // Index all events events.forEach(e => eventMap.set(e.id, { event: e, children: [] })) // Build tree events.forEach((e) => { const node = eventMap.get(e.id) if (e.parentId && eventMap.has(e.parentId)) { eventMap.get(e.parentId).children.push(node) } else { roots.push(node) } }) return roots } renderEventTree(nodes, depth = 0) { return nodes.map((node) => { const e = node.event const indent = depth * 16 const time = new Date(e.timestamp).toLocaleTimeString('en-US', { hour12: false }) return `
${depth > 0 ? '' : ''} ${escapeHtml(e.type)} ${time}
${escapeHtml(JSON.stringify(e.payload, null, 2))}
${node.children.length > 0 ? this.renderEventTree(node.children, depth + 1) : ''}
` }).join('') } } // ============================================================================= // Application // ============================================================================= // ============================================================================= // Layout Manager (Resizing & Maximizing) // ============================================================================= class LayoutManager { constructor() { this.root = document.documentElement this.activeSplitter = null this.startPos = 0 this.startSize = 0 } init() { this.setupSplitters() this.setupMaximizeButtons() } setupSplitters() { const splitters = [ { id: 'splitter-v1', var: '--col-left', type: 'v' }, { id: 'splitter-h1', var: '--row-1', type: 'h' }, { id: 'splitter-h2', var: '--row-2', type: 'h' }, ] splitters.forEach((config) => { const el = document.getElementById(config.id) if (!el) return el.addEventListener('mousedown', (e) => { this.activeSplitter = { el, ...config } this.startPos = config.type === 'v' ? e.clientX : e.clientY const style = getComputedStyle(this.root) const raw = style.getPropertyValue(config.var) const parsed = Number.parseFloat(raw) const fallbackSize = () => { if (config.var === '--col-left') return document.getElementById('left-column')?.getBoundingClientRect().width if (config.var === '--row-1') return document.getElementById('logs-section')?.getBoundingClientRect().height if (config.var === '--row-2') return document.getElementById('timeline-section')?.getBoundingClientRect().height if (config.var === '--row-3') return document.getElementById('conversation-section')?.getBoundingClientRect().height return undefined } this.startSize = Number.isFinite(parsed) ? parsed : (fallbackSize() || 200) el.classList.add('dragging') document.body.style.cursor = config.type === 'v' ? 'col-resize' : 'row-resize' document.body.style.userSelect = 'none' // Prevent text selection }) }) document.addEventListener('mousemove', e => this.handleDrag(e)) document.addEventListener('mouseup', () => this.handleDragEnd()) } handleDrag(e) { if (!this.activeSplitter) return const currentPos = this.activeSplitter.type === 'v' ? e.clientX : e.clientY const delta = currentPos - this.startPos const newSize = Math.max(50, this.startSize + delta) // Min 50px this.root.style.setProperty(this.activeSplitter.var, `${newSize}px`) } handleDragEnd() { if (!this.activeSplitter) return this.activeSplitter.el.classList.remove('dragging') this.activeSplitter = null document.body.style.cursor = '' document.body.style.userSelect = '' } setupMaximizeButtons() { document.querySelectorAll('.maximize-btn').forEach((btn) => { btn.addEventListener('click', (e) => { e.stopPropagation() // Prevent bubbling const panel = btn.closest('.panel') if (!panel) return panel.classList.toggle('maximized') const isMaximized = panel.classList.contains('maximized') btn.textContent = isMaximized ? '✕' : '⤢' btn.title = isMaximized ? 'Restore' : 'Maximize' }) }) } } class DebugApp { constructor() { this.client = new DebugClient() this.layoutManager = new LayoutManager() this.queuePanel = new QueuePanel(this.client) this.reflexPanel = new ReflexPanel(this.client) this.brainPanel = new BrainPanel(this.client) this.logsPanel = new LogsPanel(this.client) this.conversationPanel = new ConversationPanel(this.client) this.timelinePanel = new TimelinePanel(this.client) this.toolsPanel = new ToolsPanel(this.client) this.replPanel = new ReplPanel(this.client) this.panels = { queue: this.queuePanel, reflex: this.reflexPanel, brain: this.brainPanel, logs: this.logsPanel, conversation: this.conversationPanel, timeline: this.timelinePanel, tools: this.toolsPanel, repl: this.replPanel, } this.paused = false } init() { // Initialize layout this.layoutManager.init() // Tabs this.setupTabs() // Initialize all panels Object.values(this.panels).forEach(panel => panel.init()) // Setup controls document.getElementById('clear-logs-btn').addEventListener('click', () => { this.panels.logs.clear() }) document.getElementById('pause-btn').addEventListener('click', (e) => { this.paused = !this.paused this.panels.logs.setPaused(this.paused) e.target.textContent = this.paused ? 'Resume' : 'Pause' }) document.getElementById('reconnect-btn').addEventListener('click', () => { this.client.reconnect() }) // Connect this.client.connect() } setupTabs() { const tabs = Array.from(document.querySelectorAll('.tab-button')) const views = Array.from(document.querySelectorAll('.view')) const activate = (target) => { tabs.forEach(btn => btn.classList.toggle('active', btn.dataset.target === target)) views.forEach(view => view.classList.toggle('active', view.id === target)) } tabs.forEach((btn) => { btn.addEventListener('click', () => activate(btn.dataset.target)) }) // Default activate('main-view') } } // ============================================================================= // Bootstrap // ============================================================================= document.addEventListener('DOMContentLoaded', () => { const app = new DebugApp() app.init() window.debugApp = app // For debugging })