Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
eb28e1f8e4 WIP: issue #701 (mimo swarm)
Some checks failed
CI / test (pull_request) Failing after 8s
CI / validate (pull_request) Failing after 13s
Review Approval Gate / verify-review (pull_request) Failing after 3s
2026-04-10 19:48:31 -04:00
4 changed files with 170 additions and 504 deletions

93
app.js
View File

@@ -1,3 +1,5 @@
shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
chdir: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
@@ -5,7 +7,6 @@ import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'
import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
import { SpatialMemory } from './nexus/components/spatial-memory.js';
import { SessionRooms } from './nexus/components/session-rooms.js';
import { EvenniaRoomPanel } from './nexus/components/evennia-room-panel.js';
// ═══════════════════════════════════════════
// NEXUS v1.1 — Portal System Update
@@ -708,7 +709,6 @@ async function init() {
createAshStorm();
SpatialMemory.init(scene);
SessionRooms.init(scene, camera, null);
EvenniaRoomPanel.init();
updateLoad(90);
loadSession();
@@ -1984,24 +1984,67 @@ function setupControls() {
document.getElementById('chat-quick-actions').addEventListener('click', (e) => {
const btn = e.target.closest('.quick-action-btn');
if (!btn) return;
const action = btn.dataset.action;
switch(action) {
case 'status':
sendChatMessage("Timmy, what is the current system status?");
break;
case 'agents':
sendChatMessage("Timmy, check on all active agents.");
break;
case 'portals':
openPortalAtlas();
break;
case 'help':
sendChatMessage("Timmy, I need assistance with Nexus navigation.");
break;
}
handleQuickAction(btn.dataset.action);
});
// ═══ QUICK ACTION HANDLER ═══
function handleQuickAction(action) {
switch(action) {
case 'status': {
const portalCount = portals.length;
const onlinePortals = portals.filter(p => p.userData && p.userData.status === 'online').length;
const agentCount = agents.length;
const wsState = wsConnected ? 'ONLINE' : 'OFFLINE';
const wsColor = wsConnected ? '#4af0c0' : '#ff4466';
addChatMessage('system', `[SYSTEM STATUS]`);
addChatMessage('timmy', `Nexus operational. ${portalCount} portals registered (${onlinePortals} online). ${agentCount} agent presences active. Hermes WebSocket: ${wsState}. Navigation mode: ${NAV_MODES[navModeIdx].toUpperCase()}. Performance tier: ${performanceTier.toUpperCase()}.`);
break;
}
case 'agents': {
addChatMessage('system', `[AGENT ROSTER]`);
if (agents.length === 0) {
addChatMessage('timmy', 'No active agent presences detected in the Nexus. The thought stream and harness pulse are the primary indicators of system activity.');
} else {
const roster = agents.map(a => `- ${(a.userData && a.userData.name) || a.name || 'Unknown'}: ${(a.userData && a.userData.status) || 'active'}`).join('\n');
addChatMessage('timmy', `Active agents:\n${roster}`);
}
break;
}
case 'portals':
openPortalAtlas();
break;
case 'heartbeat': {
const agentLog = document.getElementById('agent-log-content');
const recentEntries = agentLog ? agentLog.querySelectorAll('.agent-log-entry') : [];
const entryCount = recentEntries.length;
addChatMessage('system', `[HEARTBEAT INSPECTION]`);
addChatMessage('timmy', `Hermes heartbeat ${wsConnected ? 'active' : 'inactive'}. ${entryCount} recent entries in thought stream. WebSocket reconnect timer: ${wsReconnectTimer ? 'active' : 'idle'}. Harness pulse mesh: ${harnessPulseMesh ? 'rendering' : 'standby'}.`);
break;
}
case 'thoughts': {
const agentLog = document.getElementById('agent-log-content');
const entries = agentLog ? Array.from(agentLog.querySelectorAll('.agent-log-entry')).slice(0, 5) : [];
addChatMessage('system', `[THOUGHT STREAM]`);
if (entries.length === 0) {
addChatMessage('timmy', 'The thought stream is quiet. No recent agent entries detected.');
} else {
const summary = entries.map(e => '> ' + e.textContent.trim()).join('\n');
addChatMessage('timmy', `Recent thoughts:\n${summary}`);
}
break;
}
case 'help': {
addChatMessage('system', `[NEXUS HELP]`);
addChatMessage('timmy', `Navigation: WASD to move, mouse to look around.\n` +
`Press V to cycle: Walk / Orbit / Fly mode.\n` +
`Enter to chat. Escape to close overlays.\n` +
`Press F near a portal to enter. Press E near a vision point to read.\n` +
`Press Tab for Portal Atlas.\n` +
`The Batcave Terminal shows system logs. The Workshop Terminal shows tool output.`);
break;
}
}
}
document.getElementById('portal-close-btn').addEventListener('click', closePortalOverlay);
document.getElementById('vision-close-btn').addEventListener('click', closeVisionOverlay);
@@ -2076,7 +2119,6 @@ function connectHermes() {
addChatMessage('system', 'Hermes link established.');
updateWsHudStatus(true);
refreshWorkshopPanel();
EvenniaRoomPanel.setConnected(true);
};
// Initialize MemPalace
@@ -2105,7 +2147,6 @@ function connectHermes() {
hermesWs = null;
updateWsHudStatus(false);
refreshWorkshopPanel();
EvenniaRoomPanel.setConnected(false);
if (wsReconnectTimer) clearTimeout(wsReconnectTimer);
wsReconnectTimer = setTimeout(connectHermes, 5000);
};
@@ -2116,16 +2157,6 @@ function connectHermes() {
}
function handleHermesMessage(data) {
// ── Evennia room snapshot events (#728) ──
if (data.type === 'evennia.room_snapshot') {
EvenniaRoomPanel.onRoomSnapshot(data);
return;
}
if (data.type === 'evennia.actor_located') {
EvenniaRoomPanel.onActorLocated(data);
return;
}
if (data.type === 'chat') {
addChatMessage(data.agent || 'timmy', data.text);
} else if (data.type === 'tool_call') {

View File

@@ -1,3 +1,5 @@
shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
chdir: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
@@ -125,19 +127,6 @@
<div class="agent-log-header">AGENT THOUGHT STREAM</div>
<div id="agent-log-content" class="agent-log-content"></div>
</div>
<!-- Evennia Room Snapshot Operator Panel (#728) -->
<div id="evennia-room-panel" class="evennia-room-panel" aria-live="polite">
<div class="erp-header">
<span class="erp-icon"></span>
<span class="erp-title">EVENNIA ROOM</span>
<span class="erp-status-dot erp-offline"></span>
</div>
<div class="erp-body erp-empty-state">
<div class="erp-empty-icon"></div>
<div class="erp-empty-label">DISCONNECTED</div>
<div class="erp-empty-hint">No link to Evennia world.</div>
</div>
</div>
</div>
<!-- Bottom: Chat Interface -->
@@ -156,10 +145,39 @@
</div>
</div>
<div id="chat-quick-actions" class="chat-quick-actions">
<button class="quick-action-btn" data-action="status">System Status</button>
<button class="quick-action-btn" data-action="agents">Agent Check</button>
<button class="quick-action-btn" data-action="portals">Portal Atlas</button>
<button class="quick-action-btn" data-action="help">Help</button>
<div class="starter-label">STARTER PROMPTS</div>
<div class="starter-grid">
<button class="starter-btn" data-action="heartbeat" title="Check Timmy heartbeat and system health">
<span class="starter-icon"></span>
<span class="starter-text">Inspect Heartbeat</span>
<span class="starter-desc">System health &amp; connectivity</span>
</button>
<button class="starter-btn" data-action="portals" title="Browse the portal atlas">
<span class="starter-icon">🌐</span>
<span class="starter-text">Portal Atlas</span>
<span class="starter-desc">Browse connected worlds</span>
</button>
<button class="starter-btn" data-action="agents" title="Check active agent status">
<span class="starter-icon"></span>
<span class="starter-text">Agent Status</span>
<span class="starter-desc">Who is in the fleet</span>
</button>
<button class="starter-btn" data-action="memory" title="View memory crystals">
<span class="starter-icon"></span>
<span class="starter-text">Memory Crystals</span>
<span class="starter-desc">Inspect stored knowledge</span>
</button>
<button class="starter-btn" data-action="ask" title="Ask Timmy anything">
<span class="starter-icon"></span>
<span class="starter-text">Ask Timmy</span>
<span class="starter-desc">Start a conversation</span>
</button>
<button class="starter-btn" data-action="sovereignty" title="Learn about sovereignty">
<span class="starter-icon"></span>
<span class="starter-text">Sovereignty</span>
<span class="starter-desc">What this space is</span>
</button>
</div>
</div>
<div class="chat-input-row">
<input type="text" id="chat-input" class="chat-input" placeholder="Speak to Timmy..." autocomplete="off">

View File

@@ -1,229 +0,0 @@
// ═══════════════════════════════════════════════════════
// EVENNIA ROOM SNAPSHOT OPERATOR PANEL (Issue #728)
// ═══════════════════════════════════════════════════════
//
// Renders the current Evennia room state in the Nexus HUD.
// Consumes evennia.room_snapshot and evennia.actor_located
// events from the Hermes WebSocket bridge.
//
// States:
// offline — no WS connection
// awaiting — connected but no room data yet
// in-room — room snapshot loaded, render full panel
//
// Usage from app.js:
// EvenniaRoomPanel.init();
// EvenniaRoomPanel.onRoomSnapshot(data);
// EvenniaRoomPanel.onActorLocated(data);
// EvenniaRoomPanel.setConnected(bool);
// ═══════════════════════════════════════════════════════
export const EvenniaRoomPanel = (() => {
// ─── STATE ────────────────────────────────────────────
let _connected = false;
let _roomData = null; // latest evennia.room_snapshot payload
let _actorRoomId = null; // from evennia.actor_located
let _lastUpdate = null; // timestamp of last snapshot
let _panelEl = null; // DOM root
let _init = false;
// ─── DOM REFS ─────────────────────────────────────────
function _el(id) { return document.getElementById(id); }
// ─── INIT ─────────────────────────────────────────────
function init() {
_panelEl = _el('evennia-room-panel');
if (!_panelEl) {
console.warn('[EvenniaRoomPanel] Panel element not found in DOM.');
return;
}
_init = true;
_render();
console.log('[EvenniaRoomPanel] Initialized.');
}
// ─── EVENT HANDLERS ───────────────────────────────────
function onRoomSnapshot(data) {
_roomData = data;
_lastUpdate = data.timestamp || new Date().toISOString();
_actorRoomId = data.room_id || data.room_key || null;
_render();
}
function onActorLocated(data) {
_actorRoomId = data.room_id || data.room_key || null;
// If we get a location but no snapshot yet, show awaiting
if (!_roomData || (_roomData.room_id !== _actorRoomId && _roomData.room_key !== _actorRoomId)) {
_render();
}
}
function setConnected(connected) {
_connected = connected;
if (!connected) {
// Clear room data on disconnect — stale data is lying
_roomData = null;
_actorRoomId = null;
_lastUpdate = null;
}
_render();
}
// ─── RENDER ───────────────────────────────────────────
function _render() {
if (!_panelEl) return;
if (!_connected) {
_renderOffline();
} else if (!_roomData) {
_renderAwaiting();
} else {
_renderRoom();
}
}
function _renderOffline() {
_panelEl.innerHTML = `
<div class="erp-header">
<span class="erp-icon">◈</span>
<span class="erp-title">EVENNIA ROOM</span>
<span class="erp-status-dot erp-offline"></span>
</div>
<div class="erp-body erp-empty-state">
<div class="erp-empty-icon">⊘</div>
<div class="erp-empty-label">DISCONNECTED</div>
<div class="erp-empty-hint">No link to Evennia world.</div>
</div>
`;
_panelEl.classList.add('erp-state-offline');
_panelEl.classList.remove('erp-state-awaiting', 'erp-state-inroom');
}
function _renderAwaiting() {
_panelEl.innerHTML = `
<div class="erp-header">
<span class="erp-icon">◈</span>
<span class="erp-title">EVENNIA ROOM</span>
<span class="erp-status-dot erp-online"></span>
</div>
<div class="erp-body erp-empty-state">
<div class="erp-empty-icon erp-pulse">◎</div>
<div class="erp-empty-label">AWAITING SNAPSHOT</div>
<div class="erp-empty-hint">Connected. Waiting for room data&hellip;</div>
</div>
`;
_panelEl.classList.add('erp-state-awaiting');
_panelEl.classList.remove('erp-state-offline', 'erp-state-inroom');
}
function _renderRoom() {
const room = _roomData;
const title = _esc(room.title || room.room_name || room.room_key || 'Unknown Room');
const desc = _esc(room.desc || 'No description available.');
const exits = Array.isArray(room.exits) ? room.exits : [];
const objects = Array.isArray(room.objects) ? room.objects : [];
const occupants = Array.isArray(room.occupants) ? room.occupants : [];
const roomId = _esc(room.room_id || room.room_key || '—');
const timeStr = _formatTime(_lastUpdate);
// Build exits list
let exitsHtml = '';
if (exits.length > 0) {
exitsHtml = exits.map(e => {
const name = _esc(e.key || e.name || '?');
const dest = _esc(e.destination_name || e.destination_id || e.destination_key || '');
return `<div class="erp-exit-row">
<span class="erp-exit-arrow">→</span>
<span class="erp-exit-name">${name}</span>
${dest ? `<span class="erp-exit-dest">${dest}</span>` : ''}
</div>`;
}).join('');
} else {
exitsHtml = '<div class="erp-none">No visible exits.</div>';
}
// Build objects list
let objectsHtml = '';
if (objects.length > 0) {
objectsHtml = objects.map(o => {
const name = _esc(o.key || o.id || '?');
const desc = _esc(o.short_desc || '');
return `<div class="erp-object-row">
<span class="erp-object-icon">▪</span>
<span class="erp-object-name">${name}</span>
${desc ? `<span class="erp-object-desc">${desc}</span>` : ''}
</div>`;
}).join('');
} else {
objectsHtml = '<div class="erp-none">No visible objects.</div>';
}
// Build occupants list
let occupantsHtml = '';
if (occupants.length > 0) {
occupantsHtml = occupants.map(o => {
const name = _esc(typeof o === 'string' ? o : (o.name || o.key || '?'));
return `<span class="erp-occupant">${name}</span>`;
}).join('');
}
_panelEl.innerHTML = `
<div class="erp-header">
<span class="erp-icon">◈</span>
<span class="erp-title">${title}</span>
<span class="erp-status-dot erp-online"></span>
</div>
<div class="erp-body">
<div class="erp-room-id">${roomId}</div>
<div class="erp-desc">${desc}</div>
<div class="erp-section">
<div class="erp-section-label">EXITS</div>
<div class="erp-exits">${exitsHtml}</div>
</div>
<div class="erp-section">
<div class="erp-section-label">OBJECTS</div>
<div class="erp-objects">${objectsHtml}</div>
</div>
${occupantsHtml ? `
<div class="erp-section">
<div class="erp-section-label">OCCUPANTS</div>
<div class="erp-occupants">${occupantsHtml}</div>
</div>` : ''}
<div class="erp-footer">
<span class="erp-time">${timeStr}</span>
</div>
</div>
`;
_panelEl.classList.add('erp-state-inroom');
_panelEl.classList.remove('erp-state-offline', 'erp-state-awaiting');
}
// ─── UTILS ────────────────────────────────────────────
function _esc(str) {
const d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
function _formatTime(isoStr) {
if (!isoStr) return '—';
try {
const d = new Date(isoStr);
return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
} catch {
return '—';
}
}
// ─── PUBLIC API ───────────────────────────────────────
return { init, onRoomSnapshot, onActorLocated, setConnected };
})();

300
style.css
View File

@@ -983,7 +983,7 @@ canvas#nexus-canvas {
.chat-quick-actions {
display: flex;
flex-wrap: wrap;
flex-direction: column;
gap: 6px;
padding: 8px 12px;
border-top: 1px solid var(--color-border);
@@ -991,6 +991,75 @@ canvas#nexus-canvas {
pointer-events: auto;
}
.chat-quick-actions.hidden {
display: none;
}
.starter-label {
font-family: var(--font-display);
font-size: 9px;
letter-spacing: 0.15em;
color: var(--color-primary-dim);
text-transform: uppercase;
padding: 0 2px;
}
.starter-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
}
.starter-btn {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1px;
background: rgba(74, 240, 192, 0.06);
border: 1px solid rgba(74, 240, 192, 0.15);
color: var(--color-primary);
font-family: var(--font-body);
padding: 6px 8px;
cursor: pointer;
transition: all var(--transition-ui);
text-align: left;
}
.starter-btn:hover {
background: rgba(74, 240, 192, 0.15);
border-color: var(--color-primary);
color: #fff;
}
.starter-btn:hover .starter-icon {
color: #fff;
}
.starter-btn:active {
transform: scale(0.97);
}
.starter-icon {
font-size: 12px;
color: var(--color-primary);
line-height: 1;
}
.starter-text {
font-size: 10px;
font-weight: 600;
white-space: nowrap;
}
.starter-desc {
font-size: 8px;
color: rgba(74, 240, 192, 0.5);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
/* Add hover effect for MemPalace mining button */
.quick-action-btn:hover {
background: var(--color-primary-dim);
@@ -1136,6 +1205,9 @@ canvas#nexus-canvas {
.hud-location {
font-size: var(--text-xs);
}
.starter-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
@@ -1580,229 +1652,3 @@ canvas#nexus-canvas {
text-transform: uppercase;
}
/* ═══════════════════════════════════════════════════════
EVENNIA ROOM SNAPSHOT OPERATOR PANEL (#728)
═══════════════════════════════════════════════════════ */
.evennia-room-panel {
width: 280px;
background: rgba(5, 5, 16, 0.85);
backdrop-filter: blur(12px);
border: 1px solid rgba(74, 240, 192, 0.18);
border-left: 3px solid var(--color-primary, #4af0c0);
border-radius: 6px;
font-family: var(--font-body, 'JetBrains Mono', monospace);
font-size: 11px;
color: var(--color-text, #e0f0ff);
pointer-events: auto;
overflow: hidden;
transition: border-color 0.3s ease;
}
.evennia-room-panel.erp-state-offline {
border-left-color: var(--color-danger, #ff4466);
}
.evennia-room-panel.erp-state-awaiting {
border-left-color: var(--color-warning, #ffaa22);
}
.evennia-room-panel.erp-state-inroom {
border-left-color: var(--color-primary, #4af0c0);
}
.erp-header {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-bottom: 1px solid rgba(74, 240, 192, 0.1);
background: rgba(74, 240, 192, 0.04);
}
.erp-icon {
font-size: 12px;
color: var(--color-primary, #4af0c0);
}
.erp-title {
font-size: 10px;
font-weight: 700;
letter-spacing: 1.2px;
color: var(--color-primary, #4af0c0);
flex: 1;
}
.erp-status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
.erp-status-dot.erp-offline {
background: var(--color-danger, #ff4466);
box-shadow: 0 0 6px var(--color-danger, #ff4466);
}
.erp-status-dot.erp-online {
background: var(--color-primary, #4af0c0);
box-shadow: 0 0 6px var(--color-primary, #4af0c0);
}
.erp-body {
padding: 8px 10px;
max-height: 320px;
overflow-y: auto;
}
/* Empty / offline states */
.erp-empty-state {
text-align: center;
padding: 18px 10px;
}
.erp-empty-icon {
font-size: 22px;
color: rgba(74, 240, 192, 0.25);
margin-bottom: 6px;
}
.erp-empty-icon.erp-pulse {
animation: erpPulse 2s ease-in-out infinite;
}
@keyframes erpPulse {
0%, 100% { opacity: 0.35; }
50% { opacity: 0.9; }
}
.erp-empty-label {
font-size: 10px;
font-weight: 700;
letter-spacing: 1.5px;
color: var(--color-text-muted, #8a9ab8);
margin-bottom: 4px;
}
.erp-empty-hint {
font-size: 10px;
color: rgba(138, 154, 184, 0.55);
}
/* Room content */
.erp-room-id {
font-size: 9px;
color: rgba(138, 154, 184, 0.4);
letter-spacing: 0.8px;
text-transform: uppercase;
margin-bottom: 6px;
}
.erp-desc {
font-size: 11px;
color: rgba(224, 240, 255, 0.8);
line-height: 1.45;
margin-bottom: 10px;
border-left: 2px solid rgba(74, 240, 192, 0.15);
padding-left: 8px;
}
.erp-section {
margin-bottom: 8px;
}
.erp-section-label {
font-size: 9px;
font-weight: 700;
letter-spacing: 1.5px;
color: var(--color-primary, #4af0c0);
margin-bottom: 4px;
opacity: 0.7;
}
.erp-none {
font-size: 10px;
color: rgba(138, 154, 184, 0.35);
font-style: italic;
padding: 2px 0;
}
/* Exits */
.erp-exit-row {
display: flex;
align-items: center;
gap: 6px;
padding: 2px 0;
font-size: 11px;
}
.erp-exit-arrow {
color: var(--color-secondary, #7b5cff);
font-weight: 700;
}
.erp-exit-name {
color: var(--color-secondary, #7b5cff);
font-weight: 600;
}
.erp-exit-dest {
color: rgba(138, 154, 184, 0.45);
font-size: 10px;
}
/* Objects */
.erp-object-row {
display: flex;
align-items: baseline;
gap: 5px;
padding: 2px 0;
font-size: 11px;
}
.erp-object-icon {
color: var(--color-gold, #ffd700);
font-size: 8px;
}
.erp-object-name {
color: rgba(224, 240, 255, 0.75);
font-weight: 500;
}
.erp-object-desc {
color: rgba(138, 154, 184, 0.45);
font-size: 10px;
}
/* Occupants */
.erp-occupants {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.erp-occupant {
font-size: 10px;
padding: 2px 6px;
background: rgba(74, 240, 192, 0.08);
border: 1px solid rgba(74, 240, 192, 0.15);
border-radius: 3px;
color: var(--color-primary, #4af0c0);
}
/* Footer */
.erp-footer {
margin-top: 6px;
padding-top: 4px;
border-top: 1px solid rgba(74, 240, 192, 0.06);
text-align: right;
}
.erp-time {
font-size: 9px;
color: rgba(138, 154, 184, 0.3);
letter-spacing: 0.5px;
}