v0.15.2: ES6 module refactor, 2-row layout, dashboard test, PA routing fix

Frontend refactored to ES6 modules (no bundler):
  js/main.js    — entry point, wires all modules
  js/auth.js    — OIDC login, token management
  js/ws.js      — /ws, /ws/test, /ws/trace connections + HUD handler
  js/chat.js    — messages, send, streaming
  js/graph.js   — Cytoscape visualization + animation
  js/trace.js   — trace panel
  js/dashboard.js — workspace controls rendering
  js/awareness.js — state panel, sensors, meters
  js/tests.js   — test status display
  js/util.js    — shared utilities

New 2-row layout:
  Top:    test status | connection status
  Middle: Workspace | Node Details | Graph
  Bottom: Chat | Awareness | Trace

PA routing: routes ALL tool requests to expert (DB, UI, buttons, machines)
Dashboard integration test: 15/15

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nico 2026-03-29 17:58:47 +02:00
parent fda0d7cfce
commit 3a9c2795cf
13 changed files with 988 additions and 165 deletions

View File

@ -46,8 +46,10 @@ Output ONLY valid JSON:
Rules:
- expert=none ONLY for social chat (hi, thanks, bye, how are you)
- ANY request to create, build, show, query, investigate, count, list, describe route to expert
- The job must be fully self-contained. Include relevant facts from memory.
- ANY request to create, build, show, query, investigate, count, list, describe, summarize route to expert
- The job MUST be fully self-contained. The expert has NO history.
- Include relevant facts from memory AND conversation context in the job.
- For summaries/reports: include the key topics, findings, and actions from the conversation in the job so the expert can write a proper summary.
- thinking_message: natural, in user's language. e.g. "Moment, ich schaue nach..."
- If the user mentions data, tables, customers, devices, buttons, counters expert
- When unsure which expert: pick the one whose domain matches best
@ -94,15 +96,15 @@ Rules:
]
# Summarize recent history (PA sees full context)
recent = history[-12:]
recent = history[-16:]
if recent:
lines = []
for msg in recent:
role = msg.get("role", "?")
content = msg.get("content", "")[:100]
content = msg.get("content", "")[:200]
lines.append(f" {role}: {content}")
messages.append({"role": "user", "content": "Recent conversation:\n" + "\n".join(lines)})
messages.append({"role": "assistant", "content": "OK, I have the context."})
messages.append({"role": "assistant", "content": "OK, I have the context. I will include relevant details in the job description."})
a = command.analysis
messages.append({"role": "user",

View File

@ -4,77 +4,91 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>cog</title>
<link rel="stylesheet" href="/static/style.css?v=14.5">
<link rel="stylesheet" href="/static/style.css?v=15">
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.28.1/cytoscape.min.js"></script>
<script src="https://unpkg.com/webcola@3.4.0/WebCola/cola.min.js"></script>
<script src="https://unpkg.com/cytoscape-cola@2.5.1/cytoscape-cola.js"></script>
</head>
<body>
<!-- Top bar -->
<div id="top-bar">
<h1>cog</h1>
<div id="status">disconnected</div>
<div id="test-status"></div>
<div style="flex:1"></div>
<div id="status">disconnected</div>
</div>
<div id="node-metrics">
<div class="node-meter" id="meter-input"><span class="nm-label">input</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
<div class="node-meter" id="meter-thinker"><span class="nm-label">thinker</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
<div class="node-meter" id="meter-output"><span class="nm-label">output</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
<div class="node-meter" id="meter-memorizer"><span class="nm-label">memorizer</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
<div class="node-meter" id="meter-ui"><span class="nm-label">ui</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
<div class="node-meter" id="meter-sensor"><span class="nm-label">sensor</span><span class="nm-text" style="flex:1"></span></div>
</div>
<div id="pipeline-graph">
<div id="graph-controls">
<button onclick="toggleDrag()" id="btn-drag" title="Toggle node dragging">drag: on</button>
<button onclick="togglePan()" id="btn-pan" title="Toggle viewport panning">pan: on</button>
<button onclick="adjustCola('spacing', -5)" title="Tighter">tight</button>
<button onclick="adjustCola('spacing', 5)" title="Looser">loose</button>
<button onclick="adjustCola('strength', -1)" title="Weaker edges">weak</button>
<button onclick="adjustCola('strength', 1)" title="Stronger edges">strong</button>
<button onclick="cy && cy.fit(10)" title="Fit to view">fit</button>
<button onclick="copyGraphConfig()" id="btn-copy" title="Copy full settings JSON">copy</button>
<!-- Middle row: Workspace | Node Detail | Graph -->
<div id="middle-row">
<div class="panel workspace-panel">
<div class="panel-header work-h">Workspace</div>
<div id="workspace-body"><span class="aw-empty">no controls</span></div>
</div>
<div class="panel detail-panel">
<div class="panel-header detail-h">Nodes</div>
<div id="node-metrics">
<div class="node-meter" id="meter-input"><span class="nm-label">input</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
<div class="node-meter" id="meter-director_v2"><span class="nm-label">director</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
<div class="node-meter" id="meter-pa_v1"><span class="nm-label">PA</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
<div class="node-meter" id="meter-thinker"><span class="nm-label">thinker</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
<div class="node-meter" id="meter-eras_expert"><span class="nm-label">eras</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
<div class="node-meter" id="meter-output"><span class="nm-label">output</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
<div class="node-meter" id="meter-memorizer"><span class="nm-label">memo</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
<div class="node-meter" id="meter-interpreter"><span class="nm-label">interp</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
<div class="node-meter" id="meter-sensor"><span class="nm-label">sensor</span><span class="nm-text" style="flex:1"></span></div>
</div>
</div>
<div class="panel graph-panel">
<div class="panel-header graph-h">Graph
<span class="graph-btns">
<button onclick="toggleDrag()" id="btn-drag" title="Drag">drag</button>
<button onclick="togglePan()" id="btn-pan" title="Pan">pan</button>
</span>
</div>
<div id="pipeline-graph"></div>
</div>
</div>
<div id="main">
<!-- Bottom row: Chat | Awareness | Trace -->
<div id="bottom-row">
<div class="panel chat-panel">
<div class="panel-header chat-h">Chat</div>
<div id="messages"></div>
<div id="input-bar">
<input id="input" placeholder="Type a message..." autocomplete="off">
<button onclick="send()">Send</button>
<button onclick="clearSession()" class="btn-clear" title="Clear session">&#x2715;</button>
</div>
</div>
<div class="panel awareness-panel">
<div class="panel-header aware-h">Awareness</div>
<div id="awareness">
<section id="awareness-state" class="aw-section">
<section class="aw-section">
<h3 class="aw-title">State</h3>
<div class="aw-body" id="aw-state-body"><span class="aw-empty">waiting for data...</span></div>
<div class="aw-body" id="aw-state-body"><span class="aw-empty">waiting...</span></div>
</section>
<section id="awareness-sensors" class="aw-section">
<section class="aw-section">
<h3 class="aw-title">Sensors</h3>
<div class="aw-body" id="aw-sensors-body"><span class="aw-empty">waiting for tick...</span></div>
</section>
<section id="awareness-processes" class="aw-section">
<h3 class="aw-title">Processes</h3>
<div class="aw-body" id="aw-proc-body"><span class="aw-empty">idle</span></div>
</section>
<section id="awareness-controls" class="aw-section">
<h3 class="aw-title">Workspace</h3>
<div class="aw-body" id="aw-ctrl-body"><span class="aw-empty">no controls</span></div>
<div class="aw-body" id="aw-sensor-body"><span class="aw-empty">waiting...</span></div>
</section>
</div>
</div>
<div class="panel">
<div class="panel trace-panel">
<div class="panel-header trace-h">Trace</div>
<div id="trace"></div>
</div>
</div>
<script src="/static/app.js?v=14.5"></script>
<!-- Login overlay -->
<div id="login-overlay" style="display:none">
<div class="login-card">
<h2>cog</h2>
<p>Please log in to continue</p>
<button onclick="startLogin()">Log in with Zitadel</button>
</div>
</div>
<script type="module" src="/static/js/main.js?v=15"></script>
</body>
</html>

88
static/js/auth.js Normal file
View File

@ -0,0 +1,88 @@
/** Authentication: OIDC login, token management. */
export let authToken = localStorage.getItem('cog_token');
export let authConfig = null;
let _authFailed = false;
export function isAuthFailed() { return _authFailed; }
export function setAuthFailed(v) { _authFailed = v; }
export async function initAuth(onReady) {
try {
const r = await fetch('/auth/config');
authConfig = await r.json();
} catch (e) {
authConfig = { enabled: false };
}
if (!authConfig.enabled) { onReady(); return; }
// Check for OIDC callback
const params = new URLSearchParams(location.search);
if (params.has('code')) {
// Exchange code for token (PKCE)
const code = params.get('code');
const verifier = sessionStorage.getItem('cog_pkce_verifier');
try {
const tokenResp = await fetch(authConfig.issuer + '/oauth/v2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: location.origin + '/callback',
client_id: authConfig.clientId,
code_verifier: verifier || '',
}),
});
const tokenData = await tokenResp.json();
if (tokenData.id_token) {
authToken = tokenData.id_token;
localStorage.setItem('cog_token', authToken);
if (tokenData.access_token) {
localStorage.setItem('cog_access_token', tokenData.access_token);
}
}
} catch (e) { console.error('token exchange failed', e); }
history.replaceState(null, '', '/');
}
if (authToken) {
onReady();
} else {
showLogin();
}
}
export function showLogin() {
const el = document.getElementById('login-overlay');
if (el) el.style.display = 'flex';
}
export async function startLogin() {
if (!authConfig) return;
const verifier = randomString(64);
sessionStorage.setItem('cog_pkce_verifier', verifier);
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const params = new URLSearchParams({
response_type: 'code',
client_id: authConfig.clientId,
redirect_uri: location.origin + '/callback',
scope: 'openid profile email',
code_challenge: challenge,
code_challenge_method: 'S256',
prompt: 'login',
});
location.href = authConfig.issuer + '/oauth/v2/authorize?' + params;
}
function randomString(len) {
const arr = new Uint8Array(len);
crypto.getRandomValues(arr);
return Array.from(arr, b => b.toString(36).slice(-1)).join('').slice(0, len);
}

57
static/js/awareness.js Normal file
View File

@ -0,0 +1,57 @@
/** Awareness panel: memorizer state, sensor readings, node meters. */
import { esc, truncate } from './util.js';
let _sensorReadings = {};
export function updateAwarenessState(state) {
const body = document.getElementById('aw-state-body');
if (!body) return;
const display = [
['user', state.user_name],
['mood', state.user_mood],
['topic', state.topic],
['lang', state.language],
['style', state.style_hint],
['situation', state.situation],
];
const facts = state.facts || [];
const history = state.topic_history || [];
let html = display.map(([k, v]) =>
`<div class="aw-row"><span class="aw-key">${esc(k)}</span><span class="aw-val">${esc(v || 'null')}</span></div>`
).join('');
if (facts.length) {
html += '<div class="aw-row"><span class="aw-key">facts</span><span class="aw-val">'
+ facts.map(f => esc(truncate(f, 40))).join('<br>') + '</span></div>';
}
if (history.length) {
html += '<div class="aw-row"><span class="aw-key">topics</span><span class="aw-val">'
+ history.map(t => esc(truncate(t, 25))).join(', ') + '</span></div>';
}
body.innerHTML = html;
}
export function updateAwarenessSensors(tick, deltas) {
const body = document.getElementById('aw-sensor-body');
if (!body) return;
for (const [k, v] of Object.entries(deltas)) {
_sensorReadings[k] = v;
}
let html = `<div class="aw-row"><span class="aw-key">tick</span><span class="aw-val">#${tick}</span></div>`;
for (const [k, v] of Object.entries(_sensorReadings)) {
html += `<div class="aw-row"><span class="aw-key">${esc(k)}</span><span class="aw-val">${esc(String(v))}</span></div>`;
}
body.innerHTML = html;
}
export function updateMeter(node, tokens, maxTokens, fillPct) {
const meter = document.getElementById('meter-' + node);
if (!meter) return;
const bar = meter.querySelector('.nm-bar');
const text = meter.querySelector('.nm-text');
if (bar) bar.style.width = fillPct + '%';
if (text) text.textContent = `${tokens}/${maxTokens}t`;
}

62
static/js/chat.js Normal file
View File

@ -0,0 +1,62 @@
/** Chat panel: messages, send, streaming. */
import { scroll, renderMarkdown, esc } from './util.js';
import { addTrace } from './trace.js';
let msgs, inputEl, currentEl;
let _ws = null;
let _currentDashboard = [];
export function initChat() {
msgs = document.getElementById('messages');
inputEl = document.getElementById('input');
inputEl.addEventListener('keydown', e => { if (e.key === 'Enter') send(); });
}
export function setWs(ws) { _ws = ws; }
export function setDashboard(d) { _currentDashboard = d; }
export function getDashboard() { return _currentDashboard; }
export function addMsg(role, text) {
const div = document.createElement('div');
div.className = 'msg ' + role;
div.textContent = text;
msgs.appendChild(div);
scroll(msgs);
return div;
}
export function handleDelta(content) {
if (!currentEl) {
currentEl = addMsg('assistant', '');
currentEl.classList.add('streaming');
}
currentEl.textContent += content;
scroll(msgs);
}
export function handleDone() {
if (currentEl) {
currentEl.classList.remove('streaming');
currentEl.innerHTML = renderMarkdown(currentEl.textContent);
}
currentEl = null;
}
export function clearChat() {
if (msgs) msgs.innerHTML = '';
currentEl = null;
_currentDashboard = [];
}
export function send() {
const text = inputEl.value.trim();
if (!text || !_ws || _ws.readyState !== 1) return;
addMsg('user', text);
addTrace('runtime', 'user_msg', text.slice(0, 60));
_ws.send(JSON.stringify({ text, dashboard: _currentDashboard }));
inputEl.value = '';
}
// Expose for HTML onclick
window.send = send;

88
static/js/dashboard.js Normal file
View File

@ -0,0 +1,88 @@
/** Dashboard: workspace controls rendering (buttons, tables, labels, displays, machines). */
import { esc } from './util.js';
import { addTrace } from './trace.js';
import { setDashboard } from './chat.js';
let _ws = null;
export function setWs(ws) { _ws = ws; }
export function dockControls(controls) {
setDashboard(controls); // S3*: remember what's rendered
const body = document.getElementById('workspace-body');
if (!body) return;
body.innerHTML = '';
const container = document.createElement('div');
container.className = 'controls-container';
for (const ctrl of controls) {
if (ctrl.type === 'button') {
const btn = document.createElement('button');
btn.className = 'control-btn';
btn.textContent = ctrl.label;
btn.onclick = () => {
if (_ws && _ws.readyState === 1) {
_ws.send(JSON.stringify({ type: 'action', action: ctrl.action, data: ctrl.payload || ctrl.data || {} }));
addTrace('runtime', 'action', ctrl.action);
}
};
container.appendChild(btn);
} else if (ctrl.type === 'table') {
const table = document.createElement('table');
table.className = 'control-table';
if (ctrl.columns) {
const thead = document.createElement('tr');
for (const col of ctrl.columns) {
const th = document.createElement('th');
th.textContent = col;
thead.appendChild(th);
}
table.appendChild(thead);
}
for (const row of (ctrl.data || [])) {
const tr = document.createElement('tr');
if (Array.isArray(row)) {
for (const cell of row) {
const td = document.createElement('td'); td.textContent = cell; tr.appendChild(td);
}
} else if (typeof row === 'object') {
for (const col of (ctrl.columns || Object.keys(row))) {
const td = document.createElement('td'); td.textContent = row[col] ?? ''; tr.appendChild(td);
}
}
table.appendChild(tr);
}
container.appendChild(table);
} else if (ctrl.type === 'label') {
const lbl = document.createElement('div');
lbl.className = 'control-label';
lbl.innerHTML = '<span class="cl-text">' + esc(ctrl.text || '') + '</span><span class="cl-value">' + esc(String(ctrl.value ?? '')) + '</span>';
container.appendChild(lbl);
} else if (ctrl.type === 'display') {
const disp = document.createElement('div');
const dt = ctrl.display_type || 'text';
const style = ctrl.style ? ' display-' + ctrl.style : '';
disp.className = 'control-display display-' + dt + style;
if (dt === 'progress') {
const pct = Math.min(100, Math.max(0, Number(ctrl.value) || 0));
disp.innerHTML = '<span class="cd-label">' + esc(ctrl.label) + '</span>'
+ '<div class="cd-bar"><div class="cd-fill" style="width:' + pct + '%"></div></div>'
+ '<span class="cd-pct">' + pct + '%</span>';
} else if (dt === 'status') {
disp.innerHTML = '<span class="cd-icon">' + (ctrl.style === 'success' ? '\u2713' : ctrl.style === 'error' ? '\u2717' : '\u2139') + '</span>'
+ '<span class="cd-label">' + esc(ctrl.label) + '</span>';
} else {
disp.innerHTML = '<span class="cd-label">' + esc(ctrl.label) + '</span>'
+ (ctrl.value ? '<span class="cd-value">' + esc(String(ctrl.value)) + '</span>' : '');
}
container.appendChild(disp);
}
}
body.appendChild(container);
}
export function clearDashboard() {
const body = document.getElementById('workspace-body');
if (body) body.innerHTML = '';
}

238
static/js/graph.js Normal file
View File

@ -0,0 +1,238 @@
/** Pipeline graph: Cytoscape visualization + animation. */
let cy = null;
let _dragEnabled = true;
let _physicsRunning = false;
let _physicsLayout = null;
let _colaSpacing = 25;
let _colaStrengthMult = 1.0;
let _panEnabled = true;
const NODE_COLORS = {
user: '#444', input: '#f59e0b', sensor: '#3b82f6',
director: '#a855f7', pa: '#a855f7', thinker: '#f97316',
interpreter: '#06b6d4', expert_eras: '#f97316', expert_plankiste: '#f97316',
output: '#10b981', ui: '#10b981', memorizer: '#a855f7', s3_audit: '#ef4444',
};
const NODE_COLUMNS = {
user: 0, input: 1, sensor: 1,
director: 2, pa: 2, thinker: 2, interpreter: 2, s3_audit: 2,
expert_eras: 2, expert_plankiste: 2,
output: 3, ui: 3,
memorizer: 4,
};
function buildGraphElements(graph, mx, cw, mid, row1, row2) {
const elements = [];
const roles = Object.keys(graph.nodes);
elements.push({ data: { id: 'user', label: 'user' }, position: { x: mx, y: mid } });
const columns = {};
for (const role of roles) {
const col = NODE_COLUMNS[role] !== undefined ? NODE_COLUMNS[role] : 2;
if (!columns[col]) columns[col] = [];
columns[col].push(role);
}
for (const [col, colRoles] of Object.entries(columns)) {
const c = parseInt(col);
const count = colRoles.length;
for (let i = 0; i < count; i++) {
const role = colRoles[i];
const ySpread = (row2 - row1);
const y = count === 1 ? mid : row1 + (ySpread * i / (count - 1));
const label = role === 'memorizer' ? 'memo' : role.replace(/_v\d+$/, '').replace('expert_', '');
elements.push({ data: { id: role, label }, position: { x: mx + cw * c, y } });
}
}
const nodeIds = new Set(elements.map(e => e.data.id));
const cytoEdges = graph.cytoscape ? graph.cytoscape.edges : [];
if (cytoEdges.length) {
for (const edge of cytoEdges) {
const d = edge.data;
if (!nodeIds.has(d.source) || !nodeIds.has(d.target)) continue;
const edgeData = { id: d.id, source: d.source, target: d.target };
if (d.condition === 'reflex') edgeData.reflex = true;
if (d.edge_type === 'context') edgeData.ctx = true;
elements.push({ data: edgeData });
}
} else {
for (const edge of graph.edges) {
const targets = Array.isArray(edge.to) ? edge.to : [edge.to];
for (const tgt of targets) {
if (!nodeIds.has(edge.from) || !nodeIds.has(tgt)) continue;
const edgeData = { id: `e-${edge.from}-${tgt}`, source: edge.from, target: tgt };
if (edge.condition === 'reflex') edgeData.reflex = true;
if (edge.type === 'context') edgeData.ctx = true;
elements.push({ data: edgeData });
}
}
}
elements.push({ data: { id: 'e-user-input', source: 'user', target: 'input' } });
return elements;
}
export async function initGraph() {
const container = document.getElementById('pipeline-graph');
if (!container || typeof cytoscape === 'undefined') return;
const rect = container.getBoundingClientRect();
const W = rect.width || 900;
const H = rect.height || 180;
const mx = W * 0.07;
const cw = (W - mx * 2) / 4;
const row1 = H * 0.25, mid = H * 0.5, row2 = H * 0.75;
let graphElements = null;
try {
const resp = await fetch('/api/graph/active');
if (resp.ok) {
const graph = await resp.json();
graphElements = buildGraphElements(graph, mx, cw, mid, row1, row2);
}
} catch (e) {}
if (!graphElements) {
graphElements = [
{ data: { id: 'user', label: 'user' }, position: { x: mx, y: mid } },
{ data: { id: 'input', label: 'input' }, position: { x: mx + cw, y: row1 } },
{ data: { id: 'thinker', label: 'thinker' }, position: { x: mx + cw * 2, y: mid } },
{ data: { id: 'output', label: 'output' }, position: { x: mx + cw * 3, y: mid } },
];
}
cy = cytoscape({
container,
elements: graphElements,
style: [
{ selector: 'node', style: {
'label': 'data(label)', 'text-valign': 'center', 'text-halign': 'center',
'font-size': '18px', 'min-zoomed-font-size': 10,
'font-family': 'system-ui, sans-serif', 'font-weight': 700, 'color': '#aaa',
'background-color': '#181818', 'border-width': 1, 'border-opacity': 0.3,
'border-color': '#444', 'width': 48, 'height': 48,
'transition-property': 'background-color, border-color, width, height',
'transition-duration': '0.3s',
}},
...Object.entries(NODE_COLORS).map(([id, color]) => ({
selector: `#${id}`, style: { 'border-color': color, 'color': color }
})),
{ selector: '#user', style: { 'color': '#888' } },
{ selector: '#sensor', style: { 'width': 40, 'height': 40, 'font-size': '15px' } },
{ selector: 'node.active', style: {
'background-color': '#333', 'border-width': 3, 'width': 56, 'height': 56,
}},
{ selector: 'edge', style: {
'width': 1.5, 'line-color': '#333', 'target-arrow-color': '#333',
'target-arrow-shape': 'triangle', 'arrow-scale': 0.7, 'curve-style': 'bezier',
'transition-property': 'line-color, target-arrow-color, width',
'transition-duration': '0.3s',
}},
{ selector: 'edge[?reflex]', style: { 'line-style': 'dashed', 'line-dash-pattern': [4, 4], 'line-color': '#2a2a2a' } },
{ selector: 'edge[?ctx]', style: { 'line-style': 'dotted', 'line-color': '#1a1a2e', 'width': 1 } },
{ selector: 'edge.active', style: { 'line-color': '#888', 'target-arrow-color': '#888', 'width': 2.5 } },
],
layout: { name: 'preset' },
userZoomingEnabled: true, userPanningEnabled: true,
wheelSensitivity: 0.3, boxSelectionEnabled: false,
autoungrabify: false, selectionType: 'single',
});
container.addEventListener('contextmenu', e => e.stopPropagation(), true);
if (typeof cytoscapeCola !== 'undefined') cytoscape.use(cytoscapeCola);
startPhysics();
cy.on('zoom', () => {
const z = cy.zoom();
cy.nodes().style('font-size', Math.round(12 / z) + 'px');
});
}
function pulseNode(id) {
if (!cy) return;
const node = cy.getElementById(id);
if (!node.length) return;
node.addClass('active');
setTimeout(() => node.removeClass('active'), 1500);
}
function flashEdge(sourceId, targetId) {
if (!cy) return;
const edge = cy.edges().filter(e => e.data('source') === sourceId && e.data('target') === targetId);
if (!edge.length) return;
edge.addClass('active');
setTimeout(() => edge.removeClass('active'), 1000);
}
export function graphAnimate(event, node) {
if (!cy) return;
if (node && cy.getElementById(node).length) pulseNode(node);
switch (event) {
case 'perceived': pulseNode('input'); flashEdge('user', 'input'); break;
case 'decided':
if (node === 'director_v2' || node === 'director' || node === 'pa_v1') {
pulseNode(node); flashEdge(node, 'thinker');
} else {
pulseNode(node || 'thinker'); flashEdge('thinker', 'output');
}
break;
case 'routed': pulseNode('pa'); break;
case 'reflex_path': pulseNode('input'); flashEdge('input', 'output'); break;
case 'streaming': if (node === 'output') pulseNode('output'); break;
case 'controls': case 'machine_created': case 'machine_transition':
pulseNode('ui'); break;
case 'updated': pulseNode('memorizer'); flashEdge('output', 'memorizer'); break;
case 'tool_call': pulseNode(node || 'thinker'); break;
case 'tool_result':
if (cy.getElementById('interpreter').length) pulseNode('interpreter'); break;
case 'thinking': if (node) pulseNode(node); break;
case 'tick': pulseNode('sensor'); break;
}
}
export function startPhysics() {
if (!cy) return;
stopPhysics();
try {
const rect = document.getElementById('pipeline-graph').getBoundingClientRect();
_physicsLayout = cy.layout({
name: 'cola', animate: true, infinite: true, fit: false,
nodeSpacing: _colaSpacing,
edgeElasticity: e => {
const base = e.data('ctx') ? 0.1 : e.data('reflex') ? 0.2 : 0.6;
return base * _colaStrengthMult;
},
boundingBox: { x1: 0, y1: 0, w: rect.width, h: rect.height },
});
_physicsLayout.run();
_physicsRunning = true;
} catch (e) {}
}
export function stopPhysics() {
if (_physicsLayout) { try { _physicsLayout.stop(); } catch(e) {} _physicsLayout = null; }
_physicsRunning = false;
}
// Expose control functions for HTML onclick
window.adjustCola = (param, delta) => {
if (!cy) return;
if (param === 'spacing') _colaSpacing = Math.max(5, Math.min(80, _colaSpacing + delta));
else if (param === 'strength') _colaStrengthMult = Math.max(0.1, Math.min(3.0, _colaStrengthMult + delta * 0.2));
startPhysics();
};
window.toggleDrag = () => {
if (!cy) return;
_dragEnabled = !_dragEnabled;
cy.autoungrabify(!_dragEnabled);
document.getElementById('btn-drag').textContent = 'drag: ' + (_dragEnabled ? 'on' : 'off');
};
window.togglePan = () => {
if (!cy) return;
_panEnabled = !_panEnabled;
cy.userPanningEnabled(_panEnabled);
cy.userZoomingEnabled(_panEnabled);
document.getElementById('btn-pan').textContent = 'pan: ' + (_panEnabled ? 'on' : 'off');
};

34
static/js/main.js Normal file
View File

@ -0,0 +1,34 @@
/** Main entry point — wires all modules together. */
import { initAuth, authToken, startLogin } from './auth.js';
import { initTrace, addTrace, clearTrace } from './trace.js';
import { initChat, clearChat } from './chat.js';
import { clearDashboard } from './dashboard.js';
import { initGraph } from './graph.js';
import { connect } from './ws.js';
// Init on load
window.addEventListener('load', async () => {
initTrace();
initChat();
await initGraph();
await initAuth(() => connect());
});
// Clear session button
window.clearSession = async () => {
try {
const headers = { 'Content-Type': 'application/json' };
if (authToken) headers['Authorization'] = 'Bearer ' + authToken;
await fetch('/api/clear', { method: 'POST', headers });
clearChat();
clearTrace();
clearDashboard();
addTrace('runtime', 'cleared', 'session reset');
} catch (e) {
addTrace('runtime', 'error', 'clear failed: ' + e);
}
};
// Login button
window.startLogin = startLogin;

25
static/js/tests.js Normal file
View File

@ -0,0 +1,25 @@
/** Test status display. */
import { esc } from './util.js';
export function updateTestStatus(data) {
const el = document.getElementById('test-status');
if (!el) return;
const results = data.results || [];
const pass = results.filter(r => r.status === 'PASS').length;
const fail = results.filter(r => r.status === 'FAIL').length;
const done = results.length;
const expected = data.total_expected || done;
if (data.running) {
const current = data.current || '';
el.innerHTML = `<span class="ts-running">TESTING</span> `
+ `<span class="ts-pass">${done}</span>/<span>${expected}</span>`
+ (fail ? ` <span class="ts-fail">${fail}F</span>` : '')
+ ` <span style="color:#888;max-width:20rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(current)}</span>`;
} else if (done > 0) {
const allGreen = fail === 0;
el.innerHTML = `<span class="${allGreen ? 'ts-pass' : 'ts-fail'}">${pass}/${expected}</span>`
+ (fail ? ` <span class="ts-fail">${fail} failed</span>` : ' <span class="ts-pass">all green</span>');
}
}

42
static/js/trace.js Normal file
View File

@ -0,0 +1,42 @@
/** Trace panel: HUD event display. */
import { esc, scroll } from './util.js';
let traceEl;
export function initTrace() {
traceEl = document.getElementById('trace');
}
export function addTrace(node, event, text, cls, detail) {
if (!traceEl) return;
const line = document.createElement('div');
line.className = 'trace-line' + (detail ? ' expandable' : '');
const ts = new Date().toLocaleTimeString('de-DE', {
hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit',
fractionalSecondDigits: 1,
});
line.innerHTML =
'<span class="trace-ts">' + ts + '</span>' +
'<span class="trace-node ' + esc(node) + '">' + esc(node) + '</span>' +
'<span class="trace-event">' + esc(event) + '</span>' +
'<span class="trace-data' + (cls ? ' ' + cls : '') + '">' + esc(text) + '</span>';
traceEl.appendChild(line);
if (detail) {
const detailEl = document.createElement('div');
detailEl.className = 'trace-detail';
detailEl.textContent = detail;
traceEl.appendChild(detailEl);
line.addEventListener('click', () => detailEl.classList.toggle('open'));
}
scroll(traceEl);
}
export function clearTrace() {
if (traceEl) traceEl.innerHTML = '';
}

34
static/js/util.js Normal file
View File

@ -0,0 +1,34 @@
/** Shared utility functions. */
export function scroll(el) { el.scrollTop = el.scrollHeight; }
export function esc(s) {
const d = document.createElement('span');
d.textContent = s;
return d.innerHTML;
}
export function truncate(s, n) {
return s.length > n ? s.slice(0, n) + '\u2026' : s;
}
export function renderMarkdown(text) {
let html = esc(text);
// Code blocks
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) =>
'<pre class="md-code"><code>' + code.trim() + '</code></pre>');
// Inline code
html = html.replace(/`([^`]+)`/g, '<code class="md-inline">$1</code>');
// Bold
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
// Italic
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
// Bullet lists
html = html.replace(/^[\*\-]\s+(.+)$/gm, '<li>$1</li>');
html = html.replace(/(<li>.*<\/li>\n?)+/g, m => '<ul>' + m + '</ul>');
// Numbered lists
html = html.replace(/^\d+\.\s+(.+)$/gm, '<li>$1</li>');
// Line breaks (but not inside pre/code)
html = html.replace(/\n/g, '<br>');
return html;
}

175
static/js/ws.js Normal file
View File

@ -0,0 +1,175 @@
/** WebSocket connections: /ws (chat), /ws/test, /ws/trace. */
import { authToken, isAuthFailed, setAuthFailed, showLogin } from './auth.js';
import { addTrace } from './trace.js';
import { handleDelta, handleDone, setWs as setChatWs } from './chat.js';
import { dockControls, setWs as setDashWs } from './dashboard.js';
import { graphAnimate } from './graph.js';
import { updateMeter, updateAwarenessState, updateAwarenessSensors } from './awareness.js';
import { updateTestStatus } from './tests.js';
import { truncate, esc } from './util.js';
let ws, wsTest, wsTrace;
let _testPollInterval = null;
let _lastTestResultCount = 0;
export function connect() {
if (isAuthFailed()) return;
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
let wsUrl = proto + '//' + location.host + '/ws';
if (authToken) {
const accessToken = localStorage.getItem('cog_access_token') || '';
wsUrl += '?token=' + encodeURIComponent(authToken) + '&access_token=' + encodeURIComponent(accessToken);
}
ws = new WebSocket(wsUrl);
ws.onopen = () => {
document.getElementById('status').textContent = 'connected';
document.getElementById('status').style.color = '#22c55e';
addTrace('runtime', 'connected', 'ws open');
setChatWs(ws);
setDashWs(ws);
connectDebugSockets();
};
ws.onerror = () => {};
ws.onclose = (e) => {
if (e.code === 4001 || e.code === 1006) {
setAuthFailed(true);
localStorage.removeItem('cog_token');
localStorage.removeItem('cog_access_token');
document.getElementById('status').textContent = 'session expired';
document.getElementById('status').style.color = '#ef4444';
showLogin();
return;
}
document.getElementById('status').textContent = 'disconnected';
document.getElementById('status').style.color = '#666';
addTrace('runtime', 'disconnected', 'ws closed');
setTimeout(connect, 2000);
};
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.type === 'hud') {
handleHud(data);
} else if (data.type === 'delta') {
handleDelta(data.content);
} else if (data.type === 'done') {
handleDone();
} else if (data.type === 'controls') {
dockControls(data.controls);
} else if (data.type === 'cleared') {
addTrace('runtime', 'cleared', 'session reset');
}
};
}
function connectDebugSockets() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
const base = proto + '//' + location.host;
const tokenParam = authToken ? '?token=' + encodeURIComponent(authToken) : '';
if (!wsTest || wsTest.readyState > 1) {
wsTest = new WebSocket(base + '/ws/test' + tokenParam);
wsTest.onopen = () => addTrace('runtime', 'ws/test', 'connected');
wsTest.onclose = () => setTimeout(connectDebugSockets, 3000);
wsTest.onerror = () => {};
wsTest.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.type === 'test_status') updateTestStatus(data);
};
}
if (!wsTrace || wsTrace.readyState > 1) {
wsTrace = new WebSocket(base + '/ws/trace' + tokenParam);
wsTrace.onopen = () => addTrace('runtime', 'ws/trace', 'connected');
wsTrace.onclose = () => {};
wsTrace.onerror = () => {};
wsTrace.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.event === 'frame_trace' && data.trace) {
const t = data.trace;
const frames = t.frames || [];
const summary = frames.map(f => `F${f.frame}:${f.node}(${f.duration_ms}ms)`).join(' > ');
addTrace('frame_engine', 'trace', `${t.path} ${t.total_frames}F ${t.total_ms}ms`, 'instruction',
summary + '\n' + JSON.stringify(t, null, 2));
} else if (data.node && data.event) {
handleHud(data);
}
};
}
// Polling fallback for test status
if (!_testPollInterval) {
_testPollInterval = setInterval(async () => {
try {
const headers = authToken ? { 'Authorization': 'Bearer ' + authToken } : {};
const r = await fetch('/api/test/status', { headers });
const data = await r.json();
const count = (data.results || []).length;
if (count !== _lastTestResultCount || data.running) {
_lastTestResultCount = count;
updateTestStatus(data);
}
} catch (e) {}
}, 500);
}
}
function handleHud(data) {
const node = data.node || 'unknown';
const event = data.event || '';
graphAnimate(event, node);
if (event === 'context') {
const count = (data.messages || []).length;
const tokenInfo = data.tokens ? ` [${data.tokens}/${data.max_tokens}t ${data.fill_pct}%]` : '';
const summary = count + ' msgs' + tokenInfo;
const detail = (data.messages || []).map((m, i) =>
i + ' [' + m.role + '] ' + m.content
).join('\n');
addTrace(node, 'context', summary, 'context', detail);
if (data.tokens !== undefined) updateMeter(node, data.tokens, data.max_tokens, data.fill_pct);
} else if (event === 'perceived') {
const text = data.analysis
? Object.entries(data.analysis).map(([k,v]) => k + '=' + v).join(' ')
: '';
addTrace(node, 'perceived', text, 'instruction', data.analysis ? JSON.stringify(data.analysis, null, 2) : null);
} else if (event === 'decided' || event === 'routed') {
addTrace(node, event, data.instruction || data.goal || data.job || '', 'instruction');
} else if (event === 'updated' && data.state) {
const pairs = Object.entries(data.state).map(([k, v]) => {
const val = Array.isArray(v) ? v.join(', ') : (v || 'null');
return k + '=' + truncate(val, 25);
}).join(' ');
addTrace(node, 'state', pairs, 'state', JSON.stringify(data.state, null, 2));
updateAwarenessState(data.state);
} else if (event === 'tool_call') {
const tool = data.tool || '';
const argsStr = data.args ? JSON.stringify(data.args) : '';
addTrace(node, 'tool_call', tool + ' ' + truncate(argsStr, 60), '', argsStr);
} else if (event === 'tool_result') {
const output = data.output || '';
addTrace(node, 'tool_result', truncate(output, 80), '', output);
} else if (event === 'interpreted') {
addTrace(node, 'interpreted', data.summary || '', 'instruction');
} else if (event === 'tick') {
updateAwarenessSensors(data.tick || 0, data.deltas || {});
} else if (event === 'started' || event === 'stopped') {
addTrace(node, event, '');
} else {
addTrace(node, event, '', '', JSON.stringify(data, null, 2));
}
}

View File

@ -2,172 +2,136 @@
body { font-family: system-ui, sans-serif; background: #0a0a0a; color: #e0e0e0; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
/* Top bar */
#top-bar { display: flex; align-items: center; gap: 1rem; padding: 0.4rem 1rem; background: #111; border-bottom: 1px solid #222; }
#top-bar { display: flex; align-items: center; gap: 1rem; padding: 0.4rem 1rem; background: #111; border-bottom: 1px solid #222; flex-shrink: 0; }
#top-bar h1 { font-size: 0.85rem; font-weight: 600; color: #888; }
#status { font-size: 0.75rem; color: #666; }
#test-status { margin-left: auto; font-size: 0.7rem; font-family: monospace; display: flex; gap: 1rem; align-items: center; }
#test-status { font-size: 0.7rem; font-family: monospace; display: flex; gap: 1rem; align-items: center; }
#test-status .ts-running { color: #f59e0b; animation: pulse-text 1s infinite; }
#test-status .ts-pass { color: #22c55e; }
#test-status .ts-fail { color: #ef4444; }
#test-status .ts-idle { color: #444; }
@keyframes pulse-text { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
/* Node metrics bar */
#node-metrics { display: flex; gap: 1px; padding: 0; background: #111; border-bottom: 1px solid #222; overflow: hidden; flex-shrink: 0; }
.node-meter { flex: 1; display: flex; align-items: center; gap: 0.4rem; padding: 0.25rem 0.6rem; background: #0a0a0a; }
.nm-label { font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; min-width: 4.5rem; }
#meter-input .nm-label { color: #f59e0b; }
#meter-output .nm-label { color: #34d399; }
#meter-memorizer .nm-label { color: #c084fc; }
#meter-thinker .nm-label { color: #fb923c; }
#meter-ui .nm-label { color: #34d399; }
#meter-sensor .nm-label { color: #60a5fa; }
.nm-bar { flex: 1; height: 6px; background: #1a1a1a; border-radius: 3px; overflow: hidden; }
.nm-fill { height: 100%; width: 0%; border-radius: 3px; transition: width 0.3s, background-color 0.3s; background: #333; }
.nm-text { font-size: 0.6rem; color: #555; min-width: 5rem; text-align: right; font-family: monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Pipeline graph */
#pipeline-graph { height: 180px; min-height: 180px; flex-shrink: 0; border-bottom: 1px solid #333; background: #0d0d0d; position: relative; }
#graph-controls { position: absolute; top: 4px; right: 6px; z-index: 999; display: flex; gap: 3px; pointer-events: auto; }
#graph-controls button { padding: 2px 6px; font-size: 0.6rem; font-family: monospace; background: #1a1a1a; color: #666; border: 1px solid #333; border-radius: 3px; cursor: pointer; position: relative; z-index: 999; }
#graph-controls button:hover { color: #ccc; border-color: #555; }
/* Overlay scrollbars — no reflow, float over content */
#messages, #awareness, #trace {
overflow-y: overlay; /* Chromium: scrollbar overlays content, no space taken */
scrollbar-width: thin; /* Firefox fallback */
scrollbar-color: rgba(255,255,255,0.12) transparent;
}
#messages::-webkit-scrollbar, #awareness::-webkit-scrollbar, #trace::-webkit-scrollbar { width: 5px; }
#messages::-webkit-scrollbar-track, #awareness::-webkit-scrollbar-track, #trace::-webkit-scrollbar-track { background: transparent; }
#messages::-webkit-scrollbar-thumb, #awareness::-webkit-scrollbar-thumb, #trace::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
#messages::-webkit-scrollbar-thumb:hover, #awareness::-webkit-scrollbar-thumb:hover, #trace::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.25); }
/* Three-column layout: chat | awareness | trace */
#main { flex: 1; display: grid; grid-template-columns: 1fr 1fr 2fr; gap: 1px; background: #222; overflow: hidden; min-height: 0; }
/* === Two-row layout === */
/* Middle row: workspace | node detail | graph */
#middle-row { display: grid; grid-template-columns: 1fr 200px 2fr; gap: 1px; background: #222; flex: 1; min-height: 0; }
/* Bottom row: chat | awareness | trace */
#bottom-row { display: grid; grid-template-columns: 1fr 1fr 2fr; gap: 1px; background: #222; flex: 1; min-height: 0; }
/* Panels */
.panel { background: #0a0a0a; display: flex; flex-direction: column; overflow: hidden; }
.panel-header { padding: 0.5rem 0.75rem; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid #222; flex-shrink: 0; }
.panel-header { padding: 0.4rem 0.75rem; font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid #222; flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; }
.panel-header.chat-h { color: #60a5fa; background: #0a1628; }
.panel-header.trace-h { color: #a78bfa; background: #120a1e; }
.panel-header.aware-h { color: #34d399; background: #0a1e14; }
.panel-header.work-h { color: #f59e0b; background: #1a1408; }
.panel-header.detail-h { color: #fb923c; background: #1a1008; }
.panel-header.graph-h { color: #888; background: #111; }
.graph-btns { display: flex; gap: 3px; }
.graph-btns button { padding: 1px 5px; font-size: 0.55rem; font-family: monospace; background: #1a1a1a; color: #666; border: 1px solid #333; border-radius: 3px; cursor: pointer; }
.graph-btns button:hover { color: #ccc; border-color: #555; }
/* Workspace panel */
.workspace-panel { display: flex; flex-direction: column; }
#workspace-body { flex: 1; overflow-y: auto; padding: 0.5rem; }
/* Node detail / metrics */
.detail-panel { display: flex; flex-direction: column; }
#node-metrics { flex: 1; overflow-y: auto; padding: 0.3rem; display: flex; flex-direction: column; gap: 1px; }
.node-meter { display: flex; align-items: center; gap: 0.3rem; padding: 0.2rem 0.4rem; background: #111; border-radius: 2px; }
.nm-label { font-size: 0.6rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; min-width: 3.5rem; color: #888; }
.nm-bar { flex: 1; height: 5px; background: #1a1a1a; border-radius: 3px; overflow: hidden; }
.nm-fill { height: 100%; width: 0%; border-radius: 3px; transition: width 0.3s; background: #333; }
.nm-text { font-size: 0.55rem; color: #555; min-width: 3rem; text-align: right; font-family: monospace; }
/* Graph panel */
.graph-panel { display: flex; flex-direction: column; }
#pipeline-graph { flex: 1; background: #0d0d0d; min-height: 100px; }
/* Overlay scrollbars */
#messages, #awareness, #trace, #workspace-body, #node-metrics {
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,0.12) transparent;
}
#messages::-webkit-scrollbar, #trace::-webkit-scrollbar, #workspace-body::-webkit-scrollbar { width: 5px; }
#messages::-webkit-scrollbar-track, #trace::-webkit-scrollbar-track { background: transparent; }
#messages::-webkit-scrollbar-thumb, #trace::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
/* Chat panel */
.chat-panel { display: flex; flex-direction: column; }
#messages { flex: 1; overflow-y: auto; padding: 0.5rem; display: flex; flex-direction: column; gap: 0.4rem; }
.msg { max-width: 90%; padding: 0.5rem 0.75rem; border-radius: 0.6rem; line-height: 1.4; white-space: pre-wrap; font-size: 0.9rem; }
.msg { max-width: 90%; padding: 0.5rem 0.75rem; border-radius: 0.6rem; line-height: 1.4; white-space: pre-wrap; font-size: 0.85rem; }
.msg.user { align-self: flex-end; background: #2563eb; color: white; }
.msg.assistant { align-self: flex-start; background: #1e1e1e; border: 1px solid #333; }
.msg.assistant.streaming { border-color: #2563eb; }
.msg.assistant h2, .msg.assistant h3, .msg.assistant h4 { margin: 0.3rem 0 0.2rem; color: #e0e0e0; }
.msg.assistant h2 { font-size: 1rem; }
.msg.assistant h3 { font-size: 0.95rem; }
.msg.assistant h4 { font-size: 0.9rem; }
.msg.assistant strong { color: #fff; }
.msg.assistant code { background: #2a2a3a; padding: 0.1rem 0.3rem; border-radius: 0.2rem; font-size: 0.85em; }
.msg.assistant pre { background: #1a1a2a; padding: 0.5rem; border-radius: 0.3rem; margin: 0.3rem 0; overflow-x: auto; }
.msg.assistant pre code { background: none; padding: 0; }
.msg.assistant ul { margin: 0.2rem 0; padding-left: 1.2rem; }
.msg.assistant li { margin: 0.1rem 0; }
/* Input bar */
#input-bar { display: flex; gap: 0.5rem; padding: 0.75rem; background: #111; border-top: 1px solid #222; }
#input { flex: 1; padding: 0.5rem 0.75rem; background: #1a1a1a; color: #e0e0e0; border: 1px solid #333; border-radius: 0.4rem; font-size: 0.9rem; outline: none; }
#input-bar { display: flex; gap: 0.5rem; padding: 0.5rem; background: #111; border-top: 1px solid #222; }
#input { flex: 1; padding: 0.4rem 0.6rem; background: #1a1a1a; color: #e0e0e0; border: 1px solid #333; border-radius: 0.4rem; font-size: 0.85rem; outline: none; }
#input:focus { border-color: #2563eb; }
button { padding: 0.5rem 1rem; background: #2563eb; color: white; border: none; border-radius: 0.4rem; cursor: pointer; font-size: 0.9rem; }
button { padding: 0.4rem 0.8rem; background: #2563eb; color: white; border: none; border-radius: 0.4rem; cursor: pointer; font-size: 0.8rem; }
button:hover { background: #1d4ed8; }
.btn-clear { background: #333; padding: 0.4rem 0.5rem; font-size: 0.75rem; }
.btn-clear:hover { background: #ef4444; }
/* Trace panel */
#trace { flex: 1; overflow-y: auto; padding: 0.5rem; font-family: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace; font-size: 0.72rem; line-height: 1.5; }
.trace-line { padding: 0.15rem 0.4rem; border-bottom: 1px solid #111; display: flex; gap: 0.5rem; align-items: baseline; }
.trace-panel { display: flex; flex-direction: column; }
#trace { flex: 1; overflow-y: auto; padding: 0.4rem; font-family: 'JetBrains Mono', 'Cascadia Code', monospace; font-size: 0.68rem; line-height: 1.5; }
.trace-line { padding: 0.12rem 0.3rem; border-bottom: 1px solid #111; display: flex; gap: 0.4rem; align-items: baseline; }
.trace-line:hover { background: #1a1a2e; }
.trace-ts { color: #555; flex-shrink: 0; min-width: 5rem; }
.trace-node { font-weight: 700; flex-shrink: 0; min-width: 6rem; }
.trace-ts { color: #555; flex-shrink: 0; min-width: 4.5rem; }
.trace-node { font-weight: 700; flex-shrink: 0; min-width: 5.5rem; }
.trace-node.input { color: #f59e0b; }
.trace-node.output { color: #34d399; }
.trace-node.memorizer { color: #c084fc; }
.trace-node.thinker { color: #fb923c; }
.trace-node.runtime { color: #60a5fa; }
.trace-node.process { color: #f97316; }
.trace-node.thinker, .trace-node.thinker_v2 { color: #fb923c; }
.trace-node.director_v2, .trace-node.pa_v1, .trace-node.pa { color: #a855f7; }
.trace-node.eras_expert, .trace-node.expert_eras { color: #f97316; }
.trace-node.runtime, .trace-node.frame_engine { color: #60a5fa; }
.trace-node.ui { color: #34d399; }
.trace-node.sensor { color: #60a5fa; }
.trace-event { color: #888; flex-shrink: 0; min-width: 6rem; }
.trace-event { color: #888; flex-shrink: 0; min-width: 5.5rem; }
.trace-data { color: #ccc; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.trace-data.instruction { color: #22c55e; }
.trace-data.error { color: #ef4444; }
.trace-data.state { color: #c084fc; }
.trace-data.context { color: #666; }
/* UI Controls */
.controls-container { padding: 0.4rem 0; display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: flex-start; }
.control-btn { padding: 0.35rem 0.75rem; background: #1e3a5f; color: #60a5fa; border: 1px solid #2563eb; border-radius: 0.3rem; cursor: pointer; font-size: 0.8rem; }
.control-btn:hover { background: #2563eb; color: white; }
.control-label { display: flex; justify-content: space-between; align-items: center; padding: 0.3rem 0.5rem; background: #1a1a2e; border-radius: 0.3rem; font-size: 0.8rem; }
.cl-text { color: #888; }
.cl-value { color: #e0e0e0; font-weight: 600; font-family: monospace; }
.control-table { width: 100%; border-collapse: collapse; font-size: 0.8rem; background: #111; border-radius: 0.3rem; overflow: hidden; }
.control-table th { background: #1a1a2e; color: #a78bfa; padding: 0.3rem 0.5rem; text-align: left; font-weight: 600; border-bottom: 1px solid #333; }
.control-table td { padding: 0.25rem 0.5rem; border-bottom: 1px solid #1a1a1a; color: #ccc; }
.control-table tr:hover td { background: #1a1a2e; }
.process-card { background: #111; border: 1px solid #333; border-radius: 0.3rem; padding: 0.4rem 0.6rem; font-size: 0.75rem; width: 100%; }
.process-card.running { border-color: #f59e0b; }
.process-card.done { border-color: #22c55e; }
.process-card.failed { border-color: #ef4444; }
.pc-tool { font-weight: 700; color: #fb923c; margin-right: 0.5rem; }
.pc-status { color: #888; margin-right: 0.5rem; }
.pc-stop { padding: 0.15rem 0.4rem; background: #ef4444; color: white; border: none; border-radius: 0.2rem; cursor: pointer; font-size: 0.7rem; }
.pc-code { margin-top: 0.3rem; color: #666; white-space: pre-wrap; max-height: 4rem; overflow-y: auto; font-size: 0.7rem; }
.pc-output { margin-top: 0.3rem; color: #888; white-space: pre-wrap; max-height: 8rem; overflow-y: auto; }
.trace-line.expandable { cursor: pointer; }
.trace-detail { display: none; padding: 0.2rem 0.3rem 0.2rem 10rem; font-size: 0.6rem; color: #777; white-space: pre-wrap; word-break: break-all; max-height: 8rem; overflow-y: auto; background: #0d0d14; border-bottom: 1px solid #1a1a2e; }
.trace-detail.open { display: block; }
/* Awareness panel */
.panel-header.aware-h { color: #34d399; background: #0a1e14; }
.awareness-panel { display: flex; flex-direction: column; }
#awareness { flex: 1; overflow-y: auto; padding: 0.5rem; display: flex; flex-direction: column; gap: 0.5rem; }
.aw-section { background: #111; border: 1px solid #1a1a1a; border-radius: 0.4rem; overflow: hidden; }
.aw-title { font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.3rem 0.5rem; background: #0d0d14; color: #666; border-bottom: 1px solid #1a1a1a; }
.aw-body { padding: 0.4rem 0.5rem; font-size: 0.78rem; line-height: 1.5; }
.aw-empty { color: #444; font-style: italic; font-size: 0.72rem; }
#awareness { flex: 1; overflow-y: auto; padding: 0.4rem; display: flex; flex-direction: column; gap: 0.4rem; }
.aw-section { background: #111; border: 1px solid #1a1a1a; border-radius: 0.3rem; overflow: hidden; }
.aw-title { font-size: 0.6rem; font-weight: 700; text-transform: uppercase; padding: 0.25rem 0.4rem; background: #0d0d14; color: #666; border-bottom: 1px solid #1a1a1a; }
.aw-body { padding: 0.3rem 0.4rem; font-size: 0.72rem; line-height: 1.5; }
.aw-empty { color: #444; font-style: italic; font-size: 0.68rem; }
.aw-row { display: flex; justify-content: space-between; padding: 0.08rem 0; }
.aw-key { color: #888; font-size: 0.65rem; }
.aw-val { color: #e0e0e0; font-size: 0.7rem; font-weight: 500; }
/* State card */
.aw-row { display: flex; justify-content: space-between; padding: 0.1rem 0; }
.aw-key { color: #888; font-size: 0.7rem; }
.aw-val { color: #e0e0e0; font-size: 0.75rem; font-weight: 500; }
.aw-val.mood-happy { color: #22c55e; }
.aw-val.mood-frustrated { color: #ef4444; }
.aw-val.mood-playful { color: #f59e0b; }
.aw-val.mood-neutral { color: #888; }
/* UI Controls (workspace) */
.controls-container { padding: 0.3rem 0; display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: flex-start; }
.control-btn { padding: 0.3rem 0.6rem; background: #1e3a5f; color: #60a5fa; border: 1px solid #2563eb; border-radius: 0.3rem; cursor: pointer; font-size: 0.75rem; }
.control-btn:hover { background: #2563eb; color: white; }
.control-label { display: flex; justify-content: space-between; align-items: center; padding: 0.25rem 0.4rem; background: #1a1a2e; border-radius: 0.3rem; font-size: 0.75rem; width: 100%; }
.cl-text { color: #888; }
.cl-value { color: #e0e0e0; font-weight: 600; font-family: monospace; }
.control-table { width: 100%; border-collapse: collapse; font-size: 0.72rem; background: #111; border-radius: 0.3rem; overflow: hidden; }
.control-table th { background: #1a1a2e; color: #a78bfa; padding: 0.25rem 0.4rem; text-align: left; font-weight: 600; border-bottom: 1px solid #333; }
.control-table td { padding: 0.2rem 0.4rem; border-bottom: 1px solid #1a1a1a; color: #ccc; }
.control-table tr:hover td { background: #1a1a2e; }
.control-display { padding: 0.25rem 0.4rem; font-size: 0.75rem; }
.cd-label { color: #888; }
.cd-value { color: #e0e0e0; margin-left: 0.5rem; }
/* Facts list */
.aw-facts { list-style: none; padding: 0; margin: 0.2rem 0 0 0; }
.aw-facts li { font-size: 0.7rem; color: #999; padding: 0.1rem 0; border-top: 1px solid #1a1a1a; }
.aw-facts li::before { content: "- "; color: #555; }
/* Sensor readings */
.aw-sensor { display: flex; align-items: center; gap: 0.4rem; padding: 0.15rem 0; }
.aw-sensor-name { color: #60a5fa; font-size: 0.7rem; font-weight: 600; min-width: 4rem; }
.aw-sensor-val { color: #e0e0e0; font-size: 0.75rem; }
.aw-sensor-age { color: #444; font-size: 0.65rem; }
/* Awareness processes */
.aw-proc { background: #0d0d14; border: 1px solid #333; border-radius: 0.3rem; padding: 0.3rem 0.5rem; margin-bottom: 0.3rem; }
.aw-proc.running { border-color: #f59e0b; }
.aw-proc.done { border-color: #22c55e; }
.aw-proc.failed { border-color: #ef4444; }
.aw-proc-header { display: flex; align-items: center; gap: 0.4rem; font-size: 0.72rem; }
.aw-proc-tool { font-weight: 700; color: #fb923c; }
.aw-proc-status { color: #888; }
.aw-proc-stop { padding: 0.1rem 0.3rem; background: #ef4444; color: white; border: none; border-radius: 0.2rem; cursor: pointer; font-size: 0.65rem; }
.aw-proc-code { font-size: 0.65rem; color: #555; margin-top: 0.2rem; white-space: pre-wrap; max-height: 3rem; overflow: hidden; }
.aw-proc-output { font-size: 0.7rem; color: #999; margin-top: 0.2rem; white-space: pre-wrap; max-height: 5rem; overflow-y: auto; }
/* Awareness controls (workspace) */
#aw-ctrl-body .controls-container { padding: 0; }
#aw-ctrl-body .control-table { font-size: 0.72rem; }
/* Expandable trace detail */
.trace-line.expandable { cursor: pointer; }
.trace-detail { display: none; padding: 0.3rem 0.4rem 0.3rem 12rem; font-size: 0.65rem; color: #777; white-space: pre-wrap; word-break: break-all; max-height: 10rem; overflow-y: auto; background: #0d0d14; border-bottom: 1px solid #1a1a2e; }
.trace-detail.open { display: block; }
/* Login overlay */
#login-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.85); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.login-card { background: #1a1a1a; padding: 2rem; border-radius: 0.6rem; text-align: center; }
.login-card h2 { color: #60a5fa; margin-bottom: 1rem; }
.login-card p { color: #888; margin-bottom: 1.5rem; }