aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/core/module-aiassist.js78
-rw-r--r--modules/core/module-fs.js2
-rw-r--r--modules/core/module-loader.js4
-rw-r--r--modules/core/module-system.js2
-rw-r--r--modules/core/terminal.js70
-rw-r--r--modules/ext/shell.js27
6 files changed, 168 insertions, 15 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index f77ec61..f7be209 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -16,7 +16,10 @@
this._sessionSaved = false;
this._thinkingTimer = null;
this._thinkingStartTime = 0;
+ this._validatorSuppressed = false;
this._toolDefs = null;
+ this._toolCallLog = [];
+ this._debugPanelEnabled = false;
try { const s = localStorage.getItem('webx11_ai_patterns'); if (s) this._commandFreq = JSON.parse(s); } catch (e) {}
try { const p = localStorage.getItem('webx11_user_profile'); if (p) this._userProfile = JSON.parse(p); } catch (e) {}
// Restore AI conversation context from localStorage
@@ -392,6 +395,7 @@ RULES:
'ai-context': 'ai-context — Show current conversation context',
'ai-allow': 'ai-allow — Confirm pending AI action',
'ai-deny': 'ai-deny — Deny pending AI action',
+ 'ai-ignore': 'ai-ignore — Suppress validator warnings for this session',
'ai-stop': 'ai-stop — Cancel current AI operation',
'ai-model': 'ai-model [model] — Show or set AI model'
};
@@ -404,9 +408,11 @@ RULES:
this.commands['ai-context'] = () => this.showContext();
this.commands['ai-allow'] = () => this.confirmPending();
this.commands['ai-deny'] = () => this.denyPending();
+ this.commands['ai-ignore'] = () => this.ignoreValidator();
this.commands['ai-stop'] = () => this.cancelAi();
this.commands['ai-model'] = (args) => this.showModel(args);
this.commands['ai-compress'] = () => this._manualCompress();
+ this.commands['debug'] = () => this.toggleDebug();
}
async chat(args) {
const userMessage = args.join(' ');
@@ -542,6 +548,28 @@ RULES:
}
return result.substring(0, maxLen) + '...(truncated from ' + result.length + ' chars)';
}
+ _escapeHtml(str) {
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+ }
+ _updateDebugPanel() {
+ if (!this._debugPanelEnabled) return;
+ const panel = document.getElementById('debug-panel');
+ if (!panel) return;
+ const recent = this._toolCallLog.slice(-20);
+ let html = '';
+ 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);
+ html += '<div class="debug-entry ' + statusClass + '">' +
+ '<span class="debug-turn">#' + entry.turn + '</span> ' +
+ '<span class="debug-name">' + this._escapeHtml(entry.name) + '</span> ' +
+ '<span class="debug-args">' + this._escapeHtml(argsStr) + '</span> ' +
+ '<span class="debug-time">' + entry.duration + 's</span>' +
+ (entry.error ? ' <span class="debug-err">⚠️</span>' : '') +
+ '</div>';
+ }
+ panel.innerHTML = '<div class="debug-header">🔧 Tool Log <span class="debug-count">' + this._toolCallLog.length + ' calls</span></div>' + html;
+ }
_isToolError(result) {
if (!result || typeof result !== 'string') return null;
const trimmed = result.trim();
@@ -618,6 +646,15 @@ RULES:
const t0 = Date.now();
const toolResult = await this.runTool(fn.name, fn.arguments);
const t1 = Math.round((Date.now() - t0) / 1000);
+ this._toolCallLog.push({
+ turn: toolTurns,
+ name: fn.name,
+ args: fn.arguments,
+ result: toolResult,
+ duration: t1,
+ error: this._isToolError(toolResult) ? true : false
+ });
+ if (this._debugPanelEnabled) this._updateDebugPanel();
return { fn, toolResult, t1 };
}));
for (const { fn, toolResult, t1 } of safeResults) {
@@ -628,6 +665,10 @@ RULES:
this.terminal.log('WARN', 'AI', `Tool ${fn.name} failed: ${err} (${t1}s)`);
this._trackToolFailure(fn.name, err);
this.terminal.print(`📊 Result (${t1}s): ⚠️ Error: ${err}`, 'warning');
+ } else if (toolResult && toolResult.length > 150) {
+ this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`);
+ const safeId = 'toolresult-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6);
+ this.terminal.printHTML(`📊 Result (${t1}s): <span id="${safeId}-short">${this._escapeHtml(obs)} <a href="#" onclick="var e=document.getElementById('${safeId}-full');var s=document.getElementById('${safeId}-short');if(e){e.style.display='inline';s.style.display='none'}return false" style="color:var(--info-color);cursor:pointer">[+] show full (${toolResult.length} chars)</a></span><span id="${safeId}-full" style="display:none">${this._escapeHtml(toolResult)} <a href="#" onclick="var e=document.getElementById('${safeId}-full');var s=document.getElementById('${safeId}-short');if(e&&s){e.style.display='none';s.style.display='inline'}return false" style="color:var(--warning-color);cursor:pointer">[-] collapse</a></span>`);
} else {
this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`);
this.terminal.print(`📊 Result (${t1}s): ${obs}`, 'info');
@@ -644,6 +685,15 @@ RULES:
this.terminal.print(`🔧 Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info');
const toolResult = await this.runTool(fn.name, fn.arguments);
const t1 = Math.round((Date.now() - t0) / 1000);
+ this._toolCallLog.push({
+ turn: toolTurns,
+ name: fn.name,
+ args: fn.arguments,
+ result: toolResult,
+ duration: t1,
+ error: this._isToolError(toolResult) ? true : false
+ });
+ if (this._debugPanelEnabled) this._updateDebugPanel();
const dObs = this._summarizeObservation(toolResult, 150);
const dErr = this._isToolError(toolResult);
if (dErr) {
@@ -664,7 +714,7 @@ RULES:
}
}
// Validator: check safe tool results for consistency
- if (safeResults.length > 0 && pendingText) {
+ if (safeResults.length > 0 && pendingText && !this._validatorSuppressed) {
const om = this.terminal.ollamaManager;
if (om && om.validatorEnabled && om.validatorModel) {
let invalidTools = [];
@@ -856,6 +906,10 @@ RULES:
denyPending() {
this._resolveConfirm(false);
}
+ ignoreValidator() {
+ this._validatorSuppressed = true;
+ this.terminal.print('🔍 Validator suppressed for this session', 'info');
+ }
cancelAi() {
this._cancelled = true;
this.terminal.print('⏹ Cancelling AI...', 'warning');
@@ -870,6 +924,28 @@ RULES:
// Immediately reset UI so user knows it stopped
this.terminal.setAiBusy(false);
}
+ toggleDebug() {
+ this._debugPanelEnabled = !this._debugPanelEnabled;
+ let panel = document.getElementById('debug-panel');
+ if (!panel) {
+ // Create the debug panel
+ panel = document.createElement('div');
+ panel.id = 'debug-panel';
+ panel.style.cssText = 'position:fixed;right:10px;bottom:50px;width:380px;max-height:400px;overflow-y:auto;background:#0a0a0a;border:1px solid #333;border-radius:6px;z-index:9999;font-family:monospace;font-size:0.78em;display:none;';
+ document.body.appendChild(panel);
+ // Add close button handler
+ panel.addEventListener('click', function() {
+ // Toggle collapsed/expanded state
+ });
+ }
+ panel.style.display = this._debugPanelEnabled ? 'block' : 'none';
+ if (this._debugPanelEnabled) {
+ this._updateDebugPanel();
+ this.terminal.print('🔧 Debug panel enabled — shows tool call log in bottom-right corner. Close with: debug', 'info');
+ } else {
+ this.terminal.print('🔧 Debug panel disabled', 'info');
+ }
+ }
isDestructive(name) {
return name === 'write_file' || name === 'delete_file' || name === 'run_command' || name === 'make_directory' || name === 'edit_module';
}
diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js
index 4ba34b0..ae8b5f4 100644
--- a/modules/core/module-fs.js
+++ b/modules/core/module-fs.js
@@ -288,7 +288,7 @@ class FileSystemModule {
const t = args && args[0] ? args[0] : '.';
if (t === '.') {
const dir = this.terminal.fs.getCurrentDir(); if (!dir) return;
- let total = 0; Object.keys(dir.content).forEach(k => { total += dir.content[k].content ? dir.content[k].content.length : 0; });
+ let total = 0; Object.keys(dir.content).forEach(k => { const node = dir.content[k]; total += node && typeof node.content === 'string' ? node.content.length : 0; });
this.terminal.print(total + '\t' + t);
} else {
const c = this.terminal.fs.readFile(t);
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index 667a396..61bde20 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -99,9 +99,9 @@
window.__moduleLoaderReload = reloadWithSelection;
- const CACHE = '?v=' + (localStorage.getItem('webx11_cache_ver') || '2026-05-28');
+const CACHE = '?v=' + (localStorage.getItem('webx11_cache_ver') || '2026-06-01-fix3');
if (!localStorage.getItem('webx11_cache_ver')) {
- localStorage.setItem('webx11_cache_ver', '2026-05-28');
+ localStorage.setItem('webx11_cache_ver', '2026-06-01-fix3');
}
async function init() {
diff --git a/modules/core/module-system.js b/modules/core/module-system.js
index 29a9a87..beed699 100644
--- a/modules/core/module-system.js
+++ b/modules/core/module-system.js
@@ -204,7 +204,7 @@
// Categorize all commands
const cats = {
files: ['ls','ll','cat','put','rm','mv','cp','mkdir','rmdir','cd','pwd','find','grep','head','tail','sort','wc','touch','tree','du','upload','download','vfs-export','vfs-import','edit','diff'],
- ai: ['ai','ask','ai-clear','ai-system','ai-context','ai-allow','ai-deny','ai-stop','ai-model'],
+ ai: ['ai','ask','ai-clear','ai-system','ai-context','ai-allow','ai-deny','ai-ignore','ai-stop','ai-model'],
search: ['search','s','ddg','duck','fetch','web','extract'],
tools: ['calc','cron-every','cron-list','cron-stop','cron-clear','wpm','mindmap','theme','voice','alias','unalias',
'which','env','date','cal','yes','seq','base64','printf','cut','uniq','hostname','whoami','basename','dirname','sleep','expand','tutorial'],
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index 5008842..607c51a 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -399,7 +399,7 @@
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/`/g, '&#96;');
- return `<div class="ai-render-container"><iframe sandbox="allow-scripts" srcdoc="${safeSrcdoc}" class="ai-render-iframe" onload="var _s=this;setTimeout(function(){_s.style.height=Math.max((_s.contentWindow.document.documentElement.scrollHeight||200),200)+'px'},100)"></iframe></div>`;
+ return `<div class="ai-render-container"><div style="display:flex;gap:6px;padding:4px 8px;background:var(--bg-tertiary);border-bottom:1px solid var(--border-color);align-items:center;"><span style="color:var(--text-muted);font-size:0.75em;">📄 Rendered HTML</span><span style="flex:1"></span><button class="qa-btn" style="padding:2px 10px;font-size:0.75em;" onclick="webx11.openRenderPreview(this)" data-content="${safeSrcdoc.replace(/"/g, '&quot;')}">⛶ Fullscreen</button></div><iframe sandbox="allow-scripts" srcdoc="${safeSrcdoc}" class="ai-render-iframe" onload="var _s=this;setTimeout(function(){_s.style.height=Math.max((_s.contentWindow.document.documentElement.scrollHeight||200),200)+'px'},100)"></iframe></div>`;
}
_renderHTMLBlock(text) {
const renderRegex = /:::render-html\r?\n([\s\S]*?)\r?\n:::/g;
@@ -483,9 +483,42 @@
this._streamChunks = [];
}
}
+ printUserPrompt(rawInput) {
+ const wrapper = document.createElement('div');
+ wrapper.className = 'output-line user-prompt-line';
+ const safe = rawInput.replace(/</g, '&lt;').replace(/>/g, '&gt;');
+ const hasHtml = /<[a-z][\s\S]*>/i.test(rawInput.trim());
+ const previewBtn = hasHtml ? `<button class="copy-btn" onclick="webx11.previewHtmlFromPrompt(this)" data-content="${safe.replace(/"/g, '&quot;')}" title="Preview HTML">👁 Preview</button>` : '';
+ wrapper.innerHTML = `
+ <span class="prompt-prefix">webx11@module:~$ </span>
+ <span class="prompt-text">${safe}</span>
+ <button class="copy-btn user-copy-btn" onclick="webx11.copyToClipboard(this)" title="Copy prompt">Copy</button>
+ ${previewBtn}
+ <button class="edit-btn" onclick="webx11.editPrompt('${safe.replace(/'/g, "\\'")}')" title="Edit prompt">Edit</button>
+ `;
+ this.outputContainer.appendChild(wrapper);
+ this._historyBuffer.push({ l: rawInput, t: 'command-prompt' });
+ if (this._historyBuffer.length > this._historyMaxLines) this._historyBuffer.shift();
+ this.scrollToBottom();
+ this._trimOutput();
+ }
+ editPrompt(text) {
+ this.inputField.value = text;
+ this.inputField.focus();
+ this.inputField.setSelectionRange(text.length, text.length);
+ }
copyToClipboard(btn) {
- const contentDiv = btn.nextElementSibling;
- const text = contentDiv.innerText;
+ let text;
+ if (btn.classList.contains('user-copy-btn')) {
+ // User prompt: text is in .prompt-text sibling
+ const pSpan = btn.parentElement.querySelector('.prompt-text');
+ if (pSpan) text = pSpan.textContent;
+ } else {
+ // AI response: text is in next sibling .ai-content
+ const contentDiv = btn.nextElementSibling;
+ if (contentDiv) text = contentDiv.innerText;
+ }
+ if (!text) return;
navigator.clipboard.writeText(text).then(() => {
const originalText = btn.textContent;
btn.textContent = 'Copied!';
@@ -675,6 +708,8 @@
this._dirty = false;
}, 180000);
window.addEventListener('beforeunload', () => {
+ // Flush session before tab closes — avoids losing up to 3min of output on crash/close
+ try { this._autoSaveSession(); } catch (e) {}
if (this._autoSaveTimer) {
clearInterval(this._autoSaveTimer);
this._autoSaveTimer = null;
@@ -1008,7 +1043,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'];
+ const skipList = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox'];
for (const mod of registry) {
if (mod.enabled && mod.url) {
// Skip ext modules — they load via _loadExtModules
@@ -1021,7 +1056,7 @@
async executeCommand(rawInput) {
if (!rawInput.trim()) return;
this.markDirty();
- this.print(`webx11@module:~$ ${rawInput}`, 'command-prompt');
+ this.printUserPrompt(rawInput);
const pipeParts = this.splitPipe(rawInput.trim());
if (pipeParts.length > 1) {
await this.executePipeChain(pipeParts);
@@ -1248,6 +1283,29 @@
iframe.src = 'about:blank';
this.inputField.focus();
}
+ openRenderPreview(btn) {
+ const content = btn.getAttribute('data-content');
+ if (!content) return;
+ const iframe = document.getElementById('preview-iframe');
+ // The srcdoc content is already HTML-escaped from _renderHTMLPreview
+ const decoded = content.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#96;/g, '`');
+ iframe.srcdoc = decoded;
+ document.getElementById('preview-title').textContent = '📄 AI Rendered Preview (Fullscreen)';
+ document.getElementById('preview-modal').style.display = 'flex';
+ }
+ previewHtmlFromPrompt(btn) {
+ const content = btn.getAttribute('data-content');
+ if (!content) return;
+ const iframe = document.getElementById('preview-iframe');
+ // Content is HTML-escaped, decode it back
+ const decoded = content.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#96;/g, '`');
+ // Wrap in a basic HTML template if it doesn't have html/head/body tags
+ const hasOwnDoc = /<html|<head|<body/i.test(decoded);
+ const fullDoc = hasOwnDoc ? decoded : `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><style>body{background:#000;color:#0f0;font-family:monospace;margin:16px;}</style></head><body>${decoded}</body></html>`;
+ iframe.srcdoc = fullDoc;
+ document.getElementById('preview-title').textContent = '📄 User HTML Preview';
+ document.getElementById('preview-modal').style.display = 'flex';
+ }
openSkillsManager() {
const modal = document.getElementById('skills-manager');
// Ensure /skills/ directory exists
@@ -1671,7 +1729,7 @@
'module-fetcher.js','module-skills.js','module-chatstudio.js'
];
const extFiles = [
- 'calc.js','voice.js','cron.js','theme.js','wpm.js','mindmap.js','skill-pack-core.js'
+ 'calc.js','voice.js','cron.js','theme.js','wpm.js','mindmap.js','webdav.js','skill-pack-core.js','editor.js','diff.js','alias.js','finder.js','filemgr.js','cryptogzip.js','tutorial.js','shell.js','busybox.js'
];
const addSection = (title) => {
diff --git a/modules/ext/shell.js b/modules/ext/shell.js
index 6beb0eb..e87c37c 100644
--- a/modules/ext/shell.js
+++ b/modules/ext/shell.js
@@ -37,13 +37,28 @@ class ShellModule {
// ── Hook into terminal.executeCommand ──
init() {
- // Wrap executeCommand to add shell parsing
+ // Guard: prevent double-wrapping if init is called multiple times
+ if (this._initialized) return;
+ this._initialized = true;
+
+ // Wrap executeCommand to add shell parsing — only for compound commands
const origExecute = this.terminal.executeCommand.bind(this.terminal);
const self = this;
this.terminal.executeCommand = async function(rawInput) {
if (!rawInput.trim()) return;
this.markDirty();
- this.print(`webx11@module:~$ ${rawInput}`, 'command-prompt');
+ this.printUserPrompt(rawInput);
+
+ // For simple single-word commands without special chars, skip shell parsing
+ if (/^[a-zA-Z0-9._\/-]+$/.test(rawInput.trim()) && !rawInput.includes(' ') && !rawInput.includes('$')) {
+ try {
+ return await this.security.checkAndRun(rawInput);
+ } catch(e) {
+ this.print('Error: ' + e.message, 'error');
+ return;
+ }
+ }
+
// Substitute variables in the raw input
const substituted = self._substituteVars(rawInput);
// Split on ; first (sequence)
@@ -54,7 +69,7 @@ class ShellModule {
const chainParts = self._splitChain(seqPart.trim());
let lastOk = true;
for (const chainPart of chainParts) {
- if (!chainPart.trim()) continue;
+ if (!chainPart.cmd.trim()) continue;
const { cmd: chainCmd, operator } = chainPart;
if (operator === '&&' && !lastOk) continue;
if (operator === '||' && lastOk) continue;
@@ -74,7 +89,11 @@ class ShellModule {
const parts = chainCmd.split(/\s+/);
const cmd = parts[0].toLowerCase();
const args = parts.slice(1);
- await this.security.checkAndRun(chainCmd);
+ try {
+ await this.security.checkAndRun(chainCmd);
+ } catch(e) {
+ this.terminal && this.terminal.print ? this.terminal.print('Error: ' + e.message, 'error') : console.error(e);
+ }
}
lastOk = true;
}