aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--main.html9
-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
-rw-r--r--modules/ext/debug-panel.js167
-rw-r--r--packages.json10
7 files changed, 347 insertions, 31 deletions
diff --git a/main.html b/main.html
index a7f6761..fc1b4dd 100644
--- a/main.html
+++ b/main.html
@@ -915,6 +915,7 @@
<button class="qa-btn" onclick="webx11.openConfigManager()">โš™ Config</button>
<button class="qa-btn" onclick="webx11.executeCommand('clear')">๐Ÿงน Clear</button>
<button class="qa-btn" id="voice-toggle-btn" onclick="webx11.toggleMicVisibility()">๐ŸŽค Mic</button>
+ <button class="qa-btn" onclick="webx11.toggleDebugPanel()">๐Ÿž Debug</button>
<button class="qa-btn" id="sound-toggle-btn">๐Ÿ”Š Sound</button>
</div>
@@ -1279,12 +1280,12 @@
}
</script>
- <!-- v4.5.55: Core modules + critical ext modules loaded synchronously as bundles to avoid 503 request storms -->
-<script src="modules/core/core-bundle.js?v=2026-06-15-v4.5.55"></script>
-<script src="modules/ext/critical-ext-bundle.js?v=2026-06-15-v4.5.55"></script>
+ <!-- v4.5.61: vaultmind_query passes Bearer token from localStorage -->
+<script src="modules/core/core-bundle.js?v=2026-06-16-v4.5.61"></script>
+<script src="modules/ext/critical-ext-bundle.js?v=2026-06-16-v4.5.61"></script>
<!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules -->
-<script src="modules/core/module-loader.js?v=2026-06-15-v4.5.55"></script>
+<script src="modules/core/module-loader.js?v=2026-06-16-v4.5.61"></script>
<script>
// โ•โ•โ• ONE TRUE VERSION SOURCE โ•โ•โ•
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) {
diff --git a/modules/ext/debug-panel.js b/modules/ext/debug-panel.js
new file mode 100644
index 0000000..8c37253
--- /dev/null
+++ b/modules/ext/debug-panel.js
@@ -0,0 +1,167 @@
+class DebugPanel {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ this._logs = [];
+ this._original = {};
+ this._panel = null;
+ this._body = null;
+ this._visible = false;
+ this.MAX_LOGS = 500;
+ }
+ requirements() {
+ return { met: true, missing: [] };
+ }
+ getHelpInfo() {
+ return { 'debug': 'debug โ€” Toggle the debug console panel' };
+ }
+ registerCommands() {
+ this.commands['debug'] = () => this.toggle();
+ }
+ seed() {
+ this._intercept();
+ }
+ init() {}
+ _intercept() {
+ const self = this;
+ // Console
+ for (const m of ['log','warn','error','info']) {
+ this._original[m] = console[m];
+ console[m] = function(...args) {
+ self._add(m, args);
+ self._original[m].apply(console, args);
+ };
+ }
+ // window.onerror
+ this._original.onerror = window.onerror;
+ window.onerror = function(msg, url, line, col, err) {
+ self._add('error', [`${msg} (${url}:${line}:${col})`]);
+ if (self._original.onerror) self._original.onerror(msg, url, line, col, err);
+ };
+ // Unhandled rejections
+ window.addEventListener('unhandledrejection', function(e) {
+ self._add('error', ['Unhandled Promise: ' + (e.reason?.message || e.reason)]);
+ });
+ // Fetch
+ this._original.fetch = window.fetch;
+ window.fetch = function(...args) {
+ const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || '?';
+ const method = (args[1]?.method || 'GET');
+ const start = performance.now();
+ return self._original.fetch.apply(window, args)
+ .then(function(resp) {
+ const ms = (performance.now() - start).toFixed(0);
+ const lvl = resp.status >= 400 ? 'error' : resp.status >= 300 ? 'warn' : 'log';
+ self._add(lvl, ['๐ŸŒ ' + method + ' ' + url + ' โ†’ ' + resp.status + ' (' + ms + 'ms)']);
+ return resp;
+ })
+ .catch(function(err) {
+ const ms = (performance.now() - start).toFixed(0);
+ self._add('error', ['๐ŸŒ ' + method + ' ' + url + ' FAIL (' + ms + 'ms): ' + err.message]);
+ throw err;
+ });
+ };
+ }
+ _add(level, args) {
+ const text = args.map(function(a) {
+ if (typeof a === 'object') try { return JSON.stringify(a, null, 2); } catch(e) { return String(a); }
+ return String(a);
+ }).join(' ');
+ this._logs.push({ level, text, time: new Date().toLocaleTimeString() });
+ if (this._logs.length > this.MAX_LOGS) this._logs.shift();
+ if (this._visible) this._appendEntry(this._logs[this._logs.length - 1]);
+ }
+ toggle() {
+ if (!this._panel) this._build();
+ this._visible = !this._visible;
+ this._panel.style.display = this._visible ? 'flex' : 'none';
+ if (this._visible) {
+ this._body.innerHTML = '';
+ for (const e of this._logs) this._appendEntry(e);
+ this._body.scrollTop = this._body.scrollHeight;
+ }
+ }
+ _build() {
+ const p = document.createElement('div');
+ p.id = 'debug-panel';
+ p.style.cssText = 'position:fixed;bottom:0;left:0;right:0;height:40vh;background:#0d0d12;border-top:2px solid #1e1e2a;z-index:9999;display:none;flex-direction:column;font-family:monospace;font-size:12px;';
+ // Header
+ const h = document.createElement('div');
+ h.style.cssText = 'display:flex;align-items:center;padding:4px 10px;background:#16161e;border-bottom:1px solid #1e1e2a;gap:6px;flex-shrink:0;';
+ h.innerHTML = '<span style="color:#00e5c8;font-weight:bold;font-size:13px;">๐Ÿž Debug Console</span>';
+ // Copy All
+ const ca = document.createElement('button');
+ ca.textContent = '๐Ÿ“‹ Copy All';
+ ca.onclick = () => this._copyAll();
+ ca.style.cssText = 'background:#1e1e2a;color:#e8e8f0;border:1px solid #2a2a3a;padding:2px 10px;border-radius:6px;cursor:pointer;font-size:11px;font-family:inherit;';
+ // Clear
+ const cl = document.createElement('button');
+ cl.textContent = '๐Ÿ—‘ Clear';
+ cl.onclick = () => { this._logs = []; if (this._body) this._body.innerHTML = ''; };
+ cl.style.cssText = ca.style.cssText;
+ // Close
+ const cx = document.createElement('button');
+ cx.textContent = 'โœ•';
+ cx.onclick = () => this.toggle();
+ cx.style.cssText = 'background:none;color:#8888a0;border:none;cursor:pointer;margin-left:auto;font-size:18px;padding:0 4px;';
+ h.appendChild(ca);
+ h.appendChild(cl);
+ h.appendChild(cx);
+ p.appendChild(h);
+ // Body
+ this._body = document.createElement('div');
+ this._body.style.cssText = 'flex:1;overflow-y:auto;padding:4px 10px;background:#111118;';
+ p.appendChild(this._body);
+ document.body.appendChild(p);
+ this._panel = p;
+ }
+ _appendEntry(e) {
+ const colors = { error: '#ff6bcb', warn: '#ffa500', log: '#00e5c8', info: '#7c6bff' };
+ const line = document.createElement('div');
+ line.style.cssText = 'padding:3px 4px;border-bottom:1px solid #1a1a26;display:flex;align-items:flex-start;gap:6px;';
+ // Badge
+ const b = document.createElement('span');
+ b.textContent = e.level.toUpperCase();
+ b.style.cssText = 'color:' + (colors[e.level] || '#888') + ';font-weight:700;font-size:10px;width:40px;flex-shrink:0;';
+ // Timestamp
+ const ts = document.createElement('span');
+ ts.textContent = e.time;
+ ts.style.cssText = 'color:#555;font-size:10px;width:70px;flex-shrink:0;';
+ // Message
+ const m = document.createElement('span');
+ m.textContent = e.text;
+ m.style.cssText = 'color:#c8c8d0;flex:1;word-break:break-all;white-space:pre-wrap;';
+ // Copy
+ const cp = document.createElement('button');
+ cp.textContent = 'Copy';
+ cp.onclick = function() {
+ var ta = document.createElement('textarea');
+ ta.value = e.text;
+ ta.style.cssText = 'position:fixed;left:-999px;top:0;';
+ document.body.appendChild(ta);
+ ta.select();
+ document.execCommand('copy');
+ document.body.removeChild(ta);
+ cp.textContent = 'โœ“';
+ setTimeout(function() { cp.textContent = 'Copy'; }, 1500);
+ };
+ cp.style.cssText = 'background:#1e1e2a;color:#8888a0;border:1px solid #2a2a3a;border-radius:4px;cursor:pointer;font-size:10px;padding:1px 6px;flex-shrink:0;font-family:inherit;';
+ line.appendChild(b);
+ line.appendChild(ts);
+ line.appendChild(m);
+ line.appendChild(cp);
+ this._body.appendChild(line);
+ this._body.scrollTop = this._body.scrollHeight;
+ }
+ _copyAll() {
+ const text = this._logs.map(function(e) { return '[' + e.level.toUpperCase() + '] ' + e.time + ' ' + e.text; }).join('\n');
+ var ta = document.createElement('textarea');
+ ta.value = text;
+ ta.style.cssText = 'position:fixed;left:-999px;top:0;';
+ document.body.appendChild(ta);
+ ta.select();
+ document.execCommand('copy');
+ document.body.removeChild(ta);
+ if (this.terminal) this.terminal.print('๐Ÿ“‹ Debug log copied (' + this._logs.length + ' entries)', 'success');
+ }
+} \ No newline at end of file
diff --git a/packages.json b/packages.json
index 9277701..dd1afbd 100644
--- a/packages.json
+++ b/packages.json
@@ -226,6 +226,16 @@
],
"category": "developer"
},
+ "debug-panel": {
+ "name": "Debug Panel",
+ "version": "1.0.0",
+ "desc": "Toggleable debug console โ€” intercepts console.log/error, fetch failures, JS errors. Copy All + per-line copy.",
+ "url": "modules/ext/debug-panel.js",
+ "commands": [
+ "debug"
+ ],
+ "category": "developer"
+ },
"tutorial": {
"name": "Tutorial",
"version": "1.0.0",