[claude] Batcave terminal — Hermes workshop integration (#6) #53

Closed
claude wants to merge 1 commits from claude/the-nexus:claude/issue-6 into main
2 changed files with 231 additions and 24 deletions

237
app.js
View File

@@ -28,6 +28,7 @@ let clock, playerPos, playerRot;
let keys = {};
let mouseDown = false;
let batcaveTerminals = [];
let terminalPanelMap = {}; // name → { ctx, texture, canvas, lines }
let portals = []; // Registry of active portals
let visionPoints = []; // Registry of vision points
let agents = []; // Registry of agent presences
@@ -45,6 +46,13 @@ let chatOpen = true;
let loadProgress = 0;
let performanceTier = 'high';
// ═══ HERMES WORKSHOP STATE ═══
let hermesWs = null;
let hermesConnected = false;
let hermesReconnectTimer = null;
const HERMES_SESSION_KEY = 'nexus-hermes-session';
const HERMES_MAX_STORED = 60;
// ═══ NAVIGATION SYSTEM ═══
const NAV_MODES = ['walk', 'orbit', 'fly'];
let navModeIdx = 0;
@@ -143,6 +151,11 @@ async function init() {
updateLoad(100);
// Restore session and connect Hermes workshop
restoreSession();
connectHermes();
updateNexusCommandPanel();
setTimeout(() => {
document.getElementById('loading-screen').classList.add('fade-out');
const enterPrompt = document.getElementById('enter-prompt');
@@ -338,21 +351,21 @@ function createBatcaveTerminal() {
terminalGroup.position.set(0, 0, -8);
const panelData = [
{ title: 'NEXUS COMMAND', color: NEXUS.colors.primary, rot: -0.4, x: -6, y: 3, lines: ['> STATUS: NOMINAL', '> UPTIME: 142.4h', '> HARNESS: STABLE', '> MODE: SOVEREIGN'] },
{ title: 'DEV QUEUE', color: NEXUS.colors.gold, rot: -0.2, x: -3, y: 3, lines: ['> ISSUE #4: CORE', '> ISSUE #5: PORTAL', '> ISSUE #6: TERMINAL', '> ISSUE #7: TIMMY'] },
{ title: 'METRICS', color: NEXUS.colors.secondary, rot: 0, x: 0, y: 3, lines: ['> CPU: 12% [||....]', '> MEM: 4.2GB', '> COMMITS: 842', '> ACTIVE LOOPS: 5'] },
{ title: 'THOUGHTS', color: NEXUS.colors.primary, rot: 0.2, x: 3, y: 3, lines: ['> ANALYZING WORLD...', '> SYNCING MEMORY...', '> WAITING FOR INPUT', '> SOUL ON BITCOIN'] },
{ title: 'AGENT STATUS', color: NEXUS.colors.gold, rot: 0.4, x: 6, y: 3, lines: ['> TIMMY: ● RUNNING', '> KIMI: ○ STANDBY', '> CLAUDE: ● ACTIVE', '> PERPLEXITY: ○'] },
{ name: 'NEXUS COMMAND', title: 'NEXUS COMMAND', color: NEXUS.colors.primary, rot: -0.4, x: -6, y: 3, lines: ['> STATUS: NOMINAL', '> UPTIME: --', '> HERMES: OFFLINE', '> MODE: SOVEREIGN'] },
{ name: 'DEV QUEUE', title: 'DEV QUEUE', color: NEXUS.colors.gold, rot: -0.2, x: -3, y: 3, lines: ['> ISSUE #4: ✓ DONE', '> ISSUE #5: ✓ DONE', '> ISSUE #6: ● ACTIVE', '> ISSUE #7: PENDING'] },
{ name: 'METRICS', title: 'METRICS', color: NEXUS.colors.secondary, rot: 0, x: 0, y: 3, lines: ['> MSGS: 0', '> TOOLS: 0', '> SESSION: NEW', '> ACTIVE LOOPS: 5'] },
{ name: 'THOUGHTS', title: 'THOUGHTS', color: NEXUS.colors.primary, rot: 0.2, x: 3, y: 3, lines: ['> ANALYZING WORLD...', '> SYNCING MEMORY...', '> WAITING FOR INPUT', '> SOUL ON BITCOIN'] },
{ name: 'AGENT STATUS', title: 'AGENT STATUS', color: NEXUS.colors.gold, rot: 0.4, x: 6, y: 3, lines: ['> TIMMY: ● RUNNING', '> KIMI: ○ STANDBY', '> CLAUDE: ● ACTIVE', '> PERPLEXITY: ○'] },
];
panelData.forEach(data => {
createTerminalPanel(terminalGroup, data.x, data.y, data.rot, data.title, data.color, data.lines);
createTerminalPanel(terminalGroup, data.x, data.y, data.rot, data.title, data.color, data.lines, data.name);
});
scene.add(terminalGroup);
}
function createTerminalPanel(parent, x, y, rot, title, color, lines) {
function createTerminalPanel(parent, x, y, rot, title, color, lines, panelName) {
const w = 2.8, h = 3.5;
const group = new THREE.Group();
group.position.set(x, y, 0);
@@ -435,7 +448,162 @@ function createTerminalPanel(parent, x, y, rot, title, color, lines) {
group.add(scanMesh);
parent.add(group);
batcaveTerminals.push({ group, scanMat, borderMat });
const entry = { group, scanMat, borderMat };
batcaveTerminals.push(entry);
if (panelName) {
terminalPanelMap[panelName] = {
ctx, canvas: textCanvas, texture: textTexture, title, color, lines: [...lines]
};
}
}
// ═══ WORKSHOP PANEL REFRESH ═══
function refreshTerminalPanel(name, lines) {
const p = terminalPanelMap[name];
if (!p) return;
p.lines = lines;
const { ctx, canvas, texture, title, color } = p;
const hexColor = '#' + new THREE.Color(color).getHexString();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = hexColor;
ctx.font = 'bold 32px "Orbitron", sans-serif';
ctx.fillText(title, 20, 45);
ctx.fillRect(20, 55, 472, 2);
ctx.font = '20px "JetBrains Mono", monospace';
lines.forEach((line, i) => {
let fillColor = '#a0b8d0';
if (line.includes('● RUNNING') || line.includes('● ACTIVE') || line.includes('ONLINE')) fillColor = '#4af0c0';
else if (line.includes('○ STANDBY') || line.includes('OFFLINE')) fillColor = '#5a6a8a';
else if (line.includes('NOMINAL') || line.includes('✓')) fillColor = '#4af0c0';
else if (line.includes('ERROR') || line.includes('FAIL')) fillColor = '#ff4466';
ctx.fillStyle = fillColor;
ctx.fillText(line, 20, 100 + i * 40);
});
texture.needsUpdate = true;
}
// ═══ HERMES WORKSHOP ═══
function hermesWsUrl() {
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
return `${proto}//${window.location.host}/api/world/ws`;
}
function connectHermes() {
if (hermesWs && hermesWs.readyState <= WebSocket.OPEN) return;
if (hermesReconnectTimer) { clearTimeout(hermesReconnectTimer); hermesReconnectTimer = null; }
try {
hermesWs = new WebSocket(hermesWsUrl());
} catch (e) {
scheduleHermesReconnect();
return;
}
hermesWs.onopen = () => {
hermesConnected = true;
addChatMessage('system', 'Hermes workshop connected.');
updateNexusCommandPanel();
};
hermesWs.onmessage = (evt) => {
let data;
try { data = JSON.parse(evt.data); } catch { return; }
switch (data.type) {
case 'message':
addChatMessage('hermes', escapeHtml(data.text || data.content || ''));
saveSession();
break;
case 'tool_call':
addChatMessage('tool_call', data);
saveSession();
break;
case 'tool_result':
addChatMessage('tool_result', data);
saveSession();
break;
case 'error':
addChatMessage('error', escapeHtml(data.message || 'Hermes error'));
break;
}
};
hermesWs.onclose = () => {
hermesConnected = false;
updateNexusCommandPanel();
scheduleHermesReconnect();
};
hermesWs.onerror = () => {
hermesConnected = false;
};
}
function scheduleHermesReconnect() {
if (hermesReconnectTimer) return;
hermesReconnectTimer = setTimeout(() => {
hermesReconnectTimer = null;
connectHermes();
}, 5000);
}
function updateNexusCommandPanel() {
const status = hermesConnected ? '● ONLINE' : '○ OFFLINE';
const uptime = Math.floor(performance.now() / 1000);
const h = Math.floor(uptime / 3600);
const m = Math.floor((uptime % 3600) / 60);
const sessionMsgs = loadSession().length;
refreshTerminalPanel('NEXUS COMMAND', [
`> STATUS: NOMINAL`,
`> UPTIME: ${h}h ${m}m`,
`> HERMES: ${status}`,
`> MODE: SOVEREIGN`,
]);
refreshTerminalPanel('METRICS', [
`> MSGS: ${sessionMsgs}`,
`> HERMES: ${status}`,
`> SESSION: ${sessionMsgs > 0 ? 'RESTORED' : 'NEW'}`,
`> ACTIVE LOOPS: 5`,
]);
}
// ═══ SESSION PERSISTENCE ═══
function saveSession() {
const container = document.getElementById('chat-messages');
if (!container) return;
const msgs = [];
container.querySelectorAll('[data-msg-type]').forEach(el => {
msgs.push({ type: el.dataset.msgType, html: el.innerHTML });
});
try {
localStorage.setItem(HERMES_SESSION_KEY, JSON.stringify(msgs.slice(-HERMES_MAX_STORED)));
} catch {}
}
function loadSession() {
try {
return JSON.parse(localStorage.getItem(HERMES_SESSION_KEY) || '[]');
} catch { return []; }
}
function restoreSession() {
const msgs = loadSession();
if (!msgs.length) return;
const container = document.getElementById('chat-messages');
if (!container) return;
addChatMessage('system', `Session restored — ${msgs.length} message(s) from previous workshop.`);
msgs.forEach(m => {
const div = document.createElement('div');
div.className = `chat-msg chat-msg-${m.type}`;
div.dataset.msgType = m.type;
div.innerHTML = m.html;
container.appendChild(div);
});
container.scrollTop = container.scrollHeight;
}
function escapeHtml(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// ═══ AGENT PRESENCE SYSTEM ═══
@@ -1064,29 +1232,50 @@ function sendChatMessage() {
const text = input.value.trim();
if (!text) return;
addChatMessage('user', text);
saveSession();
input.value = '';
setTimeout(() => {
const responses = [
'Processing your request through the harness...',
'I have noted this in my thought stream.',
'Acknowledged. Routing to appropriate agent loop.',
'The sovereign space recognizes your command.',
'Running analysis. Results will appear on the main terminal.',
'My crystal ball says... yes. Implementing.',
'Understood, Alexander. Adjusting priorities.',
];
const resp = responses[Math.floor(Math.random() * responses.length)];
addChatMessage('timmy', resp);
}, 500 + Math.random() * 1000);
if (hermesConnected && hermesWs && hermesWs.readyState === WebSocket.OPEN) {
hermesWs.send(JSON.stringify({ type: 'message', content: text }));
} else {
// Offline fallback
setTimeout(() => {
const responses = [
'Hermes offline — operating in sovereign mode.',
'No backend connection. Check harness status.',
'Running locally. Hermes will sync when reconnected.',
'Noted in thought stream. Hermes will process on reconnect.',
'Sovereign mode active. Message queued.',
];
addChatMessage('timmy', responses[Math.floor(Math.random() * responses.length)]);
}, 400 + Math.random() * 600);
}
input.blur();
}
function addChatMessage(type, text) {
function addChatMessage(type, payload) {
const container = document.getElementById('chat-messages');
const div = document.createElement('div');
div.className = `chat-msg chat-msg-${type}`;
const prefixes = { user: '[ALEXANDER]', timmy: '[TIMMY]', system: '[NEXUS]', error: '[ERROR]' };
div.innerHTML = `<span class="chat-msg-prefix">${prefixes[type] || '[???]'}</span> ${text}`;
div.dataset.msgType = type;
const prefixes = { user: '[ALEXANDER]', timmy: '[TIMMY]', hermes: '[HERMES]', system: '[NEXUS]', error: '[ERROR]' };
if (type === 'tool_call') {
const name = escapeHtml(payload.name || payload.tool || 'tool');
const args = escapeHtml(JSON.stringify(payload.arguments || payload.args || {}, null, 2));
div.innerHTML = `<span class="chat-msg-prefix chat-tool-call-prefix">[TOOL▶]</span> <span class="chat-tool-name">${name}</span><pre class="chat-tool-block">${args}</pre>`;
} else if (type === 'tool_result') {
const output = escapeHtml(
typeof payload.output === 'string' ? payload.output
: JSON.stringify(payload.output || payload.result || payload, null, 2)
);
div.innerHTML = `<span class="chat-msg-prefix chat-tool-result-prefix">[TOOL◀]</span><pre class="chat-tool-block">${output}</pre>`;
} else {
const text = typeof payload === 'string' ? payload : escapeHtml(String(payload));
div.innerHTML = `<span class="chat-msg-prefix">${prefixes[type] || '[???]'}</span> ${text}`;
}
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}

View File

@@ -572,8 +572,26 @@ canvas#nexus-canvas {
}
.chat-msg-system .chat-msg-prefix { color: var(--color-text-muted); }
.chat-msg-timmy .chat-msg-prefix { color: var(--color-primary); }
.chat-msg-hermes .chat-msg-prefix { color: var(--color-secondary); }
.chat-msg-user .chat-msg-prefix { color: var(--color-gold); }
.chat-msg-error .chat-msg-prefix { color: var(--color-danger); }
.chat-tool-call-prefix { color: #4af0c0; }
.chat-tool-result-prefix { color: #7b5cff; }
.chat-tool-name { color: #4af0c0; font-weight: 600; }
.chat-tool-block {
margin: 4px 0 0 0;
padding: 6px 8px;
background: rgba(10, 15, 40, 0.8);
border-left: 2px solid var(--color-border);
border-radius: 0 4px 4px 0;
font-family: var(--font-body);
font-size: 11px;
color: #8090a8;
white-space: pre-wrap;
word-break: break-all;
max-height: 120px;
overflow-y: auto;
}
.chat-input-row {
display: flex;