aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/core/module-aiassist.js10
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/ollama-manager.js72
-rw-r--r--modules/core/terminal.js58
-rw-r--r--modules/ext/busybox.js8
5 files changed, 125 insertions, 25 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index 8f8a60c..f77ec61 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -665,17 +665,19 @@ RULES:
}
// Validator: check safe tool results for consistency
if (safeResults.length > 0 && pendingText) {
- const om = this.terminal.ollamaManager;
+ const om = this.terminal.ollamaManager;
if (om && om.validatorEnabled && om.validatorModel) {
- let anyInvalid = false;
+ let invalidTools = [];
for (const { fn, toolResult } of safeResults) {
const valid = await om.validateToolResult(fn.name, fn.arguments, toolResult, pendingText || '');
if (!valid && !this._cancelled) {
- anyInvalid = true;
+ invalidTools.push(fn.name);
this.terminal.print('šŸ” Validator: ' + fn.name + ' result — AI note may not match (logged)', 'info');
- // Just log it — don't force retry (cloud models get 400 on retry)
}
}
+ if (invalidTools.length > 0) {
+ pendingText += '\n\nāš ļø **Validator flagged** ' + invalidTools.join(', ') + ' — the AI analysis may not match what was returned. Use `ai-ignore` to dismiss and proceed.';
+ }
}
}
if (pendingText) {
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index cc3a79c..667a396 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -156,7 +156,7 @@
// Phase 4: Ext modules (filtered)
if (typeof webx11._loadExtModules === 'function') {
- const extKeys = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'tutorial'];
+ const extKeys = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'tutorial', 'shell', 'busybox'];
const extSelected = selected
? extKeys.filter(k => isSelected(k + '.js', selected))
: extKeys;
diff --git a/modules/core/ollama-manager.js b/modules/core/ollama-manager.js
index ab3ec2c..9999184 100644
--- a/modules/core/ollama-manager.js
+++ b/modules/core/ollama-manager.js
@@ -15,9 +15,9 @@ class OllamaManager {
this.optimizationEnabled = localStorage.getItem('webx11_optimization_enabled') !== 'false';
// --- Streaming Toggle ---
this.streamEnabled = localStorage.getItem('webx11_stream_enabled') !== 'false';
- // --- Validator Agent Config (future) ---
- this.validatorEnabled = localStorage.getItem('webx11_validator_enabled') === 'true';
- this.validatorModel = localStorage.getItem('webx11_validator_model') || '';
+ // --- Validator Agent Config ---
+ this.validatorEnabled = localStorage.getItem('webx11_validator_enabled') !== 'false';
+ this.validatorModel = localStorage.getItem('webx11_validator_model') || 'ministral-3:3b-cloud';
this.validatorEndpoint = localStorage.getItem('webx11_validator_endpoint') || '';
// Build fallback endpoints
this.fallbackEndpoints = [];
@@ -865,6 +865,70 @@ class OllamaManager {
}
}
+ // MiniCPM5 XML tool calling mode — sends tools in message context instead of API parameter
+ // Model natively outputs <function name="x"><param name="y">val</param> XML
+ if (useModel && useModel.includes('minicpm5') && tools && tools.length > 0) {
+ this.terminal.print(`🧩 Using XML tool mode for ${useModel}...`, 'info');
+ try {
+ // Inject tool defs into a system message (the orchestrator model knows XML format)
+ const toolDesc = tools.map(t => JSON.stringify({type:'function',function:t.function})).join('\n');
+ const toolContext = { role: 'system', content:
+`You are a tool orchestrator. When you need to call a tool, output XML like:
+<function name="function-name"><param name="param-name">param-value</param>
+
+Available tools:
+<tools>
+${toolDesc}
+</tools>
+
+Only output XML function calls for tool tasks. If no tool is needed, answer normally.` };
+ const msgsWithTools = [toolContext, ...messages];
+ const body = { model: useModel, messages: msgsWithTools, stream: false, options: { temperature: 0.1 } };
+
+ const response = await fetch(`${this.endpoint}/api/chat`, {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body), signal
+ });
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
+ const data = await response.json();
+ const text = (data.message?.content || '').trim();
+
+ // Parse XML function calls from the response
+ const toolCalls = [];
+ const funcRegex = /<function\s+name="([^"]+)"(.*?)<\/function>/gs;
+ let match;
+ while ((match = funcRegex.exec(text)) !== null) {
+ const funcName = match[1];
+ const args = {};
+ const paramRegex = /<param\s+name="([^"]+)"(.*?)>(.*?)<\/param>/gs;
+ let pmatch;
+ while ((pmatch = paramRegex.exec(match[0])) !== null) {
+ args[pmatch[1]] = pmatch[3]
+ .replace('<![CDATA[', '').replace(']]>', '')
+ .replace(/^\s+|\s+$/g, '');
+ }
+ if (funcName && Object.keys(args).length > 0) {
+ toolCalls.push({
+ id: 'call_' + Math.random().toString(36).slice(2, 10),
+ type: 'function',
+ function: { name: funcName, arguments: JSON.stringify(args) }
+ });
+ }
+ }
+ if (toolCalls.length > 0) {
+ this.terminal.print(`āœ… XML tool call(s) parsed from ${useModel}`, 'success');
+ return { message: { role: 'assistant', content: '' }, tool_calls: toolCalls, error: null };
+ }
+
+ return { message: { role: 'assistant', content: text || '(no response)' }, tool_calls: null, error: null };
+ } catch (e) {
+ if (e.name === 'AbortError') {
+ return { message: null, tool_calls: null, error: 'Cancelled' };
+ }
+ return { message: null, tool_calls: null, error: e.message };
+ }
+ }
+
// Original Ollama fallback
this.terminal.print(`Model doesn't support tool calling. Retrying /api/chat without tools...`, 'warning');
try {
@@ -981,7 +1045,7 @@ class OllamaManager {
});
const data = await res.json();
const answer = (data.message?.content || '').trim().toUpperCase();
- return answer === 'YES';
+ return answer.startsWith('YES');
} catch (e) {
if (this.terminal) this.terminal.print('āš ļø Validator error: ' + e.message, 'warning');
return true;
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index d53ab70..5008842 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -54,13 +54,12 @@
let curr = new Uint16Array(n + 1);
for (let j = 0; j <= n; j++) prev[j] = j;
for (let i = 1; i <= m; i++) {
- let next = new Uint16Array(n + 1);
- next[0] = i;
+ curr[0] = i;
for (let j = 1; j <= n; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
- next[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
}
- [prev, curr] = [curr, next];
+ [prev, curr] = [curr, prev];
}
return prev[n];
}
@@ -98,8 +97,11 @@
if (['help','security','security-level'].includes(cmd)) {
return this._dispatch(cmd, args);
}
- // Unknown command handling
+ // Unknown command handling — skip at level 0 (permissive)
if (unknown && !this.whitelist.has(cmd)) {
+ if (this.level === 0) {
+ return this._dispatch(cmd, args);
+ }
this.blockedCount++;
const sug = this.suggest(cmd);
if (sug) {
@@ -107,7 +109,7 @@
} else {
this.terminal.print(`Security: Unknown command "${cmd}" — blocked. Use 'security-level 0' to disable.`, 'warning');
}
- // At levels 0-1: suggest/fix but still route to AI (terminal is also a chat interface)
+ // At level 1: suggest/fix but still route to AI (terminal is also a chat interface)
// At level 2+: block entirely (paranoid)
if (this.level >= 2) {
this.terminal.print(`Security: Unknown command "${cmd}" blocked (level ${this.level}).`, 'error');
@@ -272,6 +274,9 @@
this.selectedSuggestion = (this.selectedSuggestion + 1) % this.suggestions.length;
this.updateAutocompleteDisplay();
}
+ } else if (e.ctrlKey && (e.key === 'k' || e.key === 'K')) {
+ e.preventDefault();
+ this.toggleCmdCards();
} else if (e.key === 'Escape') {
if (this._aiBusy) {
this.executeCommand('ai-stop');
@@ -984,7 +989,7 @@
catch (e) { console.warn(`[SafeRegister] Skipped ${label}: ${e.message}`); }
}
async _loadExtModules(filterKeys) {
- const autoLoadKeys = filterKeys || ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial'];
+ const autoLoadKeys = filterKeys || ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox'];
const cacheVer = localStorage.getItem('webx11_cache_ver') || '2026-05-27-14';
try {
const res = await fetch('packages.json?v=' + cacheVer);
@@ -1098,7 +1103,7 @@
}
// Enhanced: check fuzzy suggestions before showing "not found"
const suggestion = this.security.suggest(cmd);
- if (suggestion && suggestion.dist <= 2) {
+ if (suggestion && suggestion.dist <= 2 && suggestion.dist > 0) {
this.print(`Command not found: ${cmd}`, 'error');
this.print(`Did you mean: ${suggestion.cmd}? (yes) (no)`, 'info');
// Render clickable yes/no
@@ -1111,7 +1116,7 @@
const noSpan = document.createElement('span');
noSpan.textContent = ' [no] ';
noSpan.style.cssText = 'color:var(--error-color);cursor:pointer;text-decoration:underline';
- noSpan.onclick = () => { /* do nothing — user declined */ };
+ noSpan.onclick = () => { this.print('Dismissed.', 'text-muted'); };
const line = out.lastElementChild;
if (line) { line.appendChild(yesSpan); line.appendChild(noSpan); }
}
@@ -1450,8 +1455,7 @@
const endpoint = document.getElementById('ollama-endpoint').value.trim();
this.ollamaManager.setEndpoint(endpoint);
const success = await this.ollamaManager.connect();
- this.updateOllamaStatus();
- if (success) { this.renderOllamaModels(); }
+ if (success) { this.renderOllamaModels(); this._populateValidatorModels(); this.updateOllamaStatus(); }
}
setOllamaModel() {
const select = document.getElementById('ollama-current-model');
@@ -1589,6 +1593,8 @@
valSlider.style.borderColor = enabled ? 'var(--info-color)' : 'var(--border-color)';
}
}
+ // Auto-populate validator model dropdown from connected models
+ this._populateValidatorModels();
// Initialize stream toggle from stored state
const strCb = document.getElementById('ollama-stream-toggle');
const strLabel = document.getElementById('ollama-stream-label');
@@ -1617,12 +1623,27 @@
if (storedModel) {
valSelect.innerHTML = '<option value="' + storedModel + '">' + storedModel + '</option><option value="">Disable (use main AI model)</option>';
valSelect.disabled = false;
- } else {
- valSelect.innerHTML = '<option value="">Connect to load models</option>';
- valSelect.disabled = true;
}
}
}
+ _populateValidatorModels() {
+ const select = document.getElementById('validator-model-select');
+ if (!select || !this.ollamaManager) return;
+ const models = this.ollamaManager.getModels();
+ if (!models || models.length === 0) {
+ select.innerHTML = '<option value="">Connect to load models</option>';
+ select.disabled = true;
+ return;
+ }
+ const stored = this.ollamaManager.validatorModel || '';
+ let html = '<option value="">Disable (use main AI model)</option>';
+ models.forEach(function(m) {
+ const sel = m.name === stored ? ' selected' : '';
+ html += '<option value="' + m.name + '"' + sel + '>' + m.name + '</option>';
+ });
+ select.innerHTML = html;
+ select.disabled = false;
+ }
openStartupModules() {
document.getElementById('startup-modules').style.display = 'flex';
this.renderStartupModules();
@@ -1811,6 +1832,15 @@
// Also mark tutorial as skipped so it doesn't reappear
if (this.tutorialModule) this.tutorialModule.skipTutorial();
}
+ toggleCmdCards() {
+ const panel = document.getElementById('cmd-cards');
+ if (!panel) return;
+ panel.classList.toggle('visible');
+ // If opening, focus the input field
+ if (panel.classList.contains('visible')) {
+ setTimeout(() => this.inputField.focus(), 50);
+ }
+ }
log(level, source, message) {
const d = new Date();
const ts = d.toISOString().split('T')[1].split('.')[0];
diff --git a/modules/ext/busybox.js b/modules/ext/busybox.js
index 709324f..34221f8 100644
--- a/modules/ext/busybox.js
+++ b/modules/ext/busybox.js
@@ -57,8 +57,12 @@ class BusyBoxModule {
const keys = Object.keys(all);
if (all[query]) {
- const mod = this.terminal.moduleManager._findModuleForCommand(query);
- const src = mod ? ` (from ${mod.constructor.name})` : '';
+ let foundMod = null;
+ for (const modName in this.terminal.moduleManager.modules) {
+ const mod = this.terminal.moduleManager.modules[modName];
+ if (mod.commands && mod.commands[query]) { foundMod = mod; break; }
+ }
+ const src = foundMod ? ` (from ${foundMod.constructor.name})` : '';
return this.terminal.print(`${query}${src}`, 'info');
}