feat(diagnostic): ARIA-Live (read-only Terminal-Mirror) + Not-Aus statt SSH-Tab

SSH-Tab raus — funktionierte eh nicht zuverlaessig und war konzeptionell
falsch. Stattdessen Live-Mirror der Claude-Code-Session:

- proxy-patches/routes.js: assistant + user Events parsed → POSTed Tool-
  Inputs (truncated 2 KB) + Tool-Results (truncated 4 KB) + Assistant-Text
  an aria-bridge:8090/internal/agent-stream. start/end Marker pro Session.
  Subprocess-Tracking (_activeSubprocesses Map) + interner Side-Channel
  auf Port 3457 mit POST /cancel-all fuer Hard-Kill.

- bridge: neuer /internal/agent-stream Endpoint pusht 1:1 als RVS
  agent_stream. cancel_request Handler nimmt optional 'hard'-Flag —
  triggert dann zusaetzlich _cancel_proxy_subprocesses() das den Proxy-
  Side-Channel ruft.

- rvs: agent_stream whitelisted.

- diagnostic: SSH-Tab → 'ARIA Live'. Monospace-Stream, farbcodiert
  (text=hell, tool_use=cyan, tool_result=gruen/rot, thinking=gelb-italic),
  Auto-Scroll, max 2000 Zeilen Backlog. Roter  Not-Aus-Button mit
  Confirm → aria_panic_stop action → diagnostic-server broadcastet
  cancel_request mit hard:true.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 09:23:13 +02:00
parent 1d3c45fdda
commit 31b0bfaac1
5 changed files with 360 additions and 108 deletions
+143 -90
View File
@@ -395,18 +395,29 @@
<div class="card" style="margin-top:12px; padding: 8px 0 0 0;">
<div style="padding: 0 12px;">
<div class="tab-bar">
<button class="tab-btn active" id="live-tab-ssh" onclick="switchLiveTab('ssh')">SSH Terminal</button>
<button class="tab-btn active" id="live-tab-aria" onclick="switchLiveTab('aria')">ARIA Live</button>
<button class="tab-btn" id="live-tab-desktop" onclick="switchLiveTab('desktop')">Desktop</button>
</div>
</div>
<div style="background:#080810; border:1px solid #1E1E2E; border-radius:0 0 6px 6px; position:relative;">
<!-- SSH Terminal -->
<div id="live-ssh" style="height:350px; padding:4px;">
<div id="live-ssh-bar" style="display:flex;gap:6px;align-items:center;padding:4px 4px 6px;">
<button class="btn" onclick="startLiveSSH()" id="btn-live-ssh" style="padding:4px 12px;font-size:11px;">Verbinden</button>
<span id="live-ssh-status" style="font-size:11px;color:#8888AA;">Nicht verbunden</span>
<!-- ARIA Live (read-only Mirror der Claude-Code-Session) -->
<div id="live-aria" style="height:350px; padding:4px; display:flex; flex-direction:column;">
<div id="live-aria-bar" style="display:flex;gap:6px;align-items:center;padding:4px 4px 6px;flex-shrink:0;">
<span id="live-aria-status" style="font-size:11px;color:#8888AA;flex:1;">Idle — warte auf ARIA-Aktivitaet</span>
<button class="btn" onclick="clearAriaLive()" style="padding:4px 12px;font-size:11px;" title="Live-Mitschrift leeren">Leeren</button>
<label style="font-size:11px;color:#8888AA;display:flex;align-items:center;gap:4px;cursor:pointer;" title="Bei jeder neuen Zeile ans Ende scrollen">
<input type="checkbox" id="live-aria-autoscroll" checked style="margin:0;"> Auto-Scroll
</label>
<button class="btn" onclick="ariaPanicStop()"
style="padding:4px 14px;font-size:11px;background:#FF3B30;color:#fff;border-color:#FF3B30;font-weight:bold;"
title="NOT-AUS: killt alle aktiven Claude-Code-Subprocesses sofort">
⛔ Not-Aus
</button>
</div>
<div id="live-aria-stream"
style="flex:1;overflow-y:auto;background:#040408;font-family:'Courier New',monospace;font-size:11px;line-height:1.4;color:#C0C0D0;padding:6px 8px;border-top:1px solid #1E1E2E;">
<div style="color:#555570;font-style:italic;">Sobald ARIA denkt oder ein Tool nutzt, taucht es hier in Echtzeit auf.</div>
</div>
<div id="live-ssh-term" style="height:calc(100% - 32px);"></div>
</div>
<!-- Desktop Viewer -->
<div id="live-desktop" style="height:350px; display:none; position:relative;">
@@ -1412,6 +1423,11 @@
return;
}
if (msg.type === 'agent_stream') {
appendAriaStreamEvent(msg.payload || {});
return;
}
if (msg.type === 'voice_preview_audio') {
const statusEl = document.getElementById('voice-preview-status');
const audio = document.getElementById('voice-preview-audio');
@@ -1555,8 +1571,8 @@
return;
}
// core_auth WS-Event entfernt — aria-core ist raus.
// Live SSH + Desktop
if (msg.type?.startsWith('live_ssh_')) { handleLiveSSH(msg); return; }
// SSH-Terminal entfernt — durch ARIA-Live-Mirror ersetzt.
// Desktop bleibt.
if (msg.type === 'desktop_status') { handleDesktop(msg); return; }
if (msg.type === 'term_ready') {
@@ -2962,96 +2978,133 @@
// ── ARIA Live-Ansicht (SSH + Desktop) ──────────────────
let liveSshTerm = null;
let liveSshFit = null;
function switchLiveTab(tab) {
document.getElementById('live-ssh').style.display = tab === 'ssh' ? 'block' : 'none';
document.getElementById('live-aria').style.display = tab === 'aria' ? 'flex' : 'none';
document.getElementById('live-desktop').style.display = tab === 'desktop' ? 'block' : 'none';
document.getElementById('live-tab-ssh').className = 'tab-btn' + (tab === 'ssh' ? ' active' : '');
document.getElementById('live-tab-aria').className = 'tab-btn' + (tab === 'aria' ? ' active' : '');
document.getElementById('live-tab-desktop').className = 'tab-btn' + (tab === 'desktop' ? ' active' : '');
if (tab === 'ssh' && liveSshTerm && liveSshFit) {
setTimeout(() => liveSshFit.fit(), 50);
}
}
function startLiveSSH() {
const statusEl = document.getElementById('live-ssh-status');
const btn = document.getElementById('btn-live-ssh');
// Wenn schon verbunden, trennen
if (liveSshTerm && liveSshTerm._sshConnected) {
send({ action: 'live_ssh_close' });
statusEl.textContent = 'Getrennt';
statusEl.style.color = '#FF6B6B';
btn.textContent = 'Verbinden';
liveSshTerm._sshConnected = false;
return;
// ── ARIA Live (read-only Mirror der Claude-Code-Session) ──────
//
// Empfaengt agent_stream Events vom RVS (Proxy → Bridge → RVS → wir).
// Rendert sie als monospace-Liste — Tool-Calls in cyan, Tool-Results
// in grau (truncated), ARIA-Text in weiss, Thinking kursiv. Auto-Scroll
// bleibt am unteren Rand kleben solange der User nicht hochgescrollt hat.
// Not-Aus killt via Bridge → Proxy-Side-Channel alle Subprocesses.
function _ariaStreamEl() { return document.getElementById('live-aria-stream'); }
function _ariaStatusEl() { return document.getElementById('live-aria-status'); }
function _ariaIsAtBottom() {
const el = _ariaStreamEl();
if (!el) return true;
return (el.scrollHeight - el.scrollTop - el.clientHeight) < 24;
}
function _ariaMaybeScroll() {
if (!document.getElementById('live-aria-autoscroll')?.checked) return;
const el = _ariaStreamEl();
if (el) el.scrollTop = el.scrollHeight;
}
// Truncate UI: groessere Backlogs koennen viele MB werden. Wir halten
// max 2000 Zeilen — beim Ueberlauf den oberen Block wegwerfen.
const ARIA_MAX_LINES = 2000;
function _ariaTrimBacklog() {
const el = _ariaStreamEl();
if (!el) return;
while (el.childElementCount > ARIA_MAX_LINES) {
el.removeChild(el.firstChild);
}
statusEl.textContent = 'Verbinde...';
statusEl.style.color = '#FFD60A';
function initSSHTerm() {
const container = document.getElementById('live-ssh-term');
if (!liveSshTerm) {
liveSshTerm = new Terminal({
theme: { background: '#080810', foreground: '#E0E0F0', cursor: '#0096FF' },
fontFamily: 'Courier New, monospace',
fontSize: 12,
cursorBlink: true,
});
liveSshFit = new FitAddon.FitAddon();
liveSshTerm.loadAddon(liveSshFit);
liveSshTerm.open(container);
liveSshFit.fit();
liveSshTerm.onData((data) => {
send({ action: 'live_ssh_input', data });
});
}
liveSshTerm.clear();
send({ action: 'live_ssh_start' });
}
if (typeof Terminal === 'undefined') {
const s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js';
s.onload = () => {
const s2 = document.createElement('script');
s2.src = 'https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js';
s2.onload = () => initSSHTerm();
document.head.appendChild(s2);
};
document.head.appendChild(s);
}
function _ariaTimePrefix(ts) {
try {
const d = ts ? new Date(ts) : new Date();
const h = String(d.getHours()).padStart(2, '0');
const m = String(d.getMinutes()).padStart(2, '0');
const s = String(d.getSeconds()).padStart(2, '0');
return `${h}:${m}:${s}`;
} catch (_) { return ''; }
}
function _ariaEsc(s) {
return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function _ariaPushLine(html, color, opts = {}) {
const el = _ariaStreamEl();
if (!el) return;
const wasAtBottom = _ariaIsAtBottom();
const row = document.createElement('div');
row.style.cssText = `color:${color};${opts.style||''}`;
row.innerHTML = html;
// Erste statische "Sobald ARIA..."-Zeile beim ersten Event entfernen
const placeholder = el.querySelector('div[style*="italic"]');
if (placeholder && el.childElementCount === 1) el.removeChild(placeholder);
el.appendChild(row);
_ariaTrimBacklog();
if (wasAtBottom) _ariaMaybeScroll();
}
function appendAriaStreamEvent(p) {
const t = _ariaTimePrefix(p.ts);
const kind = p.kind || '';
if (kind === 'start') {
_ariaPushLine(
`<span style="color:#444460;">━━━ ${t} session start (${_ariaEsc(p.model || 'unknown')}) ━━━</span>`,
'#444460',
);
const st = _ariaStatusEl(); if (st) { st.textContent = 'ARIA aktiv...'; st.style.color = '#34C759'; }
} else if (kind === 'end') {
const reason = p.reason || '?';
const codePart = (p.code !== undefined && p.code !== null) ? ` code=${_ariaEsc(p.code)}` : '';
const errPart = p.error ? ` err=${_ariaEsc(String(p.error).slice(0,120))}` : '';
_ariaPushLine(
`<span style="color:#444460;">━━━ ${t} session end (${_ariaEsc(reason)}${codePart}${errPart}) ━━━</span>`,
'#444460',
);
const st = _ariaStatusEl(); if (st) { st.textContent = 'Idle'; st.style.color = '#8888AA'; }
} else if (kind === 'text') {
_ariaPushLine(
`<span style="color:#777799;">[${t}]</span> ${_ariaEsc(p.text || '')}`,
'#D0D0E0',
{ style: 'white-space:pre-wrap;word-break:break-word;' },
);
} else if (kind === 'thinking') {
_ariaPushLine(
`<span style="color:#777799;">[${t}]</span> <span style="font-style:italic;color:#888866;">💭 ${_ariaEsc(p.text || '')}</span>`,
'#888866',
{ style: 'white-space:pre-wrap;word-break:break-word;' },
);
} else if (kind === 'tool_use') {
const name = _ariaEsc(p.name || '?');
const inp = _ariaEsc(p.input || '');
const tail = p.inputTruncatedBytes ? `<span style="color:#777799;"> ...(+${p.inputTruncatedBytes} bytes)</span>` : '';
_ariaPushLine(
`<span style="color:#777799;">[${t}]</span> <span style="color:#0096FF;">▶ ${name}</span> <span style="color:#8888AA;">${inp}${tail}</span>`,
'#C0C0D0',
{ style: 'white-space:pre-wrap;word-break:break-word;' },
);
} else if (kind === 'tool_result') {
const isError = p.isError === true;
const head = isError ? '<span style="color:#FF6B6B;">✗ result (ERROR)</span>' : '<span style="color:#34C759;">✓ result</span>';
const tail = p.truncatedBytes ? `<span style="color:#777799;"> ...(+${p.truncatedBytes} bytes)</span>` : '';
_ariaPushLine(
`<span style="color:#777799;">[${t}]</span> ${head}<br><span style="color:#9090A0;white-space:pre-wrap;display:block;padding-left:14px;border-left:2px solid #2A2A3E;">${_ariaEsc(p.content || '')}${tail}</span>`,
'#9090A0',
);
} else {
initSSHTerm();
_ariaPushLine(
`<span style="color:#777799;">[${t}]</span> <span style="color:#AAAACC;">${_ariaEsc(kind)}: ${_ariaEsc(JSON.stringify(p))}</span>`,
'#AAAACC',
);
}
}
function handleLiveSSH(msg) {
const statusEl = document.getElementById('live-ssh-status');
const btn = document.getElementById('btn-live-ssh');
if (msg.type === 'live_ssh_data' && liveSshTerm) {
const raw = atob(msg.data);
const bytes = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
liveSshTerm.write(bytes);
} else if (msg.type === 'live_ssh_connected') {
statusEl.textContent = 'Verbunden mit aria-wohnung';
statusEl.style.color = '#34C759';
btn.textContent = 'Trennen';
if (liveSshTerm) liveSshTerm._sshConnected = true;
} else if (msg.type === 'live_ssh_error') {
statusEl.textContent = msg.error || 'Fehler';
statusEl.style.color = '#FF6B6B';
btn.textContent = 'Verbinden';
if (liveSshTerm) liveSshTerm._sshConnected = false;
} else if (msg.type === 'live_ssh_closed') {
statusEl.textContent = 'Getrennt';
statusEl.style.color = '#8888AA';
btn.textContent = 'Verbinden';
if (liveSshTerm) liveSshTerm._sshConnected = false;
}
function clearAriaLive() {
const el = _ariaStreamEl();
if (el) el.innerHTML = '<div style="color:#555570;font-style:italic;">Geleert.</div>';
}
function ariaPanicStop() {
if (!confirm('Wirklich NOT-AUS? Alle aktiven Claude-Subprocesses werden sofort gekillt.')) return;
send({ action: 'aria_panic_stop' });
_ariaPushLine(
`<span style="color:#FF3B30;font-weight:bold;">━━━ ${_ariaTimePrefix()} ⛔ NOT-AUS ausgeloest ━━━</span>`,
'#FF3B30',
);
}
function checkDesktop() {