From b839e58aa45da1945d0c46c8070b4e7016c59f94 Mon Sep 17 00:00:00 2001 From: Agando Date: Mon, 1 Jun 2026 18:00:38 +0200 Subject: auto: 2026-06-01_18:00 changes --- main.html | 34 ++++- modules/core/module-aiassist.js | 78 +++++++++- modules/core/module-fs.js | 2 +- modules/core/module-loader.js | 4 +- modules/core/module-system.js | 2 +- modules/core/terminal.js | 70 ++++++++- modules/ext/shell.js | 27 +++- review-newos4.html | 309 +++++++++++++++------------------------- 8 files changed, 312 insertions(+), 214 deletions(-) diff --git a/main.html b/main.html index d5bd57d..804c8e4 100644 --- a/main.html +++ b/main.html @@ -139,6 +139,38 @@ margin-bottom: 10px; position: relative; } + .output-line.user-prompt-line { + display: flex; + align-items: flex-start; + gap: 6px; + flex-wrap: wrap; + padding: 4px 0; + } + .output-line.user-prompt-line .prompt-prefix { + color: var(--accent-color); + white-space: nowrap; + flex-shrink: 0; + } + .output-line.user-prompt-line .prompt-text { + color: var(--text-color); + word-break: break-word; + flex: 1; + min-width: 0; + } + .copy-btn, .edit-btn { + font-size: 0.7em; + padding: 2px 8px; + cursor: pointer; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 4px; + color: var(--text-muted); + touch-action: manipulation; + flex-shrink: 0; + transition: background 0.15s; + } + .copy-btn:hover, .edit-btn:hover, + .copy-btn:active, .edit-btn:active { background: var(--accent-color); color: var(--bg-color); } #input-area { border-top: 1px solid var(--border-color); padding: 8px 10px; @@ -988,7 +1020,7 @@ var mods = [ ['Foundation (always loaded)', ['virtual-fs.js','module-manager.js','ollama-manager.js','terminal.js'], true], ['Built-in modules', ['module-system.js','module-fs.js','module-modmgr.js','module-ollama.js','module-my.js','module-searxng.js','module-servermon.js','module-ollamamodels.js','module-config.js','module-simplegui.js','module-io.js','module-dynamicprompts.js','module-aiassist.js','module-aiasm.js','module-smartctx.js','module-duckduckgo.js','module-fetcher.js','module-skills.js','module-chatstudio.js'], false], - ['Extensions', ['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'], false] + ['Extensions', ['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'], false] ]; var isSel = function(name) { return true; }; // default: all selected mods.forEach(function(section) { 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, '&').replace(//g, '>').replace(/"/g, '"'); + } + _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 += '
' + + '#' + entry.turn + ' ' + + '' + this._escapeHtml(entry.name) + ' ' + + '' + this._escapeHtml(argsStr) + ' ' + + '' + entry.duration + 's' + + (entry.error ? ' ⚠️' : '') + + '
'; + } + panel.innerHTML = '
🔧 Tool Log ' + this._toolCallLog.length + ' calls
' + 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): ${this._escapeHtml(obs)} [+] show full (${toolResult.length} chars)`); } 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, '>') .replace(/"/g, '"') .replace(/`/g, '`'); - return `
`; + return `
📄 Rendered HTML
`; } _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, '>'); + const hasHtml = /<[a-z][\s\S]*>/i.test(rawInput.trim()); + const previewBtn = hasHtml ? `` : ''; + wrapper.innerHTML = ` + webx11@module:~$ + ${safe} + + ${previewBtn} + + `; + 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(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/`/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(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/`/g, '`'); + // Wrap in a basic HTML template if it doesn't have html/head/body tags + const hasOwnDoc = /${decoded}`; + 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; } diff --git a/review-newos4.html b/review-newos4.html index 1108c81..53e3ffe 100644 --- a/review-newos4.html +++ b/review-newos4.html @@ -3,7 +3,7 @@ - Review: Module OS v4.2 — WebX11 Terminal + Review: newOS4 v4.2 — WebX11 Terminal