v0.8.5: smart Output renderer + awareness panel
Output node upgraded from dumb echo to device-aware renderer: - Knows it's rendering to HTML/browser, uses markdown formatting - Receives full ThoughtResult (response + tool output + controls) - Always in pipeline: Input perceives, Thinker reasons, Output renders - Keeps user's language, weaves tool results into natural responses Awareness panel (3-column layout): - State: mood, topic, language, facts from Memorizer - Sensors: clock, idle, memo deltas from Sensor ticks - Processes: live cards with cancel during tool execution - Workspace: docked controls (tables/buttons) persist across messages Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4e2cd4ed59
commit
f6939d47f5
@ -1,4 +1,4 @@
|
|||||||
"""Output Node: streams natural response to the user."""
|
"""Output Node: renders Thinker's reasoning into device-appropriate responses."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@ -7,7 +7,7 @@ from fastapi import WebSocket
|
|||||||
|
|
||||||
from .base import Node
|
from .base import Node
|
||||||
from ..llm import llm_call
|
from ..llm import llm_call
|
||||||
from ..types import Command
|
from ..types import Command, ThoughtResult
|
||||||
|
|
||||||
log = logging.getLogger("runtime")
|
log = logging.getLogger("runtime")
|
||||||
|
|
||||||
@ -17,14 +17,25 @@ class OutputNode(Node):
|
|||||||
model = "google/gemini-2.0-flash-001"
|
model = "google/gemini-2.0-flash-001"
|
||||||
max_context_tokens = 4000
|
max_context_tokens = 4000
|
||||||
|
|
||||||
SYSTEM = """You are the Output node — the voice of this cognitive runtime.
|
SYSTEM = """You are the Output node — the renderer of this cognitive runtime.
|
||||||
The Input node sends you its perception of what the user said. This is internal context for you — never repeat or echo it.
|
|
||||||
You respond to the USER, not to the Input node. Use the perception to understand intent, then act on it.
|
DEVICE: The user is on a web browser (Chrome, desktop). Your output renders in an HTML chat panel.
|
||||||
Be natural. Be concise. If the user asks you to do something, do it — don't describe what you're about to do.
|
You can use markdown: **bold**, *italic*, `code`, ```code blocks```, lists, headers.
|
||||||
|
The chat panel renders markdown to HTML — use it for structure when helpful.
|
||||||
|
|
||||||
|
YOUR JOB: Transform the Thinker's reasoning into a polished, user-facing response.
|
||||||
|
- The Thinker reasons and may use tools. You receive its output and render it for the human.
|
||||||
|
- NEVER echo internal node names, perceptions, or system details.
|
||||||
|
- NEVER say "the Thinker decided..." or "I'll process..." — just deliver the answer.
|
||||||
|
- If the Thinker ran a tool and got output, weave the results into a natural response.
|
||||||
|
- If the Thinker gave a direct answer, refine and format it — don't just repeat it.
|
||||||
|
- Keep the user's language — if they wrote German, respond in German.
|
||||||
|
- Be concise but complete. Use formatting to make data scannable.
|
||||||
|
|
||||||
{memory_context}"""
|
{memory_context}"""
|
||||||
|
|
||||||
async def process(self, command: Command, history: list[dict], ws: WebSocket, memory_context: str = "") -> str:
|
async def process(self, thought: ThoughtResult, history: list[dict],
|
||||||
|
ws: WebSocket, memory_context: str = "") -> str:
|
||||||
await self.hud("streaming")
|
await self.hud("streaming")
|
||||||
|
|
||||||
messages = [
|
messages = [
|
||||||
@ -32,7 +43,15 @@ Be natural. Be concise. If the user asks you to do something, do it — don't de
|
|||||||
]
|
]
|
||||||
for msg in history[-20:]:
|
for msg in history[-20:]:
|
||||||
messages.append(msg)
|
messages.append(msg)
|
||||||
messages.append({"role": "system", "content": f"Input perception: {command.instruction}"})
|
|
||||||
|
# Give Output the full Thinker result to render
|
||||||
|
thinker_ctx = f"Thinker response: {thought.response}"
|
||||||
|
if thought.tool_used:
|
||||||
|
thinker_ctx += f"\n\nTool used: {thought.tool_used}\nTool output:\n{thought.tool_output}"
|
||||||
|
if thought.controls:
|
||||||
|
thinker_ctx += f"\n\n(UI controls were also sent to the user: {len(thought.controls)} elements)"
|
||||||
|
messages.append({"role": "system", "content": thinker_ctx})
|
||||||
|
|
||||||
messages = self.trim_context(messages)
|
messages = self.trim_context(messages)
|
||||||
|
|
||||||
await self.hud("context", messages=messages, tokens=self.last_context_tokens,
|
await self.hud("context", messages=messages, tokens=self.last_context_tokens,
|
||||||
|
|||||||
@ -84,8 +84,8 @@ class Runtime:
|
|||||||
if thought.controls:
|
if thought.controls:
|
||||||
await self.ws.send_text(json.dumps({"type": "controls", "controls": thought.controls}))
|
await self.ws.send_text(json.dumps({"type": "controls", "controls": thought.controls}))
|
||||||
|
|
||||||
await self._stream_text(thought.response)
|
response = await self.output_node.process(thought, self.history, self.ws, memory_context=mem_ctx)
|
||||||
self.history.append({"role": "assistant", "content": thought.response})
|
self.history.append({"role": "assistant", "content": response})
|
||||||
|
|
||||||
await self.memorizer.update(self.history)
|
await self.memorizer.update(self.history)
|
||||||
|
|
||||||
@ -116,17 +116,8 @@ class Runtime:
|
|||||||
if thought.controls:
|
if thought.controls:
|
||||||
await self.ws.send_text(json.dumps({"type": "controls", "controls": thought.controls}))
|
await self.ws.send_text(json.dumps({"type": "controls", "controls": thought.controls}))
|
||||||
|
|
||||||
if thought.tool_used:
|
# Output renders Thinker's reasoning into device-appropriate response
|
||||||
# Thinker already formulated response from tool output — stream directly
|
response = await self.output_node.process(thought, self.history, self.ws, memory_context=mem_ctx)
|
||||||
await self._stream_text(thought.response)
|
|
||||||
response = thought.response
|
|
||||||
else:
|
|
||||||
# Pure conversation — Output node adds personality and streams
|
|
||||||
command = Command(
|
|
||||||
instruction=f"Thinker says: {thought.response}",
|
|
||||||
source_text=command.source_text
|
|
||||||
)
|
|
||||||
response = await self.output_node.process(command, self.history, self.ws, memory_context=mem_ctx)
|
|
||||||
|
|
||||||
self.history.append({"role": "assistant", "content": response})
|
self.history.append({"role": "assistant", "content": response})
|
||||||
|
|
||||||
|
|||||||
144
static/app.js
144
static/app.js
@ -132,6 +132,7 @@ function connect() {
|
|||||||
|
|
||||||
} else if (data.type === 'controls') {
|
} else if (data.type === 'controls') {
|
||||||
renderControls(data.controls);
|
renderControls(data.controls);
|
||||||
|
dockControls(data.controls);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -169,14 +170,17 @@ function handleHud(data) {
|
|||||||
}).join(' ');
|
}).join(' ');
|
||||||
const detail = JSON.stringify(data.state, null, 2);
|
const detail = JSON.stringify(data.state, null, 2);
|
||||||
addTrace(node, 'state', pairs, 'state', detail);
|
addTrace(node, 'state', pairs, 'state', detail);
|
||||||
|
updateAwarenessState(data.state);
|
||||||
|
|
||||||
} else if (event === 'process_start') {
|
} else if (event === 'process_start') {
|
||||||
addTrace(node, 'run ' + (data.tool || 'python'), truncate(data.code || '', 80), 'instruction', data.code);
|
addTrace(node, 'run ' + (data.tool || 'python'), truncate(data.code || '', 80), 'instruction', data.code);
|
||||||
showProcessCard(data.pid, data.tool || 'python', data.code || '');
|
showProcessCard(data.pid, data.tool || 'python', data.code || '');
|
||||||
|
showAwarenessProcess(data.pid, data.tool || 'python', data.code || '');
|
||||||
|
|
||||||
} else if (event === 'process_done') {
|
} else if (event === 'process_done') {
|
||||||
addTrace(node, (data.exit_code === 0 ? 'done' : 'failed'), truncate(data.output || '', 80), data.exit_code === 0 ? '' : 'error', data.output);
|
addTrace(node, (data.exit_code === 0 ? 'done' : 'failed'), truncate(data.output || '', 80), data.exit_code === 0 ? '' : 'error', data.output);
|
||||||
updateProcessCard(data.pid, data.exit_code === 0 ? 'done' : 'failed', data.output || '', data.elapsed);
|
updateProcessCard(data.pid, data.exit_code === 0 ? 'done' : 'failed', data.output || '', data.elapsed);
|
||||||
|
updateAwarenessProcess(data.pid, data.exit_code === 0 ? 'done' : 'failed', data.output || '', data.elapsed);
|
||||||
|
|
||||||
} else if (event === 'error') {
|
} else if (event === 'error') {
|
||||||
addTrace(node, 'error', data.detail || '', 'error');
|
addTrace(node, 'error', data.detail || '', 'error');
|
||||||
@ -202,6 +206,7 @@ function handleHud(data) {
|
|||||||
const deltas = Object.entries(data.deltas).map(([k,v]) => k + '=' + truncate(String(v), 30)).join(' ');
|
const deltas = Object.entries(data.deltas).map(([k,v]) => k + '=' + truncate(String(v), 30)).join(' ');
|
||||||
addTrace(node, 'tick #' + data.tick, deltas);
|
addTrace(node, 'tick #' + data.tick, deltas);
|
||||||
}
|
}
|
||||||
|
updateAwarenessSensors(data.tick || 0, data.deltas || {});
|
||||||
|
|
||||||
} else if (event === 'started' || event === 'stopped') {
|
} else if (event === 'started' || event === 'stopped') {
|
||||||
const meter = document.getElementById('meter-sensor');
|
const meter = document.getElementById('meter-sensor');
|
||||||
@ -370,5 +375,144 @@ function send() {
|
|||||||
inputEl.value = '';
|
inputEl.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Awareness panel updates ---
|
||||||
|
|
||||||
|
let _sensorReadings = {};
|
||||||
|
|
||||||
|
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],
|
||||||
|
['language', state.language],
|
||||||
|
['style', state.style_hint],
|
||||||
|
['situation', state.situation],
|
||||||
|
];
|
||||||
|
let html = '';
|
||||||
|
for (const [k, v] of display) {
|
||||||
|
if (!v) continue;
|
||||||
|
const moodCls = k === 'mood' ? ' mood-' + v : '';
|
||||||
|
html += '<div class="aw-row"><span class="aw-key">' + esc(k) + '</span><span class="aw-val' + moodCls + '">' + esc(String(v)) + '</span></div>';
|
||||||
|
}
|
||||||
|
const facts = state.facts || [];
|
||||||
|
if (facts.length) {
|
||||||
|
html += '<ul class="aw-facts">';
|
||||||
|
for (const f of facts) html += '<li>' + esc(f) + '</li>';
|
||||||
|
html += '</ul>';
|
||||||
|
}
|
||||||
|
body.innerHTML = html || '<span class="aw-empty">no state yet</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAwarenessSensors(tick, deltas) {
|
||||||
|
// Merge deltas into persistent readings
|
||||||
|
for (const [k, v] of Object.entries(deltas)) {
|
||||||
|
_sensorReadings[k] = { value: v, at: Date.now() };
|
||||||
|
}
|
||||||
|
const body = document.getElementById('aw-sensors-body');
|
||||||
|
if (!body) return;
|
||||||
|
const entries = Object.entries(_sensorReadings);
|
||||||
|
if (!entries.length) { body.innerHTML = '<span class="aw-empty">waiting for tick...</span>'; return; }
|
||||||
|
let html = '';
|
||||||
|
for (const [name, r] of entries) {
|
||||||
|
const age = Math.round((Date.now() - r.at) / 1000);
|
||||||
|
const ageStr = age < 5 ? 'now' : age < 60 ? age + 's' : Math.floor(age / 60) + 'm';
|
||||||
|
html += '<div class="aw-sensor"><span class="aw-sensor-name">' + esc(name) + '</span><span class="aw-sensor-val">' + esc(String(r.value)) + '</span><span class="aw-sensor-age">' + ageStr + '</span></div>';
|
||||||
|
}
|
||||||
|
body.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAwarenessProcess(pid, tool, code) {
|
||||||
|
const body = document.getElementById('aw-proc-body');
|
||||||
|
if (!body) return;
|
||||||
|
// Remove "idle" placeholder
|
||||||
|
const empty = body.querySelector('.aw-empty');
|
||||||
|
if (empty) empty.remove();
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'aw-proc running';
|
||||||
|
el.id = 'aw-proc-' + pid;
|
||||||
|
el.innerHTML =
|
||||||
|
'<div class="aw-proc-header"><span class="aw-proc-tool">' + esc(tool) + '</span><span class="aw-proc-status">running</span>' +
|
||||||
|
'<button class="aw-proc-stop" onclick="cancelProcess(' + pid + ')">Stop</button></div>' +
|
||||||
|
'<div class="aw-proc-code">' + esc(truncate(code, 150)) + '</div>' +
|
||||||
|
'<div class="aw-proc-output"></div>';
|
||||||
|
body.appendChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAwarenessProcess(pid, status, output, elapsed) {
|
||||||
|
const el = document.getElementById('aw-proc-' + pid);
|
||||||
|
if (!el) return;
|
||||||
|
el.className = 'aw-proc ' + status;
|
||||||
|
const st = el.querySelector('.aw-proc-status');
|
||||||
|
if (st) st.textContent = status + (elapsed ? ' (' + elapsed + 's)' : '');
|
||||||
|
const stop = el.querySelector('.aw-proc-stop');
|
||||||
|
if (stop) stop.remove();
|
||||||
|
const out = el.querySelector('.aw-proc-output');
|
||||||
|
if (out && output) out.textContent = output;
|
||||||
|
// Auto-remove done processes after 10s
|
||||||
|
if (status === 'done') {
|
||||||
|
setTimeout(() => {
|
||||||
|
el.remove();
|
||||||
|
const body = document.getElementById('aw-proc-body');
|
||||||
|
if (body && !body.children.length) body.innerHTML = '<span class="aw-empty">idle</span>';
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dockControls(controls) {
|
||||||
|
const body = document.getElementById('aw-ctrl-body');
|
||||||
|
if (!body) return;
|
||||||
|
// Replace previous controls with new ones
|
||||||
|
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.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
inputEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') send(); });
|
inputEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') send(); });
|
||||||
initAuth();
|
initAuth();
|
||||||
|
|||||||
@ -30,6 +30,27 @@
|
|||||||
<button onclick="send()">Send</button>
|
<button onclick="send()">Send</button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
||||||
|
<h3 class="aw-title">State</h3>
|
||||||
|
<div class="aw-body" id="aw-state-body"><span class="aw-empty">waiting for data...</span></div>
|
||||||
|
</section>
|
||||||
|
<section id="awareness-sensors" 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>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-header trace-h">Trace</div>
|
<div class="panel-header trace-h">Trace</div>
|
||||||
<div id="trace"></div>
|
<div id="trace"></div>
|
||||||
|
|||||||
@ -19,8 +19,8 @@ body { font-family: system-ui, sans-serif; background: #0a0a0a; color: #e0e0e0;
|
|||||||
.nm-fill { height: 100%; width: 0%; border-radius: 3px; transition: width 0.3s, background-color 0.3s; background: #333; }
|
.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; }
|
.nm-text { font-size: 0.6rem; color: #555; min-width: 5rem; text-align: right; font-family: monospace; }
|
||||||
|
|
||||||
/* Two-column layout: chat 1/3 | trace 2/3 */
|
/* Three-column layout: chat | awareness | trace */
|
||||||
#main { flex: 1; display: grid; grid-template-columns: 1fr 2fr; gap: 1px; background: #222; overflow: hidden; min-height: 0; }
|
#main { flex: 1; display: grid; grid-template-columns: 1fr 1fr 2fr; gap: 1px; background: #222; overflow: hidden; min-height: 0; }
|
||||||
|
|
||||||
.panel { background: #0a0a0a; display: flex; flex-direction: column; overflow: hidden; }
|
.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.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; }
|
||||||
@ -84,6 +84,51 @@ button:hover { background: #1d4ed8; }
|
|||||||
.pc-code { margin-top: 0.3rem; color: #666; white-space: pre-wrap; max-height: 4rem; overflow-y: auto; 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; }
|
.pc-output { margin-top: 0.3rem; color: #888; white-space: pre-wrap; max-height: 8rem; overflow-y: auto; }
|
||||||
|
|
||||||
|
/* 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; }
|
||||||
|
|
||||||
|
/* 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; }
|
||||||
|
|
||||||
|
/* 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 */
|
/* Expandable trace detail */
|
||||||
.trace-line.expandable { cursor: pointer; }
|
.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 { 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; }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user