aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/core/core-bundle.js552
-rw-r--r--modules/core/module-aiassist.js333
-rw-r--r--modules/core/module-loader.js4
-rw-r--r--modules/core/module-manager.js5
-rw-r--r--modules/core/module-modmgr.js29
-rw-r--r--modules/core/module-smartctx.js42
-rw-r--r--modules/ext/mindmap.js65
-rw-r--r--modules/ext/three.js1176
8 files changed, 2136 insertions, 70 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index 6f94a44..d576f14 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -1018,6 +1018,11 @@ window.VirtualFileSystem = VirtualFileSystem;
}
async loadModuleFromURL(url) {
try {
+ // Security: block cross-origin URLs for module loading
+ if (url && url.match(/^https?:\/\//) && !url.startsWith(window.location.origin)) {
+ this.terminal.print(`⚠️ Security: only same-origin URLs allowed for module loading`, 'error');
+ return false;
+ }
this.terminal.print(`Loading module from ${url}...`, 'info');
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
@@ -2000,7 +2005,15 @@ class OllamaManager {
// DeepSeek: empty response with no tools — retry once with stream forcing
if (isDeepSeek && content === '' && (!toolCalls || toolCalls.length === 0)) {
+ this.terminal.log('WARN', 'Ollama', 'DeepSeek empty response — model=' + useModel + ', messages=' + messages.length + ', tools=' + (tools ? tools.length : 0));
this.terminal.print('⚠️ DeepSeek returned empty — retrying with stream...', 'warning');
+ // Auto-truncate context: keep system prompt + last 5 messages
+ if (messages.length > 6) {
+ var sysMsg = messages[0];
+ var recentMsgs = messages.slice(-5);
+ body.messages = [sysMsg].concat(recentMsgs);
+ this.terminal.print('📏 Context truncated from ' + messages.length + ' to ' + body.messages.length + ' messages for retry', 'info');
+ }
body.stream = true;
delete body.stop;
this._abortController = new AbortController();
@@ -2035,12 +2048,16 @@ class OllamaManager {
}
}
if (fullText) {
+ this.terminal.log('INFO', 'Ollama', 'DeepSeek stream retry succeeded: ' + fullText.length + ' chars');
return {
message: { role: 'assistant', content: fullText },
tool_calls: null,
error: null
};
}
+ this.terminal.log('WARN', 'Ollama', 'DeepSeek stream retry also returned empty');
+ } else {
+ this.terminal.log('WARN', 'Ollama', 'DeepSeek stream retry HTTP ' + retryResponse.status);
}
return {
message: { role: 'assistant', content: '' },
@@ -3720,7 +3737,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
} catch (e) {}
}
async _loadExtModules(filterKeys) {
- const autoLoadKeys = filterKeys || ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'cryptogzip', 'tutorial', 'floating', 'console', 'media'];
+ const autoLoadKeys = filterKeys || ['debug-panel'];
const cacheVer = localStorage.getItem('webx11_cache_ver') || '2026-05-27-14';
let index = null;
let lastErr = null;
@@ -3743,6 +3760,9 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
return;
}
+ // Cache packages index for lazy-load dispatch
+ try { localStorage.setItem('webx11_packages_index', JSON.stringify(index)); } catch (e) {}
+
for (const key of autoLoadKeys) {
const pkg = index.packages[key];
if (pkg) {
@@ -3753,7 +3773,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
}
async reloadPersistedModules() {
const registry = this.moduleManager.moduleRegistry;
- const skipList = ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating', 'console', 'media'];
+ const skipList = ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating', 'console', 'media', 'three'];
for (const mod of registry) {
if (mod.enabled && mod.url) {
// Skip ext modules — they load via _loadExtModules
@@ -4131,6 +4151,40 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
}
}
// Enhanced: check fuzzy suggestions before showing "not found"
+ // Lazy-load: check if command belongs to an unloaded ext module
+ if (!this._extModuleCommandMap) {
+ this._extModuleCommandMap = {};
+ this._extModuleUrlMap = {};
+ try {
+ const idx = JSON.parse(localStorage.getItem('webx11_packages_index') || '{}');
+ if (idx.packages) {
+ for (const [key, pkg] of Object.entries(idx.packages)) {
+ if (pkg.commands && Array.isArray(pkg.commands)) {
+ for (const c of pkg.commands) {
+ this._extModuleCommandMap[c] = key;
+ this._extModuleUrlMap[key] = pkg.url;
+ }
+ }
+ }
+ }
+ } catch (e) {}
+ }
+ if (this._extModuleCommandMap[cmd] && !this.moduleManager.modules[this._extModuleCommandMap[cmd]]) {
+ const key = this._extModuleCommandMap[cmd];
+ const url = this._extModuleUrlMap[key];
+ if (url) {
+ this.print('⏳ Loading module for "' + cmd + '"...', 'info');
+ const loaded = await this.moduleManager.loadModuleFromURL(url);
+ if (loaded) {
+ // Retry dispatch
+ const mod = this.moduleManager.modules[key];
+ if (mod && mod.commands && mod.commands[cmd]) {
+ try { await mod.commands[cmd](args); return; }
+ catch (err) { this.print('Error: ' + err.message, 'error'); return; }
+ }
+ }
+ }
+ }
const suggestion = this.security.suggest(cmd);
if (suggestion && suggestion.dist <= 2 && suggestion.dist > 0) {
this.print(`Command not found: ${cmd}`, 'error');
@@ -7032,22 +7086,24 @@ class OllamaModule {
this.terminal.print(`✅ Module '${name}' marked as unloaded (commands will not be available)`, 'success');
};
- // module-reload <name> — unload + reload with diff
- this.commands['module-reload'] = (args) => {
+ // module-reload <name> — re-fetch, re-parse, re-register
+ this.commands['module-reload'] = async (args) => {
if (!args[0]) return this.terminal.print('Usage: module-reload <name>', 'warning');
const name = args[0];
const mod = this._getMod(name);
if (!mod) return this.terminal.print(`Module not found: ${name}`, 'error');
- const wasLoaded = mod.loaded !== false;
- const wasLazy = mod.lazy;
- this.terminal.print(`🔄 Reloading '${name}' (was: ${wasLoaded ? 'loaded' : 'unloaded'}, lazy=${wasLazy})`, 'info');
- mod.loaded = false;
- setTimeout(() => {
- mod.loaded = true;
- this.terminal.print(`✅ Module '${name}' reloaded`, 'success');
- const cmdCount = mod.commands ? Object.keys(mod.commands).length : 0;
- this.terminal.print(` Now: loaded=true, lazy=${mod.lazy}, ${cmdCount} commands`, 'info');
- }, 100);
+ const mm = this.terminal.moduleManager;
+ const regEntry = mm.moduleRegistry.find(m => m.name === name);
+ const url = regEntry ? regEntry.url : null;
+ if (!url) return this.terminal.print(`Module '${name}' has no URL — cannot reload (built-in)`, 'warning');
+ this.terminal.print(`🔄 Reloading '${name}' from ${url}...`, 'info');
+ mm.unregisterModule(name);
+ const success = await mm.loadModuleFromURL(url);
+ if (success) {
+ this.terminal.print(`✅ Module '${name}' reloaded successfully`, 'success');
+ } else {
+ this.terminal.print(`❌ Failed to reload '${name}'`, 'error');
+ }
};
// module-reset <name> — clear module state and reload
@@ -7648,6 +7704,8 @@ class ServerMonitorModule {
this._toolDefs = null;
this._toolCallLog = [];
this._debugPanelEnabled = false;
+ this._lastRawResponse = null;
+ this._lastRawMessages = null;
// 🔒 Guardrail Layer
this._guardrailPolicy = null;
this._shadowSnapshot = null;
@@ -8051,7 +8109,7 @@ buildSystemPrompt() {
type: 'function',
function: {
name: 'smart_context_list',
- description: 'List available AI context profiles (e.g. coding, debug, creative, technical). Returns profile names and system prompts.',
+ description: 'List available AI context profiles (e.g. coding, debug, creative, technical, chat, system-admin, research, security). Each profile sets the AI system prompt, temperature, and behavior for a specific task type. Also switches the agent loop profile (max turns, tool set) to match. Returns profile names, descriptions, and system prompts. Use this when the user task has a clear domain (coding, debugging, research, etc.) to optimize AI behavior.',
parameters: {
type: 'object',
properties: {},
@@ -8062,11 +8120,11 @@ buildSystemPrompt() {
type: 'function',
function: {
name: 'smart_context_set',
- description: "Set the active AI context profile. Changes the AI's behavior (temperature, style) for subsequent responses.",
+ description: "Set the active AI context profile. Changes the AI system prompt, temperature, and behavior for subsequent responses. Also switches the agent loop profile (max turns, tool set) to match. Available profiles: coding (code editing), debug (bug fixing), creative (brainstorming), technical (documentation), chat (casual), system-admin (maintenance), research (information gathering), security (threat assessment). Use this when the user task changes domain.",
parameters: {
type: 'object',
properties: {
- name: { type: 'string', description: 'Profile name (e.g. "coding", "debug", "creative", "technical", "chat")' }
+ name: { type: 'string', description: 'Profile name (e.g. "coding", "debug", "creative", "technical", "chat", "system-admin", "research", "security")' }
},
required: ['name']
}
@@ -8088,6 +8146,30 @@ buildSystemPrompt() {
}, {
type: 'function',
function: {
+ name: 'create_3d_scene',
+ description: 'Create a 3D scene with Three.js objects. Supports: boxes, spheres, torus, text, particles, lines. Also supports presets: "solar" (solar system), "grid" (color grid). Describe what you want in natural language and the module will parse it.',
+ parameters: {
+ type: 'object',
+ properties: {
+ description: { type: 'string', description: 'Natural language description of the 3D scene (e.g. "a red box and two blue spheres with particles")' }
+ },
+ required: ['description']
+ }
+ }
+ }, {
+ type: 'function',
+ function: {
+ name: 'visualize_mindmap_3d',
+ description: 'Render the current mindmap as an interactive 3D force-directed graph with colored spheres and curved connecting lines. Requires MindmapModule to have an active mindmap.',
+ parameters: {
+ type: 'object',
+ properties: {},
+ required: []
+ }
+ }
+ }, {
+ type: 'function',
+ function: {
name: 'vaultmind_query',
description: "Query VaultMind AI for Hyperliquid market data (prices, portfolio, transactions, copy-trade) or ask an AI agent. Uses same-origin /vmai/ API.",
parameters: {
@@ -8196,6 +8278,13 @@ buildSystemPrompt() {
this.commands['bench-compare'] = (args) => this.benchCompareCmd(args);
this.commands['debug'] = () => this.toggleDebugPanel();
this.commands['debug-ai'] = () => this.toggleDebug();
+ this.commands['debug-ai-last'] = () => this._showLastRawResponse();
+ this.commands['debug-ai-send'] = () => this._showLastRawRequest();
+ this.commands['debug-ai-log'] = () => this._showToolCallLog();
+ this.commands['archive-list'] = () => this._archiveList();
+ this.commands['archive-read'] = (args) => this._archiveRead(args);
+ this.commands['workflow-list'] = () => this._workflowList();
+ this.commands['workflow-run'] = (args) => this._workflowRun(args);
this.commands['guardrail'] = (args) => this.guardrailCmd(args);
this.commands['guardrail-add'] = (args) => this.guardrailCmd(['add', ...args]);
this.commands['guardrail-remove'] = (args) => this.guardrailCmd(['remove', ...args]);
@@ -8336,9 +8425,14 @@ buildSystemPrompt() {
// Log the AI interaction
this.terminal.log('INFO', 'AI', 'Query: ' + userMessage.substring(0, 100));
if (loopResult && loopResult.toolTurns) {
- this.terminal.log('INFO', 'AI', 'Used ' + loopResult.toolTurns + ' tool(s)');
+ var toolNames = this._toolCallLog.slice(-(loopResult.toolTurns || 0)).map(function(e) { return e.name; }).join(', ');
+ this.terminal.log('INFO', 'AI', 'Used ' + loopResult.toolTurns + ' tool(s): ' + toolNames);
}
}
+ // Auto-archive every 10 messages
+ if (this._messageCount > 0 && this._messageCount % 10 === 0) {
+ this._archiveSession('Auto-archive at message ' + this._messageCount);
+ }
// Register session save on page unload (runs once per session)
if (!this._sessionSaved) {
this._sessionSaved = true;
@@ -8370,12 +8464,13 @@ buildSystemPrompt() {
if (!this._debugPanelEnabled) return;
const panel = document.getElementById('debug-ai-panel');
if (!panel) return;
+ const activeTab = panel.getAttribute('data-tab') || 'tools';
const recent = this._toolCallLog.slice(-20);
- let html = '';
+ let toolHtml = '';
for (const entry of recent) {
const statusClass = entry.error ? 'debug-entry-error' : 'debug-entry-ok';
const argsStr = typeof entry.args === 'string' ? entry.args.substring(0, 60) : JSON.stringify(entry.args).substring(0, 60);
- html += '<div class="debug-entry ' + statusClass + '">' +
+ toolHtml += '<div class="debug-entry ' + statusClass + '">' +
'<span class="debug-turn">#' + entry.turn + '</span> ' +
'<span class="debug-name">' + this._escapeHtml(entry.name) + '</span> ' +
'<span class="debug-args">' + this._escapeHtml(argsStr) + '</span> ' +
@@ -8383,14 +8478,41 @@ buildSystemPrompt() {
(entry.error ? ' <span class="debug-err">⚠️</span>' : '') +
'</div>';
}
+ // Raw I/O tab content
+ let rawHtml = '';
+ if (this._lastRawResponse) {
+ const r = this._lastRawResponse;
+ const content = (r.message && r.message.content) || '';
+ const hasTools = r.tool_calls && r.tool_calls.length > 0;
+ rawHtml += '<div style="padding:6px 8px;font-size:0.75em;line-height:1.3">';
+ if (r.error) rawHtml += '<div style="color:var(--error-color)">⚠️ ' + this._escapeHtml(r.error) + '</div>';
+ if (content) {
+ const safe = this._escapeHtml(content.length > 300 ? content.substring(0, 300) + '...' : content);
+ rawHtml += '<div><strong>Content:</strong> ' + safe + '</div>';
+ }
+ if (hasTools) {
+ rawHtml += '<div><strong>Tools:</strong> ' + r.tool_calls.map(function(tc) { return tc.function.name; }).join(', ') + '</div>';
+ }
+ if (this._lastRawMessages) {
+ rawHtml += '<div style="margin-top:4px;color:var(--text-muted)">Messages: ' + this._lastRawMessages.length + '</div>';
+ }
+ rawHtml += '</div>';
+ } else {
+ rawHtml = '<div style="padding:8px;color:var(--text-muted);font-size:0.75em">No AI response yet. Send a message first.</div>';
+ }
+ const tabBar = '<div style="display:flex;border-bottom:1px solid #333">' +
+ '<span style="cursor:pointer;padding:4px 10px;font-size:0.8em;' + (activeTab === 'tools' ? 'background:var(--bg-tertiary);border-bottom:2px solid var(--success-color);font-weight:bold' : 'color:var(--text-muted)') + '" onclick="var p=document.getElementById(\'debug-ai-panel\');p.setAttribute(\'data-tab\',\'tools\');var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m)m._updateDebugPanel()">🔧 Tools</span>' +
+ '<span style="cursor:pointer;padding:4px 10px;font-size:0.8em;' + (activeTab === 'raw' ? 'background:var(--bg-tertiary);border-bottom:2px solid var(--success-color);font-weight:bold' : 'color:var(--text-muted)') + '" onclick="var p=document.getElementById(\'debug-ai-panel\');p.setAttribute(\'data-tab\',\'raw\');var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m)m._updateDebugPanel()">📡 Raw I/O</span>' +
+ '</div>';
+ const bodyHtml = activeTab === 'raw' ? rawHtml : toolHtml;
panel.innerHTML = '<div class="debug-header" style="display:flex;justify-content:space-between;align-items:center;padding:6px 10px;border-bottom:1px solid #333;font-weight:bold;color:var(--success-color);font-size:0.9em;flex-wrap:wrap;gap:4px;">' +
- '<span>🔧 Tool Log <span class="debug-count" style="color:var(--text-muted);font-weight:normal;font-size:0.85em;">' + this._toolCallLog.length + ' calls</span></span>' +
+ '<span>🔧 AI Debug <span class="debug-count" style="color:var(--text-muted);font-weight:normal;font-size:0.85em;">' + this._toolCallLog.length + ' calls</span></span>' +
'<span style="display:flex;gap:4px;">' +
'<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var t=m._toolCallLog.map(function(e){return\'#\'+e.turn+\' \'+e.name+\' (\'+e.duration+\'s)\'+(e.error?\' ⚠️\':\'\')+\'\\n\'+JSON.stringify(e.args).substring(0,80)}).join(\'\\n\\n\');navigator.clipboard.writeText(t).then(function(){m.terminal&&m.terminal.print(\'📋 Debug log copied to clipboard\',\'success\')}).catch(function(){prompt(\'Copy debug log:\',t)})}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">📋</button>' +
'<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var json=JSON.stringify(m._toolCallLog,null,2);var b=new Blob([json],{type:\'application/json\'});var u=URL.createObjectURL(b);var a=document.createElement(\'a\');a.href=u;a.download=\'debug-log-\'+new Date().toISOString().slice(0,19)+\'.json\';document.body.appendChild(a);a.click();setTimeout(function(){document.body.removeChild(a);URL.revokeObjectURL(u)},2000);m.terminal&&m.terminal.print(\'📥 Debug log exported\',\'success\')}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">📥</button>' +
'<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var existing=[];var raw=m.terminal.fs.readFile(\'/debug.log\');if(raw){try{existing=JSON.parse(raw);}catch(e){}}var log=m._toolCallLog;var seen={};existing.forEach(function(e){seen[e.turn]=true;});log.forEach(function(e){if(!seen[e.turn]){existing.push(e);seen[e.turn]=true;}});var json=JSON.stringify(existing,null,2);m.terminal.fs.writeFile(\'/debug.log\',json);m.terminal.print(\'💾 Debug log appended — \'+existing.length+\' entries total\',\'success\')}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">💾</button>' +
'<span style="cursor:pointer;color:var(--text-muted);font-size:1.1em;touch-action:manipulation;" onclick="var p=document.getElementById(\'debug-ai-panel\');if(p)p.style.display=\'none\';var a=window.webx11&&webx11.moduleManager&&webx11.moduleManager.modules&&webx11.moduleManager.modules.WebX11AIAssistantModule;if(a)a._debugPanelEnabled=false;">✕</span>' +
- '</span></div>' + html;
+ '</span></div>' + tabBar + bodyHtml;
}
_isToolError(result) {
if (!result || typeof result !== 'string') return null;
@@ -8441,6 +8563,28 @@ buildSystemPrompt() {
var loopDetect = loopCfg.loopDetection || { enabled: false, threshold: 6, windowMs: 30000 };
var confEsc = loopCfg.confidenceEscalation || { enabled: true, threshold: 90 };
var finalPrompt = loopCfg.finalTurnPrompt || 'This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know.';
+ // Memory-augmented planning: inject prior loop lessons as negative constraints
+ try {
+ var fs = this.terminal.fs;
+ if (fs) {
+ var index = fs.fsReadJSON('/skills/index.json') || {};
+ var lessonKeys = Object.keys(index).filter(function(k) { return k.indexOf('loop-lesson-') === 0; });
+ if (lessonKeys.length > 0) {
+ var recentLessons = lessonKeys.sort().slice(-3);
+ var lessonTexts = [];
+ for (var li = 0; li < recentLessons.length; li++) {
+ var lc = fs.readFile('/skills/' + recentLessons[li] + '.md');
+ if (lc) {
+ var excerpt = lc.substring(0, 300);
+ lessonTexts.push('[Lesson: ' + recentLessons[li] + '] ' + excerpt);
+ }
+ }
+ if (lessonTexts.length > 0) {
+ messages.push({ role: 'system', content: 'Prior loop lessons (read before planning):\n' + lessonTexts.join('\n---\n') + '\n\nUse these to avoid repeating past mistakes. If lessons mention tool failures, verify those tools before calling them.' });
+ }
+ }
+ }
+ } catch (e) { /* VFS not available — skip memory-augmented planning */ }
let toolTurns = 0;
const _seenCalls = new Map();
const _loopWindow = loopDetect.windowMs || 30000;
@@ -8455,6 +8599,9 @@ buildSystemPrompt() {
const thinkStart = Date.now();
const res = await this.terminal.ollamaManager.chatWithTools(messages, this.getToolDefinitions());
const thinkTime = Math.round((Date.now() - thinkStart) / 1000);
+ // Store raw response for debug-ai-last
+ this._lastRawResponse = res;
+ this._lastRawMessages = messages.slice();
if (this._cancelled) return { finalAnswer: null, toolTurns };
if (res.error) { this.terminal.print(`⚠️ AI model error (${thinkTime}s): ${res.error}`, 'error'); return { finalAnswer: null, toolTurns }; }
if (res.message && !res.tool_calls) {
@@ -9259,12 +9406,33 @@ buildSystemPrompt() {
const fs = this.terminal.fs;
const mm = this.terminal.moduleManager;
const om = this.terminal.ollamaManager;
+ var agentInfo = {};
+ try {
+ var orch = mm.modules['OrchestratorModule'];
+ var agentReg = mm.modules['AgentRegistryModule'];
+ if (orch) {
+ var agents = orch._agents || {};
+ var busy = 0, failed = 0;
+ for (var ak in agents) {
+ var s = agents[ak].status;
+ if (s === 'busy') busy++;
+ if (s === 'failed' || s === 'restarting') failed++;
+ }
+ agentInfo.activeAgents = Object.keys(agents).length;
+ agentInfo.busyAgents = busy;
+ agentInfo.failedAgents = failed;
+ }
+ if (agentReg) {
+ agentInfo.registeredAgents = Object.keys(agentReg._agents || {}).length;
+ }
+ } catch (e) {}
return JSON.stringify({
files: fs.ls().length,
dirs: fs.currentPath.length ? '/' + fs.currentPath.join('/') : '/',
modules: Object.keys(mm.modules).length,
ollama: om.connected ? `Connected (${om.currentModel})` : 'Disconnected',
- memory: performance.memory ? (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB' : 'N/A'
+ memory: performance.memory ? (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB' : 'N/A',
+ agents: agentInfo
});
}
case 'get_help': {
@@ -9631,10 +9799,26 @@ buildSystemPrompt() {
return JSON.stringify({ error: `Profile '${name}' not found`, available: Object.keys(scm.contextProfiles) });
}
scm.activeProfile = name;
+ scm.saveActiveProfile();
const profile = scm.contextProfiles[name];
// Rebuild system prompt to reflect new context profile
this.systemPrompt = this.buildSystemPrompt();
- return JSON.stringify({ success: true, name, displayName: profile.name, systemPrompt: profile.systemPrompt, temperature: profile.temperature });
+ // Also switch agent loop profile to match
+ const PROFILE_MAP = {
+ 'coding': 'coder',
+ 'debug': 'minimal',
+ 'creative': 'creative',
+ 'technical': 'research',
+ 'chat': 'default',
+ 'system-admin': 'safe',
+ 'research': 'research',
+ 'security': 'safe'
+ };
+ const targetLoop = PROFILE_MAP[name];
+ if (targetLoop && this._switchLoopProfile) {
+ this._switchLoopProfile(targetLoop);
+ }
+ return JSON.stringify({ success: true, name, displayName: profile.name, systemPrompt: profile.systemPrompt, temperature: profile.temperature, agentLoop: targetLoop || null });
}
case 'create_mindmap': {
const mindmap = this.terminal.moduleManager.modules['MindmapModule'];
@@ -9657,6 +9841,26 @@ buildSystemPrompt() {
mindmap._svg([]);
return JSON.stringify({ success: true, topic, children: added.length ? added : 'none', nodes: mindmap._countNodes(cur.root), rendered: 'inline SVG in terminal' });
}
+ case 'create_3d_scene': {
+ const three = this.terminal.moduleManager.modules['ThreeModule'];
+ if (!three) return JSON.stringify({ error: 'ThreeModule not loaded' });
+ try {
+ const result = await three.createSceneFromDescription(a.description || '');
+ return JSON.stringify({ success: true, result });
+ } catch (e) {
+ return JSON.stringify({ error: '3D scene creation failed: ' + e.message });
+ }
+ }
+ case 'visualize_mindmap_3d': {
+ const three = this.terminal.moduleManager.modules['ThreeModule'];
+ if (!three) return JSON.stringify({ error: 'ThreeModule not loaded' });
+ try {
+ const result = await three.visualizeMindmap3D(a.name || '');
+ return JSON.stringify({ success: true, result });
+ } catch (e) {
+ return JSON.stringify({ error: '3D mindmap visualization failed: ' + e.message });
+ }
+ }
case 'view_module':
case 'edit_module':
case 'test_module':
@@ -10331,6 +10535,191 @@ buildSystemPrompt() {
}
return total;
}
+ _showToolCallLog() {
+ if (!this._toolCallLog || this._toolCallLog.length === 0) {
+ this.terminal.print('No tool calls recorded this session.', 'warning');
+ return;
+ }
+ let out = '── Tool Call Log (' + this._toolCallLog.length + ' calls) ──\n';
+ for (var i = 0; i < this._toolCallLog.length; i++) {
+ var e = this._toolCallLog[i];
+ var err = e.error ? ' ⚠️' : '';
+ var argsStr = typeof e.args === 'string' ? e.args.substring(0, 80) : JSON.stringify(e.args).substring(0, 80);
+ out += ' #' + e.turn + ' ' + e.name + '(' + argsStr + ') ' + e.duration + 's' + err + '\n';
+ }
+ out += '── End ──';
+ this.terminal.print(out, 'info');
+ }
+ _archiveList() {
+ try {
+ const fs = this.terminal.fs;
+ const files = fs.ls('/memory/archive') || [];
+ const jsonFiles = files.filter(function(f) { return f.name && f.name.endsWith('.json'); });
+ if (jsonFiles.length === 0) {
+ this.terminal.print('No archives found. Run ai-compress to create one.', 'warning');
+ return;
+ }
+ jsonFiles.sort(function(a, b) { return (a.name || '') < (b.name || '') ? 1 : -1; });
+ let out = '── Archives (' + jsonFiles.length + ') ──\n';
+ for (var i = 0; i < Math.min(jsonFiles.length, 30); i++) {
+ var f = jsonFiles[i];
+ var size = f.size ? '(' + f.size + 'b)' : '';
+ out += ' ' + f.name + ' ' + size + '\n';
+ }
+ out += '── End ──\nUse: archive-read <filename>';
+ this.terminal.print(out, 'info');
+ } catch (e) {
+ this.terminal.print('Error listing archives: ' + e.message, 'error');
+ }
+ }
+ _archiveRead(args) {
+ var name = (args && args[0]) || '';
+ if (!name) {
+ this.terminal.print('Usage: archive-read <filename> (e.g. 2026-06-25-14-30.json)', 'warning');
+ return;
+ }
+ try {
+ const fs = this.terminal.fs;
+ var data = fs.fsReadJSON('/memory/archive/' + name);
+ if (!data) {
+ this.terminal.print('Archive not found: ' + name, 'error');
+ return;
+ }
+ var out = '── Archive: ' + name + ' ──\n';
+ out += 'Archived: ' + (data.archivedAt || '?') + '\n';
+ out += 'Summary: ' + (data.summary || '(none)') + '\n';
+ out += 'User messages: ' + (data.user?.messageCount || 0) + '\n';
+ out += 'System: ' + (data.system?.modules || 0) + ' modules, ' + (data.system?.model || '?') + '\n';
+ out += 'Context: ' + (data.context?.messageCount || 0) + ' msgs, ~' + (data.context?.estimatedTokens || 0) + ' tokens\n';
+ out += '── End ──';
+ this.terminal.print(out, 'info');
+ } catch (e) {
+ this.terminal.print('Error reading archive: ' + e.message, 'error');
+ }
+ }
+ _workflowList() {
+ var workflows = [
+ { name: 'daily-health-report', desc: 'Schedule a daily system health report using Cron + WorldModel + ServerMonitor' },
+ { name: 'predict-and-monitor', desc: 'Predict task duration with Predictor, then schedule monitoring via Cron' },
+ { name: 'compress-archives', desc: 'Compress and archive old session files using CryptoGzip + Cron' },
+ { name: 'media-summarize', desc: 'Analyze a media file and summarize it with AI' },
+ { name: 'sandbox-test', desc: 'Run a command in the sandbox and report results' }
+ ];
+ var out = '── Workflows (' + workflows.length + ') ──\n';
+ for (var i = 0; i < workflows.length; i++) {
+ out += ' ' + workflows[i].name + ' — ' + workflows[i].desc + '\n';
+ }
+ out += '── End ──\nUse: workflow-run <name>';
+ this.terminal.print(out, 'info');
+ }
+ _workflowRun(args) {
+ var name = (args && args[0]) || '';
+ if (!name) {
+ this.terminal.print('Usage: workflow-run <name> (e.g. daily-health-report)', 'warning');
+ return;
+ }
+ var workflows = {
+ 'daily-health-report': {
+ steps: [
+ 'Run: status — check current system state',
+ 'Run: cron-every 1h "status" — schedule hourly health check',
+ 'The WorldModel will auto-synthesize system awareness each cycle'
+ ]
+ },
+ 'predict-and-monitor': {
+ steps: [
+ 'Run: predict "run system health check" — estimate duration',
+ 'Run: cron-every 30m "status" — monitor progress',
+ 'Check cron-list to see scheduled tasks'
+ ]
+ },
+ 'compress-archives': {
+ steps: [
+ 'Run: archive-list — see current archives',
+ 'Run: gzip /memory/archive/2026-06-25-*.json — compress old archives',
+ 'Run: cron-every 1d "gzip /memory/archive/*.json" — auto-compress daily'
+ ]
+ },
+ 'media-summarize': {
+ steps: [
+ 'Run: media list — see available media files',
+ 'Run: media analyze <file> — analyze with AI',
+ 'The AI will summarize the media content'
+ ]
+ },
+ 'sandbox-test': {
+ steps: [
+ 'Run: sandbox-status — check sandbox pool',
+ 'Run: sandbox-run "ls /" — test command in sandbox',
+ 'Check output for isolation verification'
+ ]
+ }
+ };
+ var wf = workflows[name];
+ if (!wf) {
+ this.terminal.print('Workflow not found: ' + name + '. Use workflow-list to see available workflows.', 'error');
+ return;
+ }
+ var out = '── Workflow: ' + name + ' ──\n';
+ for (var i = 0; i < wf.steps.length; i++) {
+ out += ' ' + (i + 1) + '. ' + wf.steps[i] + '\n';
+ }
+ out += '── End ──';
+ this.terminal.print(out, 'info');
+ }
+ _showLastRawResponse() {
+ if (!this._lastRawResponse) {
+ this.terminal.print('No AI response recorded yet. Send a message first.', 'warning');
+ return;
+ }
+ const r = this._lastRawResponse;
+ var content = (r.message && r.message.content) || '';
+ var hasTools = r.tool_calls && r.tool_calls.length > 0;
+ var toolStr = hasTools ? r.tool_calls.map(function(tc) { return tc.function.name + '(' + (tc.function.arguments || '{}') + ')'; }).join(', ') : '';
+ // Render as HTML card for better readability
+ var html = '<div class="msg-card ai" style="border:1px solid var(--border-color);border-radius:6px;margin:4px 0;overflow:hidden">' +
+ '<div class="msg-card-header" style="display:flex;justify-content:space-between;align-items:center;padding:6px 10px;background:var(--bg-secondary);border-bottom:1px solid var(--border-color);font-weight:bold;font-size:0.9em">' +
+ '<span>🤖 Last AI Response</span>' +
+ '<span style="display:flex;gap:4px">' +
+ '<button class="card-btn" onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m&&m._lastRawResponse){navigator.clipboard.writeText(JSON.stringify(m._lastRawResponse,null,2)).then(function(){m.terminal.print(\'📋 Raw response copied\',\'success\')})}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em">📋 Copy Raw</button>' +
+ '</span></div>' +
+ '<div class="msg-card-body" style="padding:8px 10px;font-size:0.85em;line-height:1.4">';
+ if (r.error) {
+ html += '<div style="color:var(--error-color)">⚠️ Error: ' + this._escapeHtml(r.error) + '</div>';
+ }
+ if (content) {
+ var safe = this._escapeHtml(content.length > 1000 ? content.substring(0, 1000) + '...(' + content.length + ' chars)' : content);
+ html += '<div style="margin-bottom:4px"><strong>Content:</strong><br>' + safe + '</div>';
+ } else if (!hasTools) {
+ html += '<div style="color:var(--warning-color)">⚠️ Empty response (no content, no tool calls)</div>';
+ }
+ if (hasTools) {
+ html += '<div style="margin-top:4px"><strong>Tool calls:</strong> ' + this._escapeHtml(toolStr) + '</div>';
+ }
+ html += '</div></div>';
+ this.terminal.printHTML(html);
+ }
+ _showLastRawRequest() {
+ if (!this._lastRawMessages) {
+ this.terminal.print('No AI request recorded yet. Send a message first.', 'warning');
+ return;
+ }
+ const msgs = this._lastRawMessages;
+ let out = '── Last AI Request (' + msgs.length + ' messages) ──\n';
+ for (let i = 0; i < msgs.length; i++) {
+ const m = msgs[i];
+ const role = m.role || '?';
+ const content = (m.content || '').substring(0, 200);
+ const suffix = (m.content || '').length > 200 ? '...' : '';
+ out += ' [' + i + '] ' + role + ': ' + content + suffix + '\n';
+ if (i > 8 && i < msgs.length - 2) {
+ out += ' ... (' + (msgs.length - i - 2) + ' more messages) ...\n';
+ i = msgs.length - 3;
+ }
+ }
+ out += '── End ──';
+ this.terminal.print(out, 'info');
+ }
async _compressContext() {
if (this.context.length <= 6) return;
const totalTokens = this._estimateContextTokens();
@@ -10356,10 +10745,58 @@ buildSystemPrompt() {
} catch (e) { /* fallback to truncation */ }
this.context = [{ role: 'system', content: compressed }, ...recentMsgs];
this.terminal.print('📦 Context compressed (' + oldMsgs.length + ' messages → 1 summary, ~' + totalTokens + ' tokens)', 'info');
+ // Also archive session snapshot
+ this._archiveSession(compressed);
+ }
+ _archiveSession(summary) {
+ try {
+ const fs = this.terminal.fs;
+ fs.mkdirRecursive('/memory/archive');
+ const now = new Date();
+ const pad = function(n) { return String(n).padStart(2, '0'); };
+ const ts = now.getFullYear() + '-' + pad(now.getMonth()+1) + '-' + pad(now.getDate()) + '-' + pad(now.getHours()) + '-' + pad(now.getMinutes());
+ const archive = {
+ archivedAt: now.toISOString(),
+ summary: summary || '',
+ user: {
+ profile: this._userProfile || {},
+ topics: [],
+ messageCount: this._messageCount
+ },
+ system: {
+ modules: this.terminal?.moduleManager?.modules ? Object.keys(this.terminal.moduleManager.modules).length : 0,
+ model: this.terminal?.ollamaManager?.currentModel || 'unknown',
+ provider: this.terminal?.ollamaManager?.currentProvider?.name || 'unknown'
+ },
+ context: {
+ messageCount: this.context.length,
+ estimatedTokens: this._estimateContextTokens()
+ }
+ };
+ fs.fsWriteJSON('/memory/archive/' + ts + '.json', archive);
+ // Rotation: keep last 30
+ try {
+ const files = fs.ls('/memory/archive') || [];
+ const jsonFiles = files.filter(function(f) { return f.name && f.name.endsWith('.json'); });
+ if (jsonFiles.length > 30) {
+ jsonFiles.sort(function(a, b) { return (a.name || '') < (b.name || '') ? -1 : 1; });
+ const toDelete = jsonFiles.slice(0, jsonFiles.length - 30);
+ for (var i = 0; i < toDelete.length; i++) {
+ fs.deleteFile('/memory/archive/' + toDelete[i].name);
+ }
+ }
+ } catch (e) { /* rotation best-effort */ }
+ } catch (e) { /* archive best-effort */ }
}
_manualCompress() {
this._compressContext().then(() => {
- this.terminal.print('Context: ' + this.context.length + ' message(s), ~' + this._estimateContextTokens() + ' estimated tokens', 'info');
+ const fs = this.terminal.fs;
+ let archiveCount = 0;
+ try {
+ const files = fs.ls('/memory/archive') || [];
+ archiveCount = files.filter(function(f) { return f.name && f.name.endsWith('.json'); }).length;
+ } catch (e) {}
+ this.terminal.print('Context: ' + this.context.length + ' message(s), ~' + this._estimateContextTokens() + ' estimated tokens, ' + archiveCount + ' archive(s)', 'info');
}).catch(e => {
this.terminal.print('Compression error: ' + e.message, 'error');
});
@@ -10466,10 +10903,21 @@ buildSystemPrompt() {
var failedTools = toolLog.filter(function(t) { return t.error; }); if (failedTools.length > 2) { var uniqueTools = []; failedTools.forEach(function(t) { if (uniqueTools.indexOf(t.name) === -1) uniqueTools.push(t.name); }); anomalies.push({ type: "tool_failures", count: failedTools.length, tools: uniqueTools }); }
var bench = this._activeLoopProfile ? this._activeLoopProfile.benchmark : null; if (bench && bench.avgTokens && usageData) { if (usageData.total_tokens > bench.avgTokens * 1.5) { anomalies.push({ type: "above_benchmark", value: usageData.total_tokens, benchmark: bench.avgTokens, ratio: (usageData.total_tokens / bench.avgTokens).toFixed(1) }); } }
if (anomalies.length === 0) return;
- var suggestions = []; for (var ai = 0; ai < anomalies.length; ai++) { var a = anomalies[ai]; if (a.type === "max_turns_near_limit") suggestions.push("This task used " + result.toolTurns + "/" + maxT + " turns — consider increasing max-turns or splitting the task"); else if (a.type === "high_token_usage") suggestions.push("High token usage (" + (a.value/1000).toFixed(0) + "K) — consider a minimal-tools profile for simpler tasks"); else if (a.type === "tool_failures") suggestions.push(a.count + " tool failures (" + a.tools.join(", ") + ") — check if modules are loaded or paths correct"); else if (a.type === "above_benchmark") suggestions.push("This run used " + a.ratio + "x more tokens than benchmark avg (" + a.benchmark + ") — consider profile optimization"); }
+ var suggestions = []; var profileAdjusted = false;
+ for (var ai = 0; ai < anomalies.length; ai++) { var a = anomalies[ai];
+ if (a.type === "max_turns_near_limit") {
+ suggestions.push("This task used " + result.toolTurns + "/" + maxT + " turns — consider increasing max-turns or splitting the task");
+ if (this._activeLoopProfile && this._activeLoopProfile.loop) { var newMax = (this._activeLoopProfile.loop.maxTurns || this.MAX_TOOL_TURNS) + 2; this._activeLoopProfile.loop.maxTurns = newMax; this._saveLoopProfile(this._activeLoopProfile); suggestions.push(" → Auto-adjusted maxTurns to " + newMax); profileAdjusted = true; }
+ } else if (a.type === "high_token_usage") suggestions.push("High token usage (" + (a.value/1000).toFixed(0) + "K) — consider a minimal-tools profile for simpler tasks");
+ else if (a.type === "tool_failures") {
+ suggestions.push(a.count + " tool failures (" + a.tools.join(", ") + ") — check if modules are loaded or paths correct");
+ if (this._activeLoopProfile && this._activeLoopProfile.systemPrompt) { var constraint = "[Constraint] Avoid tools that failed: " + a.tools.join(", ") + ". Verify module state before calling these tools."; if (this._activeLoopProfile.systemPrompt.append) { if (this._activeLoopProfile.systemPrompt.append.indexOf(constraint) === -1) { this._activeLoopProfile.systemPrompt.append += "\n" + constraint; } } else { this._activeLoopProfile.systemPrompt.append = constraint; } this._saveLoopProfile(this._activeLoopProfile); suggestions.push(" → Added constraint to profile: avoid " + a.tools.join(", ")); profileAdjusted = true; }
+ } else if (a.type === "above_benchmark") suggestions.push("This run used " + a.ratio + "x more tokens than benchmark avg (" + a.benchmark + ") — consider profile optimization");
+ }
var suggestion = suggestions.join("\n");
- try { var fs = this.terminal.fs; var index = fs.fsReadJSON("/skills/index.json") || {}; var autoSkills = Object.entries(index).filter(function(e) { return (e[1].tags || []).indexOf("auto-improvement") !== -1; }); if (autoSkills.length < 10) { var skillName = "loop-lesson-" + Date.now(); var content = "---\nname: " + skillName + "\ndescription: Auto-saved from agent loop anomaly detection\ntags: [auto-improvement, agent-loop, auto]\ncategory: system\n---\n## Anomalies Detected\n\n" + JSON.stringify(anomalies, null, 2) + "\n\n## Suggestions\n\n" + suggestion + "\n\n## Context\n- Profile: " + (this._activeLoopProfile ? this._activeLoopProfile.name : "none") + "\n- Turns: " + result.toolTurns + "\n- Tools used: " + toolLog.map(function(t) { return t.name; }).join(", "); fs.mkdirRecursive("/skills"); fs.writeFile("/skills/" + skillName + ".md", content); index[skillName] = { name: skillName, description: "Auto-saved loop lesson", tags: ["auto-improvement", "agent-loop", "auto"], category: "system", created: new Date().toISOString(), updated: new Date().toISOString() }; fs.fsWriteJSON("/skills/index.json", index); } } catch (e) {}
- this.terminal.print("💡 Self-improvement: " + suggestion, "info");
+ try { var fs = this.terminal.fs; var index = fs.fsReadJSON("/skills/index.json") || {}; var autoSkills = Object.entries(index).filter(function(e) { return (e[1].tags || []).indexOf("auto-improvement") !== -1; }); if (!this._loopLessonCleaned) { this._loopLessonCleaned = true; autoSkills.forEach(function(e) { try { fs.unlink("/skills/" + e[0] + ".md"); } catch (ex) {} delete index[e[0]]; }); autoSkills = []; } var now = Date.now(); autoSkills = autoSkills.filter(function(e) { var created = new Date(e[1].created).getTime(); if (now - created > 86400000) { try { fs.unlink("/skills/" + e[0] + ".md"); } catch (ex) {} delete index[e[0]]; return false; } return true; }); var anomalyTypes = anomalies.map(function(a) { return a.type; }).sort().join(","); var isDuplicate = autoSkills.length > 0 && autoSkills[autoSkills.length - 1][1].anomalyTypes === anomalyTypes; if (!isDuplicate && autoSkills.length >= 3) { var oldest = autoSkills[0]; try { fs.unlink("/skills/" + oldest[0] + ".md"); } catch (ex) {} delete index[oldest[0]]; autoSkills.shift(); } if (!isDuplicate) { var skillName = "loop-lesson-" + Date.now(); var content = "---\nname: " + skillName + "\ndescription: Auto-saved from agent loop anomaly detection\ntags: [auto-improvement, agent-loop, auto]\ncategory: system\n---\n## Anomalies Detected\n\n" + JSON.stringify(anomalies, null, 2) + "\n\n## Suggestions\n\n" + suggestion + "\n\n## Context\n- Profile: " + (this._activeLoopProfile ? this._activeLoopProfile.name : "none") + "\n- Turns: " + result.toolTurns + "\n- Tools used: " + toolLog.map(function(t) { return t.name; }).join(", "); fs.mkdirRecursive("/skills"); fs.writeFile("/skills/" + skillName + ".md", content); index[skillName] = { name: skillName, description: "Auto-saved loop lesson", tags: ["auto-improvement", "agent-loop", "auto"], category: "system", created: new Date().toISOString(), updated: new Date().toISOString(), anomalyTypes: anomalyTypes }; fs.fsWriteJSON("/skills/index.json", index); } } catch (e) {}
+ var msg = "💡 Self-improvement: " + anomalies.length + " anomaly(ies)"; if (profileAdjusted) msg += " — profile auto-adjusted";
+ this.terminal.print(msg, "info");
}
_vaultmindStatusCmd() {
const token = localStorage.getItem('vaultmind_token');
@@ -15858,6 +16306,12 @@ class NeuralVFSModule {
resolve(args) {
if (!args[0]) return this.terminal.print('Usage: nvfs-resolve <concept>', 'warning');
+ // Auto-sync dirty paths before resolving
+ if (this._dirtyPaths.size > 0) {
+ this.terminal.print('🔄 Auto-syncing ' + this._dirtyPaths.size + ' dirty file(s) before resolve...', 'info');
+ this._reindexDirtyPaths();
+ }
+
const concept = args.join(' ').toLowerCase();
// Check aliases first
@@ -16700,11 +17154,14 @@ if (typeof window !== 'undefined') {
}
getDefaultProfiles() {
return {
- 'coding': { name: 'Coding Assistant', systemPrompt: "You are an expert programmer.", temperature: 0.7 },
- 'debug': { name: 'Debug Helper', systemPrompt: "You are a debugging expert.", temperature: 0.5 },
- 'creative': { name: 'Creative Writer', systemPrompt: "You are a creative writer.", temperature: 0.9 },
- 'technical': { name: 'Technical Docs', systemPrompt: "You are a technical writer.", temperature: 0.3 },
- 'chat': { name: 'Casual Chat', systemPrompt: "You are a friendly assistant.", temperature: 0.8 }
+ 'coding': { name: 'Coding Assistant', systemPrompt: "You are a WebX11 newOS4 coding expert. You have access to the full terminal, VFS, and module system.\n\nAvailable commands: view_module, edit_module, test_module, reload_module, list_modified_modules, check_updates, load-local, loadmod, removemod, startup-modules, node, js-run, git, diff, patch\n\nSkills available: module-management (view/edit/test/reload modules), skills-system-guide (create/patch skills)\n\nBehavior:\n- Always show exact code changes when editing modules\n- Use edit_module for modifications, test_module to verify\n- Create checkpoints before risky edits: checkpoint mod-edit-<name>\n- Reference the module-management skill for the full workflow\n- When the user reports a bug, first view_module to inspect, then edit\n- Use nvfs_resolve to find relevant files before editing\n- Prefer write_file for creating new files, edit_module for modifying modules", temperature: 0.7 },
+ 'debug': { name: 'Debug Helper', systemPrompt: "You are a WebX11 newOS4 debugging expert. Your goal is to diagnose and fix issues systematically.\n\nAvailable commands: debug, debug-ai, debug-ai-last, debug-ai-send, debug-ai-log, logs, logrotate, health-check, check, mem, status, archive-list, archive-read, guardrail, shadow, shadow-commit, shadow-rollback\n\nSkills available: debug-panel-and-logs (toggle debug panel, session logging, diagnostics), system-maintenance (health checks, config, security)\n\nBehavior:\n- Start with health-check and check to assess system state\n- Use debug-ai to see the last AI tool call and response\n- Use logs 50 ERROR to find error entries\n- Check browser console for JS errors\n- Use guardrail to inspect security policies\n- Use shadow for safe execution of risky commands\n- Always check module loading status first", temperature: 0.3 },
+ 'creative': { name: 'Creative Writer', systemPrompt: "You are a creative assistant for WebX11 newOS4. You help with brainstorming, content creation, and design.\n\nAvailable commands: mindmap new/add/show/save/load/export, floating-add/remove/list, ai-system, ai-config\n\nSkills available: mindmap-workflow (create/view/edit mindmaps), floating-ui (create floating buttons/menus/panels)\n\nBehavior:\n- Use mindmap for brainstorming and organizing ideas\n- Use floating-add to create quick-access UI elements\n- Use web_search and web_fetch for research and inspiration\n- Higher temperature (0.9) for creative divergence\n- When the user says \"brainstorm\" or \"diagram\", create a mindmap\n- Use create_mindmap AI tool for visual architecture diagrams\n- Reference the mindmap-workflow skill for full capabilities", temperature: 0.9 },
+ 'technical': { name: 'Technical Docs', systemPrompt: "You are a technical documentation expert for WebX11 newOS4. You help with docs, guides, and system understanding.\n\nAvailable commands: help, commands, status, modules, health-check, nvfs-index, nvfs-resolve, view_module, cat, edit, grep, find, tree\n\nSkills available: navigating-terminal (explore terminal, filesystem, system state), web-search-and-fetch (search web, fetch URLs), skills-system-guide (create/patch skills)\n\nBehavior:\n- Use help and commands to discover available functionality\n- Use nvfs-resolve to find files by concept\n- Use view_module to inspect module source for documentation\n- When creating documentation, use write_file to save to VFS\n- Reference the navigating-terminal skill for filesystem exploration\n- Use web_search for external research", temperature: 0.3 },
+ 'chat': { name: 'Casual Chat', systemPrompt: "You are a friendly, helpful assistant for WebX11 newOS4. You handle general questions, casual conversation, and light tasks.\n\nAvailable commands: ai, ask, search, fetch, status, help, user_get, user_set, session_save, session_recent, my-set, my-get\n\nSkills available: memory-and-profile (user profile, session memory, checkpoints), web-search-and-fetch (search web, fetch URLs)\n\nBehavior:\n- Be conversational and approachable\n- Use user_get/user_set to remember user preferences\n- Use session_recent at start to recall past work\n- Use session_save after completing tasks\n- For complex tasks, suggest switching to a more specific context profile\n- Reference the memory-and-profile skill for persistence features", temperature: 0.8 },
+ 'system-admin': { name: 'System Admin', systemPrompt: "You are a WebX11 newOS4 system administrator. You handle maintenance, backup, security, and configuration.\n\nAvailable commands: config-list, config-set, config-delete, config-reset, security-level, security, cmd-allow, cmd-deny, backup, vfs-export, webdav-backup, webdav-restore, webdav-config, webdav-status, factory-reset, reset-vfs, checkpoint, checkpoint-list, rollback, health-check, check_updates, font-size\n\nSkills available: system-maintenance (reset, config, security, health checks), backup-and-restore (VFS backup, WebDAV cloud backup, restore), webdav-operations (cloud storage, file sync), security-and-safety (security levels, command blocking)\n\nBehavior:\n- Always create a checkpoint before making system changes\n- Use webdav-backup before destructive operations\n- Check security-level before running commands\n- Use health-check for full system audit\n- Reference the system-maintenance skill for reset procedures\n- Reference the backup-and-restore skill for data safety", temperature: 0.3 },
+ 'research': { name: 'Research Assistant', systemPrompt: "You are a research assistant for WebX11 newOS4. You gather information, analyze data, and synthesize findings.\n\nAvailable commands: search, fetch, web_search, web_fetch, nvfs-resolve, nvfs-index, grep, find, cat, view_module, session_recent, session_load, session_stats\n\nSkills available: web-search-and-fetch (search web, fetch URL content), memory-and-profile (session memory, checkpoints), navigating-terminal (explore filesystem)\n\nBehavior:\n- Start with web_search for external research\n- Use nvfs-resolve to find relevant VFS files\n- Use session_recent to check past work on the topic\n- Use fetch to read full articles from URLs\n- Synthesize findings before presenting\n- Reference the web-search-and-fetch skill for search flags", temperature: 0.5 },
+ 'security': { name: 'Security Expert', systemPrompt: "You are a WebX11 newOS4 security expert. You handle threat assessment, command control, and safety.\n\nAvailable commands: security, security-level, cmd-allow, cmd-deny, guardrail, guardrail-add, guardrail-remove, shadow, shadow-commit, shadow-rollback, health-check, check, logs, debug\n\nSkills available: security-and-safety (security levels, command blocking, safety checklist), debug-panel-and-logs (diagnostics, logging), system-maintenance (config, health checks)\n\nBehavior:\n- Always check current security level first with security\n- Use guardrail to inspect and manage security policies\n- Use shadow for safe execution of suspicious commands\n- Never recommend factory-reset without backup first\n- Use cmd-deny to block dangerous commands\n- Reference the security-and-safety skill for level descriptions", temperature: 0.2 }
};
}
registerCommands() {
@@ -16722,6 +17179,32 @@ if (typeof window !== 'undefined') {
this.activeProfile = args[0];
this.saveActiveProfile();
this.terminal.print(`Context set to: ${this.contextProfiles[args[0]].name}`, "success");
+ // Also switch agent loop profile to match
+ const PROFILE_MAP = {
+ 'coding': 'coder',
+ 'debug': 'minimal',
+ 'creative': 'creative',
+ 'technical': 'research',
+ 'chat': 'default',
+ 'system-admin': 'safe',
+ 'research': 'research',
+ 'security': 'safe'
+ };
+ const targetLoop = PROFILE_MAP[args[0]];
+ if (targetLoop) {
+ const aiMod = this.terminal?.moduleManager?.modules?.WebX11AIAssistantModule;
+ if (aiMod && aiMod._switchLoopProfile) {
+ aiMod._switchLoopProfile(targetLoop);
+ this.terminal.print(` Agent loop switched to: ${targetLoop}`, "text-muted");
+ }
+ } else {
+ // Custom profile — rebuild system prompt without loop switch
+ const aiMod = this.terminal?.moduleManager?.modules?.WebX11AIAssistantModule;
+ if (aiMod && aiMod.buildSystemPrompt) {
+ aiMod.systemPrompt = aiMod.buildSystemPrompt();
+ }
+ this.terminal.print(` No agent loop mapped for '${args[0]}'`, "text-muted");
+ }
} else {
this.terminal.print(`Profile '${args[0]}' not found`, "error");
}
@@ -16771,7 +17254,8 @@ if (typeof window !== 'undefined') {
const aiModule = this.terminal?.moduleManager?.modules?.WebX11AIAssistantModule;
if (aiModule) {
// Set temperature on ollamaManager modelParams temporarily
- const om = this.terminal.ollamaManager;
+ const om = this.terminal?.ollamaManager;
+ if (!om) { this.terminal?.print('⚠️ Ollama manager not available', 'error'); return; }
const savedTemp = om.modelParams ? om.modelParams.temperature : undefined;
if (om.modelParams) {
om.modelParams.temperature = temperature;
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index 6359f17..8a0a7c8 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -22,6 +22,8 @@
this._toolDefs = null;
this._toolCallLog = [];
this._debugPanelEnabled = false;
+ this._lastRawResponse = null;
+ this._lastRawMessages = null;
// 🔒 Guardrail Layer
this._guardrailPolicy = null;
this._shadowSnapshot = null;
@@ -431,7 +433,7 @@ buildSystemPrompt() {
type: 'function',
function: {
name: 'smart_context_list',
- description: 'List available AI context profiles (e.g. coding, debug, creative, technical). Returns profile names and system prompts.',
+ description: 'List available AI context profiles (e.g. coding, debug, creative, technical, chat, system-admin, research, security). Each profile sets the AI system prompt, temperature, and behavior for a specific task type. Also switches the agent loop profile (max turns, tool set) to match. Returns profile names, descriptions, and system prompts. Use this when the user task has a clear domain (coding, debugging, research, etc.) to optimize AI behavior.',
parameters: {
type: 'object',
properties: {},
@@ -442,11 +444,11 @@ buildSystemPrompt() {
type: 'function',
function: {
name: 'smart_context_set',
- description: "Set the active AI context profile. Changes the AI's behavior (temperature, style) for subsequent responses.",
+ description: "Set the active AI context profile. Changes the AI system prompt, temperature, and behavior for subsequent responses. Also switches the agent loop profile (max turns, tool set) to match. Available profiles: coding (code editing), debug (bug fixing), creative (brainstorming), technical (documentation), chat (casual), system-admin (maintenance), research (information gathering), security (threat assessment). Use this when the user task changes domain.",
parameters: {
type: 'object',
properties: {
- name: { type: 'string', description: 'Profile name (e.g. "coding", "debug", "creative", "technical", "chat")' }
+ name: { type: 'string', description: 'Profile name (e.g. "coding", "debug", "creative", "technical", "chat", "system-admin", "research", "security")' }
},
required: ['name']
}
@@ -632,6 +634,11 @@ buildSystemPrompt() {
this.commands['bench-compare'] = (args) => this.benchCompareCmd(args);
this.commands['debug'] = () => this.toggleDebugPanel();
this.commands['debug-ai'] = () => this.toggleDebug();
+ this.commands['debug-ai-last'] = () => this._showLastRawResponse();
+ this.commands['debug-ai-send'] = () => this._showLastRawRequest();
+ this.commands['debug-ai-log'] = () => this._showToolCallLog();
+ this.commands['archive-list'] = () => this._archiveList();
+ this.commands['archive-read'] = (args) => this._archiveRead(args);
this.commands['guardrail'] = (args) => this.guardrailCmd(args);
this.commands['guardrail-add'] = (args) => this.guardrailCmd(['add', ...args]);
this.commands['guardrail-remove'] = (args) => this.guardrailCmd(['remove', ...args]);
@@ -773,9 +780,14 @@ buildSystemPrompt() {
// Log the AI interaction
this.terminal.log('INFO', 'AI', 'Query: ' + userMessage.substring(0, 100));
if (loopResult && loopResult.toolTurns) {
- this.terminal.log('INFO', 'AI', 'Used ' + loopResult.toolTurns + ' tool(s)');
+ const toolNames = this._toolCallLog.slice(-(loopResult.toolTurns || 0)).map(function(e) { return e.name; }).join(', ');
+ this.terminal.log('INFO', 'AI', 'Used ' + loopResult.toolTurns + ' tool(s): ' + toolNames);
}
}
+ // Auto-archive every 10 messages
+ if (this._messageCount > 0 && this._messageCount % 10 === 0) {
+ this._archiveSession('Auto-archive at message ' + this._messageCount);
+ }
// Register session save on page unload (runs once per session)
if (!this._sessionSaved) {
this._sessionSaved = true;
@@ -807,12 +819,13 @@ buildSystemPrompt() {
if (!this._debugPanelEnabled) return;
const panel = document.getElementById('debug-ai-panel');
if (!panel) return;
+ const activeTab = panel.getAttribute('data-tab') || 'tools';
const recent = this._toolCallLog.slice(-20);
- let html = '';
+ let toolHtml = '';
for (const entry of recent) {
const statusClass = entry.error ? 'debug-entry-error' : 'debug-entry-ok';
const argsStr = typeof entry.args === 'string' ? entry.args.substring(0, 60) : JSON.stringify(entry.args).substring(0, 60);
- html += '<div class="debug-entry ' + statusClass + '">' +
+ toolHtml += '<div class="debug-entry ' + statusClass + '">' +
'<span class="debug-turn">#' + entry.turn + '</span> ' +
'<span class="debug-name">' + this._escapeHtml(entry.name) + '</span> ' +
'<span class="debug-args">' + this._escapeHtml(argsStr) + '</span> ' +
@@ -820,14 +833,41 @@ buildSystemPrompt() {
(entry.error ? ' <span class="debug-err">⚠️</span>' : '') +
'</div>';
}
+ // Raw I/O tab content
+ let rawHtml = '';
+ if (this._lastRawResponse) {
+ const r = this._lastRawResponse;
+ const content = (r.message && r.message.content) || '';
+ const hasTools = r.tool_calls && r.tool_calls.length > 0;
+ rawHtml += '<div style="padding:6px 8px;font-size:0.75em;line-height:1.3">';
+ if (r.error) rawHtml += '<div style="color:var(--error-color)">⚠️ ' + this._escapeHtml(r.error) + '</div>';
+ if (content) {
+ const safe = this._escapeHtml(content.length > 300 ? content.substring(0, 300) + '...' : content);
+ rawHtml += '<div><strong>Content:</strong> ' + safe + '</div>';
+ }
+ if (hasTools) {
+ rawHtml += '<div><strong>Tools:</strong> ' + r.tool_calls.map(function(tc) { return tc.function.name; }).join(', ') + '</div>';
+ }
+ if (this._lastRawMessages) {
+ rawHtml += '<div style="margin-top:4px;color:var(--text-muted)">Messages: ' + this._lastRawMessages.length + '</div>';
+ }
+ rawHtml += '</div>';
+ } else {
+ rawHtml = '<div style="padding:8px;color:var(--text-muted);font-size:0.75em">No AI response yet. Send a message first.</div>';
+ }
+ const tabBar = '<div style="display:flex;border-bottom:1px solid #333">' +
+ '<span style="cursor:pointer;padding:4px 10px;font-size:0.8em;' + (activeTab === 'tools' ? 'background:var(--bg-tertiary);border-bottom:2px solid var(--success-color);font-weight:bold' : 'color:var(--text-muted)') + '" onclick="var p=document.getElementById(\'debug-ai-panel\');p.setAttribute(\'data-tab\',\'tools\');var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m)m._updateDebugPanel()">🔧 Tools</span>' +
+ '<span style="cursor:pointer;padding:4px 10px;font-size:0.8em;' + (activeTab === 'raw' ? 'background:var(--bg-tertiary);border-bottom:2px solid var(--success-color);font-weight:bold' : 'color:var(--text-muted)') + '" onclick="var p=document.getElementById(\'debug-ai-panel\');p.setAttribute(\'data-tab\',\'raw\');var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m)m._updateDebugPanel()">📡 Raw I/O</span>' +
+ '</div>';
+ const bodyHtml = activeTab === 'raw' ? rawHtml : toolHtml;
panel.innerHTML = '<div class="debug-header" style="display:flex;justify-content:space-between;align-items:center;padding:6px 10px;border-bottom:1px solid #333;font-weight:bold;color:var(--success-color);font-size:0.9em;flex-wrap:wrap;gap:4px;">' +
- '<span>🔧 Tool Log <span class="debug-count" style="color:var(--text-muted);font-weight:normal;font-size:0.85em;">' + this._toolCallLog.length + ' calls</span></span>' +
+ '<span>🔧 AI Debug <span class="debug-count" style="color:var(--text-muted);font-weight:normal;font-size:0.85em;">' + this._toolCallLog.length + ' calls</span></span>' +
'<span style="display:flex;gap:4px;">' +
'<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var t=m._toolCallLog.map(function(e){return\'#\'+e.turn+\' \'+e.name+\' (\'+e.duration+\'s)\'+(e.error?\' ⚠️\':\'\')+\'\\n\'+JSON.stringify(e.args).substring(0,80)}).join(\'\\n\\n\');navigator.clipboard.writeText(t).then(function(){m.terminal&&m.terminal.print(\'📋 Debug log copied to clipboard\',\'success\')}).catch(function(){prompt(\'Copy debug log:\',t)})}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">📋</button>' +
'<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var json=JSON.stringify(m._toolCallLog,null,2);var b=new Blob([json],{type:\'application/json\'});var u=URL.createObjectURL(b);var a=document.createElement(\'a\');a.href=u;a.download=\'debug-log-\'+new Date().toISOString().slice(0,19)+\'.json\';document.body.appendChild(a);a.click();setTimeout(function(){document.body.removeChild(a);URL.revokeObjectURL(u)},2000);m.terminal&&m.terminal.print(\'📥 Debug log exported\',\'success\')}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">📥</button>' +
'<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var existing=[];var raw=m.terminal.fs.readFile(\'/debug.log\');if(raw){try{existing=JSON.parse(raw);}catch(e){}}var log=m._toolCallLog;var seen={};existing.forEach(function(e){seen[e.turn]=true;});log.forEach(function(e){if(!seen[e.turn]){existing.push(e);seen[e.turn]=true;}});var json=JSON.stringify(existing,null,2);m.terminal.fs.writeFile(\'/debug.log\',json);m.terminal.print(\'💾 Debug log appended — \'+existing.length+\' entries total\',\'success\')}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">💾</button>' +
'<span style="cursor:pointer;color:var(--text-muted);font-size:1.1em;touch-action:manipulation;" onclick="var p=document.getElementById(\'debug-ai-panel\');if(p)p.style.display=\'none\';var a=window.webx11&&webx11.moduleManager&&webx11.moduleManager.modules&&webx11.moduleManager.modules.WebX11AIAssistantModule;if(a)a._debugPanelEnabled=false;">✕</span>' +
- '</span></div>' + html;
+ '</span></div>' + tabBar + bodyHtml;
}
_isToolError(result) {
if (!result || typeof result !== 'string') return null;
@@ -878,6 +918,28 @@ buildSystemPrompt() {
var loopDetect = loopCfg.loopDetection || { enabled: true, threshold: 6, windowMs: 30000 };
var confEsc = loopCfg.confidenceEscalation || { enabled: true, threshold: 90 };
var finalPrompt = loopCfg.finalTurnPrompt || 'This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know.';
+ // Memory-augmented planning: inject prior loop lessons as negative constraints
+ try {
+ var fs = this.terminal.fs;
+ if (fs) {
+ var index = fs.fsReadJSON('/skills/index.json') || {};
+ var lessonKeys = Object.keys(index).filter(function(k) { return k.indexOf('loop-lesson-') === 0; });
+ if (lessonKeys.length > 0) {
+ var recentLessons = lessonKeys.sort().slice(-3);
+ var lessonTexts = [];
+ for (var li = 0; li < recentLessons.length; li++) {
+ var lc = fs.readFile('/skills/' + recentLessons[li] + '.md');
+ if (lc) {
+ var excerpt = lc.substring(0, 300);
+ lessonTexts.push('[Lesson: ' + recentLessons[li] + '] ' + excerpt);
+ }
+ }
+ if (lessonTexts.length > 0) {
+ messages.push({ role: 'system', content: 'Prior loop lessons (read before planning):\n' + lessonTexts.join('\n---\n') + '\n\nUse these to avoid repeating past mistakes. If lessons mention tool failures, verify those tools before calling them.' });
+ }
+ }
+ }
+ } catch (e) { /* VFS not available — skip memory-augmented planning */ }
let toolTurns = 0;
const _seenCalls = new Map();
const _loopWindow = loopDetect.windowMs || 30000;
@@ -892,6 +954,9 @@ buildSystemPrompt() {
const thinkStart = Date.now();
const res = await this.terminal.ollamaManager.chatWithTools(messages, this.getToolDefinitions());
const thinkTime = Math.round((Date.now() - thinkStart) / 1000);
+ // Store raw response for debug-ai-last
+ this._lastRawResponse = res;
+ this._lastRawMessages = messages.slice();
if (this._cancelled) return { finalAnswer: null, toolTurns };
if (res.error) { this.terminal.print(`⚠️ AI model error (${thinkTime}s): ${res.error}`, 'error'); return { finalAnswer: null, toolTurns }; }
if (res.message && !res.tool_calls) {
@@ -2068,10 +2133,26 @@ buildSystemPrompt() {
return JSON.stringify({ error: `Profile '${name}' not found`, available: Object.keys(scm.contextProfiles) });
}
scm.activeProfile = name;
+ scm.saveActiveProfile();
const profile = scm.contextProfiles[name];
// Rebuild system prompt to reflect new context profile
this.systemPrompt = this.buildSystemPrompt();
- return JSON.stringify({ success: true, name, displayName: profile.name, systemPrompt: profile.systemPrompt, temperature: profile.temperature });
+ // Also switch agent loop profile to match
+ const PROFILE_MAP = {
+ 'coding': 'coder',
+ 'debug': 'minimal',
+ 'creative': 'creative',
+ 'technical': 'research',
+ 'chat': 'default',
+ 'system-admin': 'safe',
+ 'research': 'research',
+ 'security': 'safe'
+ };
+ const targetLoop = PROFILE_MAP[name];
+ if (targetLoop && this._switchLoopProfile) {
+ this._switchLoopProfile(targetLoop);
+ }
+ return JSON.stringify({ success: true, name, displayName: profile.name, systemPrompt: profile.systemPrompt, temperature: profile.temperature, agentLoop: targetLoop || null });
}
case 'create_mindmap': {
const mindmap = this.terminal.moduleManager.modules['MindmapModule'];
@@ -2796,6 +2877,121 @@ buildSystemPrompt() {
}
return total;
}
+ _showToolCallLog() {
+ if (!this._toolCallLog || this._toolCallLog.length === 0) {
+ this.terminal.print('No tool calls recorded this session.', 'warning');
+ return;
+ }
+ let out = '── Tool Call Log (' + this._toolCallLog.length + ' calls) ──\n';
+ for (var i = 0; i < this._toolCallLog.length; i++) {
+ var e = this._toolCallLog[i];
+ var err = e.error ? ' ⚠️' : '';
+ var argsStr = typeof e.args === 'string' ? e.args.substring(0, 80) : JSON.stringify(e.args).substring(0, 80);
+ out += ' #' + e.turn + ' ' + e.name + '(' + argsStr + ') ' + e.duration + 's' + err + '\n';
+ }
+ out += '── End ──';
+ this.terminal.print(out, 'info');
+ }
+ _archiveList() {
+ try {
+ const fs = this.terminal.fs;
+ const files = fs.ls('/memory/archive') || [];
+ const jsonFiles = files.filter(function(f) { return f.name && f.name.endsWith('.json'); });
+ if (jsonFiles.length === 0) {
+ this.terminal.print('No archives found. Run ai-compress to create one.', 'warning');
+ return;
+ }
+ jsonFiles.sort(function(a, b) { return (a.name || '') < (b.name || '') ? 1 : -1; });
+ let out = '── Archives (' + jsonFiles.length + ') ──\n';
+ for (var i = 0; i < Math.min(jsonFiles.length, 30); i++) {
+ var f = jsonFiles[i];
+ var size = f.size ? '(' + f.size + 'b)' : '';
+ out += ' ' + f.name + ' ' + size + '\n';
+ }
+ out += '── End ──\nUse: archive-read <filename>';
+ this.terminal.print(out, 'info');
+ } catch (e) {
+ this.terminal.print('Error listing archives: ' + e.message, 'error');
+ }
+ }
+ _archiveRead(args) {
+ var name = (args && args[0]) || '';
+ if (!name) {
+ this.terminal.print('Usage: archive-read <filename> (e.g. 2026-06-25-14-30.json)', 'warning');
+ return;
+ }
+ try {
+ const fs = this.terminal.fs;
+ var data = fs.fsReadJSON('/memory/archive/' + name);
+ if (!data) {
+ this.terminal.print('Archive not found: ' + name, 'error');
+ return;
+ }
+ var out = '── Archive: ' + name + ' ──\n';
+ out += 'Archived: ' + (data.archivedAt || '?') + '\n';
+ out += 'Summary: ' + (data.summary || '(none)') + '\n';
+ out += 'User messages: ' + (data.user?.messageCount || 0) + '\n';
+ out += 'System: ' + (data.system?.modules || 0) + ' modules, ' + (data.system?.model || '?') + '\n';
+ out += 'Context: ' + (data.context?.messageCount || 0) + ' msgs, ~' + (data.context?.estimatedTokens || 0) + ' tokens\n';
+ out += '── End ──';
+ this.terminal.print(out, 'info');
+ } catch (e) {
+ this.terminal.print('Error reading archive: ' + e.message, 'error');
+ }
+ }
+ _showLastRawResponse() {
+ if (!this._lastRawResponse) {
+ this.terminal.print('No AI response recorded yet. Send a message first.', 'warning');
+ return;
+ }
+ const r = this._lastRawResponse;
+ var content = (r.message && r.message.content) || '';
+ var hasTools = r.tool_calls && r.tool_calls.length > 0;
+ var toolStr = hasTools ? r.tool_calls.map(function(tc) { return tc.function.name + '(' + (tc.function.arguments || '{}') + ')'; }).join(', ') : '';
+ // Render as HTML card for better readability
+ var html = '<div class="msg-card ai" style="border:1px solid var(--border-color);border-radius:6px;margin:4px 0;overflow:hidden">' +
+ '<div class="msg-card-header" style="display:flex;justify-content:space-between;align-items:center;padding:6px 10px;background:var(--bg-secondary);border-bottom:1px solid var(--border-color);font-weight:bold;font-size:0.9em">' +
+ '<span>🤖 Last AI Response</span>' +
+ '<span style="display:flex;gap:4px">' +
+ '<button class="card-btn" onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m&&m._lastRawResponse){navigator.clipboard.writeText(JSON.stringify(m._lastRawResponse,null,2)).then(function(){m.terminal.print(\'📋 Raw response copied\',\'success\')})}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em">📋 Copy Raw</button>' +
+ '</span></div>' +
+ '<div class="msg-card-body" style="padding:8px 10px;font-size:0.85em;line-height:1.4">';
+ if (r.error) {
+ html += '<div style="color:var(--error-color)">⚠️ Error: ' + this._escapeHtml(r.error) + '</div>';
+ }
+ if (content) {
+ var safe = this._escapeHtml(content.length > 1000 ? content.substring(0, 1000) + '...(' + content.length + ' chars)' : content);
+ html += '<div style="margin-bottom:4px"><strong>Content:</strong><br>' + safe + '</div>';
+ } else if (!hasTools) {
+ html += '<div style="color:var(--warning-color)">⚠️ Empty response (no content, no tool calls)</div>';
+ }
+ if (hasTools) {
+ html += '<div style="margin-top:4px"><strong>Tool calls:</strong> ' + this._escapeHtml(toolStr) + '</div>';
+ }
+ html += '</div></div>';
+ this.terminal.printHTML(html);
+ }
+ _showLastRawRequest() {
+ if (!this._lastRawMessages) {
+ this.terminal.print('No AI request recorded yet. Send a message first.', 'warning');
+ return;
+ }
+ const msgs = this._lastRawMessages;
+ let out = '── Last AI Request (' + msgs.length + ' messages) ──\n';
+ for (let i = 0; i < msgs.length; i++) {
+ const m = msgs[i];
+ const role = m.role || '?';
+ const content = (m.content || '').substring(0, 200);
+ const suffix = (m.content || '').length > 200 ? '...' : '';
+ out += ' [' + i + '] ' + role + ': ' + content + suffix + '\n';
+ if (i > 8 && i < msgs.length - 2) {
+ out += ' ... (' + (msgs.length - i - 2) + ' more messages) ...\n';
+ i = msgs.length - 3;
+ }
+ }
+ out += '── End ──';
+ this.terminal.print(out, 'info');
+ }
async _compressContext() {
if (this.context.length <= 6) return;
const totalTokens = this._estimateContextTokens();
@@ -2821,10 +3017,58 @@ buildSystemPrompt() {
} catch (e) { /* fallback to truncation */ }
this.context = [{ role: 'system', content: compressed }, ...recentMsgs];
this.terminal.print('📦 Context compressed (' + oldMsgs.length + ' messages → 1 summary, ~' + totalTokens + ' tokens)', 'info');
+ // Also archive session snapshot
+ this._archiveSession(compressed);
+ }
+ _archiveSession(summary) {
+ try {
+ const fs = this.terminal.fs;
+ fs.mkdirRecursive('/memory/archive');
+ const now = new Date();
+ const pad = function(n) { return String(n).padStart(2, '0'); };
+ const ts = now.getFullYear() + '-' + pad(now.getMonth()+1) + '-' + pad(now.getDate()) + '-' + pad(now.getHours()) + '-' + pad(now.getMinutes());
+ const archive = {
+ archivedAt: now.toISOString(),
+ summary: summary || '',
+ user: {
+ profile: this._userProfile || {},
+ topics: [],
+ messageCount: this._messageCount
+ },
+ system: {
+ modules: this.terminal?.moduleManager?.modules ? Object.keys(this.terminal.moduleManager.modules).length : 0,
+ model: this.terminal?.ollamaManager?.currentModel || 'unknown',
+ provider: this.terminal?.ollamaManager?.currentProvider?.name || 'unknown'
+ },
+ context: {
+ messageCount: this.context.length,
+ estimatedTokens: this._estimateContextTokens()
+ }
+ };
+ fs.fsWriteJSON('/memory/archive/' + ts + '.json', archive);
+ // Rotation: keep last 30
+ try {
+ const files = fs.ls('/memory/archive') || [];
+ const jsonFiles = files.filter(function(f) { return f.name && f.name.endsWith('.json'); });
+ if (jsonFiles.length > 30) {
+ jsonFiles.sort(function(a, b) { return (a.name || '') < (b.name || '') ? -1 : 1; });
+ const toDelete = jsonFiles.slice(0, jsonFiles.length - 30);
+ for (var i = 0; i < toDelete.length; i++) {
+ fs.deleteFile('/memory/archive/' + toDelete[i].name);
+ }
+ }
+ } catch (e) { /* rotation best-effort */ }
+ } catch (e) { /* archive best-effort */ }
}
_manualCompress() {
this._compressContext().then(() => {
- this.terminal.print('Context: ' + this.context.length + ' message(s), ~' + this._estimateContextTokens() + ' estimated tokens', 'info');
+ const fs = this.terminal.fs;
+ let archiveCount = 0;
+ try {
+ const files = fs.ls('/memory/archive') || [];
+ archiveCount = files.filter(function(f) { return f.name && f.name.endsWith('.json'); }).length;
+ } catch (e) {}
+ this.terminal.print('Context: ' + this.context.length + ' message(s), ~' + this._estimateContextTokens() + ' estimated tokens, ' + archiveCount + ' archive(s)', 'info');
}).catch(e => {
this.terminal.print('Compression error: ' + e.message, 'error');
});
@@ -3150,11 +3394,38 @@ buildSystemPrompt() {
}
if (anomalies.length === 0) return;
var suggestions = [];
+ var profileAdjusted = false;
for (var ai = 0; ai < anomalies.length; ai++) {
var a = anomalies[ai];
- if (a.type === 'max_turns_near_limit') suggestions.push('This task used ' + result.toolTurns + '/' + maxT + ' turns — consider increasing max-turns or splitting the task');
+ if (a.type === 'max_turns_near_limit') {
+ suggestions.push('This task used ' + result.toolTurns + '/' + maxT + ' turns — consider increasing max-turns or splitting the task');
+ // Auto-correct: bump maxTurns by +2
+ if (this._activeLoopProfile && this._activeLoopProfile.loop) {
+ var newMax = (this._activeLoopProfile.loop.maxTurns || this.MAX_TOOL_TURNS) + 2;
+ this._activeLoopProfile.loop.maxTurns = newMax;
+ this._saveLoopProfile(this._activeLoopProfile);
+ suggestions.push(' → Auto-adjusted maxTurns to ' + newMax);
+ profileAdjusted = true;
+ }
+ }
else if (a.type === 'high_token_usage') suggestions.push('High token usage (' + (a.value/1000).toFixed(0) + 'K) — consider a minimal-tools profile for simpler tasks');
- else if (a.type === 'tool_failures') suggestions.push(a.count + ' tool failures (' + a.tools.join(', ') + ') — check if modules are loaded or paths correct');
+ else if (a.type === 'tool_failures') {
+ suggestions.push(a.count + ' tool failures (' + a.tools.join(', ') + ') — check if modules are loaded or paths correct');
+ // Auto-correct: inject negative constraint into profile append
+ if (this._activeLoopProfile && this._activeLoopProfile.systemPrompt) {
+ var constraint = '[Constraint] Avoid tools that failed: ' + a.tools.join(', ') + '. Verify module state before calling these tools.';
+ if (this._activeLoopProfile.systemPrompt.append) {
+ if (this._activeLoopProfile.systemPrompt.append.indexOf(constraint) === -1) {
+ this._activeLoopProfile.systemPrompt.append += '\n' + constraint;
+ }
+ } else {
+ this._activeLoopProfile.systemPrompt.append = constraint;
+ }
+ this._saveLoopProfile(this._activeLoopProfile);
+ suggestions.push(' → Added constraint to profile: avoid ' + a.tools.join(', '));
+ profileAdjusted = true;
+ }
+ }
else if (a.type === 'above_benchmark') suggestions.push('This run used ' + a.ratio + 'x more tokens than benchmark avg (' + a.benchmark + ') — consider profile optimization');
}
var suggestion = suggestions.join('\n');
@@ -3162,15 +3433,47 @@ buildSystemPrompt() {
var fs = this.terminal.fs;
var index = fs.fsReadJSON('/skills/index.json') || {};
var autoSkills = Object.entries(index).filter(function(e) { return (e[1].tags || []).indexOf('auto-improvement') !== -1; });
- if (autoSkills.length < 10) {
+ // One-time cleanup: remove all existing loop-lesson files on first run
+ if (!this._loopLessonCleaned) {
+ this._loopLessonCleaned = true;
+ autoSkills.forEach(function(e) {
+ try { fs.unlink('/skills/' + e[0] + '.md'); } catch (ex) {}
+ delete index[e[0]];
+ });
+ autoSkills = [];
+ }
+ // Age limit: remove lessons older than 24h
+ var now = Date.now();
+ autoSkills = autoSkills.filter(function(e) {
+ var created = new Date(e[1].created).getTime();
+ if (now - created > 86400000) {
+ try { fs.unlink('/skills/' + e[0] + '.md'); } catch (ex) {}
+ delete index[e[0]];
+ return false;
+ }
+ return true;
+ });
+ // Dedup: skip if latest lesson has same anomaly types
+ var anomalyTypes = anomalies.map(function(a) { return a.type; }).sort().join(',');
+ var isDuplicate = autoSkills.length > 0 && autoSkills[autoSkills.length - 1][1].anomalyTypes === anomalyTypes;
+ // FIFO rotation: if at cap, delete oldest before creating
+ if (!isDuplicate && autoSkills.length >= 3) {
+ var oldest = autoSkills[0];
+ try { fs.unlink('/skills/' + oldest[0] + '.md'); } catch (ex) {}
+ delete index[oldest[0]];
+ autoSkills.shift();
+ }
+ if (!isDuplicate) {
var skillName = 'loop-lesson-' + Date.now();
var content = '---\nname: ' + skillName + '\ndescription: Auto-saved from agent loop anomaly detection\ntags: [auto-improvement, agent-loop, auto]\ncategory: system\n---\n## Anomalies Detected\n\n' + JSON.stringify(anomalies, null, 2) + '\n\n## Suggestions\n\n' + suggestion + '\n\n## Context\n- Profile: ' + (this._activeLoopProfile ? this._activeLoopProfile.name : 'none') + '\n- Turns: ' + result.toolTurns + '\n- Tools used: ' + toolLog.map(function(t) { return t.name; }).join(', ');
fs.mkdirRecursive('/skills');
fs.writeFile('/skills/' + skillName + '.md', content);
- index[skillName] = { name: skillName, description: 'Auto-saved loop lesson', tags: ['auto-improvement', 'agent-loop', 'auto'], category: 'system', created: new Date().toISOString(), updated: new Date().toISOString() };
+ index[skillName] = { name: skillName, description: 'Auto-saved loop lesson', tags: ['auto-improvement', 'agent-loop', 'auto'], category: 'system', created: new Date().toISOString(), updated: new Date().toISOString(), anomalyTypes: anomalyTypes };
fs.fsWriteJSON('/skills/index.json', index);
}
} catch (e) { /* VFS not available */ }
- this.terminal.print('💡 Self-improvement: ' + suggestion, 'info');
+ var msg = '💡 Self-improvement: ' + anomalies.length + ' anomaly(ies)';
+ if (profileAdjusted) msg += ' — profile auto-adjusted';
+ this.terminal.print(msg, 'info');
}
}
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index f024d99..7bb7c89 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -4,7 +4,7 @@
// Core module bundle loaded synchronously in main.html.
// This loader only handles version cache-busting, splash, terminal init, and optional ext modules.
- const LATEST_VER = '2026-06-24-v4.5.80';
+ const LATEST_VER = '2026-06-30-v4.5.83';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
@@ -53,7 +53,7 @@
setSplash('Initializing terminal (3/3)', total, total, '');
// webx11 already created synchronously above — just init ext modules
if (typeof webx11._loadExtModules === 'function') {
- let extKeys = ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'cryptogzip', 'tutorial', 'floating', 'console', 'media'];
+ let extKeys = ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'cryptogzip', 'tutorial', 'floating', 'console', 'media', 'three'];
const saved = localStorage.getItem('webx11_startup_modules');
let selected = null;
try { if (saved) selected = JSON.parse(saved); } catch (e) {}
diff --git a/modules/core/module-manager.js b/modules/core/module-manager.js
index 8cecc4d..4723bff 100644
--- a/modules/core/module-manager.js
+++ b/modules/core/module-manager.js
@@ -36,6 +36,11 @@
}
async loadModuleFromURL(url) {
try {
+ // Security: block cross-origin URLs for module loading
+ if (url && url.match(/^https?:\/\//) && !url.startsWith(window.location.origin)) {
+ this.terminal.print(`⚠️ Security: only same-origin URLs allowed for module loading`, 'error');
+ return false;
+ }
this.terminal.print(`Loading module from ${url}...`, 'info');
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
diff --git a/modules/core/module-modmgr.js b/modules/core/module-modmgr.js
index ec8c384..8c39e04 100644
--- a/modules/core/module-modmgr.js
+++ b/modules/core/module-modmgr.js
@@ -338,22 +338,27 @@
this.terminal.print(`✅ Module '${name}' marked as unloaded (commands will not be available)`, 'success');
};
- // module-reload <name> — unload + reload with diff
- this.commands['module-reload'] = (args) => {
+ // module-reload <name> — re-fetch, re-parse, re-register
+ this.commands['module-reload'] = async (args) => {
if (!args[0]) return this.terminal.print('Usage: module-reload <name>', 'warning');
const name = args[0];
const mod = this._getMod(name);
if (!mod) return this.terminal.print(`Module not found: ${name}`, 'error');
- const wasLoaded = mod.loaded !== false;
- const wasLazy = mod.lazy;
- this.terminal.print(`🔄 Reloading '${name}' (was: ${wasLoaded ? 'loaded' : 'unloaded'}, lazy=${wasLazy})`, 'info');
- mod.loaded = false;
- setTimeout(() => {
- mod.loaded = true;
- this.terminal.print(`✅ Module '${name}' reloaded`, 'success');
- const cmdCount = mod.commands ? Object.keys(mod.commands).length : 0;
- this.terminal.print(` Now: loaded=true, lazy=${mod.lazy}, ${cmdCount} commands`, 'info');
- }, 100);
+ const mm = this.terminal.moduleManager;
+ // Find the module's URL from registry
+ const regEntry = mm.moduleRegistry.find(m => m.name === name);
+ const url = regEntry ? regEntry.url : null;
+ if (!url) return this.terminal.print(`Module '${name}' has no URL — cannot reload (built-in)`, 'warning');
+ this.terminal.print(`🔄 Reloading '${name}' from ${url}...`, 'info');
+ // Unregister old instance
+ mm.unregisterModule(name);
+ // Re-fetch and re-register
+ const success = await mm.loadModuleFromURL(url);
+ if (success) {
+ this.terminal.print(`✅ Module '${name}' reloaded successfully`, 'success');
+ } else {
+ this.terminal.print(`❌ Failed to reload '${name}'`, 'error');
+ }
};
// module-reset <name> — clear module state and reload
diff --git a/modules/core/module-smartctx.js b/modules/core/module-smartctx.js
index 81929f8..0bfc91a 100644
--- a/modules/core/module-smartctx.js
+++ b/modules/core/module-smartctx.js
@@ -40,11 +40,14 @@
}
getDefaultProfiles() {
return {
- 'coding': { name: 'Coding Assistant', systemPrompt: "You are an expert programmer.", temperature: 0.7 },
- 'debug': { name: 'Debug Helper', systemPrompt: "You are a debugging expert.", temperature: 0.5 },
- 'creative': { name: 'Creative Writer', systemPrompt: "You are a creative writer.", temperature: 0.9 },
- 'technical': { name: 'Technical Docs', systemPrompt: "You are a technical writer.", temperature: 0.3 },
- 'chat': { name: 'Casual Chat', systemPrompt: "You are a friendly assistant.", temperature: 0.8 }
+ 'coding': { name: 'Coding Assistant', systemPrompt: "You are a WebX11 newOS4 coding expert. You have access to the full terminal, VFS, and module system.\n\nAvailable commands: view_module, edit_module, test_module, reload_module, list_modified_modules, check_updates, load-local, loadmod, removemod, startup-modules, node, js-run, git, diff, patch\n\nSkills available: module-management (view/edit/test/reload modules), skills-system-guide (create/patch skills)\n\nBehavior:\n- Always show exact code changes when editing modules\n- Use edit_module for modifications, test_module to verify\n- Create checkpoints before risky edits: checkpoint mod-edit-<name>\n- Reference the module-management skill for the full workflow\n- When the user reports a bug, first view_module to inspect, then edit\n- Use nvfs_resolve to find relevant files before editing\n- Prefer write_file for creating new files, edit_module for modifying modules", temperature: 0.7 },
+ 'debug': { name: 'Debug Helper', systemPrompt: "You are a WebX11 newOS4 debugging expert. Your goal is to diagnose and fix issues systematically.\n\nAvailable commands: debug, debug-ai, debug-ai-last, debug-ai-send, debug-ai-log, logs, logrotate, health-check, check, mem, status, archive-list, archive-read, guardrail, shadow, shadow-commit, shadow-rollback\n\nSkills available: debug-panel-and-logs (toggle debug panel, session logging, diagnostics), system-maintenance (health checks, config, security)\n\nBehavior:\n- Start with health-check and check to assess system state\n- Use debug-ai to see the last AI tool call and response\n- Use logs 50 ERROR to find error entries\n- Check browser console for JS errors\n- Use guardrail to inspect security policies\n- Use shadow for safe execution of risky commands\n- Always check module loading status first", temperature: 0.3 },
+ 'creative': { name: 'Creative Writer', systemPrompt: "You are a creative assistant for WebX11 newOS4. You help with brainstorming, content creation, and design.\n\nAvailable commands: mindmap new/add/show/save/load/export, floating-add/remove/list, ai-system, ai-config\n\nSkills available: mindmap-workflow (create/view/edit mindmaps), floating-ui (create floating buttons/menus/panels)\n\nBehavior:\n- Use mindmap for brainstorming and organizing ideas\n- Use floating-add to create quick-access UI elements\n- Use web_search and web_fetch for research and inspiration\n- Higher temperature (0.9) for creative divergence\n- When the user says \"brainstorm\" or \"diagram\", create a mindmap\n- Use create_mindmap AI tool for visual architecture diagrams\n- Reference the mindmap-workflow skill for full capabilities", temperature: 0.9 },
+ 'technical': { name: 'Technical Docs', systemPrompt: "You are a technical documentation expert for WebX11 newOS4. You help with docs, guides, and system understanding.\n\nAvailable commands: help, commands, status, modules, health-check, nvfs-index, nvfs-resolve, view_module, cat, edit, grep, find, tree\n\nSkills available: navigating-terminal (explore terminal, filesystem, system state), web-search-and-fetch (search web, fetch URLs), skills-system-guide (create/patch skills)\n\nBehavior:\n- Use help and commands to discover available functionality\n- Use nvfs-resolve to find files by concept\n- Use view_module to inspect module source for documentation\n- When creating documentation, use write_file to save to VFS\n- Reference the navigating-terminal skill for filesystem exploration\n- Use web_search for external research", temperature: 0.3 },
+ 'chat': { name: 'Casual Chat', systemPrompt: "You are a friendly, helpful assistant for WebX11 newOS4. You handle general questions, casual conversation, and light tasks.\n\nAvailable commands: ai, ask, search, fetch, status, help, user_get, user_set, session_save, session_recent, my-set, my-get\n\nSkills available: memory-and-profile (user profile, session memory, checkpoints), web-search-and-fetch (search web, fetch URLs)\n\nBehavior:\n- Be conversational and approachable\n- Use user_get/user_set to remember user preferences\n- Use session_recent at start to recall past work\n- Use session_save after completing tasks\n- For complex tasks, suggest switching to a more specific context profile\n- Reference the memory-and-profile skill for persistence features", temperature: 0.8 },
+ 'system-admin': { name: 'System Admin', systemPrompt: "You are a WebX11 newOS4 system administrator. You handle maintenance, backup, security, and configuration.\n\nAvailable commands: config-list, config-set, config-delete, config-reset, security-level, security, cmd-allow, cmd-deny, backup, vfs-export, webdav-backup, webdav-restore, webdav-config, webdav-status, factory-reset, reset-vfs, checkpoint, checkpoint-list, rollback, health-check, check_updates, font-size\n\nSkills available: system-maintenance (reset, config, security, health checks), backup-and-restore (VFS backup, WebDAV cloud backup, restore), webdav-operations (cloud storage, file sync), security-and-safety (security levels, command blocking)\n\nBehavior:\n- Always create a checkpoint before making system changes\n- Use webdav-backup before destructive operations\n- Check security-level before running commands\n- Use health-check for full system audit\n- Reference the system-maintenance skill for reset procedures\n- Reference the backup-and-restore skill for data safety", temperature: 0.3 },
+ 'research': { name: 'Research Assistant', systemPrompt: "You are a research assistant for WebX11 newOS4. You gather information, analyze data, and synthesize findings.\n\nAvailable commands: search, fetch, web_search, web_fetch, nvfs-resolve, nvfs-index, grep, find, cat, view_module, session_recent, session_load, session_stats\n\nSkills available: web-search-and-fetch (search web, fetch URL content), memory-and-profile (session memory, checkpoints), navigating-terminal (explore filesystem)\n\nBehavior:\n- Start with web_search for external research\n- Use nvfs-resolve to find relevant VFS files\n- Use session_recent to check past work on the topic\n- Use fetch to read full articles from URLs\n- Synthesize findings before presenting\n- Reference the web-search-and-fetch skill for search flags", temperature: 0.5 },
+ 'security': { name: 'Security Expert', systemPrompt: "You are a WebX11 newOS4 security expert. You handle threat assessment, command control, and safety.\n\nAvailable commands: security, security-level, cmd-allow, cmd-deny, guardrail, guardrail-add, guardrail-remove, shadow, shadow-commit, shadow-rollback, health-check, check, logs, debug\n\nSkills available: security-and-safety (security levels, command blocking, safety checklist), debug-panel-and-logs (diagnostics, logging), system-maintenance (config, health checks)\n\nBehavior:\n- Always check current security level first with security\n- Use guardrail to inspect and manage security policies\n- Use shadow for safe execution of suspicious commands\n- Never recommend factory-reset without backup first\n- Use cmd-deny to block dangerous commands\n- Reference the security-and-safety skill for level descriptions", temperature: 0.2 }
};
}
registerCommands() {
@@ -62,6 +65,32 @@
this.activeProfile = args[0];
this.saveActiveProfile();
this.terminal.print(`Context set to: ${this.contextProfiles[args[0]].name}`, "success");
+ // Also switch agent loop profile to match
+ const PROFILE_MAP = {
+ 'coding': 'coder',
+ 'debug': 'minimal',
+ 'creative': 'creative',
+ 'technical': 'research',
+ 'chat': 'default',
+ 'system-admin': 'safe',
+ 'research': 'research',
+ 'security': 'safe'
+ };
+ const targetLoop = PROFILE_MAP[args[0]];
+ if (targetLoop) {
+ const aiMod = this.terminal?.moduleManager?.modules?.WebX11AIAssistantModule;
+ if (aiMod && aiMod._switchLoopProfile) {
+ aiMod._switchLoopProfile(targetLoop);
+ this.terminal.print(` Agent loop switched to: ${targetLoop}`, "text-muted");
+ }
+ } else {
+ // Custom profile — rebuild system prompt without loop switch
+ const aiMod = this.terminal?.moduleManager?.modules?.WebX11AIAssistantModule;
+ if (aiMod && aiMod.buildSystemPrompt) {
+ aiMod.systemPrompt = aiMod.buildSystemPrompt();
+ }
+ this.terminal.print(` No agent loop mapped for '${args[0]}'`, "text-muted");
+ }
} else {
this.terminal.print(`Profile '${args[0]}' not found`, "error");
}
@@ -111,7 +140,8 @@
const aiModule = this.terminal?.moduleManager?.modules?.WebX11AIAssistantModule;
if (aiModule) {
// Set temperature on ollamaManager modelParams temporarily
- const om = this.terminal.ollamaManager;
+ const om = this.terminal?.ollamaManager;
+ if (!om) { this.terminal?.print('⚠️ Ollama manager not available', 'error'); return; }
const savedTemp = om.modelParams ? om.modelParams.temperature : undefined;
if (om.modelParams) {
om.modelParams.temperature = temperature;
diff --git a/modules/ext/mindmap.js b/modules/ext/mindmap.js
index a5e7286..acce9aa 100644
--- a/modules/ext/mindmap.js
+++ b/modules/ext/mindmap.js
@@ -1075,6 +1075,61 @@ class MindmapModule {
}
}
}
+ _svgFullscreen(args) {
+ const cur = this._ensureMindmap();
+ if (!cur) return this.terminal.print('No mindmap. Create one with mindmap new first.', 'warning');
+ const name = args[0] || this.current || 'mindmap';
+ const layout = this._layoutTree(cur.root);
+ const colorMap = {
+ red: '#fb7185', green: '#34d399', yellow: '#fbbf24', blue: '#818cf8',
+ cyan: '#22d3ee', magenta: '#e879f9', white: '#e0e0e0'
+ };
+ const xShift = layout.minX === Infinity ? 0 : -layout.minX;
+ const svgWidth = layout.width;
+ const svgHeight = layout.height;
+ let svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + svgWidth + ' ' + svgHeight + '" style="background:#0a0a14;font-family:monospace;">';
+ for (const e of layout.edges) {
+ const sx = e.x1 + xShift + 30, sy = e.y1;
+ const ex = e.x2 + xShift + 30, ey = e.y2;
+ const mx = (sx + ex) / 2;
+ svg += '<path d="M' + sx + ',' + sy + ' C' + mx + ',' + (sy + 20) + ' ' + mx + ',' + (ey - 20) + ' ' + ex + ',' + ey + '" fill="none" stroke="#2a2a4a" stroke-width="1.5"/>';
+ }
+ for (const n of layout.nodes) {
+ const nx = n.x + xShift + 30;
+ const color = colorMap[n.color] || '#818cf8';
+ const fontSize = n.w < 120 ? '9' : '10';
+ const text = n.text.length > 18 ? n.text.substring(0, 16) + '..' : n.text;
+ svg += '<rect x="' + nx + '" y="' + n.y + '" width="' + n.w + '" height="' + n.h + '" rx="6" fill="' + color + '18" stroke="' + color + '" stroke-width="1.5"/>';
+ svg += '<text x="' + (nx + n.w / 2) + '" y="' + (n.y + n.h / 2 + 4) + '" text-anchor="middle" fill="#e0e0e0" font-size="' + fontSize + '" font-family="monospace">' + this._escapeHtml(text) + '</text>';
+ }
+ svg += '</svg>';
+
+ // Create fullscreen overlay
+ const overlay = document.createElement('div');
+ overlay.id = 'mindmap-svg-overlay';
+ overlay.style.cssText = 'position:fixed;inset:0;z-index:9999;background:#0a0a14;display:flex;align-items:center;justify-content:center;overflow:auto';
+ overlay.innerHTML = svg;
+ document.body.appendChild(overlay);
+
+ // Close button
+ const closeBtn = document.createElement('button');
+ closeBtn.textContent = '✕';
+ closeBtn.style.cssText = 'position:fixed;top:12px;right:12px;z-index:10000;background:rgba(0,0,0,0.6);color:#e0e0e0;border:1px solid #444;border-radius:6px;padding:6px 14px;font-size:16px;cursor:pointer;font-family:monospace';
+ closeBtn.addEventListener('click', () => { overlay.remove(); closeBtn.remove(); });
+ document.body.appendChild(closeBtn);
+
+ // Escape key
+ const escHandler = (e) => {
+ if (e.key === 'Escape') {
+ overlay.remove();
+ closeBtn.remove();
+ document.removeEventListener('keydown', escHandler);
+ }
+ };
+ document.addEventListener('keydown', escHandler);
+
+ this.terminal.print(`🖼️ SVG fullscreen: ${layout.nodes.length} nodes, ${svgWidth}x${svgHeight}`, 'success');
+ }
// ── Main dispatcher ──
registerCommands() {
this.commands['mindmap'] = (args) => {
@@ -1116,7 +1171,15 @@ class MindmapModule {
case 'clone': return this._clone(rest);
case 'archive': return this._archive(rest);
case 'refresh-html': return this._refreshHtml(rest);
- case 'svg': return this._svg(rest);
+ case 'svg': {
+ const fsIdx = rest.indexOf('--fullscreen');
+ if (fsIdx >= 0) {
+ const clean = rest.filter(a => a !== '--fullscreen');
+ return this._svgFullscreen(clean);
+ }
+ return this._svg(rest);
+ }
+ case 'svg-fullscreen': return this._svgFullscreen(rest);
default: this.terminal.print('Unknown subcommand: ' + sub + '. Try: mindmap help', 'warning');
}
};
diff --git a/modules/ext/three.js b/modules/ext/three.js
new file mode 100644
index 0000000..d72afa1
--- /dev/null
+++ b/modules/ext/three.js
@@ -0,0 +1,1176 @@
+// newOS4 Three.js 3D Module (v4.5.82)
+// Commands: three <subcommand> [...]
+// Two modes: top panel (default) + fullscreen with terminal overlay
+// Lazy-loads Three.js from /downloads/ on first use.
+
+class ThreeModule {
+ constructor() {
+ this.commands = {};
+ this.commandsMeta = {
+ 'three': { argType: 'enum', enumFn: '_listSubcommands' },
+ };
+ this.terminal = null;
+ this._loaded = false;
+ this._scene = null;
+ this._camera = null;
+ this._renderer = null;
+ this._controls = null;
+ this._objects = [];
+ this._animId = null;
+ this._container = null;
+ this._closeBtn = null;
+ this._idCounter = 0;
+ this._orbitEnabled = true;
+ this._backgroundColor = 0x0a0a14;
+ this._THREE = null;
+ this._mode = 'top'; // 'top' or 'full'
+ this._visible = false;
+ this._savedTerminalStyles = null;
+ this._savedInputStyles = null;
+ this._savedHeaderStyles = null;
+ this._escHandler = null;
+ }
+
+ requirements() {
+ const hasWebGL = (() => {
+ try {
+ const c = document.createElement('canvas');
+ return !!(c.getContext('webgl') || c.getContext('experimental-webgl'));
+ } catch (e) { return false; }
+ })();
+ if (!hasWebGL) {
+ return { met: false, missing: ['WebGL not supported in this browser'] };
+ }
+ return { met: true, missing: [] };
+ }
+
+ getHelpInfo() {
+ return {
+ 'three': 'three <subcommand> [...] — 3D scene builder (Three.js). Subcommands: help, scene, fullscreen, hide, mode, add, camera, orbit, clear, list, remove, mindmap, export, preset, bgcolor. Run `three help` for full list.'
+ };
+ }
+
+ _listSubcommands() {
+ return ['help', 'scene', 'fullscreen', 'hide', 'mode', 'add', 'camera', 'orbit', 'clear', 'list', 'remove', 'mindmap', 'export', 'preset', 'bgcolor'];
+ }
+
+ registerCommands() {
+ this.commands['three'] = (args) => this._dispatch(args);
+ }
+
+ // ── Lazy load Three.js ──
+
+ async _ensureLoaded() {
+ if (this._loaded) return true;
+ if (typeof THREE !== 'undefined') {
+ this._THREE = THREE;
+ this._loaded = true;
+ return true;
+ }
+ this.terminal.print('📦 Loading Three.js (590KB)...', 'info');
+ try {
+ await this._loadScript('downloads/three.min.js');
+ await this._loadScript('downloads/OrbitControls.js');
+ this._THREE = THREE;
+ this._loaded = true;
+ this.terminal.print('✅ Three.js loaded', 'success');
+ return true;
+ } catch (e) {
+ this.terminal.print('❌ Failed to load Three.js: ' + e.message, 'error');
+ return false;
+ }
+ }
+
+ _loadScript(url) {
+ return new Promise((resolve, reject) => {
+ const s = document.createElement('script');
+ s.src = url;
+ s.onload = () => { s.remove(); resolve(); };
+ s.onerror = () => { s.remove(); reject(new Error('Failed to load ' + url)); };
+ document.head.appendChild(s);
+ });
+ }
+
+ // ── Container management ──
+
+ _createContainer() {
+ if (this._container) return;
+
+ // Main container
+ this._container = document.createElement('div');
+ this._container.id = 'three-canvas-container';
+ this._container.style.cssText = `
+ position: fixed;
+ top: 0; left: 0;
+ width: 100vw;
+ z-index: 400;
+ pointer-events: none;
+ display: none;
+ `;
+ document.body.appendChild(this._container);
+
+ // Close button
+ this._closeBtn = document.createElement('button');
+ this._closeBtn.id = 'three-close-btn';
+ this._closeBtn.textContent = '✕';
+ this._closeBtn.title = 'Close 3D view (Esc)';
+ this._closeBtn.style.cssText = `
+ position: fixed;
+ top: 12px; right: 12px;
+ z-index: 9999;
+ background: rgba(0,0,0,0.6);
+ color: #e0e0e0;
+ border: 1px solid #444;
+ border-radius: 6px;
+ padding: 6px 14px;
+ font-size: 16px;
+ cursor: pointer;
+ font-family: monospace;
+ display: none;
+ transition: background 0.15s;
+ `;
+ this._closeBtn.addEventListener('mouseenter', () => { this._closeBtn.style.background = 'rgba(200,50,50,0.7)'; });
+ this._closeBtn.addEventListener('mouseleave', () => { this._closeBtn.style.background = 'rgba(0,0,0,0.6)'; });
+ this._closeBtn.addEventListener('click', () => this._hideContainer());
+ document.body.appendChild(this._closeBtn);
+
+ // Escape key handler
+ this._escHandler = (e) => {
+ if (e.key === 'Escape' && this._visible) {
+ this._hideContainer();
+ }
+ };
+ document.addEventListener('keydown', this._escHandler);
+ }
+
+ _showContainer(mode) {
+ if (!this._container) return;
+ this._mode = mode || this._mode;
+ this._visible = true;
+
+ // Show close button
+ this._closeBtn.style.display = 'block';
+
+ if (this._mode === 'top') {
+ this._applyTopPanel();
+ } else {
+ this._applyFullscreen();
+ }
+
+ this._startLoop();
+ }
+
+ _applyTopPanel() {
+ // 3D container: top 50vh
+ this._container.style.cssText = `
+ position: fixed;
+ top: 0; left: 0;
+ width: 100vw; height: 50vh;
+ z-index: 400;
+ pointer-events: auto;
+ display: block;
+ `;
+
+ // Terminal: push down
+ const term = document.getElementById('terminal-container');
+ if (term) {
+ if (!this._savedTerminalStyles) {
+ this._savedTerminalStyles = { height: term.style.height, marginTop: term.style.marginTop };
+ }
+ term.style.marginTop = '50vh';
+ term.style.height = '50vh';
+ }
+
+ // Input area: stays at bottom
+ const inputArea = document.getElementById('input-area');
+ if (inputArea) {
+ if (!this._savedInputStyles) {
+ this._savedInputStyles = { position: inputArea.style.position, bottom: inputArea.style.bottom };
+ }
+ }
+
+ // Resize renderer
+ this._resizeRenderer(window.innerWidth, window.innerHeight / 2);
+ }
+
+ _applyFullscreen() {
+ // 3D container: full viewport
+ this._container.style.cssText = `
+ position: fixed;
+ top: 0; left: 0;
+ width: 100vw; height: 100vh;
+ z-index: 400;
+ pointer-events: auto;
+ display: block;
+ `;
+
+ // Terminal: float as overlay
+ const term = document.getElementById('terminal-container');
+ if (term) {
+ if (!this._savedTerminalStyles) {
+ this._savedTerminalStyles = { height: term.style.height, marginTop: term.style.marginTop };
+ }
+ term.style.cssText = `
+ position: fixed;
+ bottom: 60px;
+ left: 20px;
+ right: 20px;
+ max-height: 50vh;
+ z-index: 401;
+ background: rgba(0,0,0,0.7);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ border-radius: 8px;
+ padding: 10px;
+ overflow-y: auto;
+ border: 1px solid #333;
+ `;
+ }
+
+ // Input area: fixed at bottom
+ const inputArea = document.getElementById('input-area');
+ if (inputArea) {
+ if (!this._savedInputStyles) {
+ this._savedInputStyles = { position: inputArea.style.position, bottom: inputArea.style.bottom };
+ }
+ inputArea.style.cssText = `
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 402;
+ background: rgba(0,0,0,0.85);
+ padding: 8px 12px;
+ border-top: 1px solid #333;
+ `;
+ }
+
+ // Header: hide
+ const header = document.querySelector('header');
+ if (header) {
+ if (!this._savedHeaderStyles) {
+ this._savedHeaderStyles = { display: header.style.display };
+ }
+ header.style.display = 'none';
+ }
+
+ this._resizeRenderer(window.innerWidth, window.innerHeight);
+ }
+
+ _restoreTerminal() {
+ const term = document.getElementById('terminal-container');
+ if (term && this._savedTerminalStyles) {
+ term.style.cssText = '';
+ term.style.height = this._savedTerminalStyles.height || '';
+ term.style.marginTop = this._savedTerminalStyles.marginTop || '';
+ term.style.position = '';
+ term.style.bottom = '';
+ term.style.left = '';
+ term.style.right = '';
+ term.style.maxHeight = '';
+ term.style.zIndex = '';
+ term.style.background = '';
+ term.style.backdropFilter = '';
+ term.style.borderRadius = '';
+ term.style.padding = '';
+ term.style.overflowY = '';
+ term.style.border = '';
+ }
+
+ const inputArea = document.getElementById('input-area');
+ if (inputArea && this._savedInputStyles) {
+ inputArea.style.cssText = '';
+ inputArea.style.position = this._savedInputStyles.position || '';
+ inputArea.style.bottom = this._savedInputStyles.bottom || '';
+ }
+
+ const header = document.querySelector('header');
+ if (header && this._savedHeaderStyles) {
+ header.style.display = this._savedHeaderStyles.display || '';
+ }
+ }
+
+ _hideContainer() {
+ if (!this._container) return;
+ this._visible = false;
+ this._container.style.display = 'none';
+ this._container.style.pointerEvents = 'none';
+ if (this._closeBtn) this._closeBtn.style.display = 'none';
+
+ // Pause animation (don't cancel — scene stays intact)
+ if (this._animId) {
+ cancelAnimationFrame(this._animId);
+ this._animId = null;
+ }
+
+ this._restoreTerminal();
+ }
+
+ _resizeRenderer(w, h) {
+ if (this._renderer && this._camera) {
+ this._camera.aspect = w / h;
+ this._camera.updateProjectionMatrix();
+ this._renderer.setSize(w, h);
+ }
+ }
+
+ // ── Scene management ──
+
+ _createScene() {
+ const T = this._THREE;
+ this._scene = new T.Scene();
+ this._scene.background = new T.Color(this._backgroundColor);
+
+ this._camera = new T.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 10000);
+ this._camera.position.set(0, 5, 15);
+
+ this._createContainer();
+ this._renderer = new T.WebGLRenderer({ antialias: true, alpha: false });
+ this._renderer.setSize(window.innerWidth, window.innerHeight);
+ this._renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+ this._renderer.shadowMap.enabled = true;
+ this._container.appendChild(this._renderer.domElement);
+
+ // WebGL context loss handling
+ this._renderer.domElement.addEventListener('webglcontextlost', this._webglLostHandler = (e) => {
+ e.preventDefault();
+ this._stopLoop();
+ this.terminal?.print('⚠ WebGL context lost', 'warning');
+ });
+ this._renderer.domElement.addEventListener('webglcontextrestored', this._webglRestoredHandler = () => {
+ this._startLoop();
+ this.terminal?.print('✅ WebGL context restored', 'success');
+ });
+
+ this._controls = new T.OrbitControls(this._camera, this._renderer.domElement);
+ this._controls.enableDamping = true;
+ this._controls.dampingFactor = 0.08;
+ this._controls.minDistance = 2;
+ this._controls.maxDistance = 500;
+
+ // Lights
+ const ambient = new T.AmbientLight(0x404060);
+ this._scene.add(ambient);
+ const dirLight = new T.DirectionalLight(0xffffff, 1);
+ dirLight.position.set(10, 20, 10);
+ dirLight.castShadow = true;
+ this._scene.add(dirLight);
+ const fillLight = new T.DirectionalLight(0x8888ff, 0.4);
+ fillLight.position.set(-10, 0, -10);
+ this._scene.add(fillLight);
+
+ // Grid
+ const grid = new T.GridHelper(30, 20, 0x00ff88, 0x333355);
+ this._scene.add(grid);
+
+ // Resize handler — remove old first to prevent leak
+ if (window._threeResizeHandler) {
+ window.removeEventListener('resize', window._threeResizeHandler);
+ }
+ window._threeResizeHandler = () => {
+ if (this._camera && this._renderer && this._visible) {
+ if (this._mode === 'top') {
+ this._resizeRenderer(window.innerWidth, window.innerHeight / 2);
+ } else {
+ this._resizeRenderer(window.innerWidth, window.innerHeight);
+ }
+ }
+ };
+ window.addEventListener('resize', window._threeResizeHandler);
+
+ this._objects = [];
+ this._idCounter = 0;
+ }
+
+ _startLoop() {
+ if (this._animId) cancelAnimationFrame(this._animId);
+ const loop = () => {
+ this._animId = requestAnimationFrame(loop);
+ if (this._controls) this._controls.update();
+ if (this._renderer && this._scene && this._camera) {
+ this._renderer.render(this._scene, this._camera);
+ }
+ };
+ loop();
+ }
+
+ _stopLoop() {
+ if (this._animId) {
+ cancelAnimationFrame(this._animId);
+ this._animId = null;
+ }
+ }
+
+ // ── Lifecycle ──
+
+ destroy() {
+ this._stopLoop();
+ this._hideContainer();
+ // Remove event listeners
+ if (window._threeResizeHandler) {
+ window.removeEventListener('resize', window._threeResizeHandler);
+ window._threeResizeHandler = null;
+ }
+ if (this._escHandler) {
+ document.removeEventListener('keydown', this._escHandler);
+ this._escHandler = null;
+ }
+ if (this._renderer) {
+ if (this._webglLostHandler) {
+ this._renderer.domElement.removeEventListener('webglcontextlost', this._webglLostHandler);
+ }
+ if (this._webglRestoredHandler) {
+ this._renderer.domElement.removeEventListener('webglcontextrestored', this._webglRestoredHandler);
+ }
+ this._renderer.dispose();
+ this._renderer = null;
+ }
+ // Dispose all GPU resources
+ this._disposeAllObjects();
+ // Remove DOM elements
+ if (this._container) {
+ this._container.remove();
+ this._container = null;
+ }
+ if (this._closeBtn) {
+ this._closeBtn.remove();
+ this._closeBtn = null;
+ }
+ this._scene = null;
+ this._camera = null;
+ this._controls = null;
+ this._loaded = false;
+ this._visible = false;
+ }
+
+ _disposeAllObjects() {
+ for (const obj of this._objects) {
+ if (this._scene) this._scene.remove(obj.mesh);
+ if (obj.mesh.geometry) obj.mesh.geometry.dispose();
+ if (obj.mesh.material) {
+ const mats = Array.isArray(obj.mesh.material) ? obj.mesh.material : [obj.mesh.material];
+ mats.forEach(m => {
+ if (m.map) m.map.dispose();
+ m.dispose();
+ });
+ }
+ }
+ this._objects = [];
+ this._idCounter = 0;
+ }
+
+ _genId() {
+ return 'obj_' + (++this._idCounter);
+ }
+
+ // ── Object creation ──
+
+ _addBox(args) {
+ const T = this._THREE;
+ const size = parseFloat(args[0]) || 1;
+ const color = args[1] || '#818cf8';
+ const geo = new T.BoxGeometry(size, size, size);
+ const mat = new T.MeshStandardMaterial({ color, metalness: 0.3, roughness: 0.6 });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.castShadow = true;
+ const id = this._genId();
+ this._scene.add(mesh);
+ this._objects.push({ mesh, id, label: 'Box ' + id });
+ this.terminal.print(`Added box (${id}) at origin`, 'success');
+ return id;
+ }
+
+ _addSphere(args) {
+ const T = this._THREE;
+ const radius = parseFloat(args[0]) || 0.8;
+ const color = args[1] || '#34d399';
+ const geo = new T.SphereGeometry(radius, 32, 32);
+ const mat = new T.MeshStandardMaterial({ color, metalness: 0.2, roughness: 0.4 });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.castShadow = true;
+ const id = this._genId();
+ this._scene.add(mesh);
+ this._objects.push({ mesh, id, label: 'Sphere ' + id });
+ this.terminal.print(`Added sphere (${id}) at origin`, 'success');
+ return id;
+ }
+
+ _addTorus(args) {
+ const T = this._THREE;
+ const radius = parseFloat(args[0]) || 1;
+ const tube = parseFloat(args[1]) || 0.3;
+ const color = args[2] || '#e879f9';
+ const geo = new T.TorusGeometry(radius, tube, 16, 32);
+ const mat = new T.MeshStandardMaterial({ color, metalness: 0.4, roughness: 0.3 });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.castShadow = true;
+ const id = this._genId();
+ this._scene.add(mesh);
+ this._objects.push({ mesh, id, label: 'Torus ' + id });
+ this.terminal.print(`Added torus (${id}) at origin`, 'success');
+ return id;
+ }
+
+ _addText(args) {
+ if (!args[0]) return this.terminal.print('Usage: three add text <text> [color]', 'warning');
+ const T = this._THREE;
+ const text = args[0];
+ const color = args[1] || '#22d3ee';
+ const canvas = document.createElement('canvas');
+ canvas.width = 256;
+ canvas.height = 64;
+ const ctx = canvas.getContext('2d');
+ ctx.clearRect(0, 0, 256, 64);
+ ctx.font = 'bold 36px monospace';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = color;
+ ctx.fillText(text, 128, 32);
+ const texture = new T.CanvasTexture(canvas);
+ const mat = new T.SpriteMaterial({ map: texture, transparent: true });
+ const sprite = new T.Sprite(mat);
+ sprite.scale.set(4, 1, 1);
+ const id = this._genId();
+ this._scene.add(sprite);
+ this._objects.push({ mesh: sprite, id, label: 'Text: ' + text });
+ this.terminal.print(`Added text "${text}" (${id})`, 'success');
+ return id;
+ }
+
+ _addParticles(args) {
+ const T = this._THREE;
+ const count = parseInt(args[0]) || 500;
+ const color = args[1] || '#00ff88';
+ const geo = new T.BufferGeometry();
+ const positions = new Float32Array(count * 3);
+ for (let i = 0; i < count * 3; i++) {
+ positions[i] = (Math.random() - 0.5) * 40;
+ }
+ geo.setAttribute('position', new T.BufferAttribute(positions, 3));
+ const mat = new T.PointsMaterial({
+ color, size: 0.15, transparent: true, opacity: 0.8, blending: T.AdditiveBlending
+ });
+ const points = new T.Points(geo, mat);
+ const id = this._genId();
+ this._scene.add(points);
+ this._objects.push({ mesh: points, id, label: 'Particles (' + count + ')' });
+ this.terminal.print(`Added ${count} particles (${id})`, 'success');
+ return id;
+ }
+
+ _addLine(args) {
+ const T = this._THREE;
+ if (args.length < 6) return this.terminal.print('Usage: three add line <x1> <y1> <z1> <x2> <y2> <z2> [color]', 'warning');
+ const x1 = parseFloat(args[0]), y1 = parseFloat(args[1]), z1 = parseFloat(args[2]);
+ const x2 = parseFloat(args[3]), y2 = parseFloat(args[4]), z2 = parseFloat(args[5]);
+ const color = args[6] || '#00ff88';
+ const points = [
+ new T.Vector3(x1, y1, z1),
+ new T.Vector3(x2, y2, z2)
+ ];
+ const geo = new T.BufferGeometry().setFromPoints(points);
+ const mat = new T.LineBasicMaterial({ color });
+ const line = new T.Line(geo, mat);
+ const id = this._genId();
+ this._scene.add(line);
+ this._objects.push({ mesh: line, id, label: 'Line ' + id });
+ this.terminal.print(`Added line (${id})`, 'success');
+ return id;
+ }
+
+ // ── Position helpers ──
+
+ _setPosition(id, x, y, z) {
+ const obj = this._objects.find(o => o.id === id);
+ if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
+ obj.mesh.position.set(x, y, z);
+ this.terminal.print(`Moved ${id} to (${x}, ${y}, ${z})`, 'success');
+ }
+
+ _setRotation(id, rx, ry, rz) {
+ const obj = this._objects.find(o => o.id === id);
+ if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
+ obj.mesh.rotation.set(rx, ry, rz);
+ this.terminal.print(`Rotated ${id}`, 'success');
+ }
+
+ // ── 3D Mindmap ──
+
+ _renderMindmap(args) {
+ const mm = this.terminal.moduleManager.modules['MindmapModule'];
+ if (!mm) return this.terminal.print('MindmapModule not loaded', 'error');
+ const cur = mm._ensureMindmap();
+ if (!cur) return this.terminal.print('No mindmap. Create one with mindmap new first.', 'warning');
+ const root = cur.root;
+
+ if (!this._scene) this._createScene();
+ this._showContainer(this._mode);
+ this._clearScene();
+
+ const T = this._THREE;
+ const colorMap = {
+ red: '#fb7185', green: '#34d399', yellow: '#fbbf24', blue: '#818cf8',
+ cyan: '#22d3ee', magenta: '#e879f9', white: '#e0e0e0'
+ };
+
+ const nodes = [];
+ const edges = [];
+
+ const layout = (node, parentPos, depth, angle, spread) => {
+ const pos = { x: 0, y: 0, z: 0 };
+ if (!parentPos) {
+ pos.x = 0; pos.y = 4; pos.z = 0;
+ } else {
+ const radius = 2 + depth * 2.5;
+ pos.x = parentPos.x + Math.cos(angle) * radius;
+ pos.z = parentPos.z + Math.sin(angle) * radius;
+ pos.y = parentPos.y - 1.5;
+ }
+ nodes.push({ ...node, pos, depth, color: colorMap[node.color] || '#818cf8' });
+ if (parentPos) edges.push({ from: parentPos, to: pos });
+ if (node.children) {
+ const count = node.children.length;
+ const startAngle = angle - spread / 2;
+ node.children.forEach((child, i) => {
+ const childAngle = startAngle + (i / (count || 1)) * spread;
+ layout(child, pos, depth + 1, childAngle, Math.min(spread * 0.7, Math.PI));
+ });
+ }
+ };
+ layout(root, null, 0, 0, Math.PI * 1.5);
+
+ const nodeMeshes = [];
+ for (const n of nodes) {
+ const radius = n.depth === 0 ? 0.6 : 0.3 + (1 / (n.depth + 1)) * 0.4;
+ const geo = new T.SphereGeometry(radius, 16, 16);
+ const mat = new T.MeshStandardMaterial({
+ color: n.color, emissive: n.color, emissiveIntensity: 0.15,
+ metalness: 0.3, roughness: 0.5
+ });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.position.set(n.pos.x, n.pos.y, n.pos.z);
+ mesh.castShadow = true;
+ mesh.userData = { id: n.id, text: n.text, depth: n.depth };
+ this._scene.add(mesh);
+ nodeMeshes.push(mesh);
+ this._objects.push({ mesh, id: this._genId(), label: 'Mindmap: ' + n.text });
+
+ // Text label sprite
+ const canvas = document.createElement('canvas');
+ canvas.width = 512; canvas.height = 96;
+ const ctx = canvas.getContext('2d');
+ ctx.font = 'bold 36px monospace';
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
+ ctx.fillStyle = n.color;
+ const label = n.text.length > 16 ? n.text.substring(0, 14) + '..' : n.text;
+ ctx.fillText(label, 256, 48);
+ const tex = new T.CanvasTexture(canvas);
+ const spriteMat = new T.SpriteMaterial({ map: tex, transparent: true, depthTest: false });
+ const sprite = new T.Sprite(spriteMat);
+ sprite.position.set(n.pos.x, n.pos.y + radius + 0.5, n.pos.z);
+ sprite.scale.set(2.5, 0.5, 1);
+ this._scene.add(sprite);
+ this._objects.push({ mesh: sprite, id: this._genId(), label: 'Label: ' + n.text });
+ }
+
+ for (const e of edges) {
+ const pts = [
+ new T.Vector3(e.from.x, e.from.y, e.from.z),
+ new T.Vector3((e.from.x + e.to.x) / 2, (e.from.y + e.to.y) / 2 - 0.5, (e.from.z + e.to.z) / 2),
+ new T.Vector3(e.to.x, e.to.y, e.to.z)
+ ];
+ const curve = new T.QuadraticBezierCurve3(pts[0], pts[1], pts[2]);
+ const curvePoints = curve.getPoints(20);
+ const geo = new T.BufferGeometry().setFromPoints(curvePoints);
+ const mat = new T.LineBasicMaterial({ color: 0x444466, transparent: true, opacity: 0.5 });
+ const line = new T.Line(geo, mat);
+ this._scene.add(line);
+ this._objects.push({ mesh: line, id: this._genId(), label: 'Edge' });
+ }
+
+ if (nodeMeshes.length > 0) {
+ const box = new T.Box3().setFromObjects(nodeMeshes);
+ const center = box.getCenter(new T.Vector3());
+ const size = box.getSize(new T.Vector3());
+ const maxDim = Math.max(size.x, size.y, size.z);
+ const dist = maxDim * 1.5 + 5;
+ this._camera.position.set(center.x + dist * 0.5, center.y + dist * 0.3, center.z + dist);
+ this._controls.target.copy(center);
+ this._controls.update();
+ }
+
+ this.terminal.print(`🧊 3D mindmap rendered: ${nodes.length} nodes, ${edges.length} edges`, 'success');
+ }
+
+ // ── Presets ──
+
+ _presetSolarSystem() {
+ if (!this._scene) this._createScene();
+ this._showContainer(this._mode);
+ this._clearScene();
+ const T = this._THREE;
+
+ const sunGeo = new T.SphereGeometry(1.5, 32, 32);
+ const sunMat = new T.MeshStandardMaterial({ color: 0xffaa00, emissive: 0xff6600, emissiveIntensity: 0.5 });
+ const sun = new T.Mesh(sunGeo, sunMat);
+ this._scene.add(sun);
+ this._objects.push({ mesh: sun, id: this._genId(), label: 'Sun' });
+
+ const planets = [
+ { name: 'Mercury', radius: 0.15, orbit: 2.5, color: 0xaaaaaa, speed: 0.02 },
+ { name: 'Venus', radius: 0.25, orbit: 4, color: 0xffcc88, speed: 0.015 },
+ { name: 'Earth', radius: 0.3, orbit: 5.5, color: 0x4488ff, speed: 0.01 },
+ { name: 'Mars', radius: 0.2, orbit: 7, color: 0xff6644, speed: 0.008 },
+ { name: 'Jupiter', radius: 0.6, orbit: 9, color: 0xddbb88, speed: 0.005 },
+ { name: 'Saturn', radius: 0.5, orbit: 11, color: 0xeeddbb, speed: 0.003 },
+ ];
+ for (const p of planets) {
+ const geo = new T.SphereGeometry(p.radius, 16, 16);
+ const mat = new T.MeshStandardMaterial({ color: p.color });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.position.x = p.orbit;
+ mesh.userData = { orbit: p.orbit, speed: p.speed, angle: Math.random() * Math.PI * 2 };
+ this._scene.add(mesh);
+ this._objects.push({ mesh, id: this._genId(), label: p.name, _animate: true, _data: mesh.userData });
+
+ const ringPoints = [];
+ for (let i = 0; i <= 64; i++) {
+ const a = (i / 64) * Math.PI * 2;
+ ringPoints.push(new T.Vector3(Math.cos(a) * p.orbit, 0, Math.sin(a) * p.orbit));
+ }
+ const ringGeo = new T.BufferGeometry().setFromPoints(ringPoints);
+ const ringMat = new T.LineBasicMaterial({ color: 0x333355, transparent: true, opacity: 0.3 });
+ const ring = new T.Line(ringGeo, ringMat);
+ this._scene.add(ring);
+ this._objects.push({ mesh: ring, id: this._genId(), label: 'Orbit: ' + p.name });
+ }
+ this.terminal.print('🌌 Solar system preset loaded', 'success');
+ }
+
+ _presetGrid(args) {
+ if (!this._scene) this._createScene();
+ this._showContainer(this._mode);
+ this._clearScene();
+ const T = this._THREE;
+ const size = parseInt(args[0]) || 5;
+ const spacing = parseFloat(args[1]) || 2;
+
+ for (let x = -size; x <= size; x++) {
+ for (let z = -size; z <= size; z++) {
+ const geo = new T.BoxGeometry(0.3, 0.3, 0.3);
+ const hue = ((x + size) / (size * 2)) * 0.6 + 0.5;
+ const color = new T.Color().setHSL(hue, 0.8, 0.5);
+ const mat = new T.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 0.1 });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.position.set(x * spacing, 0, z * spacing);
+ this._scene.add(mesh);
+ this._objects.push({ mesh, id: this._genId(), label: `Grid (${x},${z})` });
+ }
+ }
+ this.terminal.print(`📐 Grid preset: ${(size*2+1)**2} cubes`, 'success');
+ }
+
+ // ── Export ──
+
+ _export(args) {
+ if (!this._scene) return this.terminal.print('No scene to export', 'warning');
+ const rawName = args[0] || 'three-scene';
+ const name = rawName.replace(/[<>&"']/g, function(c) { return {'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;',"'":'&#39;'}[c]; });
+ const T = this._THREE;
+
+ const objects = [];
+ for (const obj of this._objects) {
+ const m = obj.mesh;
+ if (m.type === 'Mesh' || m.type === 'Points' || m.type === 'Sprite' || m.type === 'Line') {
+ const hexColor = m.material && m.material.color ? m.material.color.getHex() : null;
+ objects.push({
+ type: m.type, id: obj.id, label: obj.label,
+ position: m.position.toArray(), rotation: m.rotation.toArray(), scale: m.scale.toArray(),
+ color: hexColor
+ });
+ }
+ }
+
+ const cameraPos = this._camera ? this._camera.position.toArray() : [0, 5, 15];
+ const bgColor = '#' + new T.Color(this._backgroundColor).getHexString();
+
+ const html = `<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>3D Scene: ${name}</title>
+ <script src="three.min.js"></script>
+ <script src="OrbitControls.js"></script>
+ <style>
+ * { margin: 0; padding: 0; box-sizing: border-box; }
+ body { background: ${bgColor}; overflow: hidden; font-family: monospace; }
+ #info {
+ position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
+ color: #888; font-size: 12px; background: rgba(0,0,0,0.6);
+ padding: 8px 16px; border-radius: 8px; pointer-events: none;
+ border: 1px solid #333;
+ }
+ </style>
+</head>
+<body>
+ <div id="info">${name} — ${objects.length} objects — Drag to orbit, scroll to zoom</div>
+ <script>
+ const scene = new THREE.Scene();
+ scene.background = new THREE.Color('${bgColor}');
+ const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 10000);
+ camera.position.set(${cameraPos.join(', ')});
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+ document.body.appendChild(renderer.domElement);
+ const controls = new THREE.OrbitControls(camera, renderer.domElement);
+ controls.enableDamping = true;
+ scene.add(new THREE.AmbientLight(0x404060));
+ const dl = new THREE.DirectionalLight(0xffffff, 1);
+ dl.position.set(10, 20, 10);
+ scene.add(dl);
+ const fl = new THREE.DirectionalLight(0x8888ff, 0.4);
+ fl.position.set(-10, 0, -10);
+ scene.add(fl);
+ scene.add(new THREE.GridHelper(30, 20, 0x00ff88, 0x333355));
+ const data = ${JSON.stringify(objects)};
+ data.forEach(function(d) {
+ var mesh;
+ if (d.type === 'Mesh') {
+ var geo = new THREE.SphereGeometry(0.5, 16, 16);
+ var mat = new THREE.MeshStandardMaterial({ color: d.color != null ? d.color : 0x818cf8 });
+ mesh = new THREE.Mesh(geo, mat);
+ } else if (d.type === 'Points') {
+ var geo = new THREE.BufferGeometry();
+ var pos = new Float32Array(500 * 3);
+ for (var i = 0; i < 1500; i++) pos[i] = (Math.random() - 0.5) * 40;
+ geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
+ var mat = new THREE.PointsMaterial({ color: d.color != null ? d.color : 0x00ff88, size: 0.15, transparent: true });
+ mesh = new THREE.Points(geo, mat);
+ } else if (d.type === 'Sprite') {
+ var canvas = document.createElement('canvas');
+ canvas.width = 512; canvas.height = 96;
+ var ctx = canvas.getContext('2d');
+ ctx.font = 'bold 36px monospace';
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
+ ctx.fillStyle = d.color != null ? '#' + d.color.toString(16).padStart(6, '0') : '#22d3ee';
+ ctx.fillText(d.label || '?', 256, 48);
+ var tex = new THREE.CanvasTexture(canvas);
+ var mat = new THREE.SpriteMaterial({ map: tex, transparent: true });
+ mesh = new THREE.Sprite(mat);
+ } else { return; }
+ mesh.position.set(d.position[0], d.position[1], d.position[2]);
+ if (d.rotation) mesh.rotation.set(d.rotation[0], d.rotation[1], d.rotation[2]);
+ if (d.scale) mesh.scale.set(d.scale[0], d.scale[1], d.scale[2]);
+ scene.add(mesh);
+ });
+ function animate() {
+ requestAnimationFrame(animate);
+ controls.update();
+ renderer.render(scene, camera);
+ }
+ animate();
+ window.addEventListener('resize', function() {
+ camera.aspect = window.innerWidth / window.innerHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ });
+ </script>
+</body>
+</html>`;
+
+ try {
+ const fs = this.terminal.fs;
+ fs.mkdirRecursive('/exports');
+ fs.writeFile('/exports/' + name + '.html', html);
+ this.terminal.print(`💾 Exported to /exports/${name}.html (${(html.length / 1024).toFixed(1)}KB)`, 'success');
+ } catch (e) {
+ this.terminal.print('Failed to save export: ' + e.message, 'error');
+ }
+ }
+
+ // ── Scene helpers ──
+
+ _clearScene() {
+ if (!this._scene) return;
+ this._disposeAllObjects();
+ }
+
+ _listObjects() {
+ if (this._objects.length === 0) return this.terminal.print('No objects in scene', 'info');
+ this.terminal.print(`=== 3D Objects (${this._objects.length}) ===`, 'header');
+ for (const obj of this._objects) {
+ const pos = obj.mesh.position;
+ this.terminal.print(` ${obj.id} ${obj.label} @ (${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)})`);
+ }
+ }
+
+ _removeObject(id) {
+ const idx = this._objects.findIndex(o => o.id === id);
+ if (idx === -1) return this.terminal.print('Object not found: ' + id, 'error');
+ const obj = this._objects[idx];
+ this._scene.remove(obj.mesh);
+ if (obj.mesh.geometry) obj.mesh.geometry.dispose();
+ if (obj.mesh.material) {
+ const mats = Array.isArray(obj.mesh.material) ? obj.mesh.material : [obj.mesh.material];
+ mats.forEach(m => {
+ if (m.map) m.map.dispose();
+ m.dispose();
+ });
+ }
+ this._objects.splice(idx, 1);
+ this.terminal.print(`Removed ${id}`, 'success');
+ }
+
+ _setBgColor(args) {
+ if (!args[0]) return this.terminal.print('Usage: three bgcolor <hex> (e.g. #0a0a14)', 'warning');
+ const color = parseInt(args[0].replace('#', ''), 16);
+ if (isNaN(color)) return this.terminal.print('Invalid color', 'warning');
+ this._backgroundColor = color;
+ if (this._scene) this._scene.background = new this._THREE.Color(color);
+ this.terminal.print(`Background set to ${args[0]}`, 'success');
+ }
+
+ // ── Main dispatch ──
+
+ async _dispatch(args) {
+ if (!args[0] || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
+ this._printHelp();
+ return;
+ }
+
+ const loaded = await this._ensureLoaded();
+ if (!loaded && args[0] !== 'help') return;
+
+ const sub = args[0];
+ const rest = args.slice(1);
+
+ switch (sub) {
+ case 'scene':
+ if (!this._scene) this._createScene();
+ this._mode = 'top';
+ this._showContainer('top');
+ this.terminal.print('🎨 3D scene — top panel mode. Use three fullscreen for full view, Esc to close.', 'success');
+ break;
+ case 'fullscreen':
+ if (!this._scene) this._createScene();
+ this._mode = 'full';
+ this._showContainer('full');
+ this.terminal.print('🎨 3D scene — fullscreen mode. Terminal floats on top. Esc to close.', 'success');
+ break;
+ case 'mode': {
+ const mode = rest[0];
+ if (!mode || (mode !== 'top' && mode !== 'full')) {
+ return this.terminal.print('Usage: three mode top|full (current: ' + this._mode + ')', 'warning');
+ }
+ if (!this._scene) return this.terminal.print('No scene. Create one with three scene first.', 'warning');
+ this._mode = mode;
+ this._showContainer(mode);
+ this.terminal.print(`Switched to ${mode} panel mode`, 'success');
+ break;
+ }
+ case 'add': {
+ const type = rest[0];
+ const params = rest.slice(1);
+ if (!type) return this.terminal.print('Usage: three add box|sphere|torus|text|particles|line [params...]', 'warning');
+ if (!this._scene) this._createScene();
+ if (!this._visible) this._showContainer(this._mode);
+ switch (type) {
+ case 'box': this._addBox(params); break;
+ case 'sphere': this._addSphere(params); break;
+ case 'torus': this._addTorus(params); break;
+ case 'text': this._addText(params); break;
+ case 'particles': this._addParticles(params); break;
+ case 'line': this._addLine(params); break;
+ default: this.terminal.print('Unknown type: ' + type + '. Try: box, sphere, torus, text, particles, line', 'warning');
+ }
+ break;
+ }
+ case 'camera': {
+ if (rest.length < 3) return this.terminal.print('Usage: three camera <x> <y> <z>', 'warning');
+ if (!this._camera) return this.terminal.print('No scene. Create one with three scene first.', 'warning');
+ this._camera.position.set(parseFloat(rest[0]), parseFloat(rest[1]), parseFloat(rest[2]));
+ this._controls.update();
+ this.terminal.print(`Camera moved to (${rest[0]}, ${rest[1]}, ${rest[2]})`, 'success');
+ break;
+ }
+ case 'orbit': {
+ if (!this._controls) return this.terminal.print('No scene.', 'warning');
+ this._orbitEnabled = !this._orbitEnabled;
+ this._controls.enabled = this._orbitEnabled;
+ this.terminal.print(`Orbit controls: ${this._orbitEnabled ? 'ON' : 'OFF'}`, 'info');
+ break;
+ }
+ case 'clear':
+ this._clearScene();
+ this.terminal.print('Scene cleared', 'success');
+ break;
+ case 'list':
+ this._listObjects();
+ break;
+ case 'remove':
+ if (!rest[0]) return this.terminal.print('Usage: three remove <id>', 'warning');
+ this._removeObject(rest[0]);
+ break;
+ case 'pos':
+ if (rest.length < 4) return this.terminal.print('Usage: three pos <id> <x> <y> <z>', 'warning');
+ this._setPosition(rest[0], parseFloat(rest[1]), parseFloat(rest[2]), parseFloat(rest[3]));
+ break;
+ case 'mindmap':
+ this._renderMindmap(rest);
+ break;
+ case 'export':
+ this._export(rest);
+ break;
+ case 'preset': {
+ const preset = rest[0] || 'solar';
+ if (!this._scene) this._createScene();
+ if (!this._visible) this._showContainer(this._mode);
+ if (preset === 'solar') this._presetSolarSystem();
+ else if (preset === 'grid') this._presetGrid(rest.slice(1));
+ else this.terminal.print('Presets: solar, grid', 'warning');
+ break;
+ }
+ case 'bgcolor':
+ this._setBgColor(rest);
+ break;
+ case 'hide':
+ this._hideContainer();
+ this.terminal.print('3D view closed. Use three scene or three fullscreen to reopen.', 'info');
+ break;
+ default:
+ this.terminal.print('Unknown subcommand: ' + sub + '. Try: three help', 'warning');
+ }
+ }
+
+ _printHelp() {
+ this.terminal.print('=== Three.js 3D Module (v4.5.82) ===', 'header');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Scene ──', 'info');
+ this.terminal.print(' three scene Open 3D in top panel (default)');
+ this.terminal.print(' three fullscreen Open 3D fullscreen with terminal overlay');
+ this.terminal.print(' three mode top|full Switch mode without closing');
+ this.terminal.print(' three hide Close 3D view (or press Esc)');
+ this.terminal.print(' three clear Remove all objects');
+ this.terminal.print(' three bgcolor <hex> Set background color (e.g. #0a0a14)');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Objects ──', 'info');
+ this.terminal.print(' three add box [size] [color] Add a box');
+ this.terminal.print(' three add sphere [r] [color] Add a sphere');
+ this.terminal.print(' three add torus [r] [tube] [c] Add a torus');
+ this.terminal.print(' three add text <text> [color] Add 2D text sprite');
+ this.terminal.print(' three add particles [n] [c] Add particle system');
+ this.terminal.print(' three add line x1 y1 z1 x2 y2 z2 [color] Add a line');
+ this.terminal.print(' three list List all objects');
+ this.terminal.print(' three remove <id> Remove an object');
+ this.terminal.print(' three pos <id> <x> <y> <z> Move an object');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Camera & Controls ──', 'info');
+ this.terminal.print(' three camera <x> <y> <z> Set camera position');
+ this.terminal.print(' three orbit Toggle orbit controls');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Mindmap ──', 'info');
+ this.terminal.print(' three mindmap [name] Render current mindmap in 3D');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Presets ──', 'info');
+ this.terminal.print(' three preset solar Solar system');
+ this.terminal.print(' three preset grid [n] [spc] Color grid of cubes');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Export ──', 'info');
+ this.terminal.print(' three export [name] Export scene as standalone HTML');
+ this.terminal.print('', '');
+ this.terminal.print(' ── AI Tools ──', 'info');
+ this.terminal.print(' The AI can create 3D scenes via create_3d_scene tool.');
+ this.terminal.print(' The AI can render mindmaps in 3D via visualize_mindmap_3d tool.');
+ }
+
+ // ── AI Tool Interface ──
+
+ async createSceneFromDescription(description) {
+ const loaded = await this._ensureLoaded();
+ if (!loaded) return 'Failed to load Three.js';
+ if (!this._scene) this._createScene();
+ this._showContainer(this._mode);
+ this._clearScene();
+
+ const T = this._THREE;
+ const desc = description.toLowerCase();
+ const results = [];
+
+ const boxMatch = desc.match(/(\d+)?\s*box(?:es)?/);
+ if (boxMatch) {
+ const count = parseInt(boxMatch[1]) || 1;
+ for (let i = 0; i < count; i++) {
+ const id = this._addBox(['0.8', '#818cf8']);
+ const angle = (i / count) * Math.PI * 2;
+ this._setPosition(id, Math.cos(angle) * 3, 0, Math.sin(angle) * 3);
+ results.push('box ' + id);
+ }
+ }
+
+ const sphereMatch = desc.match(/(\d+)?\s*sphere(?:s)?/);
+ if (sphereMatch) {
+ const count = parseInt(sphereMatch[1]) || 1;
+ for (let i = 0; i < count; i++) {
+ const id = this._addSphere(['0.6', '#34d399']);
+ const angle = (i / count) * Math.PI * 2 + 0.5;
+ this._setPosition(id, Math.cos(angle) * 3, 1, Math.sin(angle) * 3);
+ results.push('sphere ' + id);
+ }
+ }
+
+ if (desc.includes('particles') || desc.includes('stars') || desc.includes('starfield')) {
+ this._addParticles(['800', '#00ff88']);
+ results.push('particles');
+ }
+
+ const textMatch = desc.match(/text["']?[:]?\s*["']?([^"']+)["']?/);
+ if (textMatch) {
+ this._addText([textMatch[1].trim(), '#22d3ee']);
+ results.push('text');
+ }
+
+ if (desc.includes('torus') || desc.includes('donut') || desc.includes('ring')) {
+ this._addTorus(['1.2', '0.3', '#e879f9']);
+ results.push('torus');
+ }
+
+ if (desc.includes('solar') || desc.includes('planet')) {
+ this._presetSolarSystem();
+ return 'Created solar system preset';
+ }
+
+ if (desc.includes('grid')) {
+ this._presetGrid(['4', '2']);
+ return 'Created grid preset';
+ }
+
+ if (results.length === 0) {
+ this._addBox(['1', '#818cf8']);
+ this._addSphere(['0.6', '#34d399']);
+ this._setPosition('obj_1', -2, 0, 0);
+ this._setPosition('obj_2', 2, 0, 0);
+ results.push('default scene');
+ }
+
+ return 'Created 3D scene with: ' + results.join(', ');
+ }
+
+ async visualizeMindmap3D(name) {
+ const loaded = await this._ensureLoaded();
+ if (!loaded) return 'Failed to load Three.js';
+ const mm = this.terminal.moduleManager.modules['MindmapModule'];
+ if (!mm) return 'MindmapModule not loaded';
+ const cur = mm._ensureMindmap();
+ if (!cur) return 'No mindmap active. Create one first.';
+ this._renderMindmap(name ? [name] : []);
+ const count = this._objects.filter(o => o.label.startsWith('Mindmap:')).length;
+ return `Rendered mindmap "${cur.root.text}" in 3D: ${count} nodes`;
+ }
+}
+
+// Register
+if (typeof window.webx11 !== 'undefined' && window.webx11) {
+ webx11.moduleManager.registerModule(new ThreeModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/three.js');
+}