// Extracted from newOS4 index.html — WebX11Terminal class WebX11SecurityManager { constructor(terminal, level = 1) { this.terminal = terminal; this.level = level; this.commandMeta = new Map(); this.lastRouteTime = 0; this.lastRouteCmd = ''; this.blockedCount = 0; this._baseDestructive = ['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']; this._baseSystem = ['reboot','shutdown','format','kill','halt','poweroff']; this._baseSuspiciousFlags = ['--dump','--execute','--eval','--exec','--shell','--script']; this._baseWhitelist = ['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','sed','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','reset','factory-reset','reset-vfs','js','js-run','node','debug','debug-ai','debug-stats','debug-history','debug-analyze','debug-trends','debug-fix','debug-health','session-new','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','dashboard','context-auto']; this.destructiveCmds = new Set(this._baseDestructive); this.systemCmds = new Set(this._baseSystem); this.suspiciousFlags = new Set(this._baseSuspiciousFlags); this.whitelist = new Set(this._baseWhitelist); // AI loop-guard: depth counter (reentrant, race-safe vs parallel tool calls) this._aiToolDepth = 0; // Load persisted user deltas (add/remove) so hardcoded defaults can be demoted. this._loadSecurityDeltas(); this._registerBuiltIn(); } _loadSecurityDeltas() { try { const schema = this.terminal._getDataSchemaVersion ? this.terminal._getDataSchemaVersion() : 1; localStorage.setItem('webx11_data_schema_version', String(schema)); const raw = localStorage.getItem('webx11_security_deltas'); if (!raw) { // One-time migration from legacy flat keys to delta format. const legacy = {}; try { legacy.destructive = JSON.parse(localStorage.getItem('webx11_security_destructive') || '[]'); } catch (e) {} try { legacy.system = JSON.parse(localStorage.getItem('webx11_security_system') || '[]'); } catch (e) {} try { legacy.whitelist = JSON.parse(localStorage.getItem('webx11_security_whitelist') || '[]'); } catch (e) {} try { legacy.flags = JSON.parse(localStorage.getItem('webx11_security_flags') || '[]'); } catch (e) {} const delta = { add: {}, remove: {} }; for (const c of (legacy.destructive || [])) if (!this._baseDestructive.includes(c)) { if (!delta.add.destructive) delta.add.destructive = []; delta.add.destructive.push(c); } for (const c of (legacy.system || [])) if (!this._baseSystem.includes(c)) { if (!delta.add.system) delta.add.system = []; delta.add.system.push(c); } for (const c of (legacy.whitelist || [])) if (!this._baseWhitelist.includes(c)) { if (!delta.add.whitelist) delta.add.whitelist = []; delta.add.whitelist.push(c); } for (const f of (legacy.flags || [])) if (!this._baseSuspiciousFlags.includes(f)) { if (!delta.add.flags) delta.add.flags = []; delta.add.flags.push(f); } localStorage.setItem('webx11_security_deltas', JSON.stringify(delta)); localStorage.removeItem('webx11_security_destructive'); localStorage.removeItem('webx11_security_system'); localStorage.removeItem('webx11_security_whitelist'); localStorage.removeItem('webx11_security_flags'); return; } const d = JSON.parse(raw); if (d.remove) { for (const c of (d.remove.destructive || [])) this.destructiveCmds.delete(c); for (const c of (d.remove.system || [])) this.systemCmds.delete(c); for (const c of (d.remove.whitelist || [])) this.whitelist.delete(c); for (const f of (d.remove.flags || [])) this.suspiciousFlags.delete(f); } if (d.add) { for (const c of (d.add.destructive || [])) this.destructiveCmds.add(c); for (const c of (d.add.system || [])) this.systemCmds.add(c); for (const c of (d.add.whitelist || [])) this.whitelist.add(c); for (const f of (d.add.flags || [])) this.suspiciousFlags.add(f); } } catch (e) {} } _saveSecurityDeltas() { const delta = { add: {}, remove: {} }; for (const c of this.destructiveCmds) if (!this._baseDestructive.includes(c)) { if (!delta.add.destructive) delta.add.destructive = []; delta.add.destructive.push(c); } for (const c of this._baseDestructive) if (!this.destructiveCmds.has(c)) { if (!delta.remove.destructive) delta.remove.destructive = []; delta.remove.destructive.push(c); } for (const c of this.systemCmds) if (!this._baseSystem.includes(c)) { if (!delta.add.system) delta.add.system = []; delta.add.system.push(c); } for (const c of this._baseSystem) if (!this.systemCmds.has(c)) { if (!delta.remove.system) delta.remove.system = []; delta.remove.system.push(c); } for (const c of this.whitelist) if (!this._baseWhitelist.includes(c)) { if (!delta.add.whitelist) delta.add.whitelist = []; delta.add.whitelist.push(c); } for (const c of this._baseWhitelist) if (!this.whitelist.has(c)) { if (!delta.remove.whitelist) delta.remove.whitelist = []; delta.remove.whitelist.push(c); } for (const f of this.suspiciousFlags) if (!this._baseSuspiciousFlags.includes(f)) { if (!delta.add.flags) delta.add.flags = []; delta.add.flags.push(f); } for (const f of this._baseSuspiciousFlags) if (!this.suspiciousFlags.has(f)) { if (!delta.remove.flags) delta.remove.flags = []; delta.remove.flags.push(f); } localStorage.setItem('webx11_security_deltas', JSON.stringify(delta)); } _registerBuiltIn() { const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','dashboard']; internals.forEach(c => this.registerCommand(c, { category: 'builtin' })); } setLevel(n) { if (n >= 0 && n <= 3) { this.level = n; this.terminal.print(`Security level set to ${n} (${this.getLevelName()})`, 'success'); if (n === 0) { this.terminal.print('Warning: Permissive mode — commands will not be blocked or confirmed.', 'warning'); } } else { this.terminal.print('Level must be 0-3', 'error'); } } getLevel() { return this.level; } getLevelName() { return ['Permissive','Standard','Paranoid','Audit'][this.level] || 'Unknown'; } registerCommand(name, meta = {}) { this.commandMeta.set(name, { destructive: false, category: 'user', ...meta }); this.invalidateCommandCache(); } invalidateCommandCache() { this._commandCacheDirty = true; } _collectAllCommands() { if (!this._commandCacheDirty && this._commandCache) return this._commandCache; const cmds = new Set([...this.commandMeta.keys()]); for (const modName in this.terminal.moduleManager.modules) { const mod = this.terminal.moduleManager.modules[modName]; if (mod.commands) { for (const c in mod.commands) cmds.add(c); } } const builtins = ['clear','help','ls','df','security','security-level']; builtins.forEach(c => cmds.add(c)); this._commandCache = [...cmds]; this._commandCacheDirty = false; return this._commandCache; } _levenshtein(a, b) { a = a.toLowerCase(); b = b.toLowerCase(); const m = a.length, n = b.length; if (!m) return n; if (!n) return m; let prev = new Uint16Array(n + 1); let curr = new Uint16Array(n + 1); for (let j = 0; j <= n; j++) prev[j] = j; for (let i = 1; i <= m; i++) { curr[0] = i; for (let j = 1; j <= n; j++) { const cost = a[i - 1] === b[j - 1] ? 0 : 1; curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost); } [prev, curr] = [curr, prev]; } return prev[n]; } suggest(input, maxDist = 2) { const all = this._collectAllCommands(); const lower = input.toLowerCase(); let best = '', bestDist = Infinity; for (const c of all) { const d = this._levenshtein(lower, c); if (d < bestDist && d <= maxDist) { bestDist = d; best = c; } } return best ? { cmd: best, dist: bestDist } : null; } _parseArgs(input) { const args = []; let current = ''; let inDouble = false; let inSingle = false; for (const ch of input) { if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; } if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; } if (ch === ' ' && !inDouble && !inSingle) { if (current) { args.push(current); current = ''; } } else { current += ch; } } if (current) args.push(current); return args; } _classify(raw) { const parts = this._parseArgs(String(raw || '').trim()); // `help |` and `| wc -l` produce an empty pipeline stage; parts[0] was undefined // and every such invocation threw TypeError from this line (989 occurrences, // first filed 2026-10-20). Classify the empty command as inert instead. if (!parts.length) { return { cmd: '', args: [], known: true, destructive: false, system: false, unknown: false, hasSuspicious: false }; } const cmd = parts[0].toLowerCase(); const args = parts.slice(1); // Internal commands always allowed at terminal level, but security still classifies const all = this._collectAllCommands(); const known = all.includes(cmd); const hasSuspicious = args.some(a => this.suspiciousFlags.has(a.toLowerCase())); const destructive = this.destructiveCmds.has(cmd) || hasSuspicious; const system = this.systemCmds.has(cmd); const unknown = !known; return { cmd, args, known, destructive, system, unknown, hasSuspicious }; } // v4.6.58: this terminal has no shell. `;` at least failed loudly, but `&&`/`||` // folded the right-hand side into the left command's argv, and a `|` arriving from // the AI run_command tool (which bypasses splitPipe) degraded to an empty pattern // matching every line. Both are silent wrong answers — refuse instead (packet §1). detectComposition(input) { const s = String(input || ''); let inSingle = false, inDouble = false; for (let i = 0; i < s.length; i++) { const ch = s[i]; if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; } if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; } if (inSingle || inDouble) continue; if (ch === '&' && s[i + 1] === '&') return '&&'; if (ch === '|' && s[i + 1] === '|') return '||'; if (ch === '|') return '|'; if (ch === ';') return ';'; } return null; } async checkAndRun(rawInput) { const badOp = this.detectComposition(rawInput); if (badOp) { this.terminal.print('⚠️ No shell here — `' + badOp + '` is not interpreted. ' + 'Run one command per line (the interactive terminal supports `cmd | cmd`; ' + 'AI run_command does not).', 'error'); return; } const { cmd, args, known, destructive, system, unknown, hasSuspicious } = this._classify(rawInput); // Level 3 Audit: just log and allow if (this.level === 3) { this.terminal.print(`[AUDIT] ${rawInput} → known=${known} destructive=${destructive} system=${system}`, 'info'); return this._dispatch(cmd, args); } // Always allow builtins that are safe, regardless of level if (['help','security','security-level'].includes(cmd)) { return this._dispatch(cmd, args); } // 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); // At level 2+: block entirely (paranoid) if (this.level >= 2) { this.terminal.print(`Security: Unknown command "${cmd}" blocked (level ${this.level}).${sug ? ` Did you mean: "${sug.cmd}"?` : ''}`, 'error'); return; } // Level 1: typo of a safe whitelisted command → one-tap clickable fix, no AI call burned if (sug && sug.dist <= 1 && this.whitelist.has(sug.cmd) && this.terminal.showSuggestionBar && !(this._aiToolDepth > 0)) { this.terminal.print(`Unknown command "${cmd}" — did you mean:`, 'warning'); this.terminal.showSuggestionBar(sug.cmd, args); return; } // AI-invoked commands must not route back to AI (infinite loop) — return structured error if (this._aiToolDepth > 0) { this.terminal.print(`Command not found: ${cmd}${sug ? `. Did you mean: ${sug.cmd}?` : ''}`, 'error'); return; } // Level 1 fallback: route to AI (terminal is also a chat interface) if (sug) { this.terminal.print(`Security: Unknown command "${cmd}" — routing to AI (did you mean "${sug.cmd}"?)...`, 'warning'); } else { this.terminal.print(`Security: Unknown command "${cmd}" — routing to AI...`, 'info'); } return this._tryAIRoute(rawInput); } // Suspicious flags (e.g. --execute) at any level > 0 if (hasSuspicious && this.level > 0) { this.terminal.print(`Security: Suspicious flag detected in "${rawInput}" — blocked.`, 'error'); this.blockedCount++; return; } // Destructive commands if (destructive) { if (this.level === 0) { this.terminal.print(`Warning: destructive command "${cmd}"`, 'warning'); return this._dispatch(cmd, args); } if (this.level >= 1) { // Reuse your existing confirm UI if present, otherwise print confirm message if (this.terminal.showConfirmationBar) { this.terminal._pendingCommand = () => this._dispatch(cmd, args); this.terminal.showConfirmationBar(`Run destructive: ${rawInput}`); return; } else { this.terminal.print(`Security: Destructive command "${cmd}" requires confirmation.`, 'warning'); return; } } } // System commands at level >=1 if (system && this.level >= 1) { this.terminal.print(`Security: System command "${cmd}" blocked (level ${this.level}).`, 'error'); this.blockedCount++; return; } // Standard dispatch for everything else return this._dispatch(cmd, args); } async _dispatch(cmd, args) { return this.terminal.dispatchCommand(cmd, args); } async _tryAIRoute(rawInput) { // AI routing cooldown guard const now = Date.now(); if (this.lastRouteCmd === rawInput && (now - this.lastRouteTime) < 5000) { this.terminal.print(`Security: Already routed "${rawInput}" to AI recently. Use 'security-level 0' to force.`, 'warning'); return; } this.lastRouteCmd = rawInput; this.lastRouteTime = now; try { const aiMod = this.terminal.moduleManager.modules['WebX11AIAssistantModule']; if (aiMod) { this.terminal.print('→ Routing to AI assistant...', 'info'); await aiMod.chat([rawInput.trim()]); } } catch (e) { this.terminal.print(`Security: AI error: ${e.message}`, 'error'); } } } class WebX11Terminal { constructor() { this.outputContainer = document.getElementById('terminal-container'); this.inputField = document.getElementById('command-input'); this.dropdown = document.getElementById('autocomplete-dropdown'); this.statusBar = document.getElementById('status-text'); this.networkStatus = document.getElementById('network-status'); this.fs = new VirtualFileSystem(); this.secretVault = null; try { if (typeof SecretVault !== 'undefined') { this.secretVault = new SecretVault(this); window.webx11_secretVault = this.secretVault; } } catch (e) { console.warn('SecretVault init:', e.message); } this.moduleManager = new ModuleManager(this); this.security = new WebX11SecurityManager(this, parseInt(localStorage.getItem('webx11_security_level')) || 1); this.ollamaManager = new OllamaManager(this); // Config registry — modules can register keys for the unified `config` command this._configRegistry = {}; this._configDefaults = {}; this._configSetters = {}; this._loadTerminalConfig(); // Register built-in terminal config keys this.registerConfigItem('terminal-max-lines', 100, (v) => { this.MAX_OUTPUT_LINES = v; }); this.registerConfigItem('terminal-hist-output', 50, (v) => { this._historyMaxLines = v; }); this.registerConfigItem('terminal-cmd-history', 200, (v) => { /* applied on next save */ }); this.registerConfigItem('terminal-pipe-buffer', 5000, (v) => { this._pipeBufferSize = v; }); this.registerConfigItem('terminal-sysmsg', true, (v) => { this._showSystemMessages = !!v; }); this._pipeInput = null; this._pipeBufferSize = 5000; this._activeFollow = null; // Print-capture stack: [{output: ''}] — see print() head. LIFO order: // nested/parallel captures each get their own buffer. this._printCaptureStack = []; this._historyBuffer = []; this._historyMaxLines = 50; this._historySavePending = false; this.MAX_OUTPUT_LINES = 100; this._showSystemMessages = true; this._historyCap = 200; // Apply history cap from localStorage on startup (was applied "on next save" before — broken) try { const cfgCap = parseInt(localStorage.getItem('webx11_terminal_cmd_history')); if (cfgCap && cfgCap > 0) this._historyCap = cfgCap; } catch (e) {} this._aiBusy = false; this._dirty = false; this.history = JSON.parse(localStorage.getItem('webx11_command_history') || '[]'); this.historyIndex = this.history.length; this.selectedSuggestion = -1; this.suggestions = []; this._isCdAutocomplete = false; this._autocompletePrefix = ''; this._welcomeShown = false; this.initEventListeners(); this.updateNetworkStatus(); this.loadBuiltInModules(); this._loadHistory(); // Auto-restore session this._autoRestoreSession(); this._restoreFontSize(); // Restore saved theme on boot this._restoreTheme(); // Init auto-save timer — 60s after boot, then every 60s setTimeout(() => this._initAutoSave(), 30000); // Init sound toggle button text const soundBtn = document.getElementById('sound-toggle-btn'); if (soundBtn) { soundBtn.textContent = localStorage.getItem('webx11_config_ai_sound') === 'off' ? '🔇 Muted' : '🔊 Sound'; } setTimeout(() => this.inputField.focus(), 100); // First-run welcome check — fires after ext modules are loaded setTimeout(() => this._checkFirstRun(), 2000); // Init all registered modules (safe now — constructor is done) this._initRegisteredModules(); // Apply config overrides after everything is initialized this._applyTerminalConfig(); } /* ---------- helpers ---------- */ static _escapeHtml(str) { if (str == null) return ''; return String(str).replace(/[&<>'"]/g, m => ({'&':'&','<':'<','>':'>',"'":''','"':'"'}[m])); } _escapeHtml(str) { return WebX11Terminal._escapeHtml(str); } _getDataSchemaVersion() { return 1; } _escapeYamlString(str) { return '"' + String(str == null ? '' : str).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'; } _invalidateCommandCache() { if (this.security) this.security.invalidateCommandCache(); } _pushPrintCapture(onChunk) { // onChunk(buffer) called at pop with the full captured text if (!this._printCaptureStack) this._printCaptureStack = []; this._printCaptureStack.push({ output: '', onChunk }); } _popPrintCapture() { const cap = this._printCaptureStack && this._printCaptureStack.pop(); if (cap && cap.onChunk) cap.onChunk(cap.output); } stopFollow(msg) { if (!this._activeFollow) return false; clearInterval(this._activeFollow.interval); const f = this._activeFollow.file; this._activeFollow = null; if (msg) this.print(msg, 'warning'); else this.print('^C — stopped following ' + f, 'warning'); return true; } initEventListeners() { this.inputField.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (this.selectedSuggestion >= 0 && this.suggestions[this.selectedSuggestion]) { if (this._autocompletePrefix) { this.inputField.value = this._autocompletePrefix + this.suggestions[this.selectedSuggestion]; } else if (!this._isCdAutocomplete) { this.inputField.value = this.suggestions[this.selectedSuggestion]; } } const cmd = this.inputField.value; // Dedup: don't push if same as last entry if (this.history.length === 0 || this.history[this.history.length - 1] !== cmd) { this.history.push(cmd); } // Trim to max 200 entries if (this.history.length > this._historyCap) { this.history = this.history.slice(-this._historyCap); } this.historyIndex = this.history.length; this._saveCommandHistory(); this.executeCommand(cmd); this.inputField.value = ''; this.inputField.style.height = 'auto'; this.hideAutocomplete(); } else if (e.key === 'ArrowUp') { e.preventDefault(); if (this.historyIndex > 0) { this.historyIndex--; this.inputField.value = this.history[this.historyIndex]; } this.hideAutocomplete(); } else if (e.key === 'ArrowDown') { e.preventDefault(); if (this.historyIndex < this.history.length - 1) { this.historyIndex++; this.inputField.value = this.history[this.historyIndex]; } else { this.historyIndex = this.history.length; this.inputField.value = ''; } this.hideAutocomplete(); } else if (e.key === 'Tab') { e.preventDefault(); if (this.suggestions.length > 0) { if (e.shiftKey) { this.selectedSuggestion = this.selectedSuggestion <= 0 ? this.suggestions.length - 1 : this.selectedSuggestion - 1; } else { 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.ctrlKey && (e.key === 'l' || e.key === 'L')) { // Ctrl+L — clear output (standard terminal) e.preventDefault(); this.clearOutput(); } else if (e.ctrlKey && (e.key === 'c' || e.key === 'C')) { // Ctrl+C — stop tail -f if active, else cancel current input line (standard terminal) e.preventDefault(); if (this.stopFollow()) return; if (this.inputField.value) { this.print('^C', 'warning'); this.inputField.value = ''; this.inputField.style.height = 'auto'; this.hideAutocomplete(); } return; } else if (e.ctrlKey && (e.key === 'u' || e.key === 'U')) { // Ctrl+U — clear line from cursor to start (standard terminal) e.preventDefault(); this.inputField.value = ''; this.inputField.style.height = 'auto'; } else if (e.key === 'Escape') { if (this._aiBusy) { this.executeCommand('ai-stop'); } else if (this.stopFollow()) { // tail -f stopped } else { this.hideAutocomplete(); } } }); this.inputField.addEventListener('input', () => { this.inputField.style.height = 'auto'; this.inputField.style.height = Math.min(this.inputField.scrollHeight, 120) + 'px'; this.showAutocomplete(); }); this.inputField.addEventListener('focus', () => { this.showAutocomplete(); }); document.addEventListener('click', (e) => { if (!e.target.closest('#input-area')) { this.hideAutocomplete(); } }); // 2026-10-22: dashboard.html wrote this key on ⟳ Refresh and nothing listened. window.addEventListener('storage', function (ev) { if (ev.key !== 'webx11_dashboard_data_request') return; const aiMod = this.moduleManager && this.moduleManager.modules ? this.moduleManager.modules.WebX11AIAssistantModule : null; if (aiMod && typeof aiMod._collectDashboardData === 'function') { aiMod._collectDashboardData(); } }.bind(this)); window.addEventListener('online', () => this.updateNetworkStatus()); window.addEventListener('offline', () => this.updateNetworkStatus()); document.getElementById('file-upload-input').addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (evt) => { this.fs.writeFile(file.name, evt.target.result); this.markDirty(); this.print(`File '${file.name}' uploaded successfully.`, 'success'); }; reader.readAsText(file); } e.target.value = ''; }); // Voice mic button // Sound toggle button const soundBtn = document.getElementById('sound-toggle-btn'); if (soundBtn) { soundBtn.addEventListener('click', () => { const current = localStorage.getItem('webx11_config_ai_sound'); if (current === 'off') { localStorage.setItem('webx11_config_ai_sound', 'on'); soundBtn.textContent = '🔊 Sound'; } else { localStorage.setItem('webx11_config_ai_sound', 'off'); soundBtn.textContent = '🔇 Muted'; } }); } // Mic toggle button const micToggleBtn = document.getElementById('voice-toggle-btn'); if (micToggleBtn) { micToggleBtn.addEventListener('click', () => this.toggleMicVisibility()); } } updateNetworkStatus() { const isOnline = navigator.onLine; this.networkStatus.textContent = isOnline ? '📶 Online' : '❌ Offline'; this.networkStatus.style.color = isOnline ? 'var(--text-color)' : 'var(--error-color)'; } updatePrompt() { const path = '/' + this.fs.currentPath.join('/'); const label = document.querySelector('.prompt-label'); if (!label) return; if (localStorage.getItem('webx11_smart_prompt') === 'off') { label.textContent = path === '/' ? '$' : path + ' $'; return; } const indicators = []; try { const p = localStorage.getItem('webx11_user_profile'); if (p) { const pf = JSON.parse(p); if (pf.knownFacts && pf.knownFacts.name) indicators.push('👤' + pf.knownFacts.name); } } catch (e) {} try { const idx = this.fs.fsReadJSON('/skills/index.json'); if (idx) { const c = Object.keys(idx).length; if (c > 0) indicators.push('📚' + c); } } catch (e) {} try { const memIdx = this.fs.fsReadJSON('/memory/index.json'); if (memIdx) { const c = Object.keys(memIdx).length; if (c > 0) indicators.push('🧠' + c); } } catch (e) {} try { if (performance.memory && performance.memory.usedJSHeapSize > 80 * 1048576) { indicators.push('💾' + (performance.memory.usedJSHeapSize / 1048576).toFixed(0) + 'MB'); } } catch (e) {} try { const md = this.fs.readDir('_mods'); if (md && md.content) { const e = Object.keys(md.content).filter(k => k.endsWith('.meta.json')); if (e.length > 0) indicators.push('⚡' + e.length); } } catch (e) {} const prefix = indicators.length ? indicators.join(' ') + ' ' : ''; label.textContent = prefix + (path === '/' ? '$' : path + ' $'); } print(text, type = '') { // Print-capture stack: while active, output goes to the innermost capture // buffer instead of the DOM. Reentrant — parallel/nested captures each // collect their own output (race-safe, replaces monkey-patched print). if (this._printCaptureStack && this._printCaptureStack.length > 0) { const cap = this._printCaptureStack[this._printCaptureStack.length - 1]; cap.output += text + '\n'; return; } if (['error','warning','success','info','text-muted'].includes(type) && !this._showSystemMessages && !type.startsWith('command')) { // Still log to history buffer so it can be shown again when toggled on, // but don't create DOM nodes while system messages are hidden. this._historyBuffer.push({ l: text, t: type }); if (this._historyBuffer.length > this._historyMaxLines) this._historyBuffer.shift(); return; } const line = document.createElement('div'); // Use card layout for typed messages (error/warning/info/success/text-muted), plain for bare text const isTyped = ['error','warning','success'].includes(type); if (isTyped) { line.className = 'output-line msg-card system ' + type; const safeText = String(text).replace(//g, ">"); line.innerHTML = '
${runCmd}?
`;
area.classList.add('visible');
const allowBtn = document.getElementById('cmd-confirm-allow');
const denyBtn = document.getElementById('cmd-confirm-deny');
const onAllow = () => { area.classList.remove('visible'); area.innerHTML = ''; this.executeCommand(runCmd); };
const onDeny = () => { area.classList.remove('visible'); area.innerHTML = ''; this.print('Dismissed.', 'text-muted'); };
allowBtn.addEventListener('click', onAllow);
denyBtn.addEventListener('click', onDeny);
allowBtn.__allow = onAllow;
denyBtn.__deny = onDeny;
}
showConfirmationBar(desc) {
const area = document.getElementById('ai-confirm-area');
if (!area) {
this.print('Confirmation UI unavailable — command blocked.', 'error');
return;
}
this._pendingCommandDesc = desc;
area.innerHTML = `
⚠️ ${desc}
Type: cmd-allow or cmd-deny
`;
area.classList.add('visible');
const allowBtn = document.getElementById('cmd-confirm-allow');
const denyBtn = document.getElementById('cmd-confirm-deny');
const onAllow = () => {
this._resolveCmdConfirm(true);
};
const onDeny = () => {
this._resolveCmdConfirm(false);
};
allowBtn.addEventListener('click', onAllow);
denyBtn.addEventListener('click', onDeny);
allowBtn.__allow = onAllow;
denyBtn.__deny = onDeny;
}
_resolveCmdConfirm(approved) {
const area = document.getElementById('ai-confirm-area');
if (area) {
area.classList.remove('visible');
area.innerHTML = '';
}
if (approved) {
this.print(`Allowed: ${this._pendingCommandDesc || 'command'}`, 'success');
if (this._pendingCommand) {
const fn = this._pendingCommand;
this._pendingCommand = null;
this._pendingCommandDesc = null;
fn();
}
} else {
this.print(`Denied: ${this._pendingCommandDesc || 'command'}`, 'warning');
this._pendingCommand = null;
this._pendingCommandDesc = null;
}
}
_loadHistory() {
try {
const saved = localStorage.getItem('webx11_terminal_output');
if (!saved) return;
const entries = JSON.parse(saved);
if (!Array.isArray(entries)) return;
this._historyBuffer = entries.slice(-this._historyMaxLines);
// Check localStorage quota warning
let quotaWarned = false;
try {
const testKey = '_quota_test_' + Date.now();
localStorage.setItem(testKey, 'x'.repeat(1024));
localStorage.removeItem(testKey);
} catch (e) {
quotaWarned = true;
}
// Lazy-load history: render first 20 immediately, batch the rest
const batchSize = 20;
const toRender = entries.slice(-this._historyMaxLines);
const initial = toRender.slice(0, batchSize);
for (const entry of initial) {
const line = document.createElement('div');
line.className = `output-line ${entry.t || ''}`;
line.textContent = entry.l || '';
this.outputContainer.appendChild(line);
}
if (toRender.length > batchSize) {
const remaining = toRender.slice(batchSize);
const MAX_BATCHES = 50;
const doBatch = (index, batchCount) => {
if (index >= remaining.length || batchCount >= MAX_BATCHES) {
this.scrollToBottom();
return;
}
requestAnimationFrame(() => {
const end = Math.min(index + batchSize, remaining.length);
for (let i = index; i < end; i++) {
const line = document.createElement('div');
line.className = `output-line ${remaining[i].t || ''}`;
line.textContent = remaining[i].l || '';
this.outputContainer.appendChild(line);
}
doBatch(end, (batchCount || 0) + 1);
});
};
doBatch(0, 0);
} else {
this.scrollToBottom();
}
if (quotaWarned) {
const warnLine = document.createElement('div');
warnLine.className = 'output-line warning';
warnLine.textContent = '⚠ localStorage is near quota — history may be incomplete';
this.outputContainer.appendChild(warnLine);
}
} catch (e) {
// Silently ignore corrupt history data
}
}
_saveHistory() {
if (this._historySaveTimer) clearTimeout(this._historySaveTimer);
this._historySaveTimer = setTimeout(() => {
this._historySaveTimer = null;
try {
const slice = this._historyBuffer.slice(-this._historyMaxLines);
localStorage.setItem('webx11_terminal_output', JSON.stringify(slice));
} catch (e) {
// Silently ignore storage errors (e.g. quota exceeded)
}
}, 3000);
}
_saveCommandHistory() {
try {
localStorage.setItem('webx11_command_history', JSON.stringify(this.history));
} catch (e) {
// Silently ignore storage errors
}
}
_trimOutput() {
const container = this.outputContainer;
while (container.children.length > this.MAX_OUTPUT_LINES) {
container.removeChild(container.firstChild);
}
}
toggleExpand() {
const container = document.getElementById('terminal-container');
if (!container) return;
container.classList.toggle('terminal-expanded');
const on = container.classList.contains('terminal-expanded');
this.print(on ? 'Terminal expanded' : 'Terminal compact', 'text-muted');
}
toggleSystemMessages(arg) {
let desired;
if (arg === 'on') desired = true;
else if (arg === 'off') desired = false;
else desired = !this._showSystemMessages;
this._showSystemMessages = desired;
this._saveTerminalConfig();
this.print('System messages ' + (desired ? 'on' : 'off'), 'text-muted');
}
clearOutput() {
if (this.outputContainer) {
this.outputContainer.innerHTML = '';
}
this._historyBuffer = [];
if (this._historySaveTimer) {
clearTimeout(this._historySaveTimer);
this._historySaveTimer = null;
}
try { localStorage.removeItem('webx11_terminal_output'); } catch (e) {}
}
markDirty() {
this._dirty = true;
}
_initAutoSave() {
if (this._autoSaveTimer) return;
this._autoSaveTimer = setInterval(() => {
// Auto-trim on memory pressure
try {
if (performance.memory && performance.memory.usedJSHeapSize > 80 * 1048576) {
const oldMax = this.MAX_OUTPUT_LINES;
this.MAX_OUTPUT_LINES = performance.memory.usedJSHeapSize > 120 * 1048576 ? 50 : 100;
this._trimOutput();
this.MAX_OUTPUT_LINES = oldMax;
this.print('🧹 Terminal trimmed (memory: ' + (performance.memory.usedJSHeapSize / 1048576).toFixed(0) + ' MB)', 'text-muted');
}
} catch (e) {}
if (!this._dirty) return;
this._autoSaveSession();
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;
}
});
}
_autoSaveSession() {
try {
this.fs.mkdirRecursive('_sessions');
const snapshot = {
type: 'session',
version: 2,
savedAt: new Date().toISOString(),
currentPath: this.fs.currentPath,
terminalLines: this._historyBuffer.slice(-50)
};
const snapshotJson = JSON.stringify(snapshot);
this.fs.writeFile('_sessions/_auto.json', snapshotJson);
try {
const ctx = localStorage.getItem('webx11_ai_context');
if (ctx) this.fs.writeFile('_sessions/_auto-context.json', ctx);
} catch (e) {}
} catch (e) {
// Silently handle auto-save failures
}
}
_autoRestoreSession() {
try {
const lastReset = parseInt(localStorage.getItem('webx11_last_reset') || '0', 10);
this.fs.mkdirRecursive('_sessions');
const autoSession = this.fs.readFile('_sessions/_auto.json');
if (autoSession) {
const data = JSON.parse(autoSession);
const savedAt = data.savedAt || 0;
// v4.5.47: skip restoring backups created before the last reset
if (lastReset && savedAt && savedAt < lastReset) {
this.fs.deleteFile('_sessions/_auto.json');
this.fs.deleteFile('_sessions/_auto-context.json');
this.print('↻ Skipped stale auto-session backup (older than last reset)', 'info');
return;
}
if (data.version === 2) {
// v2: metadata-only restore (VFS is persisted independently in localStorage)
this.fs.currentPath = data.currentPath || [];
this.fs.saveToStorage();
this.updatePrompt();
this.print('↻ Auto-restored session from ' + (data.savedAt ? new Date(data.savedAt).toLocaleTimeString() : 'recent'), 'success');
// Optionally re-render last few terminal lines
if (data.terminalLines && Array.isArray(data.terminalLines)) {
data.terminalLines.slice(-10).forEach(line => {
const text = typeof line === 'string' ? line : (line.l || '');
this.print(text, typeof line === 'object' ? (line.t || '') : '');
});
}
try {
const aiCtx = this.fs.readFile('_sessions/_auto-context.json');
if (aiCtx) {
localStorage.setItem('webx11_ai_context', aiCtx);
}
} catch (e) {}
this.fs.deleteFile('_sessions/_auto.json');
this.fs.deleteFile('_sessions/_auto-context.json');
} else if (data && data.fsRoot) {
// v1: full VFS restore (legacy)
this.fs.root = data.fsRoot;
this.fs.currentPath = data.currentPath || [];
this.fs.saveToStorage();
this.updatePrompt();
this.print('↻ Auto-restored session from ' + (data.savedAt ? new Date(data.savedAt).toLocaleTimeString() : 'recent'), 'success');
try {
const aiCtx = this.fs.readFile('_sessions/_auto-context.json');
if (aiCtx) {
localStorage.setItem('webx11_ai_context', aiCtx);
}
} catch (e) {}
this.fs.deleteFile('_sessions/_auto.json');
this.fs.deleteFile('_sessions/_auto-context.json');
}
}
} catch (e) {
// Silent on first boot — no auto-session yet
}
}
scrollToBottom() {
this.outputContainer.scrollTop = this.outputContainer.scrollHeight;
}
_getPathSuggestions(partial, type) {
// type: 'dir' = dirs only, 'file' = files + dirs (dirs get trailing /), 'session' = _sessions/*.json
if (type === 'session') {
this.fs.mkdirRecursive('_sessions');
const all = this.fs.ls('_sessions');
const names = all
.filter(e => e.type === 'file' && e.name.endsWith('.json'))
.map(e => e.name.replace('.json', ''));
return partial
? names.filter(n => n.startsWith(partial)).map(n => n)
: names;
}
// Resolve the partial path to determine which directory to list
let dirPath = '';
let filterPrefix = '';
let entries = [];
let prefix = '';
if (partial.includes('/')) {
// "dir1/dir2" or "dir1/dir2/" or "dir1/"
const lastSlash = partial.lastIndexOf('/');
const beforeSlash = partial.slice(0, lastSlash); // dir1
filterPrefix = partial.slice(lastSlash + 1); // dir2 (or '' if ends with /)
// Resolve "dir1" relative to current path
const resolvedDir = this.fs._resolveDir(beforeSlash || '.');
if (!resolvedDir) return [];
entries = Object.keys(resolvedDir.content).map(name => ({
name,
type: resolvedDir.content[name].type,
size: resolvedDir.content[name].content ? resolvedDir.content[name].content.length : 0
}));
prefix = beforeSlash + '/';
} else {
// No slash — list current dir
entries = this.fs.ls();
filterPrefix = partial;
prefix = '';
}
if (type === 'dir') {
return entries
.filter(e => e.type === 'dir' && e.name.startsWith(filterPrefix))
.map(e => prefix + e.name + '/');
}
// 'file' — show files + dirs (dirs with trailing /)
return entries
.filter(e => e.name.startsWith(filterPrefix))
.map(e => e.type === 'dir' ? prefix + e.name + '/' : prefix + e.name);
}
// ── Enum autocomplete from commandsMeta ──
_tryEnumAutocomplete(cmdName) {
this.suggestions = [];
for (const modName in this.moduleManager.modules) {
const mod = this.moduleManager.modules[modName];
const meta = mod.commandsMeta && mod.commandsMeta[cmdName];
if (meta && meta.argType === 'enum' && meta.enumFn && typeof mod[meta.enumFn] === 'function') {
const values = mod[meta.enumFn]();
if (Array.isArray(values) && values.length > 0) {
this.suggestions.push(...values);
return true;
}
}
}
return false;
}
showAutocomplete() {
if (!this.moduleManager || !this.security) return;
const input = this.inputField.value;
// Always clear dropdown innerHTML at the start — prevents stale state from prior input
this.dropdown.innerHTML = '';
this.suggestions = [];
this.selectedSuggestion = -1;
this._isCdAutocomplete = false;
const trimmed = input.trim();
const commands = this.moduleManager.getAllCommands() || {};
// Merge built-in internals into autocomplete (they're handled via if-chain, not module commands)
for (const c of this.security.commandMeta.keys()) {
if (!(c in commands)) commands[c] = 'builtin';
}
if (input.trim().length === 0) {
this.hideAutocomplete();
return;
}
const parts = input.split(' ');
const cmdPart = parts[0].toLowerCase();
const lowerInput = input.toLowerCase();
// Expanded command lists for autocomplete (fallback when commandsMeta lacks argType)
// NOTE: Commands with argType:'enum' in their commandsMeta are handled by _tryEnumAutocomplete
// and should NOT be in these hardcoded lists (the enum check is called BEFORE these).
const fileArgCommands = ['cat', 'preview', 'rm', 'rmdir', 'edit', 'view', 'cp', 'mv', 'touch', 'put', 'run', 'chmod', 'gzip', 'gunzip', 'encrypt', 'decrypt', 'download', 'webdav-backup', 'webdav-restore', 'webdav-upload', 'webdav-download', 'webdav-list', 'trash', 'ls-trash', 'upload', 'vfs-export', 'vfs-import', 'files', 'preview', 'skill-view', 'skill-create', 'skill-patch', 'skill-delete', 'skill-sync', 'webdav-ls', 'webdav-push', 'webdav-pull', 'webdav-rm', 'webdav-mkdir', 'webdav-rename', 'webdav-move', 'webdav-config', 'nvfs-index', 'nvfs-resolve', 'nvfs-tree'];
const dirArgCommands = ['ls', 'cd', 'mkdir', 'rmdir', 'upload', 'webdav-ls', 'webdav-mkdir', 'vfs-export', 'vfs-import', 'skill-create'];
const sessionArgCommands = ['session-load', 'session-delete', 'session-export', 'session-save', 'session-list', 'session-import', 'session-new'];
const pathArgCommands = ['cat', 'preview', 'rm', 'edit', 'view', 'cp', 'mv', 'touch', 'put', 'run', 'chmod', 'gzip', 'gunzip', 'encrypt', 'decrypt', 'download', 'webdav-backup', 'webdav-restore', 'webdav-upload', 'webdav-download', 'webdav-list', 'ls', 'cd', 'mkdir', 'rmdir', 'cat', 'preview'];
// Collect commands that take arguments from ext module metadata
const extFileCmds = new Set();
const extDirCmds = new Set();
for (const modName in this.moduleManager.modules) {
const mod = this.moduleManager.modules[modName];
if (mod.commandsMeta) {
for (const [c, meta] of Object.entries(mod.commandsMeta)) {
if (meta.argType === 'file' || meta.argType === 'path') extFileCmds.add(c);
if (meta.argType === 'dir') extDirCmds.add(c);
}
}
}
// If we have more than one word, we're past the command name — autocomplete file/dir/session names
if (parts.length > 1 && parts[1].length > 0) {
const partial = parts.slice(1).join(' ');
const partialTrimmed = partial.trim();
if (partialTrimmed === '..') {
this.hideAutocomplete();
return;
}
if (this._tryEnumAutocomplete(cmdPart)) {
this.suggestions = this.suggestions.filter(s => s.startsWith(partialTrimmed));
} else if (sessionArgCommands.includes(cmdPart)) {
this.suggestions = this._getPathSuggestions(partialTrimmed, 'session');
} else if (dirArgCommands.includes(cmdPart) || extDirCmds.has(cmdPart)) {
this.suggestions = this._getPathSuggestions(partialTrimmed, 'dir');
this._isCdAutocomplete = true;
} else if (fileArgCommands.includes(cmdPart) || extFileCmds.has(cmdPart)) {
this.suggestions = this._getPathSuggestions(partialTrimmed, 'file');
} else {
// Fallback: show files anyway
this.suggestions = this._getPathSuggestions(partialTrimmed, 'file');
}
this._autocompletePrefix = cmdPart + ' ';
if (this.suggestions.length > 0) {
this.renderAutocomplete();
} else {
this.hideAutocomplete();
}
return;
}
// Just typed a space after command — show all matching entries
if (parts.length > 1) {
if (this._tryEnumAutocomplete(cmdPart)) {
// suggestions already populated
} else if (sessionArgCommands.includes(cmdPart)) {
this.suggestions = this._getPathSuggestions('', 'session');
} else if (dirArgCommands.includes(cmdPart) || extDirCmds.has(cmdPart)) {
this.suggestions = this._getPathSuggestions('', 'dir');
this._isCdAutocomplete = true;
} else if (fileArgCommands.includes(cmdPart) || extFileCmds.has(cmdPart)) {
this.suggestions = this._getPathSuggestions('', 'file');
} else {
// Fallback: show files
this.suggestions = this._getPathSuggestions('', 'file');
}
this._autocompletePrefix = cmdPart + ' ';
if (this.suggestions.length > 0) {
this.renderAutocomplete();
} else {
this.hideAutocomplete();
}
return;
}
// Directory autocomplete for bare 'cd ' (no partial yet)
if (lowerInput.startsWith('cd ')) {
this.suggestions = this._getPathSuggestions('', 'dir');
if (this.suggestions.length > 0) {
this._isCdAutocomplete = true;
this.renderAutocomplete();
} else {
this.hideAutocomplete();
}
return;
}
// Command name autocomplete (first word only) — also clears any stale prefix
this._autocompletePrefix = '';
for (const cmd in commands) {
if (cmd.startsWith(cmdPart) && cmd !== cmdPart) {
this.suggestions.push(cmd);
}
}
if (this.suggestions.length > 0) {
this.renderAutocomplete();
} else {
this.hideAutocomplete();
}
}
renderAutocomplete() {
this.dropdown.innerHTML = '';
this.suggestions.forEach((item, index) => {
const div = document.createElement('div');
div.className = `autocomplete-item ${index === this.selectedSuggestion ? 'selected' : ''}`;
div.innerHTML = this._isCdAutocomplete
? `${item}`
: `${item}`;
div.onclick = () => {
if (this._autocompletePrefix) {
this.inputField.value = this._autocompletePrefix + item;
} else if (this._isCdAutocomplete) {
this.inputField.value = 'cd ' + item;
} else {
this.inputField.value = item + ' ';
}
this.hideAutocomplete();
this.inputField.focus();
};
this.dropdown.appendChild(div);
});
this.dropdown.style.display = 'block';
}
updateAutocompleteDisplay() {
const items = this.dropdown.querySelectorAll('.autocomplete-item');
items.forEach((item, index) => {
item.classList.toggle('selected', index === this.selectedSuggestion);
});
if (this.selectedSuggestion >= 0 && this.suggestions[this.selectedSuggestion]) {
if (this._autocompletePrefix) {
this.inputField.value = this._autocompletePrefix + this.suggestions[this.selectedSuggestion];
} else if (this._isCdAutocomplete) {
this.inputField.value = 'cd ' + this.suggestions[this.selectedSuggestion];
} else {
this.inputField.value = this.suggestions[this.selectedSuggestion] + ' ';
}
}
}
hideAutocomplete() {
this.dropdown.style.display = 'none';
this.suggestions = [];
this.selectedSuggestion = -1;
this._isCdAutocomplete = false;
this._autocompletePrefix = '';
}
loadBuiltInModules() {
this._safeRegister('SystemModule', 'system');
this._safeRegister('FileSystemModule', 'filesystem');
this._safeRegister('ModuleManagerModule', 'module manager');
this._safeRegister('OllamaModule', 'ollama');
this._safeRegister('SearXNGModule', 'searxng search');
this._safeRegister('ConfigManagerModule', 'config');
this._safeRegister('WebX11AIAssistantModule', 'AI assistant');
this._safeRegister('WebFetcherModule', 'web fetcher');
this._safeRegister('WebXAIAliasModule', 'AI alias');
this._safeRegister('SecretVault', 'secret vault');
this.reloadPersistedModules();
this._safeRegister('ChatStudioModule', 'chat studio');
this._safeRegister('AgentRegistryModule', 'agent registry');
this._safeRegister('LeaseModule', 'lease system');
this._safeRegister('OrchestratorModule', 'orchestrator');
this._safeRegister('NeuralVFSModule', 'neural VFS');
this._safeRegister('SandboxModule', 'sandbox');
this._safeRegister('MemoryGraphModule', 'memory graph');
this._safeRegister('PredictorModule', 'predictor');
this._safeRegister('WorldModelModule', 'world model');
this._safeRegister('ServerMonitorModule', 'server monitor');
this._safeRegister('SmartContextModule', 'smart context');
this._safeRegister('SimpleGUIModule', 'simple GUI builder');
this._safeRegister('DebugSupervisorModule', 'debug supervisor');
// v4.5.47: Critical ext modules promoted to boot path
this._safeRegister('EditorModule', 'editor');
this._safeRegister('ShellModule', 'shell');
this._safeRegister('AliasModule', 'aliases');
this._safeRegister('FinderModule', 'finder');
this._safeRegister('FileManagerModule', 'file manager');
this._safeRegister('DiffModule', 'diff');
this._safeRegister('BusyBoxModule', 'busybox');
}
_initRegisteredModules() {
for (const [name, mod] of Object.entries(this.moduleManager.modules)) {
if (typeof mod.init === 'function') {
try { mod.init(this); } catch (e) {
console.warn(`[Init] ${name} failed: ${e.message}`);
}
}
}
}
_safeRegister(className, label) {
// Safe class lookup — no eval(). Classes are registered in global scope by the bundle.
const registry = {
SystemModule: typeof SystemModule !== 'undefined' ? SystemModule : undefined,
FileSystemModule: typeof FileSystemModule !== 'undefined' ? FileSystemModule : undefined,
ModuleManagerModule: typeof ModuleManagerModule !== 'undefined' ? ModuleManagerModule : undefined,
OllamaModule: typeof OllamaModule !== 'undefined' ? OllamaModule : undefined,
SearXNGModule: typeof SearXNGModule !== 'undefined' ? SearXNGModule : undefined,
ConfigManagerModule: typeof ConfigManagerModule !== 'undefined' ? ConfigManagerModule : undefined,
WebX11AIAssistantModule: typeof WebX11AIAssistantModule !== 'undefined' ? WebX11AIAssistantModule : undefined,
WebFetcherModule: typeof WebFetcherModule !== 'undefined' ? WebFetcherModule : undefined,
WebXAIAliasModule: typeof WebXAIAliasModule !== 'undefined' ? WebXAIAliasModule : undefined,
SecretVault: typeof SecretVault !== 'undefined' ? SecretVault : undefined,
ChatStudioModule: typeof ChatStudioModule !== 'undefined' ? ChatStudioModule : undefined,
AgentRegistryModule: typeof AgentRegistryModule !== 'undefined' ? AgentRegistryModule : undefined,
LeaseModule: typeof LeaseModule !== 'undefined' ? LeaseModule : undefined,
OrchestratorModule: typeof OrchestratorModule !== 'undefined' ? OrchestratorModule : undefined,
NeuralVFSModule: typeof NeuralVFSModule !== 'undefined' ? NeuralVFSModule : undefined,
SandboxModule: typeof SandboxModule !== 'undefined' ? SandboxModule : undefined,
MemoryGraphModule: typeof MemoryGraphModule !== 'undefined' ? MemoryGraphModule : undefined,
PredictorModule: typeof PredictorModule !== 'undefined' ? PredictorModule : undefined,
WorldModelModule: typeof WorldModelModule !== 'undefined' ? WorldModelModule : undefined,
ServerMonitorModule: typeof ServerMonitorModule !== 'undefined' ? ServerMonitorModule : undefined,
AliasModule: typeof AliasModule !== 'undefined' ? AliasModule : undefined,
FinderModule: typeof FinderModule !== 'undefined' ? FinderModule : undefined,
FileManagerModule: typeof FileManagerModule !== 'undefined' ? FileManagerModule : undefined,
DiffModule: typeof DiffModule !== 'undefined' ? DiffModule : undefined,
BusyBoxModule: typeof BusyBoxModule !== 'undefined' ? BusyBoxModule : undefined,
SmartContextModule: typeof SmartContextModule !== 'undefined' ? SmartContextModule : undefined,
DebugSupervisorModule: typeof DebugSupervisorModule !== 'undefined' ? DebugSupervisorModule : undefined,
};
const cls = registry[className];
if (typeof cls === 'undefined') {
console.log(`[SafeRegister] Skipped ${label}: class not loaded (skipped in boot selection)`);
return;
}
try { this.moduleManager.registerModule(new cls()); }
catch (e) { console.warn(`[SafeRegister] Skipped ${label}: ${e.message}`); }
}
// ── Config Registry ──
registerConfigItem(key, defaultValue, setterFn) {
this._configRegistry[key] = { key, defaultValue };
this._configDefaults[key] = defaultValue;
this._configSetters[key] = setterFn;
// Apply persisted override if present
const saved = localStorage.getItem('webx11_terminal_config');
if (saved) {
try {
const parsed = JSON.parse(saved);
if (parsed[key] !== undefined && parsed[key] !== defaultValue) {
setterFn(parsed[key]);
}
} catch (e) {}
}
}
_loadTerminalConfig() {
try {
const saved = localStorage.getItem('webx11_terminal_config');
if (!saved) return;
const parsed = JSON.parse(saved);
// Apply registered config items from persistence
for (const [key, val] of Object.entries(parsed)) {
if (this._configSetters[key] && val !== this._configDefaults[key]) {
this._configSetters[key](val);
}
}
} catch (e) {}
}
_saveTerminalConfig() {
const values = {};
for (const key of Object.keys(this._configRegistry)) {
const actual = this[key] !== undefined ? this[key] : this._configDefaults[key];
if (actual !== this._configDefaults[key]) {
values[key] = actual;
}
}
localStorage.setItem('webx11_terminal_config', JSON.stringify(values));
}
_applyTerminalConfig() {
try {
const saved = localStorage.getItem('webx11_terminal_config');
if (!saved) return;
const parsed = JSON.parse(saved);
if (parsed['terminal-max-lines'] !== undefined) this.MAX_OUTPUT_LINES = parsed['terminal-max-lines'];
if (parsed['terminal-hist-output'] !== undefined) this._historyMaxLines = parsed['terminal-hist-output'];
if (parsed['terminal-cmd-history'] !== undefined) this._historyCap = parsed['terminal-cmd-history'];
if (parsed['terminal-pipe-buffer'] !== undefined) this._pipeBufferSize = parsed['terminal-pipe-buffer'];
this._trimOutput();
} catch (e) {}
}
async _loadExtModules(filterKeys) {
const autoLoadKeys = filterKeys || ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'cryptogzip', 'tutorial', 'floating', 'console', 'media'];
const cacheVer = localStorage.getItem('webx11_cache_ver') || '2026-07-29-1';
let index = null;
let lastErr = null;
// v4.5.47: retry packages.json fetch up to 3 times
for (let attempt = 1; attempt <= 3; attempt++) {
try {
const res = await fetch('packages.json?v=' + cacheVer);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
index = await res.json();
lastErr = null;
break;
} catch (err) {
lastErr = err;
if (attempt < 3) await new Promise(r => setTimeout(r, 300 * attempt));
}
}
if (!index) {
this.print(`Failed to auto-load ext modules: ${lastErr ? lastErr.message : 'unknown error'}`, 'error');
return;
}
for (const key of autoLoadKeys) {
const pkg = index.packages[key];
if (pkg) {
const url = pkg.url + (pkg.url.includes('?') ? '&v=' : '?v=') + cacheVer;
await this.moduleManager.loadModuleFromURL(url);
}
}
}
async reloadPersistedModules() {
const registry = this.moduleManager.moduleRegistry;
const skipList = ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating', 'console', 'media'];
for (const mod of registry) {
if (mod.enabled && mod.url) {
// Skip ext modules — they load via _loadExtModules
const name = (mod.url.match(/(\w+)\.js/) || [])[1];
if (name && skipList.includes(name)) continue;
await this.moduleManager.loadModuleFromURL(mod.url);
}
}
}
async executeCommand(rawInput) {
if (!rawInput.trim()) return;
this.markDirty();
// Push to command history (dedup + trim)
const cmd = rawInput.trim();
if (this.history.length === 0 || this.history[this.history.length - 1] !== cmd) {
this.history.push(cmd);
}
if (this.history.length > this._historyCap) {
this.history = this.history.slice(-this._historyCap);
}
this.historyIndex = this.history.length;
this._saveCommandHistory();
this.printUserPrompt(rawInput);
const pipeParts = this.splitPipe(rawInput.trim());
if (pipeParts.length > 1) {
await this.executePipeChain(pipeParts);
return;
}
// Security gate for all single commands and internal builtins
await this.security.checkAndRun(rawInput);
}
splitPipe(input) {
const parts = [];
let current = '';
let inSingle = false;
let inDouble = false;
for (const ch of input) {
if (ch === '"' && !inSingle) { inDouble = !inDouble; current += ch; }
else if (ch === "'" && !inDouble) { inSingle = !inSingle; current += ch; }
else if (ch === '|' && !inSingle && !inDouble) { parts.push(current.trim()); current = ''; }
else { current += ch; }
}
parts.push(current.trim());
return parts;
}
async executePipeChain(pipeParts) {
// Parse every stage once, then drop the ones with no argv. `help |` splits to
// ['help',''], `help | ""` to ['help',''] and `| wc -l` to ['','wc -l'] — all three
// produced an empty argv whose parts[0] was undefined and threw TypeError from
// parts[0].toLowerCase() (989 occurrences, first filed 2026-10-20). Filtering on
// argv (not just the string) also keeps the LAST real stage final, so its output is
// never swallowed by a trailing empty segment.
const stages = pipeParts
.map(t => this.security._parseArgs(String(t || '').trim()))
.filter(argv => argv.length > 0);
if (!stages.length) {
this.print('⚠️ Empty pipeline — nothing to run', 'warning');
return;
}
let pipeInput = null;
for (let i = 0; i < stages.length; i++) {
const argv = stages[i];
const cmd = argv[0].toLowerCase();
const args = argv.slice(1);
// Rebuilt from argv so the security classifier sees the same tokenisation the
// dispatcher will use (quotes already stripped by _parseArgs).
const cmdText = argv.join(' ');
// v4.5.67: Security gate for pipe stages — was completely bypassed
const cls = this.security._classify(cmdText);
if (cls.destructive && this.security.level > 0) {
this.print(`⚠️ Security: Destructive command "${cmd}" blocked in pipe at level ${this.security.level}`, 'error');
return;
}
if (cls.system && this.security.level > 0) {
this.print(`⚠️ Security: System command "${cmd}" blocked in pipe at level ${this.security.level}`, 'error');
return;
}
if (!cls.known && this.security.level >= 2) {
this.print(`⚠️ Security: Unknown command "${cmd}" blocked in pipe (paranoid mode)`, 'error');
return;
}
this._pipeInput = pipeInput;
if (i < stages.length - 1) {
pipeInput = await this.capturePipeOutput(cmd, args);
} else {
await this.dispatchCommand(cmd, args);
}
}
this._pipeInput = null;
}
async capturePipeOutput(cmd, args) {
let output = '';
this._pushPrintCapture(buf => { output += buf; });
try {
await this.dispatchCommand(cmd, args);
} finally {
this._popPrintCapture();
}
const trimmed = output.trim();
if (trimmed.length > this._pipeBufferSize) {
return trimmed.slice(0, this._pipeBufferSize);
}
return trimmed;
}
async dispatchCommand(cmd, args) {
if (cmd === 'clear') { this.clearOutput(); return; }
if (cmd === 'expand') { this.toggleExpand(); return; }
if (cmd === 'sysmsg') { this.toggleSystemMessages(args[0]); return; }
if (cmd === 'startup-modules') { this.openStartupModules(); return; }
if (cmd === 'font-size' || cmd === 'setfont') { this.setFontSize(args[0]); return; }
if (cmd === 'load-local' || cmd === 'loadlocal') { this.loadLocalModule(args[0]); return; }
if (cmd === 'smart-prompt') { const val = args[0] && args[0] === 'off' ? 'off' : 'on'; localStorage.setItem('webx11_smart_prompt', val); this.print('Smart prompt ' + val, 'success'); this.updatePrompt(); return; }
if (cmd === 'mem') { this.printMemoryStats(); return; }
if (cmd === 'dashboard') {
const aiMod = this.moduleManager && this.moduleManager.modules
? this.moduleManager.modules.WebX11AIAssistantModule : null;
const collect = function () {
if (aiMod && typeof aiMod._collectDashboardData === 'function') return aiMod._collectDashboardData();
return Promise.resolve(null);
};
if (args.indexOf('--stop') !== -1) {
if (this._dashTimer) { clearInterval(this._dashTimer); this._dashTimer = null; this.print('📊 Dashboard watch stopped', 'info'); }
else this.print('📊 Dashboard watch is not running', 'warning');
return;
}
if (args.indexOf('--watch') !== -1) {
if (this._dashTimer) clearInterval(this._dashTimer);
collect();
this._dashTimer = setInterval(collect, 5000);
this.print('📊 Dashboard watch on (5s) — open the dashboard tab; `dashboard --stop` to stop', 'success');
return;
}
collect().then(function () {
this.print('📊 Dashboard data collected — opening dashboard...', 'success');
window.open('/spaces/newos4/downloads/dashboard.html', 'newOS4 Dashboard', 'width=1200,height=800');
}.bind(this));
return;
}
if (cmd === 'config') {
if (args.length === 0) {
this.print('=== Terminal Config ===', 'info');
this.print(`terminal-max-lines: ${this.MAX_OUTPUT_LINES} (default: 100)`, '');
this.print(`terminal-hist-output: ${this._historyMaxLines} (default: 50)`, '');
this.print(`terminal-cmd-history: ${this._historyCap} (default: 200)`, '');
this.print(`terminal-pipe-buffer: ${this._pipeBufferSize} (default: 5000)`, '');
this.print(`terminal-sysmsg: ${this._showSystemMessages ? 'on' : 'off'} (default: on)`, '');
this.print('Usage: config OLLAMA_ORIGINS includes this page\'s origin.';
}
if (select) { select.disabled = true; select.innerHTML = ''; }
}
}
initOllamaUI() {
// Initialize optimization toggle from stored state
const optCb = document.getElementById('ollama-optimization-toggle');
const optLabel = document.getElementById('ollama-optimization-label');
const optSlider = document.getElementById('ollama-optimization-slider');
if (optCb && this.ollamaManager) {
const enabled = this.ollamaManager.optimizationEnabled !== false;
optCb.checked = enabled;
if (optLabel) {
optLabel.textContent = enabled ? 'Enabled' : 'Disabled';
optLabel.style.color = enabled ? 'var(--success-color)' : 'var(--text-muted)';
}
if (optSlider) {
optSlider.style.background = enabled ? 'var(--accent-color)' : 'var(--bg-tertiary)';
optSlider.style.borderColor = enabled ? 'var(--success-color)' : 'var(--border-color)';
}
}
// Initialize validator toggle from stored state
const valCb = document.getElementById('ollama-validator-toggle');
const valLabel = document.getElementById('ollama-validator-label');
const valSlider = document.getElementById('ollama-validator-slider');
if (valCb && this.ollamaManager) {
const enabled = this.ollamaManager.validatorEnabled === true;
valCb.checked = enabled;
if (valLabel) {
valLabel.textContent = enabled ? 'Enabled' : 'Disabled';
valLabel.style.color = enabled ? 'var(--success-color)' : 'var(--text-muted)';
}
if (valSlider) {
valSlider.style.background = enabled ? 'var(--info-color)' : 'var(--bg-tertiary)';
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');
const strSlider = document.getElementById('ollama-stream-slider');
if (strCb && this.ollamaManager) {
const enabled = this.ollamaManager.streamEnabled !== false;
strCb.checked = enabled;
if (strLabel) {
strLabel.textContent = enabled ? 'Enabled' : 'Disabled';
strLabel.style.color = enabled ? 'var(--success-color)' : 'var(--text-muted)';
}
if (strSlider) {
strSlider.style.background = enabled ? 'var(--accent-color)' : 'var(--bg-tertiary)';
strSlider.style.borderColor = enabled ? 'var(--success-color)' : 'var(--border-color)';
}
}
this.updateStreamIndicator();
// Populate validator model dropdown from stored config
const valSelect = document.getElementById('validator-model-select');
const valEndpoint = document.getElementById('validator-endpoint-input');
if (valEndpoint && this.ollamaManager) {
valEndpoint.value = this.ollamaManager.validatorEndpoint || 'http://localhost:11434';
}
if (valSelect && this.ollamaManager) {
const storedModel = this.ollamaManager.validatorModel || '';
if (storedModel) {
valSelect.innerHTML = '';
valSelect.disabled = false;
}
}
}
_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 = '';
select.disabled = true;
return;
}
const stored = this.ollamaManager.validatorModel || '';
let html = '';
models.forEach(function(m) {
const sel = m.name === stored ? ' selected' : '';
html += '';
});
select.innerHTML = html;
select.disabled = false;
}
openStartupModules() {
document.getElementById('startup-modules').style.display = 'flex';
this.renderStartupModules();
}
closeStartupModules() {
document.getElementById('startup-modules').style.display = 'none';
}
renderStartupModules() {
const body = document.getElementById('startup-modules-body');
const selected = (() => {
try {
const s = localStorage.getItem('webx11_startup_modules');
return s ? JSON.parse(s) : null;
} catch (e) { return null; }
})();
const isSel = (name) => !selected || selected.includes(name);
body.innerHTML = '';
const foundation = ['virtual-fs.js','module-manager.js','ollama-manager.js','terminal.js'];
const builtin = [
'module-system.js','module-fs.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-chatstudio.js'
];
const extFiles = [
'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','floating.js','console.js','media.js'
];
const addSection = (title) => {
const h = document.createElement('div');
h.className = 'mod-section-header';
h.textContent = title;
body.appendChild(h);
};
const addCard = (name, badge, isDisabled) => {
const card = document.createElement('div');
card.className = 'mod-card' + (isDisabled ? ' disabled' : '');
const label = document.createElement('label');
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = isDisabled || isSel(name);
cb.disabled = isDisabled;
cb.dataset.name = name;
const nameSpan = document.createElement('span');
nameSpan.className = 'mod-name';
nameSpan.textContent = name;
const badgeSpan = document.createElement('span');
badgeSpan.className = 'mod-badge' + (badge === 'foundation' ? ' foundation' : badge === 'ext' ? ' ext' : '');
badgeSpan.textContent = badge;
label.appendChild(cb);
label.appendChild(nameSpan);
card.appendChild(label);
card.appendChild(badgeSpan);
body.appendChild(card);
};
addSection('Foundation (always loaded)');
foundation.forEach(n => addCard(n, 'foundation', true));
addSection('Built-in modules');
builtin.forEach(n => addCard(n, 'built-in', false));
addSection('Extensions');
extFiles.forEach(n => addCard(n, 'ext', false));
}
saveStartupModules() {
const cbs = document.querySelectorAll('#startup-modules-body input[type="checkbox"]:not(:disabled)');
const selected = [];
cbs.forEach(cb => {
if (cb.checked) selected.push(cb.dataset.name);
});
try {
localStorage.setItem('webx11_startup_modules', JSON.stringify(selected));
} catch (e) {
this.print('Failed to save startup modules', 'error');
return;
}
this.print('Startup modules saved, reloading...', 'success');
setTimeout(() => location.reload(), 500);
}
openCodeEditor(name, url, code) {
const modal = document.getElementById('code-editor');
const title = document.getElementById('code-editor-title');
const info = document.getElementById('code-editor-info');
const ta = document.getElementById('code-editor-textarea');
if (!modal || !ta) return;
this._editingModule = { name: name, url: url };
title.textContent = '📝 Edit: ' + name;
info.textContent = 'Module: ' + name + (url ? ' | Source: ' + url : '');
ta.value = code;
modal.style.display = 'flex';
}
closeCodeEditor() {
document.getElementById('code-editor').style.display = 'none';
this._editingModule = null;
}
async loadLocalModule(path) {
if (!path) {
this.print('Usage: load-local