diff options
| author | Agando <roduccomm@gmail.com> | 2026-06-01 06:00:39 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-06-01 06:00:39 +0200 |
| commit | 6aed9c9e0252b0c8f681a358e5a8707271454137 (patch) | |
| tree | 332a29a515c87c2295355cf81f18b818cfbaa846 /modules/core | |
| parent | 7cc82d2d32e44ab1c5b7d042d5522d036d9aa0be (diff) | |
auto: 2026-06-01_06:00 changes
Diffstat (limited to 'modules/core')
| -rw-r--r-- | modules/core/module-aiassist.js | 10 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/core/ollama-manager.js | 72 | ||||
| -rw-r--r-- | modules/core/terminal.js | 58 |
4 files changed, 119 insertions, 23 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]; |