aboutsummaryrefslogtreecommitdiff
path: root/modules/core
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core')
-rw-r--r--modules/core/core-bundle.js61
-rw-r--r--modules/core/module-aiasm.js17
-rw-r--r--modules/core/module-aiassist.js42
-rw-r--r--modules/core/module-loader.js2
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
+ // 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
+ // 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).