diff options
Diffstat (limited to 'modules/core')
| -rw-r--r-- | modules/core/core-bundle.js | 61 | ||||
| -rw-r--r-- | modules/core/module-aiasm.js | 17 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 42 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 |
4 files changed, 92 insertions, 30 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index 2b95828..69b5e1a 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -10496,6 +10496,28 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re _escapeHtml(str) { return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); } + // Tool-call args are whatever the model sent: often a JSON string, but + // undefined when the call carried no arguments and an object when the + // caller passed one directly. JSON.stringify(undefined) is undefined, + // so the old inline `JSON.stringify(x).substring(...)` threw a + // TypeError inside the debug panel. Never throw here. + _formatToolArgs(args, max) { + let s; + if (typeof args === 'string') { + s = args; + } else { + try { s = JSON.stringify(args); } catch (_e) { s = String(args); } + if (s === undefined || s === null) s = 'null'; + } + const cap = (typeof max === 'number' && max > 0) ? max : 80; + return s.substring(0, cap); + } + // Path arguments are schema-typed as strings, but a model can send a + // number/array/object anyway. Coerce instead of letting .replace() / + // .startsWith() throw "originalPath.replace is not a function". + _pathArg(v) { + return typeof v === 'string' ? v : (v == null ? '' : String(v)); + } _updateDebugPanel() { if (!this._debugPanelEnabled) return; const panel = document.getElementById('debug-ai-panel'); @@ -10505,7 +10527,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re 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); + const argsStr = this._formatToolArgs(entry.args, 60); toolHtml += '<div class="debug-entry ' + statusClass + '">' + '<span class="debug-turn">#' + entry.turn + '</span> ' + '<span class="debug-name">' + this._escapeHtml(entry.name) + '</span> ' + @@ -10544,7 +10566,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re 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>🔧 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 t=m._toolCallLog.map(function(e){return\'#\'+e.turn+\' \'+e.name+\' (\'+e.duration+\'s)\'+(e.error?\' ⚠️\':\'\')+\'\\n\'+m._formatToolArgs(e.args,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>' + @@ -11518,7 +11540,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re a = typeof argsStr === 'object' ? argsStr : (argsStr ? JSON.parse(argsStr) : {}); switch (name) { case 'list_files': { - const originalPath = a.path || ''; + const originalPath = this._pathArg(a.path); const p = originalPath.replace(/^\//, '').replace(/\/+$/, ''); const fsapi = this.terminal.fs; const saved = [...fsapi.currentPath]; @@ -11535,7 +11557,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re return JSON.stringify(entries.map(f => ({ name: f.name, type: f.type, size: f.size }))); } case 'read_file': { - const rp = a.path || ''; + const rp = this._pathArg(a.path); const savedR = [...this.terminal.fs.currentPath]; if (rp.startsWith('/')) this.terminal.fs.currentPath = []; const path = rp.replace(/^\//, ''); @@ -11574,7 +11596,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re }); } case 'write_file': { - const wp = a.path || ''; + const wp = this._pathArg(a.path); const savedW = [...this.terminal.fs.currentPath]; if (wp.startsWith('/')) this.terminal.fs.currentPath = []; const path = wp.replace(/^\//, ''); @@ -11593,7 +11615,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re return `File written: ${wp} (${content.length} chars)`; } case 'make_directory': { - const mp = a.path || ''; + const mp = this._pathArg(a.path); const savedM = [...this.terminal.fs.currentPath]; if (mp.startsWith('/')) this.terminal.fs.currentPath = []; const path = mp.replace(/^\//, ''); @@ -11603,7 +11625,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re return JSON.stringify({ error: `Cannot create: ${path}` }); } case 'delete_file': { - const dp = a.path || ''; + const dp = this._pathArg(a.path); const savedD = [...this.terminal.fs.currentPath]; if (dp.startsWith('/')) this.terminal.fs.currentPath = []; const path = dp.replace(/^\//, ''); @@ -12523,7 +12545,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re if (!code) return JSON.stringify({ error: `No local edit for ${modName}` }); // Save current module state for rollback const oldInstance = this.terminal.moduleManager.modules[modName]; - const oldCommands = oldInstance ? Object.keys(oldInstance.commands) : []; + const oldCommands = (oldInstance && oldInstance.commands) ? Object.keys(oldInstance.commands) : []; // Load new version const ok = await this.terminal.moduleManager.loadModuleFromCode(code, `blob://_mods/${modName}.js`); if (!ok) { @@ -12538,7 +12560,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re } // Verify commands registered const newInstance = this.terminal.moduleManager.modules[modName]; - const newCommands = newInstance ? Object.keys(newInstance.commands) : []; + const newCommands = (newInstance && newInstance.commands) ? Object.keys(newInstance.commands) : []; const lostCommands = oldCommands.filter(c => !newCommands.includes(c)); if (lostCommands.length > 0) { if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance; @@ -13247,7 +13269,7 @@ maxFetchChars: this.maxFetchChars, 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); + var argsStr = this._formatToolArgs(e.args, 80); out += ' #' + e.turn + ' ' + e.name + '(' + argsStr + ') ' + e.duration + 's' + err + '\n'; } out += '── End ──'; @@ -14113,11 +14135,20 @@ maxFetchChars: this.maxFetchChars, if (!args[0]) { this.terminal.print("Usage: webxai <your-question>", "warning"); return; } - if (this.terminal.moduleManager.modules['WebX11AIAssistantModule']) { - this.terminal.moduleManager.modules['WebX11AIAssistantModule'].chat(args); - } else { - this.terminal.ollamaManager.chat(args.join(' ')); - } + const ai = this.terminal.moduleManager.modules['WebX11AIAssistantModule']; + let p; + try { + p = ai ? ai.chat(args) : this.terminal.ollamaManager.chat(args.join(' ')); + } catch (e) { + // Synchronous throw — report it through the same path as a rejection. + p = Promise.reject(e); + } + // Chat-lock timeouts are normal UX (user sent twice). Print, don't let the rejection + // reach window.unhandledrejection → debug-supervisor auto-report. chat() releases its + // own lock in finally, so nothing is cleared here. + return Promise.resolve(p).catch((e) => { + this.terminal.print('AI error: ' + (e && e.message ? e.message : e), 'error'); + }); } } diff --git a/modules/core/module-aiasm.js b/modules/core/module-aiasm.js index 60e1757..34c1582 100644 --- a/modules/core/module-aiasm.js +++ b/modules/core/module-aiasm.js @@ -17,10 +17,19 @@ if (!args[0]) { this.terminal.print("Usage: webxai <your-question>", "warning"); return; } - if (this.terminal.moduleManager.modules['WebX11AIAssistantModule']) { - this.terminal.moduleManager.modules['WebX11AIAssistantModule'].chat(args); - } else { - this.terminal.ollamaManager.chat(args.join(' ')); + const ai = this.terminal.moduleManager.modules['WebX11AIAssistantModule']; + let p; + try { + p = ai ? ai.chat(args) : this.terminal.ollamaManager.chat(args.join(' ')); + } catch (e) { + // Synchronous throw — report it through the same path as a rejection. + p = Promise.reject(e); } + // Chat-lock timeouts are normal UX (user sent twice). Print, don't let the rejection + // reach window.unhandledrejection → debug-supervisor auto-report. chat() releases its + // own lock in finally, so nothing is cleared here. + return Promise.resolve(p).catch((e) => { + this.terminal.print('AI error: ' + (e && e.message ? e.message : e), 'error'); + }); } } diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index a3ffb6f..fb4329f 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -1551,6 +1551,28 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re _escapeHtml(str) { return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); } + // Tool-call args are whatever the model sent: often a JSON string, but + // undefined when the call carried no arguments and an object when the + // caller passed one directly. JSON.stringify(undefined) is undefined, + // so the old inline `JSON.stringify(x).substring(...)` threw a + // TypeError inside the debug panel. Never throw here. + _formatToolArgs(args, max) { + let s; + if (typeof args === 'string') { + s = args; + } else { + try { s = JSON.stringify(args); } catch (_e) { s = String(args); } + if (s === undefined || s === null) s = 'null'; + } + const cap = (typeof max === 'number' && max > 0) ? max : 80; + return s.substring(0, cap); + } + // Path arguments are schema-typed as strings, but a model can send a + // number/array/object anyway. Coerce instead of letting .replace() / + // .startsWith() throw "originalPath.replace is not a function". + _pathArg(v) { + return typeof v === 'string' ? v : (v == null ? '' : String(v)); + } _updateDebugPanel() { if (!this._debugPanelEnabled) return; const panel = document.getElementById('debug-ai-panel'); @@ -1560,7 +1582,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re 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); + const argsStr = this._formatToolArgs(entry.args, 60); toolHtml += '<div class="debug-entry ' + statusClass + '">' + '<span class="debug-turn">#' + entry.turn + '</span> ' + '<span class="debug-name">' + this._escapeHtml(entry.name) + '</span> ' + @@ -1599,7 +1621,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re 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>🔧 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 t=m._toolCallLog.map(function(e){return\'#\'+e.turn+\' \'+e.name+\' (\'+e.duration+\'s)\'+(e.error?\' ⚠️\':\'\')+\'\\n\'+m._formatToolArgs(e.args,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>' + @@ -2573,7 +2595,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re a = typeof argsStr === 'object' ? argsStr : (argsStr ? JSON.parse(argsStr) : {}); switch (name) { case 'list_files': { - const originalPath = a.path || ''; + const originalPath = this._pathArg(a.path); const p = originalPath.replace(/^\//, '').replace(/\/+$/, ''); const fsapi = this.terminal.fs; const saved = [...fsapi.currentPath]; @@ -2590,7 +2612,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re return JSON.stringify(entries.map(f => ({ name: f.name, type: f.type, size: f.size }))); } case 'read_file': { - const rp = a.path || ''; + const rp = this._pathArg(a.path); const savedR = [...this.terminal.fs.currentPath]; if (rp.startsWith('/')) this.terminal.fs.currentPath = []; const path = rp.replace(/^\//, ''); @@ -2629,7 +2651,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re }); } case 'write_file': { - const wp = a.path || ''; + const wp = this._pathArg(a.path); const savedW = [...this.terminal.fs.currentPath]; if (wp.startsWith('/')) this.terminal.fs.currentPath = []; const path = wp.replace(/^\//, ''); @@ -2648,7 +2670,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re return `File written: ${wp} (${content.length} chars)`; } case 'make_directory': { - const mp = a.path || ''; + const mp = this._pathArg(a.path); const savedM = [...this.terminal.fs.currentPath]; if (mp.startsWith('/')) this.terminal.fs.currentPath = []; const path = mp.replace(/^\//, ''); @@ -2658,7 +2680,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re return JSON.stringify({ error: `Cannot create: ${path}` }); } case 'delete_file': { - const dp = a.path || ''; + const dp = this._pathArg(a.path); const savedD = [...this.terminal.fs.currentPath]; if (dp.startsWith('/')) this.terminal.fs.currentPath = []; const path = dp.replace(/^\//, ''); @@ -3578,7 +3600,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re if (!code) return JSON.stringify({ error: `No local edit for ${modName}` }); // Save current module state for rollback const oldInstance = this.terminal.moduleManager.modules[modName]; - const oldCommands = oldInstance ? Object.keys(oldInstance.commands) : []; + const oldCommands = (oldInstance && oldInstance.commands) ? Object.keys(oldInstance.commands) : []; // Load new version const ok = await this.terminal.moduleManager.loadModuleFromCode(code, `blob://_mods/${modName}.js`); if (!ok) { @@ -3593,7 +3615,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re } // Verify commands registered const newInstance = this.terminal.moduleManager.modules[modName]; - const newCommands = newInstance ? Object.keys(newInstance.commands) : []; + const newCommands = (newInstance && newInstance.commands) ? Object.keys(newInstance.commands) : []; const lostCommands = oldCommands.filter(c => !newCommands.includes(c)); if (lostCommands.length > 0) { if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance; @@ -4302,7 +4324,7 @@ maxFetchChars: this.maxFetchChars, 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); + var argsStr = this._formatToolArgs(e.args, 80); out += ' #' + e.turn + ' ' + e.name + '(' + argsStr + ') ' + e.duration + 's' + err + '\n'; } out += '── End ──'; diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 685b5fd..43c7e06 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 = 'v4.6.157'; + const LATEST_VER = 'v4.6.158'; const CACHE = '?v=' + LATEST_VER; // Exposed so ext modules can register their own URL with the same cache-busting version // instead of an unversioned path the browser may already have cached (shell.js did). |