/** Awareness panel: memorizer state, sensor readings. * Node detail panel: per-node model, tokens, progress, last event. */ import { esc, truncate } from './util.js'; let _sensorReadings = {}; // --- Node state tracker --- const _nodeState = {}; // { nodeName: { model, tokens, maxTokens, fillPct, lastEvent, lastDetail, status, toolCalls, startedAt } } // Normalize node names to avoid duplicates (pa_v1→pa, expert_eras→eras, etc.) function _normName(name) { return name.replace('_v1', '').replace('_v2', '').replace('expert_', ''); } function _getNode(name) { const key = _normName(name); if (!_nodeState[key]) { _nodeState[key] = { model: '', tokens: 0, maxTokens: 0, fillPct: 0, lastEvent: '', lastDetail: '', status: 'idle', toolCalls: 0, lastTool: '', }; } return _nodeState[key]; } export function updateNodeFromHud(node, event, data) { const n = _getNode(node); if (event === 'context') { if (data.model) n.model = data.model.replace('google/', '').replace('anthropic/', ''); if (data.tokens !== undefined) n.tokens = data.tokens; if (data.max_tokens !== undefined) n.maxTokens = data.max_tokens; if (data.fill_pct !== undefined) n.fillPct = data.fill_pct; } if (event === 'thinking') { n.status = 'thinking'; n.lastEvent = 'thinking'; n.lastDetail = data.detail || ''; } else if (event === 'perceived') { n.status = 'done'; n.lastEvent = 'perceived'; const a = data.analysis || {}; n.lastDetail = `${a.intent || '?'}/${a.language || '?'}/${a.tone || '?'}`; } else if (event === 'decided' || event === 'routed') { n.status = 'done'; n.lastEvent = event; n.lastDetail = data.goal || data.instruction || data.job || ''; } else if (event === 'tool_call') { n.status = 'tool'; n.lastEvent = 'tool_call'; n.lastTool = data.tool || ''; n.lastDetail = data.tool || ''; n.toolCalls++; } else if (event === 'tool_result') { n.lastEvent = 'tool_result'; n.lastDetail = truncate(data.output || '', 50); } else if (event === 'streaming') { n.status = 'streaming'; n.lastEvent = 'streaming'; } else if (event === 'done') { n.status = 'done'; n.lastEvent = 'done'; } else if (event === 'updated') { n.status = 'done'; n.lastEvent = 'updated'; } else if (event === 'planned') { n.status = 'planned'; n.lastEvent = 'planned'; n.lastDetail = `${data.tools || 0} tools`; } else if (event === 'interpreted') { n.status = 'done'; n.lastEvent = 'interpreted'; n.lastDetail = truncate(data.summary || '', 50); } renderNodes(); } // Fixed pipeline order — no re-sorting // Fixed pipeline order using normalized names const PIPELINE_ORDER = ['input', 'pa', 'director', 'eras', 'plankiste', 'thinker', 'interpreter', 'output', 'memorizer', 'ui', 'sensor']; function renderNodes() { const el = document.getElementById('node-metrics'); if (!el) return; const entries = Object.entries(_nodeState) .filter(([name]) => name !== 'runtime' && name !== 'frame_engine'); const sorted = entries.sort((a, b) => { const ia = PIPELINE_ORDER.indexOf(a[0]); const ib = PIPELINE_ORDER.indexOf(b[0]); return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib); }); let html = ''; for (const [name, n] of sorted) { const statusClass = n.status === 'thinking' || n.status === 'tool' ? 'nm-active' : n.status === 'streaming' ? 'nm-streaming' : ''; const shortName = name.replace('_v1', '').replace('_v2', '').replace('expert_', ''); const modelShort = n.model ? n.model.split('/').pop().replace('-001', '').replace('-4.5', '4.5') : ''; const tokenStr = n.maxTokens ? `${n.tokens}/${n.maxTokens}t` : ''; const fillW = n.fillPct || 0; const detail = n.lastDetail ? truncate(n.lastDetail, 45) : ''; const toolStr = n.toolCalls > 0 ? ` [${n.toolCalls} calls]` : ''; html += `