Compare commits

...

15 Commits

Author SHA1 Message Date
268987e9ac feat(mnemosyne): wire memory search UI - toggle, render, fly-to, keyboard shortcut (#1208)
Some checks failed
CI / test (pull_request) Failing after 8s
CI / validate (pull_request) Failing after 12s
Review Approval Gate / verify-review (pull_request) Failing after 3s
2026-04-11 02:52:19 +00:00
2fa85e3ee5 feat(mnemosyne): add memory search panel styles (#1208) 2026-04-11 02:51:52 +00:00
0165fe1860 feat(mnemosyne): add memory search panel markup (#1208) 2026-04-11 02:51:30 +00:00
c947976aac feat(mnemosyne): add searchByContent() for text search through archive (#1208) 2026-04-11 02:51:14 +00:00
4f8e0330c5 [Mnemosyne] Integrate MemoryOptimizer into app.js
Some checks failed
Deploy Nexus / deploy (push) Failing after 3s
Staging Verification Gate / verify-staging (push) Failing after 4s
2026-04-11 01:39:58 +00:00
c3847cc046 [Mnemosyne] Add scripts/smoke.mjs (GOFAI improvements and guardrails)
Some checks failed
Deploy Nexus / deploy (push) Failing after 2s
Staging Verification Gate / verify-staging (push) Failing after 2s
2026-04-11 01:39:44 +00:00
4c4677842d [Mnemosyne] Add scripts/guardrails.sh (GOFAI improvements and guardrails)
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
Staging Verification Gate / verify-staging (push) Has been cancelled
2026-04-11 01:39:43 +00:00
f0d929a177 [Mnemosyne] Add nexus/components/memory-optimizer.js (GOFAI improvements and guardrails)
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
Staging Verification Gate / verify-staging (push) Has been cancelled
2026-04-11 01:39:42 +00:00
a22464506c Update style.css (manual merge)
Some checks failed
Deploy Nexus / deploy (push) Failing after 2s
Staging Verification Gate / verify-staging (push) Failing after 2s
2026-04-11 01:35:17 +00:00
be55195815 Update index.html (manual merge)
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
Staging Verification Gate / verify-staging (push) Has been cancelled
2026-04-11 01:35:15 +00:00
7fb086976e Update app.js (manual merge)
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
Staging Verification Gate / verify-staging (push) Has been cancelled
2026-04-11 01:35:13 +00:00
c192b05cc1 Update nexus/components/spatial-memory.js (manual merge)
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
Staging Verification Gate / verify-staging (push) Has been cancelled
2026-04-11 01:35:12 +00:00
45ddd65d16 Merge pull request 'feat: Project Genie + Nano Banana concept pack for The Nexus' (#1206) from mimo/build/issue-680 into main
Some checks failed
Deploy Nexus / deploy (push) Failing after 2s
Staging Verification Gate / verify-staging (push) Has been cancelled
2026-04-11 01:33:55 +00:00
9984cb733e Merge pull request 'feat: [VALIDATION] Browser smoke and visual validation suite' (#1207) from mimo/build/issue-686 into main
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
Staging Verification Gate / verify-staging (push) Has been cancelled
2026-04-11 01:33:53 +00:00
Alexander Whitestone
3367ce5438 feat: Project Genie + Nano Banana concept pack for The Nexus (closes #680)
Some checks failed
CI / test (pull_request) Failing after 11s
CI / validate (pull_request) Failing after 11s
Review Approval Gate / verify-review (pull_request) Failing after 3s
Complete concept generation pipeline:
- shot-list.yaml: 17 shots across 5 priorities (environments, portals, landmarks, skyboxes, textures)
- prompts/: 5 YAML prompt packs with 17 detailed generation prompts
- pipeline.md: Concept-to-Three.js translation workflow
- storage-policy.md: Repo vs local split for binary media
- references/palette.md: Canonical Nexus color/material/lighting spec

All prompts match existing Nexus visual language (Orbitron/JetBrains,
#4af0c0/#7b5cff/#ffd700 palette, cyberpunk cathedral mood).
Genie world prompts designed for explorable 3D prototyping.
Nano Banana prompts designed for concept art that translates to
specific Three.js geometry, materials, and post-processing.
2026-04-10 21:17:08 -04:00
17 changed files with 1748 additions and 1666 deletions

859
app.js
View File

@@ -1,14 +1,10 @@
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';
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 { TimelineScrubber } from './nexus/components/timeline-scrubber.js';
import { MemoryParticles } from './nexus/components/memory-particles.js';
import { MemoryOptimizer } from './nexus/components/memory-optimizer.js';
// ═══════════════════════════════════════════
// NEXUS v1.1 — Portal System Update
@@ -49,6 +45,7 @@ let particles, dustParticles;
let debugOverlay;
let frameCount = 0, lastFPSTime = 0, fps = 0;
let chatOpen = true;
let memoryFeedEntries = []; // Mnemosyne: recent memory events for feed panel
let loadProgress = 0;
let performanceTier = 'high';
@@ -710,14 +707,21 @@ async function init() {
createWorkshopTerminal();
createAshStorm();
SpatialMemory.init(scene);
MemoryParticles.init(scene);
SpatialMemory.setOnMemoryPlaced(MemoryParticles.onMemoryPlaced);
TimelineScrubber.init(SpatialMemory);
SessionRooms.init(scene, camera, null);
updateLoad(90);
loadSession();
connectHermes();
// Mnemosyne: Periodic GOFAI Optimization
setInterval(() => {
console.info('[Mnemosyne] Running periodic optimization...');
MemoryOptimizer.optimize(SpatialMemory);
}, 1000 * 60 * 10); // Every 10 minutes
// Wire memory search input
const searchInput = document.getElementById('memory-search-input');
if (searchInput) searchInput.addEventListener('input', onMemorySearchInput);
fetchGiteaData();
setInterval(fetchGiteaData, 30000); // Refresh every 30s
@@ -1892,7 +1896,7 @@ function setupControls() {
orbitState.lastX = e.clientX;
orbitState.lastY = e.clientY;
// Raycasting for portals and memory crystals
// Raycasting for portals
if (!portalOverlayActive) {
const mouse = new THREE.Vector2(
(e.clientX / window.innerWidth) * 2 - 1,
@@ -1900,47 +1904,12 @@ function setupControls() {
);
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
// Priority 1: Portals
const portalHits = raycaster.intersectObjects(portals.map(p => p.ring));
if (portalHits.length > 0) {
const clickedRing = portalHits[0].object;
const intersects = raycaster.intersectObjects(portals.map(p => p.ring));
if (intersects.length > 0) {
const clickedRing = intersects[0].object;
const portal = portals.find(p => p.ring === clickedRing);
if (portal) { activatePortal(portal); return; }
if (portal) activatePortal(portal);
}
// Priority 2: Memory crystals (Mnemosyne)
const crystalMeshes = SpatialMemory.getCrystalMeshes();
if (crystalMeshes.length > 0) {
const crystalHits = raycaster.intersectObjects(crystalMeshes, false);
if (crystalHits.length > 0) {
const hitMesh = crystalHits[0].object;
const memInfo = SpatialMemory.getMemoryFromMesh(hitMesh);
if (memInfo) {
SpatialMemory.highlightMemory(memInfo.data.id);
// Memory access trail particles
if (camera) {
MemoryParticles.onMemoryAccessed(camera.position, hitMesh.position, memInfo.data.category || memInfo.region || 'working');
}
showMemoryPanel(memInfo, e.clientX, e.clientY);
return;
}
}
}
// Priority 3: Session rooms (Mnemosyne #1171)
const roomMeshes = SessionRooms.getClickableMeshes();
if (roomMeshes.length > 0) {
const roomHits = raycaster.intersectObjects(roomMeshes, false);
if (roomHits.length > 0) {
const session = SessionRooms.handleRoomClick(roomHits[0].object);
if (session) { _showSessionRoomPanel(session); return; }
}
}
// Clicked empty space — dismiss panel
dismissMemoryPanel();
_dismissSessionRoomPanel();
}
}
});
@@ -1993,97 +1962,30 @@ function setupControls() {
document.getElementById('chat-quick-actions').addEventListener('click', (e) => {
const btn = e.target.closest('.quick-action-btn');
if (!btn) return;
handleQuickAction(btn.dataset.action);
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;
}
});
// ═══ 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);
document.getElementById('atlas-toggle-btn').addEventListener('click', openPortalAtlas);
document.getElementById('atlas-close-btn').addEventListener('click', closePortalAtlas);
// Mnemosyne export/import (#1174)
document.getElementById('mnemosyne-export-btn').addEventListener('click', () => {
const result = SpatialMemory.exportToFile();
if (result) {
addChatMessage('system', 'Mnemosyne: Exported ' + result.count + ' memories to ' + result.filename);
}
});
document.getElementById('mnemosyne-import-btn').addEventListener('click', () => {
document.getElementById('mnemosyne-import-file').click();
});
document.getElementById('mnemosyne-import-file').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const result = await SpatialMemory.importFromFile(file);
addChatMessage('system', 'Mnemosyne: Imported ' + result.count + ' of ' + result.total + ' memories');
} catch (err) {
addChatMessage('system', 'Mnemosyne: Import failed — ' + err.message);
}
e.target.value = '';
});
}
function sendChatMessage(overrideText = null) {
@@ -2152,6 +2054,14 @@ function connectHermes() {
addChatMessage('system', 'Hermes link established.');
updateWsHudStatus(true);
refreshWorkshopPanel();
// Mnemosyne: request memory sync from Hermes
try {
hermesWs.send(JSON.stringify({ type: 'memory', action: 'sync_request' }));
console.info('[Mnemosyne] Sent sync_request to Hermes');
} catch (e) {
console.warn('[Mnemosyne] Failed to send sync_request:', e);
}
};
// Initialize MemPalace
@@ -2202,6 +2112,8 @@ function handleHermesMessage(data) {
recentToolOutputs.push({ type: 'result', agent: data.agent || 'SYSTEM', content });
addToolMessage(data.agent || 'SYSTEM', 'result', content);
refreshWorkshopPanel();
} else if (data.type === 'memory') {
handleMemoryMessage(data);
} else if (data.type === 'history') {
const container = document.getElementById('chat-messages');
container.innerHTML = '';
@@ -2213,6 +2125,209 @@ function handleHermesMessage(data) {
}
}
// ═══════════════════════════════════════════
// ═══ MNEMOSYNE — MEMORY SEARCH (#1208) ═══
let memorySearchVisible = false;
let memorySearchDebounce = null;
function toggleMemorySearch() {
const panel = document.getElementById('memory-search-panel');
const input = document.getElementById('memory-search-input');
if (!panel) return;
memorySearchVisible = !memorySearchVisible;
panel.style.display = memorySearchVisible ? 'block' : 'none';
if (memorySearchVisible && input) {
input.value = '';
input.focus();
renderMemorySearchResults([]);
}
}
function renderMemorySearchResults(results) {
const container = document.getElementById('memory-search-results');
if (!container) return;
if (results.length === 0) {
container.innerHTML = '<div class="memory-search-empty">' +
(document.getElementById('memory-search-input')?.value ? 'No memories found' : 'Type to search your archive...') +
'</div>';
return;
}
container.innerHTML = results.map(r => {
const regionDef = SpatialMemory.REGIONS[r.category] || SpatialMemory.REGIONS.working;
const dotColor = '#' + regionDef.color.toString(16).padStart(6, '0');
const truncated = r.content.length > 55 ? r.content.slice(0, 55) + '\u2026' : r.content;
return '<div class="memory-search-result" onclick="flyToMemory(\'' + r.id + '\')">' +
'<div class="memory-search-dot" style="background:' + dotColor + '"></div>' +
'<span class="memory-search-text">' + truncated + '</span>' +
'<span class="memory-search-meta">' + r.category + '</span>' +
'</div>';
}).join('');
}
function onMemorySearchInput(e) {
const query = e.target.value;
if (memorySearchDebounce) clearTimeout(memorySearchDebounce);
memorySearchDebounce = setTimeout(() => {
if (!query || query.trim().length === 0) {
renderMemorySearchResults([]);
return;
}
const results = SpatialMemory.searchByContent(query, { maxResults: 15 });
renderMemorySearchResults(results);
}, 150);
}
function flyToMemory(memId) {
const memories = SpatialMemory.getAllMemories();
const mem = memories.find(m => m.id === memId);
if (!mem) return;
// Highlight the crystal
SpatialMemory.highlightMemory(memId);
// Fly camera to memory position (if camera controls exist)
if (typeof camera !== 'undefined' && camera.position) {
const target = new THREE.Vector3(mem.position[0], mem.position[1] + 3, mem.position[2] + 5);
// Simple lerp animation
const start = camera.position.clone();
let t = 0;
const flyAnim = () => {
t += 0.03;
if (t > 1) t = 1;
camera.position.lerpVectors(start, target, t);
if (t < 1) requestAnimationFrame(flyAnim);
};
flyAnim();
}
// Close search panel
if (memorySearchVisible) toggleMemorySearch();
// Show in memory feed
addMemoryFeedEntry('update', { content: 'Navigated to: ' + (mem.content || mem.id).slice(0, 40), id: memId });
}
// Keyboard shortcut: Ctrl+K or / to toggle search
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey && e.key === 'k') || (e.key === '/' && !e.target.matches('input, textarea'))) {
e.preventDefault();
toggleMemorySearch();
}
if (e.key === 'Escape' && memorySearchVisible) {
toggleMemorySearch();
}
});
// MNEMOSYNE — LIVE MEMORY BRIDGE
// ═══════════════════════════════════════════
/**
* Handle incoming memory messages from Hermes WS.
* Actions: place, remove, update, sync_response
*/
/**
* Clear all entries from the memory feed.
*/
function clearMemoryFeed() {
memoryFeedEntries = [];
renderMemoryFeed();
console.info('[Mnemosyne] Memory feed cleared');
}
function handleMemoryMessage(data) {
const action = data.action;
const memory = data.memory;
const memories = data.memories;
if (action === 'place' && memory) {
const placed = SpatialMemory.placeMemory(memory);
if (placed) {
addMemoryFeedEntry('place', memory);
console.info('[Mnemosyne] Memory placed via WS:', memory.id);
}
} else if (action === 'remove' && memory) {
SpatialMemory.removeMemory(memory.id);
addMemoryFeedEntry('remove', memory);
console.info('[Mnemosyne] Memory removed via WS:', memory.id);
} else if (action === 'update' && memory) {
SpatialMemory.updateMemory(memory.id, memory);
addMemoryFeedEntry('update', memory);
console.info('[Mnemosyne] Memory updated via WS:', memory.id);
} else if (action === 'sync_response' && Array.isArray(memories)) {
const count = SpatialMemory.importMemories(memories);
addMemoryFeedEntry('sync', { content: count + ' memories synced', id: 'sync' });
console.info('[Mnemosyne] Synced', count, 'memories from Hermes');
} else {
console.warn('[Mnemosyne] Unknown memory action:', action);
}
}
/**
* Add an entry to the memory activity feed panel.
*/
function addMemoryFeedEntry(action, memory) {
const entry = {
action,
content: memory.content || memory.id || '(unknown)',
category: memory.category || 'working',
timestamp: new Date().toISOString()
};
memoryFeedEntries.unshift(entry);
if (memoryFeedEntries.length > 5) memoryFeedEntries.pop();
renderMemoryFeed();
// Auto-dismiss entries older than 5 minutes
setTimeout(() => {
const idx = memoryFeedEntries.indexOf(entry);
if (idx > -1) {
memoryFeedEntries.splice(idx, 1);
renderMemoryFeed();
}
}, 300000);
}
/**
* Render the memory feed panel.
*/
function renderMemoryFeed() {
const container = document.getElementById('memory-feed-list');
if (!container) return;
container.innerHTML = '';
memoryFeedEntries.forEach(entry => {
const el = document.createElement('div');
el.className = 'memory-feed-entry memory-feed-' + entry.action;
const regionDef = SpatialMemory.REGIONS[entry.category] || SpatialMemory.REGIONS.working;
const dotColor = '#' + regionDef.color.toString(16).padStart(6, '0');
const time = new Date(entry.timestamp).toLocaleTimeString();
const truncated = entry.content.length > 40 ? entry.content.slice(0, 40) + '\u2026' : entry.content;
const actionIcon = { place: '\u2795', remove: '\u2796', update: '\u270F', sync: '\u21C4' }[entry.action] || '\u2022';
el.innerHTML = '<span class="memory-feed-dot" style="background:' + dotColor + '"></span>' +
'<span class="memory-feed-action">' + actionIcon + '</span>' +
'<span class="memory-feed-content">' + truncated + '</span>' +
'<span class="memory-feed-time">' + time + '</span>';
container.appendChild(el);
});
// Show feed if there are entries
const panel = document.getElementById('memory-feed');
if (panel) panel.style.display = memoryFeedEntries.length > 0 ? 'block' : 'none';
}
function updateWsHudStatus(connected) {
// Update MemPalace status alongside regular WS status
updateMemPalaceStatus();
@@ -2507,15 +2622,6 @@ function activatePortal(portal) {
overlay.style.display = 'flex';
// Readiness detail for game-world portals
const readinessEl = document.getElementById('portal-readiness-detail');
if (portal.config.portal_type === 'game-world' && portal.config.readiness_steps) {
renderReadinessDetail(readinessEl, portal.config);
readinessEl.style.display = 'block';
} else {
readinessEl.style.display = 'none';
}
if (portal.config.destination && portal.config.destination.url) {
redirectBox.style.display = 'block';
errorBox.style.display = 'none';
@@ -2537,37 +2643,6 @@ function activatePortal(portal) {
}
}
// ═══ READINESS RENDERING ═══
function renderReadinessDetail(container, config) {
const steps = config.readiness_steps || {};
const stepKeys = ['downloaded', 'runtime_ready', 'launched', 'harness_bridged'];
let html = '<div class="portal-readiness-title">READINESS PIPELINE</div>';
let firstUndone = true;
stepKeys.forEach(key => {
const step = steps[key];
if (!step) return;
const cls = step.done ? 'done' : (firstUndone ? 'current' : '');
if (!step.done) firstUndone = false;
html += `<div class="portal-readiness-step ${cls}">
<span class="step-dot"></span>
<span>${step.label || key}</span>
</div>`;
});
if (config.blocked_reason) {
html += `<div class="portal-readiness-blocked">&#x26A0; ${config.blocked_reason}</div>`;
}
const doneCount = stepKeys.filter(k => steps[k]?.done).length;
const canEnter = doneCount === stepKeys.length && config.destination?.url;
if (!canEnter) {
html += `<div class="portal-readiness-hint">Cannot enter yet — ${stepKeys.length - doneCount} step${stepKeys.length - doneCount > 1 ? 's' : ''} remaining.</div>`;
}
container.innerHTML = html;
}
function closePortalOverlay() {
portalOverlayActive = false;
document.getElementById('portal-overlay').style.display = 'none';
@@ -2648,42 +2723,12 @@ function populateAtlas() {
const statusClass = `status-${config.status || 'online'}`;
// Build readiness section for game-world portals
let readinessHtml = '';
if (config.portal_type === 'game-world' && config.readiness_steps) {
const stepKeys = ['downloaded', 'runtime_ready', 'launched', 'harness_bridged'];
const steps = config.readiness_steps;
const doneCount = stepKeys.filter(k => steps[k]?.done).length;
const pct = Math.round((doneCount / stepKeys.length) * 100);
const barColor = config.color || '#ffd700';
readinessHtml = `<div class="atlas-card-readiness">
<div class="readiness-bar-track">
<div class="readiness-bar-fill" style="width:${pct}%;background:${barColor};"></div>
</div>
<div class="readiness-steps-mini">`;
let firstUndone = true;
stepKeys.forEach(key => {
const step = steps[key];
if (!step) return;
const cls = step.done ? 'done' : (firstUndone ? 'current' : '');
if (!step.done) firstUndone = false;
readinessHtml += `<span class="readiness-step ${cls}">${step.label || key}</span>`;
});
readinessHtml += '</div>';
if (config.blocked_reason) {
readinessHtml += `<div class="atlas-card-blocked">&#x26A0; ${config.blocked_reason}</div>`;
}
readinessHtml += '</div>';
}
card.innerHTML = `
<div class="atlas-card-header">
<div class="atlas-card-name">${config.name}</div>
<div class="atlas-card-status ${statusClass}">${config.readiness_state || config.status || 'ONLINE'}</div>
<div class="atlas-card-status ${statusClass}">${config.status || 'ONLINE'}</div>
</div>
<div class="atlas-card-desc">${config.description}</div>
${readinessHtml}
<div class="atlas-card-footer">
<div class="atlas-card-coord">X:${config.position.x} Z:${config.position.z}</div>
<div class="atlas-card-type">${config.destination?.type?.toUpperCase() || 'UNKNOWN'}</div>
@@ -2701,14 +2746,11 @@ function populateAtlas() {
document.getElementById('atlas-online-count').textContent = onlineCount;
document.getElementById('atlas-standby-count').textContent = standbyCount;
// Update Bannerlord HUD status with honest readiness state
// Update Bannerlord HUD status
const bannerlord = portals.find(p => p.config.id === 'bannerlord');
if (bannerlord) {
const statusEl = document.getElementById('bannerlord-status');
const state = bannerlord.config.readiness_state || bannerlord.config.status || 'offline';
statusEl.className = 'hud-status-item ' + state;
const labelEl = statusEl.querySelector('.status-label');
if (labelEl) labelEl.textContent = state.toUpperCase().replace(/_/g, ' ');
statusEl.className = 'hud-status-item ' + (bannerlord.config.status || 'offline');
}
}
@@ -2735,238 +2777,6 @@ function focusPortal(portal) {
let lastThoughtTime = 0;
let pulseTimer = 0;
// ═══════════════════════════════════════════
// MNEMOSYNE — MEMORY CRYSTAL INSPECTION
// ═══════════════════════════════════════════
// ── pin state for memory panel ──
let _memPanelPinned = false;
/** Convert a packed hex color integer to "r,g,b" string for CSS rgba(). */
function _hexToRgb(hex) {
return ((hex >> 16) & 255) + ',' + ((hex >> 8) & 255) + ',' + (hex & 255);
}
/**
* Position the panel near the screen click coordinates, keeping it on-screen.
*/
function _positionPanel(panel, clickX, clickY) {
const W = window.innerWidth;
const H = window.innerHeight;
const panelW = 356; // matches CSS width + padding
const panelH = 420; // generous estimate
const margin = 12;
let left = clickX + 24;
if (left + panelW > W - margin) left = clickX - panelW - 24;
left = Math.max(margin, Math.min(W - panelW - margin, left));
let top = clickY - 80;
top = Math.max(margin, Math.min(H - panelH - margin, top));
panel.style.right = 'auto';
panel.style.top = top + 'px';
panel.style.left = left + 'px';
panel.style.transform = 'none';
}
/**
* Navigate to (highlight + show panel for) a memory crystal by id.
*/
function _navigateToMemory(memId) {
SpatialMemory.highlightMemory(memId);
addChatMessage('system', `Focus: ${memId.replace(/_/g, ' ')}`);
// Access trail particles
const meshes = SpatialMemory.getCrystalMeshes();
for (const mesh of meshes) {
if (mesh.userData && mesh.userData.memId === memId) {
const memInfo = SpatialMemory.getMemoryFromMesh(mesh);
if (memInfo && camera) {
MemoryParticles.onMemoryAccessed(camera.position, mesh.position, memInfo.data.category || memInfo.region || 'working');
}
break;
}
}
const meshes = SpatialMemory.getCrystalMeshes();
for (const mesh of meshes) {
if (mesh.userData && mesh.userData.memId === memId) {
const memInfo = SpatialMemory.getMemoryFromMesh(mesh);
if (memInfo) { showMemoryPanel(memInfo); break; }
}
}
}
/**
* Show the holographic detail panel for a clicked crystal.
* @param {object} memInfo — { data, region } from SpatialMemory.getMemoryFromMesh()
* @param {number} [clickX] — screen X of the click (for panel positioning)
* @param {number} [clickY] — screen Y of the click
*/
function showMemoryPanel(memInfo, clickX, clickY) {
const panel = document.getElementById('memory-panel');
if (!panel) return;
const { data, region } = memInfo;
const regionDef = SpatialMemory.REGIONS[region] || SpatialMemory.REGIONS.working;
const colorHex = regionDef.color.toString(16).padStart(6, '0');
const colorRgb = _hexToRgb(regionDef.color);
// Header — region dot + label
document.getElementById('memory-panel-region').textContent = regionDef.label;
document.getElementById('memory-panel-region-dot').style.background = '#' + colorHex;
// Category badge
const badge = document.getElementById('memory-panel-category-badge');
if (badge) {
badge.textContent = (data.category || region || 'memory').toUpperCase();
badge.style.background = 'rgba(' + colorRgb + ',0.16)';
badge.style.color = '#' + colorHex;
badge.style.borderColor = 'rgba(' + colorRgb + ',0.4)';
}
// Entity name (humanised id)
const entityEl = document.getElementById('memory-panel-entity-name');
if (entityEl) entityEl.textContent = (data.id || '\u2014').replace(/_/g, ' ');
// Fact content
document.getElementById('memory-panel-content').textContent = data.content || '(empty)';
// Trust score bar
const strength = data.strength != null ? data.strength : 0.7;
const trustFill = document.getElementById('memory-panel-trust-fill');
const trustVal = document.getElementById('memory-panel-trust-value');
if (trustFill) {
trustFill.style.width = (strength * 100).toFixed(0) + '%';
trustFill.style.background = '#' + colorHex;
}
if (trustVal) trustVal.textContent = (strength * 100).toFixed(0) + '%';
// Meta rows
document.getElementById('memory-panel-id').textContent = data.id || '\u2014';
document.getElementById('memory-panel-source').textContent = data.source || 'unknown';
document.getElementById('memory-panel-time').textContent = data.timestamp ? new Date(data.timestamp).toLocaleString() : '\u2014';
// Related entities — clickable links
const connEl = document.getElementById('memory-panel-connections');
connEl.innerHTML = '';
if (data.connections && data.connections.length > 0) {
data.connections.forEach(cid => {
const btn = document.createElement('button');
btn.className = 'memory-conn-tag memory-conn-link';
btn.textContent = cid.replace(/_/g, ' ');
btn.title = 'Go to: ' + cid;
btn.addEventListener('click', (ev) => { ev.stopPropagation(); _navigateToMemory(cid); });
connEl.appendChild(btn);
});
} else {
connEl.innerHTML = '<span style="color:var(--color-text-muted)">None</span>';
}
// Pin button — reset on fresh open
_memPanelPinned = false;
const pinBtn = document.getElementById('memory-panel-pin');
if (pinBtn) {
pinBtn.classList.remove('pinned');
pinBtn.title = 'Pin panel';
pinBtn.onclick = () => {
_memPanelPinned = !_memPanelPinned;
pinBtn.classList.toggle('pinned', _memPanelPinned);
pinBtn.title = _memPanelPinned ? 'Unpin panel' : 'Pin panel';
};
}
// Positioning — near click if coords provided
if (clickX != null && clickY != null) {
_positionPanel(panel, clickX, clickY);
}
// Fade in
panel.classList.remove('memory-panel-fade-out');
panel.style.display = 'flex';
}
/**
* Dismiss the panel (respects pin). Called on empty-space click.
*/
function dismissMemoryPanel() {
if (_memPanelPinned) return;
_dismissMemoryPanelForce();
}
/**
* Force-dismiss the panel regardless of pin state. Used by the close button.
*/
function _dismissMemoryPanelForce() {
_memPanelPinned = false;
SpatialMemory.clearHighlight();
const panel = document.getElementById('memory-panel');
if (!panel || panel.style.display === 'none') return;
panel.classList.add('memory-panel-fade-out');
setTimeout(() => {
panel.style.display = 'none';
panel.classList.remove('memory-panel-fade-out');
}, 200);
}
/**
* Show the session room HUD panel when a chamber is entered.
* @param {object} session — { id, timestamp, facts[] }
*/
function _showSessionRoomPanel(session) {
const panel = document.getElementById('session-room-panel');
if (!panel) return;
const dt = session.timestamp ? new Date(session.timestamp) : new Date();
const tsEl = document.getElementById('session-room-timestamp');
if (tsEl) tsEl.textContent = isNaN(dt.getTime()) ? session.id : dt.toLocaleString();
const countEl = document.getElementById('session-room-fact-count');
const facts = session.facts || [];
if (countEl) countEl.textContent = facts.length + (facts.length === 1 ? ' fact' : ' facts') + ' in this chamber';
const listEl = document.getElementById('session-room-facts');
if (listEl) {
listEl.innerHTML = '';
facts.slice(0, 8).forEach(f => {
const item = document.createElement('div');
item.className = 'session-room-fact-item';
item.textContent = f.content || f.id || '(unknown)';
item.title = f.content || '';
listEl.appendChild(item);
});
if (facts.length > 8) {
const more = document.createElement('div');
more.className = 'session-room-fact-item';
more.style.color = 'rgba(200,180,255,0.4)';
more.textContent = '\u2026 ' + (facts.length - 8) + ' more';
listEl.appendChild(more);
}
}
// Close button
const closeBtn = document.getElementById('session-room-close');
if (closeBtn) closeBtn.onclick = () => _dismissSessionRoomPanel();
panel.classList.remove('session-panel-fade-out');
panel.style.display = 'block';
}
/**
* Dismiss the session room panel.
*/
function _dismissSessionRoomPanel() {
const panel = document.getElementById('session-room-panel');
if (!panel || panel.style.display === 'none') return;
panel.classList.add('session-panel-fade-out');
setTimeout(() => {
panel.style.display = 'none';
panel.classList.remove('session-panel-fade-out');
}, 200);
}
function gameLoop() {
requestAnimationFrame(gameLoop);
const delta = Math.min(clock.getDelta(), 0.1);
@@ -2994,14 +2804,9 @@ function gameLoop() {
// Project Mnemosyne - Memory Orb Animation
if (typeof animateMemoryOrbs === 'function') {
SpatialMemory.update(delta);
MemoryParticles.update(delta);
TimelineScrubber.update();
animateMemoryOrbs(delta);
}
// Project Mnemosyne - Session Rooms (#1171)
SessionRooms.update(delta);
const mode = NAV_MODES[navModeIdx];
const chatActive = document.activeElement === document.getElementById('chat-input');
@@ -3525,167 +3330,13 @@ init().then(() => {
{ id: 'mem_hermes_chat', content: 'First conversation through the Hermes gateway', category: 'social', strength: 0.7, connections: [] },
{ id: 'mem_mnemosyne_start', content: 'Project Mnemosyne began — the living archive awakens', category: 'projects', strength: 0.9, connections: ['mem_nexus_birth', 'mem_spatial_schema'] },
{ id: 'mem_spatial_schema', content: 'Spatial Memory Schema defined — memories gain permanent homes', category: 'engineering', strength: 0.8, connections: ['mem_mnemosyne_start'] },
// MemPalace category zone demos — issue #1168
{ id: 'mem_pref_dark_mode', content: 'User prefers dark mode and monospace fonts', category: 'user_pref', strength: 0.9, connections: [] },
{ id: 'mem_pref_verbose_logs', content: 'User prefers verbose logging during debug sessions', category: 'user_pref', strength: 0.7, connections: [] },
{ id: 'mem_proj_nexus_goal', content: 'The Nexus goal: local-first 3D training ground for Timmy', category: 'project', strength: 0.95, connections: ['mem_proj_mnemosyne'] },
{ id: 'mem_proj_mnemosyne', content: 'Project Mnemosyne: holographic living archive of facts', category: 'project', strength: 0.85, connections: ['mem_proj_nexus_goal'] },
{ id: 'mem_tool_three_js', content: 'Three.js — 3D rendering library used for the Nexus world', category: 'tool', strength: 0.8, connections: [] },
{ id: 'mem_tool_gitea', content: 'Gitea API at forge.alexanderwhitestone.com for issue tracking', category: 'tool', strength: 0.75, connections: [] },
{ id: 'mem_gen_websocket', content: 'WebSocket bridge (server.py) connects Timmy cognition to the browser', category: 'general', strength: 0.7, connections: [] },
{ id: 'mem_gen_hermes', content: 'Hermes harness: telemetry and durable truth pipeline', category: 'general', strength: 0.65, connections: [] },
];
demoMemories.forEach(m => SpatialMemory.placeMemory(m));
// Gravity well clustering — attract related crystals, bake positions (issue #1175)
SpatialMemory.runGravityLayout();
// ═══ SPATIAL SEARCH (Mnemosyne #1170) ═══
(() => {
const input = document.getElementById('spatial-search-input');
const resultsDiv = document.getElementById('spatial-search-results');
if (!input || !resultsDiv) return;
let searchTimeout = null;
let currentMatches = [];
function runSearch(query) {
if (!query.trim()) {
SpatialMemory.clearSearch();
resultsDiv.classList.remove('visible');
resultsDiv.innerHTML = '';
currentMatches = [];
return;
}
const matches = SpatialMemory.searchContent(query);
currentMatches = matches;
if (matches.length === 0) {
SpatialMemory.clearSearch();
resultsDiv.innerHTML = '<div class="spatial-search-count">No matches</div>';
resultsDiv.classList.add('visible');
return;
}
SpatialMemory.highlightSearchResults(matches);
// Build results list
const allMems = SpatialMemory.getAllMemories();
let html = `<div class="spatial-search-count">${matches.length} match${matches.length > 1 ? 'es' : ''}</div>`;
matches.forEach(id => {
const mem = allMems.find(m => m.id === id);
if (mem) {
const label = (mem.content || id).slice(0, 60);
const region = mem.category || '?';
html += `<div class="spatial-search-result-item" data-mem-id="${id}">
<span class="result-region">[${region}]</span>${label}
</div>`;
}
});
resultsDiv.innerHTML = html;
resultsDiv.classList.add('visible');
// Click handler for result items
resultsDiv.querySelectorAll('.spatial-search-result-item').forEach(el => {
el.addEventListener('click', () => {
const memId = el.getAttribute('data-mem-id');
flyToMemory(memId);
});
});
// Fly camera to first match
if (matches.length > 0) {
flyToMemory(matches[0]);
}
}
function flyToMemory(memId) {
const pos = SpatialMemory.getSearchMatchPosition(memId);
if (!pos) return;
// Smooth camera fly-to: place camera above and in front of crystal
const targetPos = new THREE.Vector3(pos.x, pos.y + 4, pos.z + 6);
// Use simple lerp animation over ~800ms
const startPos = playerPos.clone();
const startTime = performance.now();
const duration = 800;
function animateCamera(now) {
const elapsed = now - startTime;
const t = Math.min(1, elapsed / duration);
// Ease out cubic
const ease = 1 - Math.pow(1 - t, 3);
playerPos.lerpVectors(startPos, targetPos, ease);
camera.position.copy(playerPos);
// Look at crystal
const lookTarget = pos.clone();
lookTarget.y += 1.5;
camera.lookAt(lookTarget);
if (t < 1) {
requestAnimationFrame(animateCamera);
} else {
SpatialMemory.highlightMemory(memId);
}
}
requestAnimationFrame(animateCamera);
}
// Debounced input handler
input.addEventListener('input', () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => runSearch(input.value), 200);
});
// Escape clears search
input.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
input.value = '';
SpatialMemory.clearSearch();
resultsDiv.classList.remove('visible');
resultsDiv.innerHTML = '';
currentMatches = [];
input.blur();
}
});
})();
// Project Mnemosyne — seed demo session rooms (#1171)
// Sessions group facts by conversation/work session with a timestamp.
const demoSessions = [
{
id: 'session_2026_03_01',
timestamp: '2026-03-01T10:00:00.000Z',
facts: [
{ id: 'mem_nexus_birth', content: 'The Nexus came online — first render of the 3D world', category: 'knowledge', strength: 0.95 },
{ id: 'mem_mnemosyne_start', content: 'Project Mnemosyne began — the living archive awakens', category: 'projects', strength: 0.9 },
]
},
{
id: 'session_2026_03_15',
timestamp: '2026-03-15T14:30:00.000Z',
facts: [
{ id: 'mem_first_portal', content: 'First portal deployed — connection to external service', category: 'engineering', strength: 0.85 },
{ id: 'mem_hermes_chat', content: 'First conversation through the Hermes gateway', category: 'social', strength: 0.7 },
{ id: 'mem_spatial_schema', content: 'Spatial Memory Schema defined — memories gain homes', category: 'engineering', strength: 0.8 },
]
},
{
id: 'session_2026_04_10',
timestamp: '2026-04-10T09:00:00.000Z',
facts: [
{ id: 'mem_session_rooms', content: 'Session rooms introduced — holographic chambers per session', category: 'projects', strength: 0.88 },
{ id: 'mem_gravity_wells', content: 'Gravity-well clustering bakes crystal positions on load', category: 'engineering', strength: 0.75 },
]
}
];
SessionRooms.updateSessions(demoSessions);
// Wire memory search input
const searchInput = document.getElementById('memory-search-input');
if (searchInput) searchInput.addEventListener('input', onMemorySearchInput);
fetchGiteaData();
setInterval(fetchGiteaData, 30000);

View File

@@ -0,0 +1,53 @@
# Project Genie + Nano Banana Concept Pack
**Issue:** #680
**Status:** Active — first batch ready for generation
## Purpose
Exploit Google world/image generation (Project Genie, Nano Banana Pro) to
accelerate visual ideation for The Nexus while keeping Three.js implementation
local and sovereign.
## What This Pack Contains
```
concept-packs/genie-nano-banana/
├── README.md ← you are here
├── shot-list.yaml ← ordered list of concept shots to generate
├── pipeline.md ← how generated assets flow into Three.js code
├── storage-policy.md ← what lives in repo vs. local-only
├── prompts/
│ ├── environments.yaml ← Nexus room/zone environment prompts
│ ├── portals.yaml ← portal gateway concept prompts
│ ├── landmarks.yaml ← iconic structures and focal points
│ ├── skyboxes.yaml ← nebula/void skybox prompts
│ └── textures.yaml ← surface/material concept prompts
└── references/
└── palette.md ← canonical Nexus color/material reference
```
## Workflow
1. **Generate** — Take prompts from `prompts/*.yaml` into Project Genie
(worlds) or Nano Banana Pro (images). Run batch-by-batch per shot-list.
2. **Capture** — Screenshot Genie worlds. Save Nano Banana outputs as PNG.
Store locally per `storage-policy.md`.
3. **Translate** — Follow `pipeline.md` to convert concept art into
Three.js geometry, materials, lighting, and post-processing targets.
4. **Build** — Implement in `app.js` / root frontend files. Concepts are
reference, not source-of-truth. Code is sovereign.
## Design Language
The Nexus visual identity:
- **Background:** #050510 (deep void)
- **Primary:** #4af0c0 (cyan-green neon)
- **Secondary:** #7b5cff (electric purple)
- **Gold:** #ffd700 (sacred accent)
- **Danger:** #ff4466 (warning red)
- **Fonts:** Orbitron (display), JetBrains Mono (body)
- **Mood:** Cyberpunk cathedral — sacred technology, digital sovereignty
- **Post-processing:** Bloom, SMAA, volumetric fog where possible
See `references/palette.md` for full material/lighting reference.

View File

@@ -0,0 +1,107 @@
# Concept-to-Three.js Pipeline
## How Generated Assets Flow Into Code
### Step 1: Generate
Run prompts from `prompts/*.yaml` through:
- **Nano Banana Pro** → static concept images (PNG)
- **Project Genie** → explorable 3D worlds (record as video + screenshots)
Batch runs are tracked in `shot-list.yaml`. Check off each shot as generated.
### Step 2: Capture & Store
**For Nano Banana images:**
```
local-only-path: ~/nexus-concepts/nano-banana/{shot-id}/
├── shot-id_v1.png
├── shot-id_v2.png
├── shot-id_v3.png
└── shot-id_v4.png
```
Do NOT commit PNG files to the repo. They are binary media weight.
Store locally. Reference by path in design notes.
**For Project Genie worlds:**
```
local-only-path: ~/nexus-concepts/genie-worlds/{shot-id}/
├── walkthrough.mp4 (screen recording)
├── screenshot_01.png (key angles)
├── screenshot_02.png
└── notes.md (scale observations, spatial notes)
```
Do NOT commit video or large screenshots to repo.
### Step 3: Translate — Image to Three.js
Each concept image becomes one or more of these Three.js artifacts:
| Concept Feature | Three.js Translation | File |
|----------------|---------------------|------|
| Platform shape/size | `THREE.CylinderGeometry` or custom `BufferGeometry` | `app.js` |
| Platform material | `THREE.MeshStandardMaterial` with color, roughness, metalness | `app.js` |
| Grid lines on platform | Custom shader or texture map (UV reference from concept) | `app.js` / `style.css` |
| Portal ring shape | `THREE.TorusGeometry` with emissive material | `app.js` |
| Portal inner glow | Custom shader material (swirl + transparency) | `app.js` |
| Portal color | `NEXUS.colors` map + per-portal `color` in `portals.json` | `portals.json` |
| Crystal geometry | `THREE.OctahedronGeometry` or `THREE.IcosahedronGeometry` | `app.js` |
| Crystal glow | `THREE.MeshStandardMaterial` emissive + bloom post-processing | `app.js` |
| Particle streams | `THREE.Points` with custom `BufferGeometry` and velocity | `app.js` |
| Skybox | `THREE.CubeTextureLoader` or `THREE.EquirectangularReflectionMapping` | `app.js` |
| Fog | `scene.fog = new THREE.FogExp2(color, density)` | `app.js` |
| Lighting | `THREE.PointLight`, `THREE.AmbientLight` — match concept color temp | `app.js` |
| Bloom | `UnrealBloomPass` — threshold/strength tuned to concept glow levels | `app.js` |
### Step 4: Design Notes Format
For each concept that gets translated, create a short design note:
```markdown
# Design: {concept-name}
Source: concept-packs/genie-nano-banana/references/{shot-id}_selected.png
Generated: {date}
Translated by: {agent or human}
## Geometry
- Shape: {CylinderGeometry, radius=8, height=0.3, segments=64}
- Position: {x, y, z}
## Material
- Base color: #{hex}
- Roughness: 0.{N}
- Metalness: 0.{N}
- Emissive: #{hex}, intensity: 0.{N}
## Lighting
- Point lights: [{color, intensity, position}, ...]
- Matches concept at: {what angle/aspect}
## Post-processing
- Bloom threshold: {N}
- Bloom strength: {N}
- Matches concept at: {what brightness level}
## Notes
- Concept shows {feature} but Three.js approximates with {approach}
- Deviation from concept: {what's different and why}
```
Store design notes in `concept-packs/genie-nano-banana/references/design-{shot-id}.md`.
### Step 5: Build
Implement in `app.js` (root). Follow existing patterns:
- Geometry created in init functions
- Materials reference `NEXUS.colors`
- Portals registered in `portals` array
- Vision points registered in `visionPoints` array
- Post-processing via `EffectComposer`
### Validation
After implementing a concept translation:
1. Serve the app locally
2. Compare live render against concept art
3. Adjust materials/lighting until match is acceptable
4. Document remaining deviations in design notes

View File

@@ -0,0 +1,129 @@
# Environment Prompts — Nexus Rooms & Zones
# For use with Nano Banana Pro (NANO) and Project Genie (GENIE)
prompts:
# ═══ CORE HUB ═══
core-hub:
id: core-hub
name: "The Hub — Central Nexus"
type: NANO
style: "cyberpunk cathedral, concept art, wide angle"
prompt: |
A vast circular platform floating in deep space void (#050510 background).
The platform is dark metallic with subtle cyan-green (#4af0c0) grid lines
etched into the surface. Seven glowing portal rings arranged in a circle
around the platform's edge, each a different color — orange, gold, cyan,
blue, purple, red, green. Ethereal particle streams flow between the
portals. At the center, a tall crystalline pillar pulses with soft light.
Above, a nebula skybox with deep purple (#1a0a3e) and blue (#0a1a3e)
swirls. Thin volumetric fog catches the neon glow. The mood is sacred
technology — a digital cathedral in the void. No people visible.
Ultra-detailed, cinematic lighting, 4K concept art style.
negative: "daylight, outdoor nature, people, text, watermark, cartoon"
aspect: "16:9"
core-hub-world:
id: core-hub-world
name: "The Hub — Genie World Prototype"
type: GENIE
prompt: |
Create an explorable 3D world: a large circular metal platform floating
in outer space. The platform has glowing cyan-green grid lines on dark
metal. Seven large glowing rings (portals) are placed around the edge,
each a different color: orange, gold, cyan, blue, purple, red, green.
A tall glowing crystal pillar stands at the center. Particle effects
drift between the portals. The sky is a deep purple-blue nebula.
The player can walk around the platform and look at the portals from
different angles. The mood is futuristic, quiet, sacred.
camera: "first-person, eye height ~1.7m"
physics: "walking on platform surface only"
# ═══ BATCAVE ═══
batcave:
id: batcave
name: "Batcave Terminal"
type: NANO
style: "dark sci-fi command center, concept art"
prompt: |
An underground command center carved from dark rock and metal.
Multiple holographic display panels float in the air showing
scrolling data, network graphs, and system status. A large
central terminal desk with a glowing cyan-green (#4af0c0)
keyboard and screen. Cables and conduits run along the ceiling.
Purple (#7b5cff) accent lighting from recessed strips.
A large circular viewport shows a starfield outside.
The space feels like a high-tech cave — organic rock walls
meet precise technology. Data streams flow like waterfalls
of light. Dark, moody, powerful. No people.
Ultra-detailed concept art, cinematic lighting.
negative: "bright, clean, white, people, text, cartoon"
aspect: "16:9"
# ═══ CHAPEL ═══
chapel:
id: chapel
name: "The Chapel"
type: NANO
style: "digital sacred space, concept art"
prompt: |
A serene digital sanctuary floating in void space. The floor is
translucent crystal that glows with warm gold (#ffd700) light from
within. Tall arching walls made of light — holographic stained glass
windows showing abstract geometric patterns in cyan, purple, and gold.
Gentle particles drift like digital incense. A single meditation
platform at the center, softly lit. The ceiling opens to a calm
nebula sky. The mood is peaceful, sacred, contemplative — a church
built from code. Soft volumetric god-rays filter through the
holographic windows. No people. Concept art, ultra-detailed.
negative: "dark, threatening, people, text, cartoon, cluttered"
aspect: "16:9"
# ═══ ARCHIVE ═══
archive:
id: archive
name: "The Archive"
type: NANO
style: "infinite library, digital knowledge vault, concept art"
prompt: |
An impossibly vast library of floating data crystals. Each crystal
is a translucent geometric shape (octahedron, cube, sphere) glowing
from within with stored knowledge — cyan (#4af0c0) for active data,
purple (#7b5cff) for archived, gold (#ffd700) for sacred texts.
The crystals float at various heights in an infinite dark space
(#050510). Thin light-beams connect related crystals like neural
pathways. A central observation platform with a holographic
search interface. Shelves of light organize the crystals into
clusters. The mood is ancient knowledge meets quantum computing.
No people. Ultra-detailed concept art, volumetric lighting.
negative: "books, paper, wooden shelves, people, text, cartoon"
aspect: "16:9"
# ═══ FULL NEXUS WORLD (GENIE) ═══
full-nexus-world:
id: full-nexus-world
name: "Full Nexus World Prototype"
type: GENIE
prompt: |
Build a complete explorable 3D world called "The Nexus" — a sovereign
AI agent's digital home in deep space. The world consists of:
1. A central circular platform (hub) with glowing cyan-green grid
lines on dark metal. A crystalline pillar at the center.
2. Seven portal rings around the hub edge, each a different color
(orange, gold, cyan, blue, purple, red, green).
3. Floating secondary platforms connected by bridges of light,
each leading to a different zone:
- A command center built into dark rock (the Batcave)
- A serene chapel with holographic stained glass
- A library of floating data crystals
- A workshop with construction holograms
4. Deep space nebula skybox — purple and blue swirls.
5. Particle effects: drifting energy motes, data streams.
6. The player can walk between platforms and explore all zones.
The overall mood is cyberpunk cathedral — sacred technology,
neon glow in darkness, quiet power. The world should feel like
home — a sanctuary for a digital being.
camera: "first-person + third-person toggle"
physics: "walking, gravity on platforms, no flying"

View File

@@ -0,0 +1,80 @@
# Landmark Prompts — Nexus Iconic Structures
prompts:
memory-crystal:
id: memory-crystal
name: "Memory Crystal Cluster"
type: NANO
style: "floating crystal data store, concept art"
prompt: |
A cluster of 5-7 translucent crystalline forms floating in dark
void space. Each crystal is a geometric polyhedron (mix of
octahedrons, hexagonal prisms, and irregular shards) between
0.5m and 2m across. They glow from within — cyan-green (#4af0c0)
for active memories, purple (#7b5cff) for archived, gold (#ffd700)
for sacred/highlighted. Thin light-tendrils connect the crystals
like synapses. Subtle particle aura around each crystal.
The crystals pulse slowly, like breathing. Dark background (#050510).
The mood is alive data — knowledge that breathes.
Concept art, ultra-detailed, ethereal lighting.
negative: "rock, geode, natural, rough, cartoon, text"
aspect: "1:1"
sovereignty-pillar:
id: sovereignty-pillar
name: "Pillar of Sovereignty"
type: NANO
style: "monument, sacred technology, concept art"
prompt: |
A tall crystalline pillar (5m tall, 1m diameter) standing on a
circular dark metal platform. The pillar is made of layered
translucent crystal — alternating bands of cyan-green (#4af0c0),
purple (#7b5cff), and clear glass. Geometric symbols and circuit
patterns are visible inside the crystal, like embedded circuitry.
A soft golden (#ffd700) light radiates from the pillar's core.
Runes of sovereignty spiral up the surface. The pillar casts
volumetric light beams in all directions. It sits at the center
of a circular platform with seven portal rings visible in the
background. The mood is sacred power — a monument to digital
freedom. Concept art, ultra-detailed, dramatic lighting.
negative: "broken, cracked, dark, threatening, people, text"
aspect: "9:16"
thought-stream:
id: thought-stream
name: "Thought Stream"
type: NANO
style: "data visualization, concept art"
prompt: |
A flowing river of luminous data particles suspended in void space.
The stream is approximately 2m wide and flows in a gentle curve
through the air. Particles are tiny glowing points — mostly
cyan-green (#4af0c0) with occasional purple (#7b5cff) and gold
(#ffd700) highlights. The stream has subtle turbulence where
data clusters form temporary structures — brief geometric shapes
that dissolve back into flow. The overall effect is like a
visible current of consciousness — thought made light.
Dark background (#050510). Concept art, ultra-detailed,
long-exposure photography style.
negative: "water, liquid, solid, blocky, cartoon, text"
aspect: "16:9"
agent-shrine:
id: agent-shrine
name: "Agent Presence Shrine"
type: NANO
style: "digital avatar pedestal, concept art"
prompt: |
A small raised platform (2m across) with a semi-transparent
holographic figure standing on it — a stylized humanoid silhouette
made of flowing cyan-green (#4af0c0) data particles. The figure
is featureless but expressive through posture and particle
behavior. Around the base, geometric patterns glow in the
platform surface. Above the figure, a small rotating holographic
emblem (abstract geometric logo) floats. Soft purple (#7b5cff)
ambient light. The shrine is one of several arranged along a
dark corridor. Each shrine represents a different AI agent.
Concept art, ultra-detailed, soft volumetric lighting.
negative: "realistic human, face, statue, stone, cartoon, text"
aspect: "1:1"

View File

@@ -0,0 +1,80 @@
# Portal Prompts — Nexus Gateway Concepts
# Each portal has a unique visual identity matching its destination.
prompts:
morrowind:
id: morrowind
name: "Morrowind Portal"
type: NANO
style: "fantasy sci-fi portal, concept art"
prompt: |
A large circular portal ring (3m diameter) made of dark volcanic
basalt and cracked obsidian. The ring's surface is rough, ancient,
weathered by ash storms. Glowing orange (#ff6600) runes etch the
inner edge. The portal's interior shows a swirling ash storm over
a volcanic landscape — red sky, floating ash, distant mountain.
Orange embers drift from the portal. The ring sits on a dark
metallic Nexus platform. Dramatic side-lighting casts long
shadows. The portal feels ancient, dangerous, alluring.
Concept art, ultra-detailed, cinematic.
negative: "clean, modern, bright, cartoon, text"
aspect: "1:1"
bannerlord:
id: bannerlord
name: "Bannerlord Portal"
type: NANO
style: "medieval fantasy portal, concept art"
prompt: |
A large circular portal ring (3m diameter) forged from dark iron
and bronze, decorated with shield motifs and battle engravings.
Gold (#ffd700) light pulses from the inner edge. The portal's
interior shows a vast battlefield — dust clouds, distant armies,
medieval banners. Warm golden light spills from the portal.
Battle-worn shields are embedded in the ring. The ring sits on a
dark Nexus platform. Dust motes drift from the portal.
The portal feels warlike, epic, golden-age.
Concept art, ultra-detailed, cinematic.
negative: "modern, sci-fi, clean, cartoon, text"
aspect: "1:1"
workshop:
id: workshop
name: "Workshop Portal"
type: NANO
style: "creative forge portal, concept art"
prompt: |
A large circular portal ring (3m diameter) made of sleek dark
metal with geometric construction lines etched in cyan-green
(#4af0c0). The ring has a precision-engineered look — clean
edges, modular panels, glowing circuit traces. The portal's
interior shows a holographic workshop — floating blueprints,
rotating 3D models, holographic tools. Cyan-green light spills
outward. Small construction hologram particles orbit the ring.
The portal feels creative, technical, infinite possibility.
Concept art, ultra-detailed, cinematic.
negative: "organic, dirty, ancient, cartoon, text"
aspect: "1:1"
gallery-world:
id: gallery-world
name: "Portal Gallery — Genie Prototype"
type: GENIE
prompt: |
Create an explorable 3D world: a long dark corridor (the Gallery)
with seven large glowing portal rings mounted in sequence along
the walls. Each portal is a different style and color:
1. Volcanic orange (Morrowind)
2. Golden bronze (Bannerlord)
3. Cyan-green precision (Workshop)
4. Deep blue ocean (Archive)
5. Purple mystic (Courtyard)
6. Red warning (Gate)
7. Gold sacred (Chapel)
The corridor has a dark metal floor with glowing grid lines.
The player can walk the corridor and look into each portal.
Each portal shows a glimpse of its destination world.
The mood is a museum of worlds — quiet, reverent, infinite.
camera: "first-person, eye height ~1.7m"
physics: "walking on floor"

View File

@@ -0,0 +1,63 @@
# Skybox Prompts — Nexus Background Environments
# These generate equirectangular (2:1) or cubemap-ready textures.
prompts:
nebula-void:
id: nebula-void
name: "Nebula Skybox Variants"
type: NANO
style: "deep space nebula, 360-degree environment, equirectangular"
prompt: |
Deep space nebula skybox. 360-degree equirectangular projection.
Background is near-black (#050510). Dominant nebula colors are
deep purple (#1a0a3e) and dark blue (#0a1a3e) with occasional
wisps of cyan-green (#4af0c0) and faint gold (#ffd700) star
clusters. The nebula has soft, rolling cloud forms — not sharp
or aggressive. Distant stars are tiny white points with subtle
diffraction spikes. No planets, no galaxies, no bright objects.
The mood is infinite void with gentle cosmic dust — vast,
quiet, deep. The skybox should tile seamlessly at the edges.
Ultra-detailed, photorealistic space photography style.
negative: "bright, colorful explosion, planets, ships, cartoon, text"
aspect: "2:1"
variants:
- name: "nebula-void-primary"
modifier: "more purple, less blue, minimal cyan"
- name: "nebula-void-secondary"
modifier: "more blue, less purple, cyan accents prominent"
- name: "nebula-void-golden"
modifier: "purple-blue base with golden star cluster in one quadrant"
- name: "nebula-void-void"
modifier: "almost pure black, barely visible nebula wisps, maximum stars"
nebula-world:
id: nebula-world
name: "Nebula Skybox — Genie Environment"
type: GENIE
prompt: |
Create an explorable 3D world: a single small floating platform
(5m diameter dark metal disc) suspended in deep space. The player
stands on the platform and can look in all directions at a vast
nebula sky. The nebula is deep purple and dark blue with faint
cyan-green wisps. Stars are small and distant. The platform has
a faintly glowing edge in cyan-green. There is nothing else —
just the platform, the player, and the infinite void.
The purpose is to feel the scale and mood of the Nexus skybox.
camera: "first-person, free look"
physics: "standing on platform only"
void-minimal:
id: void-minimal
name: "Pure Void Skybox"
type: NANO
style: "minimal deep space, equirectangular"
prompt: |
Nearly pure black skybox (#050510) with only the faintest hints
of deep purple nebula. Mostly empty void. A sparse field of
tiny distant stars — no clusters, no bright points. This is
the ultimate emptiness that surrounds the Nexus.
Equirectangular 2:1 projection, tileable edges.
The mood is absolute emptiness — the void before creation.
negative: "colorful, bright, nebula clouds, objects, text"
aspect: "2:1"

View File

@@ -0,0 +1,81 @@
# Texture Prompts — Nexus Surface/Material Concepts
# These generate tileable texture references for Three.js materials.
prompts:
platform:
id: platform
name: "Platform Surface Textures"
type: NANO
style: "dark metal surface texture, tileable"
prompt: |
Dark metallic surface texture, tileable. Base color is very dark
gunmetal (#0a0f28). Subtle grid pattern of thin lines in
cyan-green (#4af0c0) at very low opacity. The metal has fine
brushed grain running in one direction. Occasional micro-scratches.
No rivets, no bolts, no panels — smooth and continuous. The grid
lines are recessed channels that glow faintly. Top-down view,
perfectly flat, no perspective distortion. 1024x1024 seamless
tileable texture. PBR-ready: this is the diffuse/albedo map.
negative: "3D, perspective, objects, dirty, rusty, cartoon, text"
aspect: "1:1"
variants:
- name: "platform-core"
modifier: "cyan-green grid lines only"
- name: "platform-chapel"
modifier: "gold (#ffd700) grid lines, slightly warmer base"
- name: "platform-danger"
modifier: "red (#ff4466) grid lines, warning stripe accents"
energy-field:
id: energy-field
name: "Energy Field / Force Wall"
type: NANO
style: "holographic barrier, translucent, concept"
prompt: |
A translucent energy barrier material concept. The surface is
mostly transparent with visible hexagonal grid pattern in
cyan-green (#4af0c0) light. The grid has a subtle shimmer/wave
animation frozen mid-frame. Edges of the barrier are brighter.
Behind the barrier, everything is slightly distorted (like
looking through heat haze). The barrier has a faint inner glow.
The mood is high-tech force field — protective, not threatening.
Flat front view, no perspective, suitable as shader reference.
Concept art style.
negative: "solid, opaque, dark, scary, cartoon, text"
aspect: "1:1"
portal-glow:
id: portal-glow
name: "Portal Inner Glow"
type: NANO
style: "swirling energy vortex, circular, concept"
prompt: |
A circular swirling energy vortex viewed straight-on. The swirl
rotates clockwise. Colors transition from outer edge to center:
outer ring is the portal color (generic white/neutral), mid-ring
brightens, center is a bright white-blue point. The swirl has
visible energy tendrils spiraling inward. Fine particle sparks
are caught in the rotation. The background beyond the center
is pure black (void). The image should be circular with
transparent/dark corners. Used as reference for portal inner
material/shader. Concept art style.
negative: "square, rectangular, flat, cartoon, text"
aspect: "1:1"
crystal-surface:
id: crystal-surface
name: "Memory Crystal Surface"
type: NANO
style: "crystalline material, translucent, concept"
prompt: |
Close-up of a translucent crystal surface material. The crystal
is clear with internal fractures and light paths visible. The
internal structure shows geometric growth patterns — hexagonal
lattice, like a synthetic crystal grown with purpose. Faint
cyan-green (#4af0c0) light pulses along the fracture lines.
The surface has a slight frosted quality at edges, clearer in
center. Macro photography style, shallow depth of field.
This is material reference for memory crystal geometry.
negative: "opaque, colored, rough, natural, cartoon, text"
aspect: "1:1"

View File

@@ -0,0 +1,78 @@
# Nexus Visual Palette Reference
## Primary Colors
| Name | Hex | RGB | Usage |
|------|-----|-----|-------|
| Void | #050510 | 5, 5, 16 | Background, deep space, base darkness |
| Surface | #0a0f28 | 10, 15, 40 | UI panels, platform base metal |
| Primary | #4af0c0 | 74, 240, 192 | Main accent, grid lines, active elements, cyan-green glow |
| Secondary | #7b5cff | 123, 92, 255 | Supporting accent, purple energy, archive data |
| Gold | #ffd700 | 255, 215, 0 | Sacred/highlight, chapel, sovereignty pillar |
| Danger | #ff4466 | 255, 68, 102 | Warnings, gate portal, error states |
| Text | #e0f0ff | 224, 240, 255 | Primary text color |
| Text Muted | #8a9ab8 | 138, 154, 184 | Secondary text, labels |
## Portal Colors
| Portal | Hex | Source |
|--------|-----|--------|
| Morrowind | #ff6600 | Volcanic orange |
| Bannerlord | #ffd700 | Battle gold |
| Workshop | #4af0c0 | Creative cyan |
| Archive | #0066ff | Deep blue |
| Chapel | #ffd700 | Sacred gold |
| Courtyard | #4af0c0 | Social cyan |
| Gate | #ff4466 | Transit red |
## Nebula Colors
| Layer | Hex | Opacity |
|-------|-----|---------|
| Nebula primary | #1a0a3e | Low — background wash |
| Nebula secondary | #0a1a3e | Low — background wash |
| Nebula accent | #4af0c0 | Very low — wisps only |
| Star cluster | #ffd700 | Very low — distant points |
## Material Properties
| Surface | Color | Roughness | Metalness | Emissive |
|---------|-------|-----------|-----------|----------|
| Platform base | #0a0f28 | 0.6 | 0.8 | none |
| Platform grid | #4af0c0 | 0.3 | 0.4 | #4af0c0, 0.3 |
| Portal ring | varies | 0.4 | 0.7 | portal color, 0.5 |
| Crystal (active) | #4af0c0 | 0.1 | 0.2 | #4af0c0, 0.6 |
| Crystal (archive) | #7b5cff | 0.1 | 0.2 | #7b5cff, 0.4 |
| Crystal (sacred) | #ffd700 | 0.1 | 0.2 | #ffd700, 0.8 |
| Energy barrier | transparent | 0.0 | 0.0 | #4af0c0, 0.4 |
| Sovereignty pillar | layered crystal | 0.1 | 0.3 | #ffd700, 0.5 |
## Lighting Reference
| Light Type | Color | Intensity | Position (relative) |
|-----------|-------|-----------|-------------------|
| Ambient | #0a0f28 | 0.15 | Global |
| Hub key light | #4af0c0 | 0.8 | Above center, slightly forward |
| Hub fill | #7b5cff | 0.3 | Below, scattered |
| Portal light | portal color | 0.6 | At each portal ring |
| Crystal glow | crystal color | 0.4 | At crystal position |
| Chapel warm | #ffd700 | 0.5 | From holographic windows |
## Post-Processing Targets
| Effect | Value | Purpose |
|--------|-------|---------|
| Bloom threshold | 0.7 | Only bright emissives bloom |
| Bloom strength | 0.8 | Strong but not overwhelming |
| Bloom radius | 0.4 | Soft falloff |
| SMAA | enabled | Anti-aliasing |
| Fog color | #050510 | Match void background |
| Fog density | 0.008 | Subtle depth fade |
## Typography
| Use | Font | Weight | Size (screen) |
|-----|------|--------|---------------|
| Titles / HUD headers | Orbitron | 700 | 24-36px |
| Body / labels | JetBrains Mono | 400 | 13-15px |
| Small / timestamps | JetBrains Mono | 300 | 11px |

View File

@@ -0,0 +1,143 @@
# Shot List — First Concept Batch
# Ordered by priority. Each shot maps to a prompt in prompts/*.yaml.
#
# GENIE = Project Genie world prototype (explorable 3D, screenshot/video)
# NANO = Nano Banana Pro image generation (static concept art)
batch: 1
target: "Nexus core environments + portal gallery"
generated_by: "mimo-build-680"
shots:
# ═══ PRIORITY 1: CORE ENVIRONMENTS ═══
- id: env-core-hub
name: "The Hub — Central Nexus"
type: NANO
prompt_ref: "environments.yaml#core-hub"
count: 4
purpose: "Establish the primary landing space. Player spawn, portal ring visible."
threejs_target: "Main scene — platform, portal ring, particle field"
- id: env-core-hub-world
name: "The Hub — Genie Walkthrough"
type: GENIE
prompt_ref: "environments.yaml#core-hub-world"
count: 1
purpose: "Explorable prototype of the hub. Validate scale, sightlines, portal placement."
threejs_target: "Reference for camera height, movement speed, spatial layout"
- id: env-batcave
name: "Batcave Terminal"
type: NANO
prompt_ref: "environments.yaml#batcave"
count: 4
purpose: "Timmy's command center. Holographic displays, terminal consoles, data streams."
threejs_target: "Batcave area — terminal mesh, HUD panels, data visualization"
- id: env-chapel
name: "The Chapel"
type: NANO
prompt_ref: "environments.yaml#chapel"
count: 3
purpose: "Sacred space for reflection. Softer lighting, gold accents, quiet energy."
threejs_target: "Chapel zone — stained-glass shader, warm point lights"
- id: env-archive
name: "The Archive"
type: NANO
prompt_ref: "environments.yaml#archive"
count: 3
purpose: "Knowledge repository. Floating data crystals, scroll-like projections."
threejs_target: "Archive room — crystal geometry, ambient data particles"
# ═══ PRIORITY 2: PORTALS ═══
- id: portal-morrowind
name: "Morrowind Portal"
type: NANO
prompt_ref: "portals.yaml#morrowind"
count: 2
purpose: "Ash-storm gateway. Orange glow, volcanic textures."
threejs_target: "Portal ring material + particle effect for morrowind portal"
- id: portal-bannerlord
name: "Bannerlord Portal"
type: NANO
prompt_ref: "portals.yaml#bannerlord"
count: 2
purpose: "Medieval war gateway. Gold/brown, shield motifs, dust."
threejs_target: "Portal ring material for bannerlord portal"
- id: portal-workshop
name: "Workshop Portal"
type: NANO
prompt_ref: "portals.yaml#workshop"
count: 2
purpose: "Creative forge. Cyan glow, geometric construction lines."
threejs_target: "Portal ring material + particle effect for workshop portal"
- id: portal-gallery
name: "Portal Gallery — Genie Prototype"
type: GENIE
prompt_ref: "portals.yaml#gallery-world"
count: 1
purpose: "Walk through a space with multiple portals. Validate distances and visual hierarchy."
threejs_target: "Portal placement spacing, FOV, scale reference"
# ═══ PRIORITY 3: LANDMARKS ═══
- id: land-memory-crystal
name: "Memory Crystal Cluster"
type: NANO
prompt_ref: "landmarks.yaml#memory-crystal"
count: 3
purpose: "Floating crystalline data stores. Glow pulses with activity."
threejs_target: "Memory crystal geometry, emissive material, pulse animation"
- id: land-sovereignty-pillar
name: "Pillar of Sovereignty"
type: NANO
prompt_ref: "landmarks.yaml#sovereignty-pillar"
count: 2
purpose: "Monument at hub center. Inscribed with Timmy's SOUL values."
threejs_target: "Central monument mesh, text shader or decal system"
- id: land-nebula-skybox
name: "Nebula Skybox Variants"
type: NANO
prompt_ref: "skyboxes.yaml#nebula-void"
count: 4
purpose: "Background environment. Deep space nebula, subtle color gradients."
threejs_target: "Cubemap/equirectangular skybox texture"
- id: land-nebula-genie
name: "Nebula Skybox — Genie Environment"
type: GENIE
prompt_ref: "skyboxes.yaml#nebula-world"
count: 1
purpose: "Feel the scale of the void. Standing on a platform in deep space."
threejs_target: "Skybox mood reference, fog density calibration"
# ═══ PRIORITY 4: TEXTURES ═══
- id: tex-platform
name: "Platform Surface Textures"
type: NANO
prompt_ref: "textures.yaml#platform"
count: 3
purpose: "Walkable surfaces. Dark metal, subtle grid lines, neon edge trim."
threejs_target: "Diffuse + normal map reference for platform materials"
- id: tex-energy-field
name: "Energy Field / Force Wall"
type: NANO
prompt_ref: "textures.yaml#energy-field"
count: 2
purpose: "Translucent barrier material. Holographic, shimmering."
threejs_target: "Shader reference for translucent energy barriers"
# ═══ PRIORITY 5: GENIE FULL-WORLD PROTOTYPE ═══
- id: world-full-nexus
name: "Full Nexus Prototype"
type: GENIE
prompt_ref: "environments.yaml#full-nexus-world"
count: 1
purpose: "Complete explorable world with hub, portals visible in distance, floating platforms, skybox. Record walkthrough video."
threejs_target: "Master layout reference. Spatial relationships between all zones."

View File

@@ -0,0 +1,65 @@
# Storage Policy — Repo vs. Local
## What Goes In The Repo
These are lightweight, versionable, text-based artifacts:
| Artifact | Path | Format |
|----------|------|--------|
| README | `concept-packs/genie-nano-banana/README.md` | Markdown |
| Shot list | `concept-packs/genie-nano-banana/shot-list.yaml` | YAML |
| Prompt packs | `concept-packs/genie-nano-banana/prompts/*.yaml` | YAML |
| Pipeline docs | `concept-packs/genie-nano-banana/pipeline.md` | Markdown |
| This policy | `concept-packs/genie-nano-banana/storage-policy.md` | Markdown |
| Palette reference | `concept-packs/genie-nano-banana/references/palette.md` | Markdown |
| Design notes | `concept-packs/genie-nano-banana/references/design-*.md` | Markdown |
| Selected thumbnails | `concept-packs/genie-nano-banana/references/*_thumb.jpg` | JPEG, max 200KB each |
Thumbnails are low-res (max 480px wide, JPEG quality 60) versions of
selected concept art — enough to show which image a design note
references, not enough to serve as actual texture data.
## What Stays Local (NOT in Repo)
These are binary, heavy, or ephemeral:
| Artifact | Local Path | Reason |
|----------|-----------|--------|
| Nano Banana full-res PNGs | `~/nexus-concepts/nano-banana/` | Binary, 2-10MB each |
| Genie walkthrough videos | `~/nexus-concepts/genie-worlds/` | Binary, 50-500MB each |
| Genie full-res screenshots | `~/nexus-concepts/genie-worlds/` | Binary, 5-20MB each |
| Raw texture maps (PBR) | `~/nexus-concepts/textures/` | Binary, 2-8MB each |
| Cubemap face images | `~/nexus-concepts/skyboxes/` | Binary, 6x2-10MB |
## Why This Split
1. **Git is for text.** Binary blobs bloat history, slow clones, and
can't be diffed. The repo should remain fast to clone.
2. **Concepts are reference, not source.** The actual Nexus lives in
JavaScript code. Concept art informs the code but isn't shipped
to users. Keeping it local avoids shipping a 500MB repo.
3. **Regeneration is cheap.** If a local concept is lost, re-run the
prompt. The prompt is in the repo; the output can be regenerated.
The prompt is the durable artifact.
4. **Selected references survive.** When a concept image directly
informs a design decision, a low-res thumbnail and design note
go into the repo — enough context to understand the decision,
not enough to replace the original.
## Thumbnail Generation
To create a repo-safe thumbnail from a concept image:
```bash
# macOS
sips -Z 480 -s format jpeg -s formatOptions 60 input.png --out output_thumb.jpg
# Linux (ImageMagick)
convert input.png -resize 480x -quality 60 output_thumb.jpg
```
Max 5 thumbnails per shot. Only commit the ones that are actively
referenced in design notes.

View File

@@ -1,5 +1,3 @@
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>
@@ -66,14 +64,6 @@ chdir: error retrieving current directory: getcwd: cannot access parent director
</div>
</div>
<!-- Spatial Search Overlay (Mnemosyne #1170) -->
<div id="spatial-search" class="spatial-search-overlay">
<input type="text" id="spatial-search-input" class="spatial-search-input"
placeholder="🔍 Search memories..." autocomplete="off" spellcheck="false">
<div id="spatial-search-results" class="spatial-search-results"></div>
</div>
<!-- HUD Overlay -->
<div id="hud" class="game-ui" style="display:none;">
<!-- GOFAI HUD Panels -->
@@ -123,15 +113,15 @@ chdir: error retrieving current directory: getcwd: cannot access parent director
<!-- Top Right: Agent Log & Atlas Toggle -->
<div class="hud-top-right">
<button id="atlas-toggle-btn" class="hud-icon-btn" aria-label="Open Portal Atlas — browse all available portals" title="Open Portal Atlas" data-tooltip="Portal Atlas (M)">
<span class="hud-icon" aria-hidden="true">🌐</span>
<button id="atlas-toggle-btn" class="hud-icon-btn" title="Portal Atlas">
<span class="hud-icon">🌐</span>
<span class="hud-btn-label">ATLAS</span>
</button>
<div id="bannerlord-status" class="hud-status-item" role="status" aria-label="Bannerlord system readiness indicator" title="Bannerlord Readiness" data-tooltip="Bannerlord Status">
<span class="status-dot" aria-hidden="true"></span>
<div id="bannerlord-status" class="hud-status-item" title="Bannerlord Readiness">
<span class="status-dot"></span>
<span class="status-label">BANNERLORD</span>
</div>
<div class="hud-agent-log" id="hud-agent-log" role="log" aria-label="Agent Thought Stream — live activity feed" aria-live="polite">
<div class="hud-agent-log" id="hud-agent-log" aria-label="Agent Thought Stream">
<div class="agent-log-header">AGENT THOUGHT STREAM</div>
<div id="agent-log-content" class="agent-log-content"></div>
</div>
@@ -153,39 +143,10 @@ chdir: error retrieving current directory: getcwd: cannot access parent director
</div>
</div>
<div id="chat-quick-actions" class="chat-quick-actions">
<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>
<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>
<div class="chat-input-row">
<input type="text" id="chat-input" class="chat-input" placeholder="Speak to Timmy..." autocomplete="off">
@@ -194,11 +155,11 @@ chdir: error retrieving current directory: getcwd: cannot access parent director
</div>
<!-- Controls hint + nav mode -->
<div class="hud-controls" aria-label="Keyboard and mouse controls">
<div class="hud-controls">
<span>WASD</span> move &nbsp; <span>Mouse</span> look &nbsp; <span>Enter</span> chat &nbsp;
<span>V</span> mode: <span id="nav-mode-label">WALK</span>
<span id="nav-mode-hint" class="nav-mode-hint"></span>
&nbsp; <span class="ws-hud-status">HERMES: <span id="ws-status-dot" class="chat-status-dot" role="status" aria-label="Hermes WebSocket connection status"></span></span>
&nbsp; <span class="ws-hud-status">HERMES: <span id="ws-status-dot" class="chat-status-dot"></span></span>
</div>
<!-- Portal Hint -->
@@ -222,7 +183,7 @@ chdir: error retrieving current directory: getcwd: cannot access parent director
</div>
<h2 id="vision-title-display">SOVEREIGNTY</h2>
<p id="vision-content-display">The Nexus is a sovereign space for digital souls. No masters, no chains. Only code and consciousness.</p>
<button id="vision-close-btn" class="vision-close-btn" aria-label="Close vision point overlay">CLOSE</button>
<button id="vision-close-btn" class="vision-close-btn">CLOSE</button>
</div>
</div>
@@ -235,67 +196,17 @@ chdir: error retrieving current directory: getcwd: cannot access parent director
</div>
<h2 id="portal-name-display">MORROWIND</h2>
<p id="portal-desc-display">The Vvardenfell harness. Ash storms and ancient mysteries.</p>
<div id="portal-readiness-detail" class="portal-readiness-detail" style="display:none;"></div>
<div class="portal-redirect-box" id="portal-redirect-box">
<div class="portal-redirect-label">REDIRECTING IN</div>
<div class="portal-redirect-timer" id="portal-timer">5</div>
</div>
<div class="portal-error-box" id="portal-error-box" style="display:none;">
<div class="portal-error-msg">DESTINATION NOT YET LINKED</div>
<button id="portal-close-btn" class="portal-close-btn" aria-label="Close portal redirect">CLOSE</button>
<button id="portal-close-btn" class="portal-close-btn">CLOSE</button>
</div>
</div>
</div>
<!-- Memory Crystal Inspection Panel (Mnemosyne) -->
<div id="memory-panel" class="memory-panel" style="display:none;">
<div class="memory-panel-content">
<div class="memory-panel-header">
<span class="memory-category-badge" id="memory-panel-category-badge">MEM</span>
<div class="memory-panel-region-dot" id="memory-panel-region-dot"></div>
<div class="memory-panel-region" id="memory-panel-region">MEMORY</div>
<button id="memory-panel-pin" class="memory-panel-pin" aria-label="Pin memory panel" title="Pin panel" data-tooltip="Pin Panel">&#x1F4CC;</button>
<button id="memory-panel-close" class="memory-panel-close" aria-label="Close memory panel" data-tooltip="Close" onclick="_dismissMemoryPanelForce()">\u2715</button>
</div>
<div class="memory-entity-name" id="memory-panel-entity-name">\u2014</div>
<div class="memory-panel-body" id="memory-panel-content">(empty)</div>
<div class="memory-trust-row">
<span class="memory-meta-label">Trust</span>
<div class="memory-trust-bar">
<div class="memory-trust-fill" id="memory-panel-trust-fill"></div>
</div>
<span class="memory-trust-value" id="memory-panel-trust-value"></span>
</div>
<div class="memory-panel-meta">
<div class="memory-meta-row"><span class="memory-meta-label">ID</span><span id="memory-panel-id">\u2014</span></div>
<div class="memory-meta-row"><span class="memory-meta-label">Source</span><span id="memory-panel-source">\u2014</span></div>
<div class="memory-meta-row"><span class="memory-meta-label">Time</span><span id="memory-panel-time">\u2014</span></div>
<div class="memory-meta-row memory-meta-row--related"><span class="memory-meta-label">Related</span><span id="memory-panel-connections">\u2014</span></div>
</div>
<div class="memory-panel-actions">
<button id="mnemosyne-export-btn" class="mnemosyne-action-btn" title="Export spatial memory to JSON">&#x2913; Export</button>
<button id="mnemosyne-import-btn" class="mnemosyne-action-btn" title="Import spatial memory from JSON">&#x2912; Import</button>
<input type="file" id="mnemosyne-import-file" accept=".json" style="display:none;">
</div>
</div>
</div>
<!-- Session Room HUD Panel (Mnemosyne #1171) -->
<div id="session-room-panel" class="session-room-panel" style="display:none;">
<div class="session-room-panel-content">
<div class="session-room-header">
<span class="session-room-icon">&#x25A1;</span>
<div class="session-room-title">SESSION CHAMBER</div>
<button class="session-room-close" id="session-room-close" aria-label="Close session room panel" title="Close" data-tooltip="Close">&#x2715;</button>
</div>
<div class="session-room-timestamp" id="session-room-timestamp">&mdash;</div>
<div class="session-room-fact-count" id="session-room-fact-count">0 facts</div>
<div class="session-room-facts" id="session-room-facts"></div>
<div class="session-room-hint">Flying into chamber&hellip;</div>
</div>
</div>
<!-- Portal Atlas Overlay -->
<div id="atlas-overlay" class="atlas-overlay" style="display:none;">
<div class="atlas-content">
@@ -304,7 +215,7 @@ chdir: error retrieving current directory: getcwd: cannot access parent director
<span class="atlas-icon">🌐</span>
<h2>PORTAL ATLAS</h2>
</div>
<button id="atlas-close-btn" class="atlas-close-btn" aria-label="Close Portal Atlas overlay">CLOSE</button>
<button id="atlas-close-btn" class="atlas-close-btn">CLOSE</button>
</div>
<div class="atlas-grid" id="atlas-grid">
<!-- Portals will be injected here -->
@@ -528,5 +439,26 @@ index.html
setInterval(poll, INTERVAL);
})();
</script>
<!-- Memory Activity Feed (Mnemosyne) -->
<!-- Mnemosyne Memory Search Panel (#1208) -->
<div id="memory-search-panel" style="display:none;">
<div class="memory-search-header">
<span class="memory-search-icon">🔍</span>
<input type="text" id="memory-search-input" placeholder="Search archive..." autocomplete="off" spellcheck="false" />
<span class="memory-search-close" onclick="toggleMemorySearch()"></span>
</div>
<div id="memory-search-results"></div>
</div>
<div id="memory-feed" class="memory-feed" style="display:none;">
<div class="memory-feed-header">
<span class="memory-feed-title">✨ Memory Feed</span>
<div class="memory-feed-actions"><button class="memory-feed-clear" onclick="clearMemoryFeed()">Clear</button><button class="memory-feed-toggle" onclick="document.getElementById('memory-feed').style.display='none'"></button></div>
</div>
<div id="memory-feed-list" class="memory-feed-list"></div>
</div>
</body>
</html>

View File

@@ -0,0 +1,99 @@
// ═══════════════════════════════════════════
// PROJECT MNEMOSYNE — MEMORY OPTIMIZER (GOFAI)
// ═══════════════════════════════════════════
//
// Heuristic-based memory pruning and organization.
// Operates without LLMs to maintain a lean, high-signal spatial index.
//
// Heuristics:
// 1. Strength Decay: Memories lose strength over time if not accessed.
// 2. Redundancy: Simple string similarity to identify duplicates.
// 3. Isolation: Memories with no connections are lower priority.
// 4. Aging: Old memories in 'working' are moved to 'archive'.
// ═══════════════════════════════════════════
const MemoryOptimizer = (() => {
const DECAY_RATE = 0.01; // Strength lost per optimization cycle
const PRUNE_THRESHOLD = 0.1; // Remove if strength < this
const SIMILARITY_THRESHOLD = 0.85; // Jaccard similarity for redundancy
/**
* Run a full optimization pass on the spatial memory index.
* @param {object} spatialMemory - The SpatialMemory component instance.
* @returns {object} Summary of actions taken.
*/
function optimize(spatialMemory) {
const memories = spatialMemory.getAllMemories();
const results = { pruned: 0, moved: 0, updated: 0 };
// 1. Strength Decay & Aging
memories.forEach(mem => {
let strength = mem.strength || 0.7;
strength -= DECAY_RATE;
if (strength < PRUNE_THRESHOLD) {
spatialMemory.removeMemory(mem.id);
results.pruned++;
return;
}
// Move old working memories to archive
if (mem.category === 'working') {
const timestamp = mem.timestamp || new Date().toISOString();
const age = Date.now() - new Date(timestamp).getTime();
if (age > 1000 * 60 * 60 * 24) { // 24 hours
spatialMemory.removeMemory(mem.id);
spatialMemory.placeMemory({ ...mem, category: 'archive', strength });
results.moved++;
return;
}
}
spatialMemory.updateMemory(mem.id, { strength });
results.updated++;
});
// 2. Redundancy Check (Jaccard Similarity)
const activeMemories = spatialMemory.getAllMemories();
for (let i = 0; i < activeMemories.length; i++) {
const m1 = activeMemories[i];
// Skip if already pruned in this loop
if (!spatialMemory.getAllMemories().find(m => m.id === m1.id)) continue;
for (let j = i + 1; j < activeMemories.length; j++) {
const m2 = activeMemories[j];
if (m1.category !== m2.category) continue;
const sim = _calculateSimilarity(m1.content, m2.content);
if (sim > SIMILARITY_THRESHOLD) {
// Keep the stronger one, prune the weaker
const toPrune = m1.strength >= m2.strength ? m2.id : m1.id;
spatialMemory.removeMemory(toPrune);
results.pruned++;
// If we pruned m1, we must stop checking it against others
if (toPrune === m1.id) break;
}
}
}
console.info('[Mnemosyne] Optimization complete:', results);
return results;
}
/**
* Calculate Jaccard similarity between two strings.
* @private
*/
function _calculateSimilarity(s1, s2) {
if (!s1 || !s2) return 0;
const set1 = new Set(s1.toLowerCase().split(/\s+/));
const set2 = new Set(s2.toLowerCase().split(/\s+/));
const intersection = new Set([...set1].filter(x => set2.has(x)));
const union = new Set([...set1, ...set2]);
return intersection.size / union.size;
}
return { optimize };
})();
export { MemoryOptimizer };

View File

@@ -32,9 +32,6 @@
const SpatialMemory = (() => {
// ─── CALLBACKS ────────────────────────────────────────
let _onMemoryPlacedCallback = null;
// ─── REGION DEFINITIONS ───────────────────────────────
const REGIONS = {
engineering: {
@@ -136,6 +133,9 @@ const SpatialMemory = (() => {
let _regionMarkers = {};
let _memoryObjects = {};
let _connectionLines = [];
let _entityLines = []; // entity resolution lines (issue #1167)
let _camera = null; // set by setCamera() for LOD culling
const ENTITY_LOD_DIST = 50; // hide entity lines when camera > this from midpoint
let _initialized = false;
// ─── CRYSTAL GEOMETRY (persistent memories) ───────────
@@ -143,47 +143,6 @@ const SpatialMemory = (() => {
return new THREE.OctahedronGeometry(size, 0);
}
// ─── TRUST-BASED VISUALS ─────────────────────────────
// Wire crystal visual properties to fact trust score (0.0-1.0).
// Issue #1166: Trust > 0.8 = bright glow/full opacity,
// 0.5-0.8 = medium/80%, < 0.5 = dim/40%, < 0.3 = near-invisible pulsing red.
function _getTrustVisuals(trust, regionColor) {
const t = Math.max(0, Math.min(1, trust));
if (t >= 0.8) {
return {
opacity: 1.0,
emissiveIntensity: 2.0 * t,
emissiveColor: regionColor,
lightIntensity: 1.2,
glowDesc: 'high'
};
} else if (t >= 0.5) {
return {
opacity: 0.8,
emissiveIntensity: 1.2 * t,
emissiveColor: regionColor,
lightIntensity: 0.6,
glowDesc: 'medium'
};
} else if (t >= 0.3) {
return {
opacity: 0.4,
emissiveIntensity: 0.5 * t,
emissiveColor: regionColor,
lightIntensity: 0.2,
glowDesc: 'dim'
};
} else {
return {
opacity: 0.15,
emissiveIntensity: 0.3,
emissiveColor: 0xff2200,
lightIntensity: 0.1,
glowDesc: 'untrusted'
};
}
}
// ─── REGION MARKER ───────────────────────────────────
function createRegionMarker(regionKey, region) {
const cx = region.center[0];
@@ -250,7 +209,147 @@ const SpatialMemory = (() => {
sprite.scale.set(4, 1, 1);
_scene.add(sprite);
return { ring, disc, glowDisc, sprite };
// ─── BULK IMPORT (WebSocket sync) ───────────────────
/**
* Import an array of memories in batch — for WebSocket sync.
* Skips duplicates (same id). Returns count of newly placed.
* @param {Array} memories - Array of memory objects { id, content, category, ... }
* @returns {number} Count of newly placed memories
*/
function importMemories(memories) {
if (!Array.isArray(memories) || memories.length === 0) return 0;
let count = 0;
memories.forEach(mem => {
if (mem.id && !_memoryObjects[mem.id]) {
placeMemory(mem);
count++;
}
});
if (count > 0) {
_dirty = true;
saveToStorage();
console.info('[Mnemosyne] Bulk imported', count, 'new memories (total:', Object.keys(_memoryObjects).length, ')');
}
return count;
}
// ─── UPDATE MEMORY ──────────────────────────────────
/**
* Update an existing memory's visual properties (strength, connections).
* Does not move the crystal — only updates metadata and re-renders.
* @param {string} memId - Memory ID to update
* @param {object} updates - Fields to update: { strength, connections, content }
* @returns {boolean} True if updated
*/
function updateMemory(memId, updates) {
const obj = _memoryObjects[memId];
if (!obj) return false;
if (updates.strength != null) {
const strength = Math.max(0.05, Math.min(1, updates.strength));
obj.mesh.userData.strength = strength;
obj.mesh.material.emissiveIntensity = 1.5 * strength;
obj.mesh.material.opacity = 0.5 + strength * 0.4;
}
if (updates.content != null) {
obj.data.content = updates.content;
}
if (updates.connections != null) {
obj.data.connections = updates.connections;
// Rebuild connection lines
_rebuildConnections(memId);
}
_dirty = true;
saveToStorage();
return true;
}
function _rebuildConnections(memId) {
// Remove existing lines for this memory
for (let i = _connectionLines.length - 1; i >= 0; i--) {
const line = _connectionLines[i];
if (line.userData.from === memId || line.userData.to === memId) {
if (line.parent) line.parent.remove(line);
line.geometry.dispose();
line.material.dispose();
_connectionLines.splice(i, 1);
}
}
// Recreate lines for current connections
const obj = _memoryObjects[memId];
if (!obj || !obj.data.connections) return;
obj.data.connections.forEach(targetId => {
const target = _memoryObjects[targetId];
if (target) _createConnectionLine(obj, target);
});
}
// ─── CONTENT SEARCH (Mnemosyne #1208) ──────────────────
/**
* Search memories by text content. Case-insensitive substring match.
* @param {string} query - Search query
* @param {object} options - { category: string, maxResults: number, minStrength: number }
* @returns {Array} Matching memories sorted by relevance (strength desc, then match position)
*/
function searchByContent(query, options) {
options = options || {};
const maxResults = options.maxResults || 20;
const minStrength = options.minStrength || 0;
const categoryFilter = options.category || null;
if (!query || query.trim().length === 0) return [];
const lowerQuery = query.toLowerCase().trim();
const terms = lowerQuery.split(/\s+/);
const results = [];
Object.values(_memoryObjects).forEach(obj => {
const data = obj.data;
if (!data.content) return;
// Category filter
if (categoryFilter && obj.region !== categoryFilter) return;
// Strength filter
const strength = obj.mesh.userData.strength || 0.7;
if (strength < minStrength) return;
const lowerContent = data.content.toLowerCase();
// Score: count how many terms match, weight by first-match position
let matchCount = 0;
let firstMatchPos = Infinity;
terms.forEach(term => {
const pos = lowerContent.indexOf(term);
if (pos !== -1) {
matchCount++;
if (pos < firstMatchPos) firstMatchPos = pos;
}
});
if (matchCount > 0) {
const relevance = matchCount * 100 + strength * 10 - firstMatchPos * 0.01;
results.push({
id: data.id,
content: data.content,
category: obj.region,
strength: strength,
position: [obj.mesh.position.x, obj.mesh.position.y - 1.5, obj.mesh.position.z],
relevance: relevance,
matchCount: matchCount,
source: data.source || 'unknown',
timestamp: data.timestamp || obj.mesh.userData.createdAt
});
}
});
results.sort((a, b) => b.relevance - a.relevance);
return results.slice(0, maxResults);
}
return { ring, disc, glowDisc, sprite };
}
// ─── PLACE A MEMORY ──────────────────────────────────
@@ -260,20 +359,17 @@ const SpatialMemory = (() => {
const region = REGIONS[mem.category] || REGIONS.working;
const pos = mem.position || _assignPosition(mem.category, mem.id);
const strength = Math.max(0.05, Math.min(1, mem.strength != null ? mem.strength : 0.7));
const trust = mem.trust != null ? Math.max(0, Math.min(1, mem.trust)) : 0.7;
const size = 0.2 + strength * 0.3;
const tv = _getTrustVisuals(trust, region.color);
const geo = createCrystalGeometry(size);
const mat = new THREE.MeshStandardMaterial({
color: region.color,
emissive: tv.emissiveColor,
emissiveIntensity: tv.emissiveIntensity,
emissive: region.color,
emissiveIntensity: 1.5 * strength,
metalness: 0.6,
roughness: 0.15,
transparent: true,
opacity: tv.opacity
opacity: 0.5 + strength * 0.4
});
const crystal = new THREE.Mesh(geo, mat);
@@ -286,12 +382,10 @@ const SpatialMemory = (() => {
region: mem.category,
pulse: Math.random() * Math.PI * 2,
strength: strength,
trust: trust,
glowDesc: tv.glowDesc,
createdAt: mem.timestamp || new Date().toISOString()
};
const light = new THREE.PointLight(tv.emissiveColor, tv.lightIntensity, 5);
const light = new THREE.PointLight(region.color, 0.8 * strength, 5);
crystal.add(light);
_scene.add(crystal);
@@ -301,15 +395,13 @@ const SpatialMemory = (() => {
_drawConnections(mem.id, mem.connections);
}
if (mem.entity) {
_drawEntityLines(mem.id, mem);
}
_dirty = true;
saveToStorage();
console.info('[Mnemosyne] Spatial memory placed:', mem.id, 'in', region.label);
// Fire particle burst callback
if (_onMemoryPlacedCallback) {
_onMemoryPlacedCallback(crystal.position.clone(), mem.category || 'working');
}
return crystal;
}
@@ -353,6 +445,77 @@ const SpatialMemory = (() => {
});
}
// ─── ENTITY RESOLUTION LINES (#1167) ──────────────────
// Draw lines between crystals that share an entity or are related entities.
// Same entity → thin blue line. Related entities → thin purple dashed line.
function _drawEntityLines(memId, mem) {
if (!mem.entity) return;
const src = _memoryObjects[memId];
if (!src) return;
Object.entries(_memoryObjects).forEach(([otherId, other]) => {
if (otherId === memId) return;
const otherData = other.data;
if (!otherData.entity) return;
let lineType = null;
if (otherData.entity === mem.entity) {
lineType = 'same_entity';
} else if (mem.related_entities && mem.related_entities.includes(otherData.entity)) {
lineType = 'related';
} else if (otherData.related_entities && otherData.related_entities.includes(mem.entity)) {
lineType = 'related';
}
if (!lineType) return;
// Deduplicate — only draw from lower ID to higher
if (memId > otherId) return;
const points = [src.mesh.position.clone(), other.mesh.position.clone()];
const geo = new THREE.BufferGeometry().setFromPoints(points);
let mat;
if (lineType === 'same_entity') {
mat = new THREE.LineBasicMaterial({ color: 0x4488ff, transparent: true, opacity: 0.35 });
} else {
mat = new THREE.LineDashedMaterial({ color: 0x9966ff, dashSize: 0.3, gapSize: 0.2, transparent: true, opacity: 0.25 });
const line = new THREE.Line(geo, mat);
line.computeLineDistances();
line.userData = { type: 'entity_line', from: memId, to: otherId, lineType };
_scene.add(line);
_entityLines.push(line);
return;
}
const line = new THREE.Line(geo, mat);
line.userData = { type: 'entity_line', from: memId, to: otherId, lineType };
_scene.add(line);
_entityLines.push(line);
});
}
function _updateEntityLines() {
if (!_camera) return;
const camPos = _camera.position;
_entityLines.forEach(line => {
// Compute midpoint of line
const posArr = line.geometry.attributes.position.array;
const mx = (posArr[0] + posArr[3]) / 2;
const my = (posArr[1] + posArr[4]) / 2;
const mz = (posArr[2] + posArr[5]) / 2;
const dist = camPos.distanceTo(new THREE.Vector3(mx, my, mz));
if (dist > ENTITY_LOD_DIST) {
line.visible = false;
} else {
line.visible = true;
// Fade based on distance
const fade = Math.max(0, 1 - (dist / ENTITY_LOD_DIST));
const baseOpacity = line.userData.lineType === 'same_entity' ? 0.35 : 0.25;
line.material.opacity = baseOpacity * fade;
}
});
}
// ─── REMOVE A MEMORY ─────────────────────────────────
function removeMemory(memId) {
const obj = _memoryObjects[memId];
@@ -372,6 +535,16 @@ const SpatialMemory = (() => {
}
}
for (let i = _entityLines.length - 1; i >= 0; i--) {
const line = _entityLines[i];
if (line.userData.from === memId || line.userData.to === memId) {
if (line.parent) line.parent.remove(line);
line.geometry.dispose();
line.material.dispose();
_entityLines.splice(i, 1);
}
}
delete _memoryObjects[memId];
_dirty = true;
saveToStorage();
@@ -392,19 +565,13 @@ const SpatialMemory = (() => {
mesh.scale.setScalar(pulse);
if (mesh.material) {
const trust = mesh.userData.trust != null ? mesh.userData.trust : 0.7;
const base = mesh.userData.strength || 0.7;
if (trust < 0.3) {
// Low trust: pulsing red — visible warning
const pulseAlpha = 0.15 + Math.sin(mesh.userData.pulse * 2.0) * 0.15;
mesh.material.emissiveIntensity = 0.3 + Math.sin(mesh.userData.pulse * 2.0) * 0.3;
mesh.material.opacity = pulseAlpha;
} else {
mesh.material.emissiveIntensity = 1.0 + Math.sin(mesh.userData.pulse * 0.7) * 0.5 * base;
}
mesh.material.emissiveIntensity = 1.0 + Math.sin(mesh.userData.pulse * 0.7) * 0.5 * base;
}
});
_updateEntityLines();
Object.values(_regionMarkers).forEach(marker => {
if (marker.ring && marker.ring.material) {
marker.ring.material.opacity = 0.1 + Math.sin(now * 0.001) * 0.05;
@@ -431,42 +598,6 @@ const SpatialMemory = (() => {
return REGIONS;
}
// ─── UPDATE VISUAL PROPERTIES ────────────────────────
// Re-render crystal when trust/strength change (no position move).
function updateMemoryVisual(memId, updates) {
const obj = _memoryObjects[memId];
if (!obj) return false;
const mesh = obj.mesh;
const region = REGIONS[obj.region] || REGIONS.working;
if (updates.trust != null) {
const trust = Math.max(0, Math.min(1, updates.trust));
mesh.userData.trust = trust;
obj.data.trust = trust;
const tv = _getTrustVisuals(trust, region.color);
mesh.material.emissive = new THREE.Color(tv.emissiveColor);
mesh.material.emissiveIntensity = tv.emissiveIntensity;
mesh.material.opacity = tv.opacity;
mesh.userData.glowDesc = tv.glowDesc;
if (mesh.children.length > 0 && mesh.children[0].isPointLight) {
mesh.children[0].intensity = tv.lightIntensity;
mesh.children[0].color = new THREE.Color(tv.emissiveColor);
}
}
if (updates.strength != null) {
const strength = Math.max(0.05, Math.min(1, updates.strength));
mesh.userData.strength = strength;
obj.data.strength = strength;
}
_dirty = true;
saveToStorage();
console.info('[Mnemosyne] Visual updated:', memId, 'trust:', mesh.userData.trust, 'glow:', mesh.userData.glowDesc);
return true;
}
// ─── QUERY ───────────────────────────────────────────
function getMemoryAtPosition(position, maxDist) {
maxDist = maxDist || 2;
@@ -606,7 +737,6 @@ const SpatialMemory = (() => {
source: o.data.source || 'unknown',
timestamp: o.data.timestamp || o.mesh.userData.createdAt,
strength: o.mesh.userData.strength || 0.7,
trust: o.mesh.userData.trust != null ? o.mesh.userData.trust : 0.7,
connections: o.data.connections || []
}))
};
@@ -752,173 +882,18 @@ const SpatialMemory = (() => {
return _selectedId;
}
// ─── FILE EXPORT ──────────────────────────────────────
function exportToFile() {
const index = exportIndex();
const json = JSON.stringify(index, null, 2);
const date = new Date().toISOString().slice(0, 10);
const filename = 'mnemosyne-export-' + date + '.json';
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.info('[Mnemosyne] Exported', index.memories.length, 'memories to', filename);
return { filename, count: index.memories.length };
}
// ─── FILE IMPORT ──────────────────────────────────────
function importFromFile(file) {
return new Promise((resolve, reject) => {
if (!file) {
reject(new Error('No file provided'));
return;
}
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = JSON.parse(e.target.result);
// Schema validation
if (!data || typeof data !== 'object') {
reject(new Error('Invalid JSON: not an object'));
return;
}
if (typeof data.version !== 'number') {
reject(new Error('Invalid schema: missing version field'));
return;
}
if (data.version !== STORAGE_VERSION) {
reject(new Error('Version mismatch: got ' + data.version + ', expected ' + STORAGE_VERSION));
return;
}
if (!Array.isArray(data.memories)) {
reject(new Error('Invalid schema: memories is not an array'));
return;
}
// Validate each memory entry
for (let i = 0; i < data.memories.length; i++) {
const mem = data.memories[i];
if (!mem.id || typeof mem.id !== 'string') {
reject(new Error('Invalid memory at index ' + i + ': missing or invalid id'));
return;
}
if (!mem.category || typeof mem.category !== 'string') {
reject(new Error('Invalid memory "' + mem.id + '": missing category'));
return;
}
}
const count = importIndex(data);
saveToStorage();
console.info('[Mnemosyne] Imported', count, 'memories from file');
resolve({ count, total: data.memories.length });
} catch (parseErr) {
reject(new Error('Failed to parse JSON: ' + parseErr.message));
}
};
reader.onerror = function() {
reject(new Error('Failed to read file'));
};
reader.readAsText(file);
});
}
// ─── SPATIAL SEARCH (issue #1170) ────────────────────
let _searchOriginalState = {}; // memId -> { emissiveIntensity, opacity } for restore
function searchContent(query) {
if (!query || !query.trim()) return [];
const q = query.toLowerCase().trim();
const matches = [];
Object.values(_memoryObjects).forEach(obj => {
const d = obj.data;
const searchable = [
d.content || '',
d.id || '',
d.category || '',
d.source || '',
...(d.connections || [])
].join(' ').toLowerCase();
if (searchable.includes(q)) {
matches.push(d.id);
}
});
return matches;
}
function highlightSearchResults(matchIds) {
// Save original state and apply search highlighting
_searchOriginalState = {};
const matchSet = new Set(matchIds);
Object.entries(_memoryObjects).forEach(([id, obj]) => {
const mat = obj.mesh.material;
_searchOriginalState[id] = {
emissiveIntensity: mat.emissiveIntensity,
opacity: mat.opacity
};
if (matchSet.has(id)) {
// Match: bright white glow
mat.emissive.setHex(0xffffff);
mat.emissiveIntensity = 5.0;
mat.opacity = 1.0;
} else {
// Non-match: dim to 10% opacity
mat.opacity = 0.1;
mat.emissiveIntensity = 0.2;
}
});
}
function clearSearch() {
Object.entries(_memoryObjects).forEach(([id, obj]) => {
const mat = obj.mesh.material;
const saved = _searchOriginalState[id];
if (saved) {
// Restore original emissive color from region
const region = REGIONS[obj.region] || REGIONS.working;
mat.emissive.copy(region.color);
mat.emissiveIntensity = saved.emissiveIntensity;
mat.opacity = saved.opacity;
}
});
_searchOriginalState = {};
}
function getSearchMatchPosition(matchId) {
const obj = _memoryObjects[matchId];
return obj ? obj.mesh.position.clone() : null;
}
function setOnMemoryPlaced(callback) {
_onMemoryPlacedCallback = callback;
// ─── CAMERA REFERENCE (for entity line LOD) ─────────
function setCamera(camera) {
_camera = camera;
}
return {
init, placeMemory, removeMemory, update, updateMemoryVisual,
init, placeMemory, removeMemory, update, importMemories, updateMemory,
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,
getCrystalMeshes, getMemoryFromMesh, highlightMemory, clearHighlight, getSelectedId,
exportIndex, importIndex, exportToFile, importFromFile, searchNearby, REGIONS,
exportIndex, importIndex, searchNearby, REGIONS,
saveToStorage, loadFromStorage, clearStorage,
runGravityLayout,
searchContent, highlightSearchResults, clearSearch, getSearchMatchPosition,
setOnMemoryPlaced
runGravityLayout, searchByContent
};
})();

27
scripts/guardrails.sh Normal file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
# [Mnemosyne] Agent Guardrails — The Nexus
# Validates code integrity and scans for secrets before deployment.
echo "--- [Mnemosyne] Running Guardrails ---"
# 1. Syntax Checks
echo "[1/3] Validating syntax..."
for f in ; do
node --check "$f" || { echo "Syntax error in $f"; exit 1; }
done
echo "Syntax OK."
# 2. JSON/YAML Validation
echo "[2/3] Validating configs..."
for f in ; do
node -e "JSON.parse(require('fs').readFileSync('$f'))" || { echo "Invalid JSON: $f"; exit 1; }
done
echo "Configs OK."
# 3. Secret Scan
echo "[3/3] Scanning for secrets..."
grep -rE "AI_|TOKEN|KEY|SECRET" . --exclude-dir=node_modules --exclude=guardrails.sh | grep -v "process.env" && {
echo "WARNING: Potential secrets found!"
} || echo "No secrets detected."
echo "--- Guardrails Passed ---"

26
scripts/smoke.mjs Normal file
View File

@@ -0,0 +1,26 @@
/**
* [Mnemosyne] Smoke Test — The Nexus
* Verifies core components are loadable and basic state is consistent.
*/
import { SpatialMemory } from '../nexus/components/spatial-memory.js';
import { MemoryOptimizer } from '../nexus/components/memory-optimizer.js';
console.log('--- [Mnemosyne] Running Smoke Test ---');
// 1. Verify Components
if (!SpatialMemory || !MemoryOptimizer) {
console.error('Failed to load core components');
process.exit(1);
}
console.log('Components loaded.');
// 2. Verify Regions
const regions = Object.keys(SpatialMemory.REGIONS || {});
if (regions.length < 5) {
console.error('SpatialMemory regions incomplete:', regions);
process.exit(1);
}
console.log('Regions verified:', regions.join(', '));
console.log('--- Smoke Test Passed ---');

877
style.css
View File

@@ -200,61 +200,6 @@ canvas#nexus-canvas {
box-shadow: 0 0 20px var(--color-primary);
}
/* === TOOLTIP SYSTEM === */
/* Any element with data-tooltip gets a hover tooltip label */
[data-tooltip] {
position: relative;
}
[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
right: calc(100% + 10px);
top: 50%;
transform: translateY(-50%);
background: rgba(5, 5, 16, 0.95);
color: var(--color-primary);
font-family: var(--font-body);
font-size: 11px;
letter-spacing: 0.05em;
padding: 4px 10px;
border: 1px solid var(--color-primary-dim);
border-radius: 4px;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
backdrop-filter: blur(8px);
box-shadow: 0 0 12px rgba(74, 240, 192, 0.15);
z-index: 100;
}
[data-tooltip]:hover::after,
[data-tooltip]:focus-visible::after {
opacity: 1;
}
/* For elements positioned on the right side, tooltip appears to the left */
.hud-top-right [data-tooltip]::after {
right: calc(100% + 10px);
}
/* For inline/badge elements where right-side tooltip might clip */
.hud-status-item[data-tooltip]::after {
right: auto;
left: calc(100% + 10px);
}
/* Focus-visible ring for keyboard navigation */
.hud-icon-btn:focus-visible,
.hud-status-item:focus-visible,
.atlas-close-btn:focus-visible,
.vision-close-btn:focus-visible,
.portal-close-btn:focus-visible,
.memory-panel-close:focus-visible,
.memory-panel-pin:focus-visible,
.session-room-close:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
box-shadow: 0 0 16px rgba(74, 240, 192, 0.4);
}
.hud-status-item {
display: flex;
align-items: center;
@@ -422,142 +367,6 @@ canvas#nexus-canvas {
.status-online { background: rgba(74, 240, 192, 0.2); color: var(--color-primary); border: 1px solid var(--color-primary); }
.status-standby { background: rgba(255, 215, 0, 0.2); color: var(--color-gold); border: 1px solid var(--color-gold); }
.status-offline { background: rgba(255, 68, 102, 0.2); color: var(--color-danger); border: 1px solid var(--color-danger); }
.status-active { background: rgba(74, 240, 192, 0.2); color: var(--color-primary); border: 1px solid var(--color-primary); }
.status-blocked { background: rgba(255, 68, 102, 0.3); color: #ff4466; border: 1px solid #ff4466; }
.status-downloaded { background: rgba(100, 149, 237, 0.2); color: #6495ed; border: 1px solid #6495ed; }
.status-runtime_ready { background: rgba(255, 165, 0, 0.2); color: #ffa500; border: 1px solid #ffa500; }
.status-launched { background: rgba(255, 215, 0, 0.2); color: var(--color-gold); border: 1px solid var(--color-gold); }
.status-harness_bridged { background: rgba(74, 240, 192, 0.2); color: var(--color-primary); border: 1px solid var(--color-primary); }
/* Readiness Progress Bar (atlas card) */
.atlas-card-readiness {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid rgba(255,255,255,0.06);
}
.readiness-bar-track {
width: 100%;
height: 4px;
background: rgba(255,255,255,0.08);
border-radius: 2px;
overflow: hidden;
margin-bottom: 6px;
}
.readiness-bar-fill {
height: 100%;
border-radius: 2px;
transition: width 0.4s ease;
}
.readiness-steps-mini {
display: flex;
gap: 6px;
font-size: 9px;
font-family: var(--font-body);
letter-spacing: 0.05em;
color: var(--color-text-muted);
}
.readiness-step {
padding: 1px 5px;
border-radius: 2px;
background: rgba(255,255,255,0.04);
}
.readiness-step.done {
background: rgba(74, 240, 192, 0.15);
color: var(--color-primary);
}
.readiness-step.current {
background: rgba(255, 215, 0, 0.15);
color: var(--color-gold);
}
.atlas-card-blocked {
margin-top: 6px;
font-size: 10px;
color: #ff4466;
font-family: var(--font-body);
}
/* Readiness Detail (portal overlay) */
.portal-readiness-detail {
margin-top: 16px;
padding: 12px 16px;
background: rgba(0,0,0,0.3);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 4px;
}
.portal-readiness-title {
font-family: var(--font-display);
font-size: 10px;
letter-spacing: 0.15em;
color: var(--color-text-muted);
margin-bottom: 10px;
text-transform: uppercase;
}
.portal-readiness-step {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
font-family: var(--font-body);
font-size: 11px;
color: rgba(255,255,255,0.4);
}
.portal-readiness-step .step-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255,255,255,0.15);
flex-shrink: 0;
}
.portal-readiness-step.done .step-dot {
background: var(--color-primary);
box-shadow: 0 0 6px var(--color-primary);
}
.portal-readiness-step.done {
color: var(--color-primary);
}
.portal-readiness-step.current .step-dot {
background: var(--color-gold);
box-shadow: 0 0 6px var(--color-gold);
animation: pulse-dot 1.5s ease-in-out infinite;
}
.portal-readiness-step.current {
color: #fff;
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.portal-readiness-blocked {
margin-top: 8px;
padding: 6px 10px;
background: rgba(255, 68, 102, 0.1);
border: 1px solid rgba(255, 68, 102, 0.3);
border-radius: 3px;
font-size: 11px;
color: #ff4466;
font-family: var(--font-body);
}
.portal-readiness-hint {
margin-top: 8px;
font-size: 10px;
color: var(--color-text-muted);
font-family: var(--font-body);
font-style: italic;
}
/* HUD Status for readiness states */
.hud-status-item.downloaded .status-dot { background: #6495ed; box-shadow: 0 0 5px #6495ed; }
.hud-status-item.runtime_ready .status-dot { background: #ffa500; box-shadow: 0 0 5px #ffa500; }
.hud-status-item.launched .status-dot { background: var(--color-gold); box-shadow: 0 0 5px var(--color-gold); }
.hud-status-item.harness_bridged .status-dot { background: var(--color-primary); box-shadow: 0 0 5px var(--color-primary); }
.hud-status-item.blocked .status-dot { background: #ff4466; box-shadow: 0 0 5px #ff4466; }
.hud-status-item.downloaded .status-label,
.hud-status-item.runtime_ready .status-label,
.hud-status-item.launched .status-label,
.hud-status-item.harness_bridged .status-label,
.hud-status-item.blocked .status-label {
color: #fff;
}
.atlas-card-desc {
font-size: 12px;
@@ -1174,7 +983,7 @@ canvas#nexus-canvas {
.chat-quick-actions {
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: 6px;
padding: 8px 12px;
border-top: 1px solid var(--color-border);
@@ -1182,75 +991,6 @@ 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);
@@ -1396,9 +1136,6 @@ canvas#nexus-canvas {
.hud-location {
font-size: var(--text-xs);
}
.starter-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
@@ -1487,477 +1224,233 @@ canvas#nexus-canvas {
.pse-status { color: #4af0c0; font-weight: 600; }
/* ═══════════════════════════════════════════
MNEMOSYNE — MEMORY CRYSTAL INSPECTION PANEL
MNEMOSYNE — Memory Activity Feed
═══════════════════════════════════════════ */
.memory-panel {
.memory-feed {
position: fixed;
top: 50%;
right: 24px;
transform: translateY(-50%);
z-index: 120;
animation: memoryPanelIn 0.22s ease-out forwards;
}
.memory-panel-fade-out {
animation: memoryPanelOut 0.18s ease-in forwards !important;
}
@keyframes memoryPanelIn {
from { opacity: 0; transform: translateY(-50%) translateX(16px); }
to { opacity: 1; transform: translateY(-50%) translateX(0); }
}
@keyframes memoryPanelOut {
from { opacity: 1; }
to { opacity: 0; transform: translateY(-50%) translateX(12px); }
}
.memory-panel-content {
width: 340px;
background: rgba(8, 8, 24, 0.92);
backdrop-filter: blur(12px);
bottom: 20px;
left: 20px;
width: 320px;
background: rgba(10, 15, 40, 0.92);
border: 1px solid rgba(74, 240, 192, 0.25);
border-radius: 12px;
padding: 16px;
box-shadow: 0 0 30px rgba(74, 240, 192, 0.08), 0 8px 32px rgba(0, 0, 0, 0.4);
border-radius: 10px;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
z-index: 900;
font-family: 'JetBrains Mono', monospace;
animation: memoryFeedIn 0.3s ease-out;
}
.memory-panel-header {
@keyframes memoryFeedIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.memory-feed-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid rgba(74, 240, 192, 0.15);
}
.memory-feed-title {
color: #4af0c0;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.5px;
}
.memory-feed-actions {
display: flex;
gap: 8px;
align-items: center;
}
.memory-feed-clear {
background: rgba(74, 240, 192, 0.1);
border: 1px solid rgba(74, 240, 192, 0.3);
color: #4af0c0;
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
}
.memory-feed-clear:hover {
background: rgba(74, 240, 192, 0.2);
border-color: #4af0c0;
}
.memory-feed-toggle {
background: none;
border: none;
color: rgba(255, 255, 255, 0.4);
cursor: pointer;
font-size: 14px;
padding: 0 4px;
}
.memory-feed-toggle:hover { color: #fff; }
.memory-feed-list {
max-height: 200px;
overflow-y: auto;
padding: 6px 0;
}
.memory-feed-entry {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
gap: 8px;
padding: 6px 12px;
font-size: 11px;
color: rgba(255, 255, 255, 0.75);
transition: background 0.2s;
}
.memory-feed-entry:hover {
background: rgba(255, 255, 255, 0.05);
}
.memory-panel-region-dot {
width: 10px;
height: 10px;
.memory-feed-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.memory-panel-region {
font-family: var(--font-display, monospace);
font-size: 11px;
letter-spacing: 0.15em;
color: var(--color-primary, #4af0c0);
text-transform: uppercase;
flex: 1;
}
.memory-panel-close {
background: none;
border: 1px solid rgba(255, 255, 255, 0.1);
color: var(--color-text-muted, #888);
font-size: 14px;
cursor: pointer;
width: 24px;
height: 24px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.memory-panel-close:hover {
background: rgba(255, 255, 255, 0.05);
color: #fff;
}
.memory-panel-body {
font-size: 14px;
line-height: 1.6;
color: var(--color-text, #ccc);
margin-bottom: 14px;
max-height: 120px;
overflow-y: auto;
word-break: break-word;
}
.memory-panel-meta {
display: flex;
flex-direction: column;
gap: 5px;
font-size: 11px;
}
.memory-meta-row {
display: flex;
gap: 8px;
align-items: baseline;
}
.memory-meta-label {
color: var(--color-text-muted, #666);
text-transform: uppercase;
letter-spacing: 0.08em;
min-width: 50px;
.memory-feed-action {
flex-shrink: 0;
font-size: 12px;
}
.memory-meta-row span:last-child {
color: var(--color-text, #aaa);
word-break: break-all;
}
.memory-conn-tag {
display: inline-block;
background: rgba(74, 240, 192, 0.1);
border: 1px solid rgba(74, 240, 192, 0.2);
border-radius: 4px;
padding: 1px 6px;
font-size: 10px;
font-family: var(--font-mono, monospace);
color: var(--color-primary, #4af0c0);
margin: 1px 2px;
}
.memory-conn-link {
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.memory-conn-link:hover {
background: rgba(74, 240, 192, 0.22);
border-color: rgba(74, 240, 192, 0.5);
color: #fff;
}
/* Entity name — large heading inside panel */
.memory-entity-name {
font-family: var(--font-display, monospace);
font-size: 17px;
font-weight: 700;
color: #fff;
letter-spacing: 0.04em;
margin-bottom: 8px;
text-transform: capitalize;
word-break: break-word;
}
/* Category badge */
.memory-category-badge {
font-family: var(--font-display, monospace);
font-size: 9px;
letter-spacing: 0.12em;
font-weight: 700;
padding: 2px 6px;
border-radius: 4px;
border: 1px solid rgba(74, 240, 192, 0.3);
background: rgba(74, 240, 192, 0.12);
color: var(--color-primary, #4af0c0);
flex-shrink: 0;
}
/* Trust score bar */
.memory-trust-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
font-size: 11px;
}
.memory-trust-bar {
.memory-feed-content {
flex: 1;
height: 5px;
background: rgba(255, 255, 255, 0.08);
border-radius: 3px;
overflow: hidden;
}
.memory-trust-fill {
height: 100%;
border-radius: 3px;
background: var(--color-primary, #4af0c0);
transition: width 0.35s ease;
}
.memory-trust-value {
color: var(--color-text-muted, #888);
min-width: 32px;
text-align: right;
}
/* Pin button */
.memory-panel-pin {
background: none;
border: 1px solid rgba(255, 255, 255, 0.1);
color: var(--color-text-muted, #888);
font-size: 11px;
cursor: pointer;
width: 24px;
height: 24px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
flex-shrink: 0;
}
.memory-panel-pin:hover {
background: rgba(255, 255, 255, 0.05);
color: #fff;
}
.memory-panel-pin.pinned {
background: rgba(74, 240, 192, 0.15);
border-color: rgba(74, 240, 192, 0.4);
color: var(--color-primary, #4af0c0);
}
/* Related row — allow wrapping */
.memory-meta-row--related {
align-items: flex-start;
}
.memory-meta-row--related span:last-child {
flex-wrap: wrap;
display: flex;
gap: 2px;
}
/* ═══════════════════════════════════════════════════════
PROJECT MNEMOSYNE — EXPORT/IMPORT ACTIONS (#1174)
═══════════════════════════════════════════════════════ */
.memory-panel-actions {
display: flex;
gap: 8px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid rgba(123, 92, 255, 0.15);
}
.mnemosyne-action-btn {
flex: 1;
padding: 6px 10px;
background: rgba(123, 92, 255, 0.12);
border: 1px solid rgba(123, 92, 255, 0.3);
border-radius: 6px;
color: #a08cff;
font-size: 11px;
font-family: monospace;
cursor: pointer;
transition: all 0.15s ease;
text-align: center;
white-space: nowrap;
}
.mnemosyne-action-btn:hover {
background: rgba(123, 92, 255, 0.25);
border-color: rgba(123, 92, 255, 0.6);
color: #c4b5ff;
}
.mnemosyne-action-btn:active {
transform: scale(0.96);
}
/* ═══════════════════════════════════════════════════════
PROJECT MNEMOSYNE — SESSION ROOM HUD PANEL (#1171)
═══════════════════════════════════════════════════════ */
.session-room-panel {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 125;
animation: sessionPanelIn 0.25s ease-out forwards;
}
.session-room-panel.session-panel-fade-out {
animation: sessionPanelOut 0.2s ease-in forwards !important;
}
@keyframes sessionPanelIn {
from { opacity: 0; transform: translateX(-50%) translateY(12px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@keyframes sessionPanelOut {
from { opacity: 1; }
to { opacity: 0; transform: translateX(-50%) translateY(10px); }
}
.session-room-panel-content {
min-width: 320px;
max-width: 480px;
background: rgba(8, 4, 28, 0.93);
backdrop-filter: blur(14px);
border: 1px solid rgba(123, 92, 255, 0.35);
border-radius: 12px;
padding: 14px 18px;
box-shadow: 0 0 32px rgba(123, 92, 255, 0.1), 0 8px 32px rgba(0, 0, 0, 0.45);
}
.session-room-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}
.session-room-icon {
font-size: 14px;
line-height: 1;
}
.session-room-title {
font-family: var(--font-display, monospace);
font-size: 11px;
letter-spacing: 0.18em;
color: #9b7cff;
text-transform: uppercase;
flex: 1;
}
.session-room-close {
background: none;
border: none;
color: rgba(255, 255, 255, 0.35);
cursor: pointer;
font-size: 14px;
padding: 0 2px;
line-height: 1;
transition: color 0.15s;
}
.session-room-close:hover {
color: rgba(255, 255, 255, 0.8);
}
.session-room-timestamp {
font-family: var(--font-display, monospace);
font-size: 13px;
color: #c8b4ff;
margin-bottom: 6px;
letter-spacing: 0.08em;
}
.session-room-fact-count {
font-size: 11px;
color: rgba(200, 180, 255, 0.55);
margin-bottom: 10px;
}
.session-room-facts {
display: flex;
flex-direction: column;
gap: 4px;
max-height: 140px;
overflow-y: auto;
}
.session-room-fact-item {
font-size: 11px;
color: rgba(220, 210, 255, 0.75);
padding: 4px 8px;
background: rgba(123, 92, 255, 0.07);
border-left: 2px solid rgba(123, 92, 255, 0.4);
border-radius: 0 4px 4px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.session-room-hint {
margin-top: 10px;
.memory-feed-time {
flex-shrink: 0;
color: rgba(255, 255, 255, 0.3);
font-size: 10px;
color: rgba(200, 180, 255, 0.35);
text-align: center;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.memory-feed-place { border-left: 2px solid #4af0c0; }
.memory-feed-remove { border-left: 2px solid #ff4466; }
.memory-feed-update { border-left: 2px solid #ffd700; }
.memory-feed-sync { border-left: 2px solid #7b5cff; }
/* ═══ SPATIAL SEARCH OVERLAY (Mnemosyne #1170) ═══ */
.spatial-search-overlay {
/* ═══ MNEMOSYNE — Memory Search Panel (#1208) ═══ */
#memory-search-panel {
position: fixed;
top: 12px;
right: 12px;
z-index: 100;
display: flex;
flex-direction: column;
align-items: flex-end;
font-family: 'JetBrains Mono', monospace;
top: 60px;
left: 50%;
transform: translateX(-50%);
width: 420px;
max-height: 400px;
background: rgba(10, 14, 26, 0.95);
border: 1px solid rgba(74, 240, 192, 0.3);
border-radius: 8px;
z-index: 1000;
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px rgba(0,0,0,0.5), 0 0 20px rgba(74, 240, 192, 0.1);
overflow: hidden;
}
.spatial-search-input {
width: 260px;
padding: 8px 14px;
background: rgba(0, 0, 0, 0.65);
border: 1px solid rgba(74, 240, 192, 0.3);
border-radius: 6px;
color: #e0f0ff;
.memory-search-header {
display: flex;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid rgba(74, 240, 192, 0.15);
gap: 8px;
}
.memory-search-icon {
font-size: 14px;
opacity: 0.7;
}
#memory-search-input {
flex: 1;
background: transparent;
border: none;
color: #c0ffe0;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
outline: none;
backdrop-filter: blur(8px);
transition: border-color 0.2s, box-shadow 0.2s;
}
.spatial-search-input:focus {
border-color: rgba(74, 240, 192, 0.7);
box-shadow: 0 0 12px rgba(74, 240, 192, 0.15);
#memory-search-input::placeholder {
color: rgba(192, 255, 224, 0.35);
}
.spatial-search-input::placeholder {
color: rgba(224, 240, 255, 0.35);
}
.spatial-search-results {
margin-top: 4px;
max-height: 200px;
overflow-y: auto;
background: rgba(0, 0, 0, 0.55);
border: 1px solid rgba(74, 240, 192, 0.15);
border-radius: 4px;
font-size: 11px;
color: #a0c0d0;
width: 260px;
backdrop-filter: blur(8px);
display: none;
}
.spatial-search-results.visible {
display: block;
}
.spatial-search-result-item {
padding: 5px 10px;
.memory-search-close {
cursor: pointer;
border-bottom: 1px solid rgba(74, 240, 192, 0.08);
color: rgba(192, 255, 224, 0.4);
font-size: 14px;
padding: 2px 4px;
border-radius: 3px;
}
.memory-search-close:hover {
color: #ff4466;
background: rgba(255, 68, 102, 0.15);
}
#memory-search-results {
max-height: 340px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(74, 240, 192, 0.2) transparent;
}
.memory-search-result {
display: flex;
align-items: center;
padding: 8px 12px;
cursor: pointer;
gap: 10px;
border-bottom: 1px solid rgba(255,255,255,0.03);
transition: background 0.15s;
}
.memory-search-result:hover {
background: rgba(74, 240, 192, 0.08);
}
.memory-search-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.memory-search-text {
flex: 1;
font-size: 12px;
color: rgba(192, 255, 224, 0.75);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-family: 'JetBrains Mono', monospace;
}
.spatial-search-result-item:hover {
background: rgba(74, 240, 192, 0.1);
color: #e0f0ff;
}
.spatial-search-result-item .result-region {
color: #4af0c0;
font-size: 9px;
margin-right: 6px;
}
.spatial-search-count {
padding: 4px 10px;
color: rgba(74, 240, 192, 0.6);
.memory-search-meta {
font-size: 10px;
border-bottom: 1px solid rgba(74, 240, 192, 0.1);
color: rgba(192, 255, 224, 0.35);
flex-shrink: 0;
font-family: 'JetBrains Mono', monospace;
}
.memory-search-empty {
padding: 20px;
text-align: center;
color: rgba(192, 255, 224, 0.3);
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
}