aboutsummaryrefslogtreecommitdiff
path: root/modules/core
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core')
-rw-r--r--modules/core/core-bundle.js168
-rw-r--r--modules/core/module-aiassist.js7
-rw-r--r--modules/core/module-loader.js4
-rw-r--r--modules/core/terminal.js13
4 files changed, 165 insertions, 27 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index 8b73403..24ec5af 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -918,6 +918,13 @@ window.VirtualFileSystem = VirtualFileSystem;
localStorage.setItem('webx11_modules', JSON.stringify(this.moduleRegistry));
} catch (e) { console.error('Module save error:', e); }
}
+ async restorePersistedModules() {
+ for (const entry of this.moduleRegistry) {
+ if (entry.enabled && entry.url) {
+ await this.loadModuleFromURL(entry.url);
+ }
+ }
+ }
async loadModuleFromURL(url) {
try {
this.terminal.print(`Loading module from ${url}...`, 'info');
@@ -927,10 +934,17 @@ window.VirtualFileSystem = VirtualFileSystem;
const ModuleClass = this.createModuleClass(code, url);
if (ModuleClass) {
const instance = new ModuleClass();
- this.registerModule(instance, url);
if (instance.init) {
- await instance.init(this.terminal);
+ try {
+ await instance.init(this.terminal);
+ instance._initComplete = true;
+ } catch (e) {
+ console.error(`[ModuleManager] init() failed for ${instance.constructor.name}:`, e);
+ this.terminal.print(`Module '${instance.constructor.name}' init() failed: ${e.message}`, 'error');
+ return false;
+ }
}
+ this.registerModule(instance, url);
this.terminal.print(`Module loaded successfully from ${url}`, 'success');
return true;
}
@@ -943,6 +957,13 @@ window.VirtualFileSystem = VirtualFileSystem;
try {
// Strip leading comments to avoid ASI: `return\n/**` → `return; /**`
const cleanCode = code.replace(/^\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*\s*/, '');
+ // Try wrapper convention first: `return class MyModule { ... };`
+ const wrapperMatch = cleanCode.match(/return\s+class\s+(\w+)/);
+ if (wrapperMatch) {
+ const mod = new Function(cleanCode)();
+ if (mod && typeof mod === 'function') return mod;
+ }
+ // Fall back to regex class extraction
const classMatch = cleanCode.match(/class\s+(\w+)\s+(extends\s+\S+)?\s*\{/);
if (classMatch) {
const className = classMatch[1];
@@ -959,7 +980,15 @@ window.VirtualFileSystem = VirtualFileSystem;
const name = instance.constructor.name;
this.modules[name] = instance;
instance.terminal = this.terminal;
- if (instance.registerCommands) { instance.registerCommands(); }
+ instance.fs = this.terminal.fs;
+ try {
+ if (instance.registerCommands) { instance.registerCommands(); }
+ } catch (e) {
+ console.error(`[ModuleManager] registerCommands() failed for ${name}:`, e);
+ delete this.modules[name];
+ this.terminal.print(`Module '${name}' registration failed: ${e.message}`, 'error');
+ return;
+ }
// Only persist modules loaded from absolute URLs (not boot-time ext modules)
if (url && url.startsWith('http') && !this.moduleRegistry.find(m => m.name === name)) {
this.moduleRegistry.push({
@@ -979,16 +1008,17 @@ window.VirtualFileSystem = VirtualFileSystem;
}
return false;
}
- loadModuleFromCode(code, url) {
+ async loadModuleFromCode(code, url) {
try {
this.terminal.print('Loading module from editor...', 'info');
const ModuleClass = this.createModuleClass(code, url);
if (ModuleClass) {
const instance = new ModuleClass();
- this.registerModule(instance, url);
if (instance.init) {
- instance.init(this.terminal);
+ await instance.init(this.terminal);
+ instance._initComplete = true;
}
+ this.registerModule(instance, url);
this.terminal.print(`Module loaded from editor: ${instance.constructor.name}`, 'success');
return true;
}
@@ -1054,6 +1084,31 @@ window.VirtualFileSystem = VirtualFileSystem;
}
return help;
}
+ diagnose() {
+ const report = {
+ totalInModules: Object.keys(this.modules).length,
+ totalInRegistry: this.moduleRegistry.length,
+ orphaned: [],
+ missingFs: [],
+ missingTerminal: [],
+ uninitialized: [],
+ healthy: 0
+ };
+ const registryNames = new Set(this.moduleRegistry.map(m => m.name));
+ for (const name in this.modules) {
+ const mod = this.modules[name];
+ if (!registryNames.has(name)) {
+ report.orphaned.push(name);
+ }
+ if (!mod.fs) report.missingFs.push(name);
+ if (!mod.terminal) report.missingTerminal.push(name);
+ if (typeof mod.init === 'function' && !mod._initComplete) {
+ report.uninitialized.push(name);
+ }
+ if (mod.fs && mod.terminal) report.healthy++;
+ }
+ return report;
+ }
}
/* === ollama-manager.js === */
@@ -1915,6 +1970,11 @@ class OllamaManager {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
const text = data.choices?.[0]?.message?.content || '';
+ // Sync provider state after successful non-Ollama fallback
+ if (provider !== this.currentProvider) {
+ this.currentProvider = provider;
+ this._syncCompatProperties(provider);
+ }
return { message: { role: 'assistant', content: text }, tool_calls: null, error: null };
} catch (e) {
if (e.name === 'AbortError') {
@@ -1941,10 +2001,11 @@ ${toolDesc}
</tools>
Only output XML function calls for tool tasks. If no tool is needed, answer normally.` };
- const msgsWithTools = [toolContext, ...messages];
+const msgsWithTools = [toolContext, ...messages];
const body = { model: useModel, messages: msgsWithTools, stream: false, options: { temperature: 0.1 } };
- const response = await fetch(`${this.endpoint}/api/chat`, {
+ const ep = this.currentProvider ? this.currentProvider.endpoint : this.endpoint;
+ const response = await fetch(`${ep}/api/chat`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body), signal
});
@@ -3411,7 +3472,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', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'cryptogzip', 'tutorial', 'floating', 'console'];
+ const autoLoadKeys = filterKeys || ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'cryptogzip', 'tutorial', 'floating', 'console'];
const cacheVer = localStorage.getItem('webx11_cache_ver') || '2026-05-27-14';
let index = null;
let lastErr = null;
@@ -3444,7 +3505,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', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating', 'console'];
+ 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'];
for (const mod of registry) {
if (mod.enabled && mod.url) {
// Skip ext modules — they load via _loadExtModules
@@ -4641,6 +4702,15 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
voiceMod.handle([]);
}
}
+ toggleDebugPanel() {
+ const mod = this.moduleManager.modules['DebugPanel'];
+ if (mod) {
+ mod.terminal = this;
+ mod.toggle();
+ } else {
+ this.print('Debug panel not loaded', 'warning');
+ }
+ }
setFontSize(size) {
const s = parseInt(size);
if (isNaN(s) || s < 8 || s > 32) {
@@ -5909,11 +5979,17 @@ class OllamaModule {
};
this.commands['ai'] = async (args) => {
if(!args[0]) return this.terminal.print("Usage: ai <prompt>", 'warning');
- await this.terminal.ollamaManager.chat(args.join(' '));
+ const result = await this.terminal.ollamaManager.chat(args.join(' '));
+ if (result === null) {
+ this.terminal.print('AI returned no response — check provider connection', 'warning');
+ }
};
this.commands['ask'] = async (args) => {
if(!args[0]) return this.terminal.print("Usage: ask <prompt>", 'warning');
- await this.terminal.ollamaManager.chat(args.join(' '));
+ const result = await this.terminal.ollamaManager.chat(args.join(' '));
+ if (result === null) {
+ this.terminal.print('AI returned no response — check provider connection', 'warning');
+ }
};
// Merged from OllamaModelsModule
this.commands['models'] = () => this.listModels();
@@ -6139,7 +6215,29 @@ class MyModule {
return mm.modules[name];
}
registerCommands() {
- this.commands['modules'] = () => { this.terminal.openModuleManager(); };
+ this.commands['modules'] = (args) => {
+ if (args[0] === '--diagnose' || args[0] === '-d') {
+ const report = this.terminal.moduleManager.diagnose();
+ this.terminal.print(`=== Module Diagnostics ===`, 'info');
+ this.terminal.print(`Total in modules: ${report.totalInModules}`);
+ this.terminal.print(`Total in registry: ${report.totalInRegistry}`);
+ this.terminal.print(`Healthy: ${report.healthy}`);
+ if (report.orphaned.length) {
+ this.terminal.print(`Orphaned (in modules but not registry): ${report.orphaned.join(', ')}`, 'warning');
+ }
+ if (report.missingFs.length) {
+ this.terminal.print(`Missing fs: ${report.missingFs.join(', ')}`, 'error');
+ }
+ if (report.missingTerminal.length) {
+ this.terminal.print(`Missing terminal: ${report.missingTerminal.join(', ')}`, 'error');
+ }
+ if (report.uninitialized.length) {
+ this.terminal.print(`Uninitialized (init() never completed): ${report.uninitialized.join(', ')}`, 'warning');
+ }
+ return;
+ }
+ this.terminal.openModuleManager();
+ };
this.commands['loadmod'] = async (args) => {
if(!args[0]) return this.terminal.print("Usage: loadmod <url>", 'warning');
await this.terminal.moduleManager.loadModuleFromURL(args[0]);
@@ -7187,7 +7285,9 @@ class ServerMonitorModule {
// Extracted from newOS4 index.html — WebX11AIAssistantModule (Full Agent Loop)
class WebX11AIAssistantModule {
constructor() {
- this.commands = {}; this.terminal = null;
+ this.commands = {}; this.commandsMeta = {
+ 'ai-config': { argType: 'enum', enumFn: '_getAiConfigKeys' }
+ }; this.terminal = null;
this.systemPrompt = this.buildSystemPrompt();
this.context = []; this.maxContext = 10;
this.maxContextTokens = 2000;
@@ -7888,6 +7988,16 @@ buildSystemPrompt() {
}
async agenticLoop(messages) {
let toolTurns = 0;
+ // Auto-connect if not connected
+ const om = this.terminal.ollamaManager;
+ if (om && !om.connected) {
+ this.terminal.print('⏳ AI provider not connected — attempting auto-connect...', 'info');
+ const connected = await om.connect();
+ if (!connected) {
+ this.terminal.print('❌ Could not connect to any AI provider.', 'error');
+ return { finalAnswer: null, toolTurns: 0 };
+ }
+ }
while (toolTurns < this.MAX_TOOL_TURNS) {
if (this._cancelled) { this.terminal.print('AI loop cancelled', 'warning'); return { finalAnswer: null, toolTurns }; }
toolTurns++;
@@ -7936,9 +8046,9 @@ buildSystemPrompt() {
const sm = this.terminal.moduleManager.modules['ServerMonitorModule'];
if (sm && sm.logToolCall) sm.logToolCall(fn.name, (t1 * 1000) || 1, this._isToolError(toolResult));
if (this._debugPanelEnabled) this._updateDebugPanel();
- return { fn, toolResult, t1 };
+ return { fn, toolResult, t1, toolCallId: tc.id };
}));
- for (const { fn, toolResult, t1 } of safeResults) {
+ for (const { fn, toolResult, t1, toolCallId } of safeResults) {
this.terminal.print(`🔧 Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info');
// Memory graph: record tool call + file access
try {
@@ -7965,7 +8075,7 @@ buildSystemPrompt() {
this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`);
this.terminal.print(`📊 Result (${t1}s): ${obs}`, 'info');
}
- messages.push({ role: 'tool', content: toolResult });
+ messages.push({ role: 'tool', tool_call_id: toolCallId, content: toolResult });
}
// Only block on destructive tools
if (destructiveTools.length > 0) {
@@ -8027,7 +8137,7 @@ buildSystemPrompt() {
this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`);
this.terminal.print(`📊 Result (${t1}s): ${dObs}`, 'info');
}
- messages.push({ role: 'tool', content: toolResult });
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: toolResult });
}
} else {
messages.push({
@@ -9062,13 +9172,24 @@ buildSystemPrompt() {
return await this.runModuleTool(name, argsStr);
case 'vaultmind_query': {
const ep = a.endpoint;
- const validEps = ['prices', 'portfolio', 'transactions', 'copy-trade', 'chat'];
- if (!validEps.includes(ep)) {
- return JSON.stringify({ error: 'Invalid endpoint. Valid: ' + validEps.join(', ') });
+ const epMap = {
+ 'prices': '/vmai-api/market/prices',
+ 'portfolio': '/vmai-api/portfolio',
+ 'transactions': '/vmai-api/transactions/summary',
+ 'copy-trade': '/vmai-api/copy-trading/start',
+ 'chat': '/vmai-api/chat',
+ };
+ const url = epMap[ep];
+ if (!url) {
+ return JSON.stringify({ error: 'Invalid endpoint. Valid: ' + Object.keys(epMap).join(', ') });
}
try {
- let url = '/vmai/' + ep;
+ // Read VaultMind session token from localStorage (set by VaultMind dashboard)
+ const vmToken = typeof localStorage !== 'undefined' ? localStorage.getItem('vaultmind_token') : null;
let opts = { method: 'GET', headers: { 'Accept': 'application/json' } };
+ if (vmToken) {
+ opts.headers['Authorization'] = 'Bearer ' + vmToken;
+ }
if (ep === 'chat') {
opts.method = 'POST';
opts.headers['Content-Type'] = 'application/json';
@@ -9409,6 +9530,9 @@ buildSystemPrompt() {
this.terminal.print('No models available. Use ollamaconnect first.', 'warning');
}
}
+ _getAiConfigKeys() {
+ return ['max-turns', 'max-context', 'max-context-tokens', 'confirm-timeout', 'chat-lock-timeout', 'skill-debounce', 'trivial-optimization'];
+ }
aiConfig(args) {
if (args.length === 0) {
this.terminal.print('=== AI Config ===', 'info');
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index 44a7574..c5a91c9 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -1,7 +1,9 @@
// Extracted from newOS4 index.html — WebX11AIAssistantModule (Full Agent Loop)
class WebX11AIAssistantModule {
constructor() {
- this.commands = {}; this.terminal = null;
+ this.commands = {}; this.commandsMeta = {
+ 'ai-config': { argType: 'enum', enumFn: '_getAiConfigKeys' }
+ }; this.terminal = null;
this.systemPrompt = this.buildSystemPrompt();
this.context = []; this.maxContext = 10;
this.maxContextTokens = 2000;
@@ -2223,6 +2225,9 @@ buildSystemPrompt() {
this.terminal.print('No models available. Use ollamaconnect first.', 'warning');
}
}
+ _getAiConfigKeys() {
+ return ['max-turns', 'max-context', 'max-context-tokens', 'confirm-timeout', 'chat-lock-timeout', 'skill-debounce', 'trivial-optimization'];
+ }
aiConfig(args) {
if (args.length === 0) {
this.terminal.print('=== AI Config ===', 'info');
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index bade62f..bd02873 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-15-v4.5.55';
+ const LATEST_VER = '2026-06-16-v4.5.61';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
@@ -51,7 +51,7 @@
// Optional ext modules via packages.json
if (typeof webx11._loadExtModules === 'function') {
- let extKeys = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'cryptogzip', 'tutorial', 'floating', 'console'];
+ let extKeys = ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'cryptogzip', 'tutorial', 'floating', 'console'];
const saved = localStorage.getItem('webx11_startup_modules');
let selected = null;
try { if (saved) selected = JSON.parse(saved); } catch (e) {}
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index c416b8b..504e08d 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -1279,7 +1279,7 @@
} catch (e) {}
}
async _loadExtModules(filterKeys) {
- const autoLoadKeys = filterKeys || ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'cryptogzip', 'tutorial', 'floating', 'console'];
+ const autoLoadKeys = filterKeys || ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'cryptogzip', 'tutorial', 'floating', 'console'];
const cacheVer = localStorage.getItem('webx11_cache_ver') || '2026-05-27-14';
let index = null;
let lastErr = null;
@@ -1312,7 +1312,7 @@
}
async reloadPersistedModules() {
const registry = this.moduleManager.moduleRegistry;
- const skipList = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating', 'console'];
+ 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'];
for (const mod of registry) {
if (mod.enabled && mod.url) {
// Skip ext modules — they load via _loadExtModules
@@ -2509,6 +2509,15 @@
voiceMod.handle([]);
}
}
+ toggleDebugPanel() {
+ const mod = this.moduleManager.modules['DebugPanel'];
+ if (mod) {
+ mod.terminal = this;
+ mod.toggle();
+ } else {
+ this.print('Debug panel not loaded', 'warning');
+ }
+ }
setFontSize(size) {
const s = parseInt(size);
if (isNaN(s) || s < 8 || s > 32) {