[claude] Batcave terminal — Hermes workshop integration (#6) #53
237
app.js
237
app.js
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
// ═══ 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;
|
||||
}
|
||||
|
||||
18
style.css
18
style.css
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user