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:
+143
-90
@@ -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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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() {
|
||||
|
||||
Reference in New Issue
Block a user