// 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(raw.trim()); 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 }; } async checkAndRun(rawInput) { 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 = '
⚙ System
' + safeText + '
'; } else { line.className = 'output-line'; line.textContent = text; // Plain lines get a lightweight inline copy button const copyBtn = document.createElement('button'); copyBtn.className = 'line-copy-btn'; copyBtn.textContent = 'Copy'; copyBtn.setAttribute('onclick', 'webx11.copyLineToClipboard(this)'); copyBtn.setAttribute('aria-label', 'Copy line'); line.appendChild(copyBtn); } this.outputContainer.appendChild(line); this._historyBuffer.push({ l: text, t: type }); if (this._historyBuffer.length > this._historyMaxLines) { this._historyBuffer.shift(); } this._saveHistory(); this.scrollToBottom(); this._trimOutput(); } _renderHTMLPreview(content) { // AI returns raw HTML. We must HTML-escape for the srcdoc attribute // so the parser treats it as attribute content, not tags. // srcdoc decodes entities back to actual HTML inside the iframe. const hasOwnStyle = /body{background:#000;color:#0f0;margin:16px;font-family:monospace;}${content}`; const safeSrcdoc = themedContent .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/`/g, '`'); return `
📄 Rendered HTML
`; } _renderHTMLBlock(text) { const renderRegex = /:::render-html\r?\n([\s\S]*?)\r?\n:::/g; let match; let lastIndex = 0; const fragments = []; while ((match = renderRegex.exec(text)) !== null) { // Text before this block const before = text.slice(lastIndex, match.index); if (before.trim()) { const safeBefore = before.replace(//g, ">"); fragments.push({ type: 'text', html: '
' + safeBefore + '
' }); } fragments.push({ type: 'render', html: this._renderHTMLPreview(match[1].trim()) }); lastIndex = match.index + match[0].length; } const after = text.slice(lastIndex); if (after.trim()) { const safeAfter = after.replace(//g, ">"); fragments.push({ type: 'text', html: '
' + safeAfter + '
' }); } return fragments.length > 0 ? fragments : null; } printAIResponse(text) { this._flushStreamChunks(); const fragments = this._renderHTMLBlock(text); if (fragments) { const wrapper = document.createElement('div'); wrapper.className = 'output-line msg-card ai'; wrapper.innerHTML = '
🤖 AI
'; const body = wrapper.querySelector('.msg-card-body'); for (const frag of fragments) { const div = document.createElement('div'); div.innerHTML = frag.html; body.appendChild(div); } this.outputContainer.appendChild(wrapper); this.scrollToBottom(); this._trimOutput(); return; } const wrapper = document.createElement('div'); wrapper.className = 'output-line msg-card ai'; const safeText = text.replace(//g, ">"); wrapper.innerHTML = '
🤖 AI
' + safeText + '
'; this.outputContainer.appendChild(wrapper); this.scrollToBottom(); this._trimOutput(); } printStreamingResponse(text, append) { if (!this._streamChunks) this._streamChunks = []; if (!append) { const safeText = text.replace(//g, ">"); const wrapper = document.createElement('div'); wrapper.className = 'output-line msg-card ai'; wrapper.innerHTML = '
🤖 AI
' + safeText + '
'; this.outputContainer.appendChild(wrapper); this._streamChunks = []; } else { const contentDiv = this.outputContainer.querySelector('.msg-card.ai:last-child .msg-card-body'); if (contentDiv) { this._streamChunks.push(text); if (this._streamChunks.length >= 5) { contentDiv.textContent += this._streamChunks.join(''); this._streamChunks = []; if (contentDiv.textContent.length > 50000) { contentDiv.textContent = contentDiv.textContent.slice(-30000); } } } } this.scrollToBottom(); this._trimOutput(); } _flushStreamChunks() { const contentDiv = this.outputContainer.querySelector('.msg-card.ai:last-child .msg-card-body'); if (contentDiv && this._streamChunks && this._streamChunks.length > 0) { contentDiv.textContent += this._streamChunks.join(''); this._streamChunks = []; } } printUserPrompt(rawInput) { const wrapper = document.createElement('div'); wrapper.className = 'output-line msg-card user'; const safe = rawInput.replace(//g, '>'); const hasHtml = /<[a-z][\s\S]*>/i.test(rawInput.trim()); let buttons = ''; if (hasHtml) buttons += ''; buttons += ''; wrapper.innerHTML = '
👤 User' + buttons + '
' + safe + '
'; 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) { let text = ''; const card = btn.closest('.msg-card'); if (card) { const cardBody = card.querySelector('.msg-card-body'); if (cardBody) text = cardBody.textContent; } else { const nd = btn.nextElementSibling; if (nd) text = nd.innerText || nd.textContent; } this._copyText(text, btn); } copyLineToClipboard(btn) { const line = btn.parentElement; const text = line ? line.childNodes[0].textContent : ''; this._copyText(text, btn); } _copyText(text, btn=null) { if (!text) return; try { var ta = document.createElement('textarea'); ta.value = text; ta.style.cssText = 'position:fixed;left:10px;top:10px;width:200px;height:40px;font-size:16px;padding:4px;z-index:-1;opacity:0.01'; document.body.appendChild(ta); ta.focus(); ta.select(); ta.setSelectionRange(0, text.length); document.execCommand('copy'); document.body.removeChild(ta); if (btn) { btn.textContent = 'Copied!'; setTimeout(function(){ btn.textContent = 'Copy'; }, 2000); } this.print('📋 Copied to clipboard', 'success'); } catch (e) { this.print('Copy failed: ' + e.message, 'error'); } } printClickable(items) { const line = document.createElement('div'); line.className = 'output-line'; items.forEach(f => { const span = document.createElement('span'); const safeName = f.name.replace(//g, '>'); if (f.type === 'dir') { span.className = 'clickable-dir'; span.textContent = '[dir]' + f.name + '/'; span.onclick = () => { this.executeCommand('cd ' + f.name); setTimeout(() => this.executeCommand('ls'), 50); }; } else { span.className = 'clickable-file'; span.textContent = ' ' + f.name; span.onclick = () => this.executeCommand('cat ' + f.name); } line.appendChild(span); }); this.outputContainer.appendChild(line); this.scrollToBottom(); } printHTML(html) { const line = document.createElement('div'); line.className = 'output-line'; line.innerHTML = html; this.outputContainer.appendChild(line); this.scrollToBottom(); } showSuggestionBar(cmd, args) { // One-tap typo fix: renders clickable "✅ Run / ❌ Dismiss" bar const area = document.getElementById('ai-confirm-area'); const runCmd = cmd + ((args && args.length) ? ' ' + args.join(' ') : ''); if (!area) { // Fallback for hosts without the confirm UI: print clickable command this.print(`Did you mean: ${runCmd} — type it to run.`, 'info'); return; } area.innerHTML = ` 💡 Did you mean: ${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) { let pipeInput = null; for (let i = 0; i < pipeParts.length; i++) { const cmdText = pipeParts[i]; const parts = this.security._parseArgs(cmdText); const cmd = parts[0].toLowerCase(); const args = parts.slice(1); // 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 < pipeParts.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 ', 'info'); this.print('Keys: terminal-max-lines, terminal-hist-output, terminal-cmd-history, terminal-pipe-buffer, terminal-sysmsg', 'info'); return; } const key = args[0]; const val = args[1]; if (val === undefined) { this.print('Usage: config ', 'warning'); return; } const n = parseInt(val); if (key === 'terminal-sysmsg') { this._showSystemMessages = val !== 'off'; this._saveTerminalConfig(); this.print(`config ${key} set to ${this._showSystemMessages ? 'on' : 'off'}`, 'success'); return; } if (isNaN(n) || n < 1) { this.print('Value must be a positive number', 'warning'); return; } if (key === 'terminal-max-lines') { this.MAX_OUTPUT_LINES = n; } else if (key === 'terminal-hist-output') { this._historyMaxLines = n; } else if (key === 'terminal-cmd-history') { this._historyCap = n; } else if (key === 'terminal-pipe-buffer') { this._pipeBufferSize = n; } else { this.print(`Unknown key: ${key}. Valid: terminal-max-lines, terminal-hist-output, terminal-cmd-history, terminal-pipe-buffer, terminal-sysmsg`, 'warning'); return; } this._saveTerminalConfig(); this.print(`config ${key} set to ${val}`, 'success'); return; } if (cmd === 'security') { this.print(`Security level: ${this.security.getLevel()} (${this.security.getLevelName()})`, 'info'); this.print(`Blocked commands: ${this.security.blockedCount}`, 'info'); this.print(`Destructive set: ${[...this.security.destructiveCmds].sort().join(', ')}`, ''); this.print(`System set: ${[...this.security.systemCmds].sort().join(', ')}`, ''); this.print(`Suspicious flags: ${[...this.security.suspiciousFlags].sort().join(', ')}`, ''); this.print(`Whitelist: ${[...this.security.whitelist].sort().join(', ')}`, ''); const all = this.security._collectAllCommands(); this.print(`Registered commands: ${all.length}`, ''); return; } if (cmd === 'security-level') { const lv = parseInt(args[0]); if (isNaN(lv)) { this.print('Usage: security-level 0|1|2|3', 'warning'); return; } this.security.setLevel(lv); localStorage.setItem('webx11_security_level', lv); return; } if (cmd === 'security-destructive') { const action = args[0]; if (!action) { this.print('Usage: security-destructive add|remove|list [cmd]', 'warning'); return; } if (action === 'list') { this.print('Destructive commands:', 'info'); for (const c of this.security.destructiveCmds) this.print(` ${c}`, ''); return; } if (!args[1]) { this.print('Usage: security-destructive add|remove ', 'warning'); return; } if (action === 'add') { this.security.destructiveCmds.add(args[1]); } else if (action === 'remove') { this.security.destructiveCmds.delete(args[1]); } else { this.print('Action must be: add, remove, or list', 'warning'); return; } this.security._saveSecurityDeltas(); this.print(`Updated destructive set (${this.security.destructiveCmds.size} commands)`, 'success'); return; } if (cmd === 'security-system') { const action = args[0]; if (!action) { this.print('Usage: security-system add|remove|list [cmd]', 'warning'); return; } if (action === 'list') { this.print('System commands:', 'info'); for (const c of this.security.systemCmds) this.print(` ${c}`, ''); return; } if (!args[1]) { this.print('Usage: security-system add|remove ', 'warning'); return; } if (action === 'add') { this.security.systemCmds.add(args[1]); } else if (action === 'remove') { this.security.systemCmds.delete(args[1]); } else { this.print('Action must be: add, remove, or list', 'warning'); return; } this.security._saveSecurityDeltas(); this.print(`Updated system set (${this.security.systemCmds.size} commands)`, 'success'); return; } if (cmd === 'security-whitelist') { const action = args[0]; if (!action) { this.print('Usage: security-whitelist add|remove|list [cmd]', 'warning'); return; } if (action === 'list') { this.print('Whitelist commands:', 'info'); for (const c of this.security.whitelist) this.print(` ${c}`, ''); return; } if (!args[1]) { this.print('Usage: security-whitelist add|remove ', 'warning'); return; } if (action === 'add') { this.security.whitelist.add(args[1]); } else if (action === 'remove') { this.security.whitelist.delete(args[1]); } else { this.print('Action must be: add, remove, or list', 'warning'); return; } this.security._saveSecurityDeltas(); this.print(`Updated whitelist (${this.security.whitelist.size} commands)`, 'success'); return; } if (cmd === 'security-flag') { const action = args[0]; if (!action) { this.print('Usage: security-flag add|remove|list [flag]', 'warning'); return; } if (action === 'list') { this.print('Suspicious flags:', 'info'); for (const f of this.security.suspiciousFlags) this.print(` ${f}`, ''); return; } if (!args[1]) { this.print('Usage: security-flag add|remove ', 'warning'); return; } const flag = args[1].startsWith('--') ? args[1] : '--' + args[1]; if (action === 'add') { this.security.suspiciousFlags.add(flag); } else if (action === 'remove') { this.security.suspiciousFlags.delete(flag); } else { this.print('Action must be: add, remove, or list', 'warning'); return; } this.security._saveSecurityDeltas(); this.print(`Updated suspicious flags (${this.security.suspiciousFlags.size} flags)`, 'success'); return; } if (cmd === 'cmd-allow') { this._resolveCmdConfirm(true); return; } if (cmd === 'cmd-deny') { this._resolveCmdConfirm(false); return; } if (cmd === 'vfs' && args[0] === 'reset') { if (args[1] !== '--confirm' && args[1] !== '--force') { this.print('⚠ This will permanently delete all files! Use vfs reset --confirm to confirm.', 'warning'); return; } this.fs.reset(); this.print('VFS reset — all files deleted.', 'success'); return; } if (cmd === 'vfs' && args[0] === 'clean') { let count = 0; const sysDirs = ['_snapshots', '_sessions', '.trash']; for (const dir of sysDirs) { if (this.fs.root.content && this.fs.root.content[dir]) { delete this.fs.root.content[dir]; count++; } } if (count > 0) { this.fs.saveToStorage(); this.print('🧹 Cleaned ' + count + ' system dirs: ' + sysDirs.join(', '), 'success'); } else { this.print('Nothing to clean — system dirs already empty.', 'info'); } return; } if (cmd === 'vfs-clean') { let count = 0; const sysDirs = ['_snapshots', '_sessions', '.trash']; for (const dir of sysDirs) { if (this.fs.root.content && this.fs.root.content[dir]) { delete this.fs.root.content[dir]; count++; } } if (count > 0) { this.fs.saveToStorage(); this.print('🧹 Cleaned ' + count + ' system dirs: ' + sysDirs.join(', '), 'success'); } else { this.print('Nothing to clean — system dirs already empty.', 'info'); } return; } if (cmd === 'empty-trash') { if (args[0] !== '--confirm' && args[0] !== '--force') { this.print('⚠ This will permanently delete all trashed files! Use empty-trash --confirm to confirm.', 'warning'); return; } if (this.fs.emptyTrash()) { this.print('Trash emptied.', 'success'); } else { this.print('Trash is already empty.', 'info'); } return; } if (cmd === 'usage') { const om = this.ollamaManager; if (!om) { this.print('Ollama manager not available', 'error'); return; } const filters = {}; for (let i = 0; i < args.length; i++) { if (args[i] === '--json') { filters._json = true; } else if (args[i] === '--reset') { om._usageLog = []; om._usageBuffer = []; om._saveUsageLog(); this.print('Usage log cleared.', 'success'); return; } else if (args[i] === '--model' && args[i+1]) { filters.model = args[++i]; } else if (args[i] === '--provider' && args[i+1]) { filters.provider = args[++i]; } else if (args[i] === '--since' && args[i+1]) { filters.since_hours = parseFloat(args[++i]); } } const stats = om.getUsageStats(filters); if (filters._json) { this.print(JSON.stringify(stats, null, 2), ''); return; } if (stats.total === 0) { this.print('No usage data recorded yet.', 'info'); return; } const fmt = (n) => n.toLocaleString(); const timeStr = stats.timeSpan < 60 ? stats.timeSpan + 's' : stats.timeSpan < 3600 ? Math.round(stats.timeSpan / 60) + 'm' : Math.round(stats.timeSpan / 3600) + 'h ' + Math.round((stats.timeSpan % 3600) / 60) + 'm'; this.print('╔══════════════════════════════════════════════════════╗', 'info'); this.print('║ LLM Usage Stats ║', 'info'); this.print('╠══════════════════════════════════════════════════════╣', 'info'); this.print(`║ Total requests: ${fmt(stats.total).padStart(30)} ║`, 'info'); this.print(`║ Total tokens: ${fmt(stats.totalPrompt + stats.totalCompletion).padStart(30)} ║`, 'info'); this.print(`║ Input: ${fmt(stats.totalPrompt).padStart(37)} ║`, 'info'); this.print(`║ Output: ${fmt(stats.totalCompletion).padStart(37)} ║`, 'info'); this.print(`║ Avg latency: ${(stats.avgLatency / 1000).toFixed(1) + 's'.padStart(30)} ║`, 'info'); this.print(`║ Time span: ${timeStr.padStart(30)} ║`, 'info'); this.print('╠══════════════════════════════════════════════════════╣', 'info'); this.print('║ Per model: ║', 'info'); for (const [model, data] of Object.entries(stats.byModel).sort((a, b) => b[1].requests - a[1].requests)) { const label = (model.length > 30 ? model.slice(0, 27) + '...' : model).padEnd(30); this.print(`║ ${label} ${fmt(data.requests).padStart(4)} req ${fmt(data.tokens).padStart(8)} tok ║`, ''); } this.print('╠══════════════════════════════════════════════════════╣', 'info'); this.print('║ Per provider: ║', 'info'); for (const [prov, data] of Object.entries(stats.byProvider).sort((a, b) => b[1].requests - a[1].requests)) { const label = (prov.length > 30 ? prov.slice(0, 27) + '...' : prov).padEnd(30); this.print(`║ ${label} ${fmt(data.requests).padStart(4)} req ${fmt(data.tokens).padStart(8)} tok ║`, ''); } this.print('╚══════════════════════════════════════════════════════╝', 'info'); this.print('Flags: --json, --model=, --provider=, --since=, --reset', 'text-muted'); return; } if (cmd === 'history') { const entries = this.history; if (!entries.length) { this.print('No command history.', 'info'); return; } // Flags if (args[0] === '--clear' || args[0] === '-c') { this.history = []; this.historyIndex = 0; localStorage.removeItem('webx11_command_history'); this.print('History cleared.', 'success'); return; } if (args[0] === '--export' || args[0] === '-e') { try { this.fs.mkdirRecursive('system'); this.fs.writeFile('system/history.json', JSON.stringify(entries)); this.print('History exported to /system/history.json (' + entries.length + ' entries).', 'success'); } catch (e) { this.print('Export failed: ' + e.message, 'error'); } return; } if (args[0] === '--search' || args[0] === '-s') { const term = args.slice(1).join(' ').toLowerCase(); if (!term) { this.print('Usage: history --search ', 'warning'); return; } const filtered = entries.filter(e => e.toLowerCase().includes(term)); if (!filtered.length) { this.print('No matches for: ' + term, 'info'); return; } this.print('=== History (matching: ' + term + ') ===', 'info'); filtered.forEach(e => this.print(' ' + e)); return; } let count = 50; if (args[0] === '-n' && args[1]) count = parseInt(args[1]) || 50; const slice = entries.slice(-count); this.print('=== Command History (' + entries.length + ' entries) ===', 'info'); slice.forEach(c => this.print(' ' + c)); if (entries.length > count) this.print('... (' + (entries.length - count) + ' older, use history --search to find)'); return; } for (const modName in this.moduleManager.modules) { const mod = this.moduleManager.modules[modName]; if (mod.commands && mod.commands[cmd]) { if (cmd !== 'course') { for (const _m of Object.values(this.moduleManager.modules)) { if (typeof _m.onCommandStart === 'function') { try { _m.onCommandStart(cmd, args); } catch (_e) {} } } } try { await mod.commands[cmd](args); if (cmd !== 'tutorial' && cmd !== 'course') { for (const _m of Object.values(this.moduleManager.modules)) { if (typeof _m.onCommandExecuted === 'function') { try { _m.onCommandExecuted(cmd, args); } catch (_e) {} } } } // Memory graph: record file access for read/file commands if (cmd === 'cat' || cmd === 'view_module' || cmd === 'edit_module') { try { const _mg = this.moduleManager.modules['MemoryGraphModule']; if (_mg && args[0]) _mg.recordFileAccess(args[0]); } catch (_e) {} } return; } catch (err) { this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error'); console.error(`[Terminal] Error in ${modName}.${cmd}:`, err); return; } } } // Enhanced: check fuzzy suggestions before showing "not found" const suggestion = this.security.suggest(cmd); if (suggestion && suggestion.dist <= 2 && suggestion.dist > 0) { this.print(`Command not found: ${cmd}`, 'error'); this.print(`Did you mean: ${suggestion.cmd}? (yes) (no)`, 'info'); // Render clickable yes/no const out = this.outputContainer; if (out) { const yesSpan = document.createElement('span'); yesSpan.textContent = ' [yes] '; yesSpan.style.cssText = 'color:var(--success-color);cursor:pointer;text-decoration:underline;margin-right:12px'; yesSpan.onclick = () => this.executeCommand(suggestion.cmd + (args.length ? ' ' + args.join(' ') : '')); const noSpan = document.createElement('span'); noSpan.textContent = ' [no] '; noSpan.style.cssText = 'color:var(--error-color);cursor:pointer;text-decoration:underline'; noSpan.onclick = () => { this.print('Dismissed.', 'text-muted'); }; const line = out.lastElementChild; if (line) { line.appendChild(yesSpan); line.appendChild(noSpan); } } return; } this.print(`Command not found: ${cmd}`, 'error'); } openFileBrowser() { const modal = document.getElementById('file-browser'); const content = document.getElementById('fb-content'); content.innerHTML = ''; const files = this.fs.ls(); if (files.length === 0) { content.innerHTML = '
Directory Empty
'; } else { files.forEach(f => { const div = document.createElement('div'); div.className = `file-item ${f.type === 'dir' ? 'dir' : ''}`; const icon = document.createElement('span'); icon.textContent = f.type === 'dir' ? '📁' : '📄'; const name = document.createElement('span'); name.textContent = ' ' + f.name; const size = document.createElement('span'); size.style.fontSize = '0.8em'; size.textContent = ' ' + f.size + 'b'; div.appendChild(icon); div.appendChild(name); div.appendChild(document.createTextNode(' ')); div.appendChild(size); div.onclick = () => { if (f.type === 'file') { const content = this.fs.readFile(f.name); this.print(`--- ${f.name} ---`); this.print(content); this.print('--- End ---'); this.closeFileBrowser(); } }; content.appendChild(div); }); } modal.style.display = 'flex'; } closeFileBrowser() { document.getElementById('file-browser').style.display = 'none'; this.inputField.focus(); } openModuleManager() { const modal = document.getElementById('module-manager'); this.renderModuleList(); modal.style.display = 'flex'; } closeModuleManager() { document.getElementById('module-manager').style.display = 'none'; this.inputField.focus(); } renderModuleList() { const list = document.getElementById('module-list'); list.innerHTML = ''; const modules = this.moduleManager.getModuleList(); if (modules.length === 0) { list.innerHTML = '
No modules loaded
'; return; } modules.forEach(mod => { const div = document.createElement('div'); div.className = `module-item ${mod.loaded ? '' : 'disabled'}`; const info = document.createElement('div'); const strong = document.createElement('strong'); strong.textContent = mod.name; info.appendChild(strong); if (!mod.url) { const badge = document.createElement('span'); badge.className = 'module-badge builtin'; badge.textContent = 'Built-in'; info.appendChild(badge); } const sub = document.createElement('div'); sub.style.cssText = 'font-size:0.8em; color:#666'; sub.textContent = mod.url || 'Built-in'; info.appendChild(sub); div.appendChild(info); if (mod.url) { const btn = document.createElement('button'); btn.className = 'btn btn-danger'; btn.style.padding = '4px 8px'; btn.textContent = '✕'; btn.onclick = () => this.removeModule(mod.name); div.appendChild(btn); } list.appendChild(div); }); } async loadModuleFromURL() { const url = document.getElementById('module-url-input').value.trim(); if (!url) { alert('Please enter a URL'); return; } const success = await this.moduleManager.loadModuleFromURL(url); if (success) { document.getElementById('module-url-input').value = ''; this.renderModuleList(); } } removeModule(name) { if (this.moduleManager.unregisterModule(name)) { this.print(`Module '${name}' removed`, 'success'); this.renderModuleList(); } } clearAllModules() { if (confirm('Remove all custom modules? Built-in modules will remain.')) { this.moduleManager.clearAll(); this.renderModuleList(); this.print('All custom modules cleared', 'success'); } } openOllamaManager() { const modal = document.getElementById('ollama-manager'); document.getElementById('ollama-endpoint').value = this.ollamaManager.endpoint; this.updateOllamaStatus(); this.renderOllamaModels(); this.initOllamaUI(); modal.style.display = 'flex'; } closeOllamaManager() { document.getElementById('ollama-manager').style.display = 'none'; this.inputField.focus(); } openPreview(args) { let path = ''; if (typeof args === 'string') path = args.trim(); else if (Array.isArray(args)) path = args.join(' ').trim(); if (!path) { this.print('Usage: preview ', 'warning'); return; } // Remove leading slash for VFS paths const vfsPath = path.replace(/^\/*/, ''); let content = this.fs.readFile(vfsPath); if (content === null) content = this.fs.readFile('/' + vfsPath); if (content === null) { this.print(`File not found: ${path}`, 'error'); return; } // Use srcdoc instead of blob URL — works reliably on mobile browsers const iframe = document.getElementById('preview-iframe'); iframe.srcdoc = content; document.getElementById('preview-title').textContent = `📄 ${path}`; document.getElementById('preview-modal').style.display = 'flex'; } closePreview() { document.getElementById('preview-modal').style.display = 'none'; const iframe = document.getElementById('preview-iframe'); iframe.srcdoc = ''; 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 this.fs.mkdirRecursive('/skills'); this.refreshSkillsList(); modal.style.display = 'flex'; } closeSkillsManager() { document.getElementById('skills-manager').style.display = 'none'; this.inputField.focus(); } openConfigManager() { document.getElementById('config-manager').style.display = 'flex'; this.renderConfigList(); } closeConfigManager() { document.getElementById('config-manager').style.display = 'none'; this.inputField.focus(); } renderConfigList() { const list = document.getElementById('config-list'); if (!list) return; list.innerHTML = ''; const prefix = 'webx11_config_'; const entries = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(prefix)) { entries.push({ key: key.replace(prefix, ''), value: localStorage.getItem(key) }); } } if (entries.length === 0) { list.innerHTML = '
No configuration saved.
'; return; } entries.forEach(e => { const item = document.createElement('div'); item.className = 'module-item'; const info = document.createElement('div'); const strong = document.createElement('strong'); strong.textContent = e.key; info.appendChild(strong); const val = document.createElement('span'); val.style.color = 'var(--text-muted)'; const sensitive = /password|token|secret|key|private|credential|auth/i.test(e.key); val.textContent = sensitive ? '****' : e.value; info.appendChild(document.createTextNode(' ')); info.appendChild(val); const btn = document.createElement('button'); btn.className = 'btn btn-danger'; btn.style.cssText = 'padding:4px 8px; font-size:0.8em'; btn.textContent = '✕'; btn.onclick = () => this.deleteConfigFromUI(e.key); item.appendChild(info); item.appendChild(btn); list.appendChild(item); }); } addConfigFromUI() { const key = document.getElementById('config-key-input').value.trim(); const value = document.getElementById('config-value-input').value.trim(); if (!key || !value) { this.print('Enter both key and value', 'warning'); return; } localStorage.setItem('webx11_config_' + key, value); document.getElementById('config-key-input').value = ''; document.getElementById('config-value-input').value = ''; this.print(`Config saved: ${key} = ${value}`, 'success'); this.renderConfigList(); } deleteConfigFromUI(key) { localStorage.removeItem('webx11_config_' + key); this.print(`Config '${key}' deleted`, 'success'); this.renderConfigList(); } exportConfigFromUI() { const mod = this.moduleManager.modules['ConfigManagerModule']; if (mod && mod.commands && mod.commands['config-export']) { mod.commands['config-export'](); } } resetConfigFromUI() { const mod = this.moduleManager.modules['ConfigManagerModule']; if (mod && mod.commands && mod.commands['config-reset']) { mod.commands['config-reset'](); this.renderConfigList(); } } refreshSkillsList() { const list = document.getElementById('skills-list'); list.innerHTML = ''; const statsEl = document.getElementById('skills-stats'); const fs = this.fs; fs.mkdirRecursive('/skills'); const index = fs.fsReadJSON('/skills/index.json') || {}; const names = Object.keys(index); // Search filter const query = (document.getElementById('skills-search-input')?.value || '').toLowerCase().trim(); let filtered = names; if (query) { filtered = names.filter(n => { const s = index[n]; return n.includes(query) || (s.description || '').toLowerCase().includes(query) || (s.tags || []).some(t => t.toLowerCase().includes(query)) || (s.category || '').toLowerCase().includes(query); }); } // Stats if (statsEl) { const cats = new Set(Object.values(index).map(s => s.category || 'general')); statsEl.innerHTML = ''; const totalSpan = document.createElement('span'); totalSpan.textContent = '📚 ' + names.length + ' skills'; statsEl.appendChild(totalSpan); const catSpan = document.createElement('span'); catSpan.textContent = '📂 ' + cats.size + ' categories'; statsEl.appendChild(catSpan); if (query) { const matchSpan = document.createElement('span'); matchSpan.textContent = '🔍 ' + filtered.length + ' match'; statsEl.appendChild(matchSpan); } } if (filtered.length === 0) { const empty = document.createElement('div'); empty.style.cssText = 'padding:20px; text-align:center; color:#666'; empty.textContent = query ? 'No skills match your search.' : 'No skills yet. Create one below!'; list.appendChild(empty); return; } // Group by category const groups = {}; for (const name of filtered) { const s = index[name]; const cat = s.category || 'general'; if (!groups[cat]) groups[cat] = []; groups[cat].push(name); } const catOrder = Object.keys(groups).sort(); for (const cat of catOrder) { const section = document.createElement('div'); section.style.marginBottom = '6px'; const header = document.createElement('div'); header.style.cssText = 'font-size:0.8em; font-weight:600; color:var(--info-color); padding:4px 0; cursor:pointer; display:flex; align-items:center; gap:4px;'; header.textContent = '📁 ' + cat + ' (' + groups[cat].length + ')'; header.onclick = () => { const body = header.nextElementSibling; if (body) body.style.display = body.style.display === 'none' ? '' : 'none'; }; section.appendChild(header); const body = document.createElement('div'); for (const name of groups[cat]) { const s = index[name]; const tags = (s.tags || []).join(', '); const div = document.createElement('div'); div.className = 'module-item'; const info = document.createElement('div'); const strong = document.createElement('strong'); strong.style.color = 'var(--success-color)'; strong.textContent = name; info.appendChild(strong); const desc = document.createElement('div'); desc.style.cssText = 'font-size:0.85em; color:#aaa'; desc.textContent = s.description || ''; info.appendChild(desc); const tagsDiv = document.createElement('div'); tagsDiv.style.cssText = 'font-size:0.75em; color:#666'; tagsDiv.textContent = tags ? '🏷 ' + tags : ''; info.appendChild(tagsDiv); const actions = document.createElement('div'); actions.style.cssText = 'display:flex;gap:4px'; const viewBtn = document.createElement('button'); viewBtn.className = 'btn'; viewBtn.style.cssText = 'padding:4px 8px; font-size:0.8em'; viewBtn.textContent = '👁'; viewBtn.onclick = () => this.viewSkillUI(name); const editBtn = document.createElement('button'); editBtn.className = 'btn'; editBtn.style.cssText = 'padding:4px 8px; font-size:0.8em'; editBtn.textContent = '✏️'; editBtn.onclick = () => this.editSkillUI(name); const delBtn = document.createElement('button'); delBtn.className = 'btn btn-danger'; delBtn.style.cssText = 'padding:4px 8px; font-size:0.8em'; delBtn.textContent = '✕'; delBtn.onclick = () => this.deleteSkillUI(name); actions.appendChild(viewBtn); actions.appendChild(editBtn); actions.appendChild(delBtn); div.appendChild(info); div.appendChild(actions); body.appendChild(div); } section.appendChild(body); list.appendChild(section); } } viewSkillUI(name) { const fs = this.fs; const content = fs.readFile(`/skills/${name}.md`); if (!content) { this.print(`Skill '${name}' not found`, 'error'); return; } document.getElementById('skill-preview-title').textContent = name; document.getElementById('skill-preview-content').textContent = content; document.getElementById('skill-preview').style.display = 'flex'; document.getElementById('skill-preview-expand-btn').textContent = '⛶ Expand'; document.getElementById('skill-preview').style.maxHeight = '250px'; } closeSkillPreview() { document.getElementById('skill-preview').style.display = 'none'; } toggleSkillPreviewSize() { const preview = document.getElementById('skill-preview'); const btn = document.getElementById('skill-preview-expand-btn'); if (preview.style.maxHeight === 'none' || preview.style.maxHeight === '80vh') { preview.style.maxHeight = '250px'; btn.textContent = '⛶ Expand'; } else { preview.style.maxHeight = '80vh'; btn.textContent = '⛶ Collapse'; } } editSkillUI(name) { this.closeSkillsManager(); if (typeof webx11.dispatchCommand === 'function') { setTimeout(() => webx11.dispatchCommand('edit', ['/skills/' + name + '.md']), 200); } } deleteSkillUI(name) { if (!confirm(`Delete skill "${name}"?`)) return; const fs = this.fs; fs.mkdirRecursive('/skills'); if (fs.root && fs.root.content && fs.root.content['skills'] && fs.root.content['skills'].content) { delete fs.root.content['skills'].content[`${name}.md`]; } const index = fs.fsReadJSON('/skills/index.json') || {}; delete index[name]; fs.fsWriteJSON('/skills/index.json', index); this.print(`Skill '${name}' deleted`, 'success'); this.refreshSkillsList(); fs.saveToStorage(); } createSkillFromUI() { const name = document.getElementById('skill-name-input').value.trim().replace(/\s+/g, '-').toLowerCase(); const description = document.getElementById('skill-desc-input').value.trim() || 'No description'; const category = document.getElementById('skill-category-input')?.value || 'general'; const tags = document.getElementById('skill-tags-input').value.trim().split(',').map(t => t.trim()).filter(Boolean); const contentInput = document.getElementById('skill-content-input').value.trim(); if (!name) { alert('Skill name is required'); return; } const fs = this.fs; fs.mkdirRecursive('/skills'); if (fs.readFile(`/skills/${name}.md`)) { alert(`Skill '${name}' already exists. Delete it first or use skill-patch command.`); return; } let content = contentInput || `---\nname: ${this._escapeYamlString(name)}\ndescription: ${this._escapeYamlString(description)}\ntags: [${tags.map(t => this._escapeYamlString(t)).join(', ')}]\ncategory: ${this._escapeYamlString(category)}\n---\n## Steps\n1. (to be filled)\n`; if (!content.startsWith('---')) { content = `---\nname: ${this._escapeYamlString(name)}\ndescription: ${this._escapeYamlString(description)}\ntags: [${tags.map(t => this._escapeYamlString(t)).join(', ')}]\ncategory: ${this._escapeYamlString(category)}\n---\n${content}`; } fs.writeFile(`/skills/${name}.md`, content); const index = fs.fsReadJSON('/skills/index.json') || {}; index[name] = { name, description, category, tags }; fs.fsWriteJSON('/skills/index.json', index); this.print(`Skill '${name}' created`, 'success'); document.getElementById('skill-name-input').value = ''; document.getElementById('skill-desc-input').value = ''; document.getElementById('skill-tags-input').value = ''; document.getElementById('skill-content-input').value = ''; this.refreshSkillsList(); } exportAllSkillsUI() { const fs = this.fs; const index = fs.fsReadJSON('/skills/index.json') || {}; const names = Object.keys(index); if (names.length === 0) { alert('No skills to export.'); return; } const exportData = {}; for (const name of names) { const content = fs.readFile(`/skills/${name}.md`); if (content !== null) { exportData[name] = { ...index[name], content }; } } const json = JSON.stringify(exportData, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'skills-export-' + new Date().toISOString().slice(0, 10) + '.json'; a.click(); URL.revokeObjectURL(url); this.print(`📤 Exported ${names.length} skills`, 'success'); } importSkillsUI(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const data = JSON.parse(e.target.result); const fs = this.fs; fs.mkdirRecursive('/skills'); const index = fs.fsReadJSON('/skills/index.json') || {}; let imported = 0; for (const [name, skill] of Object.entries(data)) { if (!skill.content) continue; fs.writeFile(`/skills/${name}.md`, skill.content); index[name] = { name, description: skill.description || name, tags: skill.tags || [], category: skill.category || 'general', created: (index[name] && index[name].created) || new Date().toISOString(), updated: new Date().toISOString() }; imported++; } fs.fsWriteJSON('/skills/index.json', index); this.print(`📥 Imported ${imported} skills`, 'success'); this.refreshSkillsList(); } catch (err) { alert('Invalid JSON file: ' + err.message); } }; reader.readAsText(file); event.target.value = ''; } syncSkillsToWebdavUI() { const mod = this.moduleManager && this.moduleManager.modules['SkillPackCoreModule']; if (mod && mod.commands && mod.commands['skill-webdav-sync']) { mod.commands['skill-webdav-sync'](); } else { this.print('WebDAV sync not available. Is skill-pack-core loaded?', 'warning'); } } updateOllamaStatus() { const statusDiv = document.getElementById('ollama-status'); const statusText = document.getElementById('ollama-status-text'); if (this.ollamaManager.connected) { statusDiv.className = 'ollama-status connected'; statusText.textContent = `Connected (${this.ollamaManager.models.length} models)`; } else { statusDiv.className = 'ollama-status disconnected'; statusText.textContent = 'Disconnected'; } } renderOllamaModels() { const container = document.getElementById('ollama-models'); const select = document.getElementById('ollama-current-model'); const searchInput = document.getElementById('ollama-search'); const countDiv = document.getElementById('ollama-model-count'); const query = (searchInput ? searchInput.value : '').toLowerCase().trim(); container.innerHTML = ''; select.innerHTML = ''; const models = this.ollamaManager.getModels(); if (models.length === 0) { container.innerHTML = '
Connect to see models
'; if (countDiv) countDiv.textContent = ''; return; } const filtered = query ? models.filter(m => m.name.toLowerCase().includes(query)) : models; if (countDiv) countDiv.textContent = filtered.length === models.length ? models.length + ' model' + (models.length !== 1 ? 's' : '') : filtered.length + ' / ' + models.length + ' model' + (models.length !== 1 ? 's' : ''); if (filtered.length === 0) { container.innerHTML = '
No models match "' + query + '"
'; return; } filtered.forEach(model => { const div = document.createElement('div'); div.className = 'model-item'; div.innerHTML = `${model.name}${model.size ? (model.size / 1e9).toFixed(1) + 'GB' : ''}`; div.onclick = () => { this.ollamaManager.setModel(model.name); select.value = model.name; }; container.appendChild(div); const option = document.createElement('option'); option.value = model.name; option.textContent = model.name; if (model.name === this.ollamaManager.currentModel) { option.selected = true; } select.appendChild(option); }); } async connectOllama() { const endpoint = document.getElementById('ollama-endpoint').value.trim(); this.ollamaManager.setEndpoint(endpoint); const success = await this.ollamaManager.connect(); if (success) { this.renderOllamaModels(); this._populateValidatorModels(); this.updateOllamaStatus(); } } setOllamaModel() { const select = document.getElementById('ollama-current-model'); this.ollamaManager.setModel(select.value); } toggleOptimization() { const cb = document.getElementById('ollama-optimization-toggle'); const label = document.getElementById('ollama-optimization-label'); const slider = document.getElementById('ollama-optimization-slider'); const enabled = cb.checked; this.ollamaManager.setOptimizationEnabled(enabled); if (label) { label.textContent = enabled ? 'Enabled' : 'Disabled'; label.style.color = enabled ? 'var(--success-color)' : 'var(--text-muted)'; } if (slider) { slider.style.background = enabled ? 'var(--accent-color)' : 'var(--bg-tertiary)'; slider.style.borderColor = enabled ? 'var(--success-color)' : 'var(--border-color)'; } this.ollamaManager.saveSettings(); } toggleValidator() { const cb = document.getElementById('ollama-validator-toggle'); const label = document.getElementById('ollama-validator-label'); const slider = document.getElementById('ollama-validator-slider'); const enabled = cb.checked; this.ollamaManager.validatorEnabled = enabled; if (label) { label.textContent = enabled ? 'Enabled' : 'Disabled'; label.style.color = enabled ? 'var(--success-color)' : 'var(--text-muted)'; } if (slider) { slider.style.background = enabled ? 'var(--info-color)' : 'var(--bg-tertiary)'; slider.style.borderColor = enabled ? 'var(--info-color)' : 'var(--border-color)'; } this.ollamaManager.saveSettings(); } toggleStream() { const cb = document.getElementById('ollama-stream-toggle'); const label = document.getElementById('ollama-stream-label'); const slider = document.getElementById('ollama-stream-slider'); const enabled = cb.checked; this.ollamaManager.setStreamEnabled(enabled); if (label) { label.textContent = enabled ? 'Enabled' : 'Disabled'; label.style.color = enabled ? 'var(--success-color)' : 'var(--text-muted)'; } if (slider) { slider.style.background = enabled ? 'var(--accent-color)' : 'var(--bg-tertiary)'; slider.style.borderColor = enabled ? 'var(--success-color)' : 'var(--border-color)'; } } updateStreamIndicator() { const el = document.getElementById('stream-status'); if (!el) return; const enabled = this.ollamaManager?.streamEnabled !== false; el.textContent = enabled ? '▶ Stream' : '🔇 No-Stream'; el.style.color = enabled ? 'var(--success-color)' : 'var(--text-muted)'; } saveValidatorConfig() { const select = document.getElementById('validator-model-select'); const endpoint = document.getElementById('validator-endpoint-input')?.value?.trim() || 'http://localhost:11434'; const model = select ? select.value : ''; localStorage.setItem('webx11_validator_endpoint', endpoint); this.ollamaManager.validatorEndpoint = endpoint; this.ollamaManager.setValidatorConfig(this.ollamaManager.validatorEnabled, model); this.print('Validator config saved. Model: ' + (model || '(none)') + ' at ' + endpoint, 'success'); } async connectValidatorEndpoint() { const endpoint = document.getElementById('validator-endpoint-input')?.value?.trim() || 'http://localhost:11434'; const statusEl = document.getElementById('validator-connection-status'); const select = document.getElementById('validator-model-select'); if (statusEl) statusEl.textContent = '⏳ Connecting to ' + endpoint + '...'; if (select) { select.disabled = true; select.innerHTML = ''; } try { const resp = await fetch(endpoint + '/api/tags'); if (!resp.ok) throw new Error('HTTP ' + resp.status); const data = await resp.json(); const models = data.models || []; if (statusEl) statusEl.textContent = '✅ Connected: ' + models.length + ' model(s)'; // Populate dropdown if (select) { select.innerHTML = ''; models.forEach(function(m) { var opt = document.createElement('option'); opt.value = m.name; opt.textContent = m.name; select.appendChild(opt); }); select.disabled = false; } // Save endpoint localStorage.setItem('webx11_validator_endpoint', endpoint); this.ollamaManager.validatorEndpoint = endpoint; this.ollamaManager.saveSettings(); } catch (e) { if (statusEl) { statusEl.innerHTML = '❌ Connection failed: ' + e.message + '
Make sure Ollama is running and ' + '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 ', 'warning'); this.print('Loads a module from VFS (e.g. "load-local modules/ext/mindmap.js") or from URL', 'info'); return; } let code = this.fs.readFile(path); if (code === null) { try { const resp = await fetch(path); if (!resp.ok) throw new Error('HTTP ' + resp.status); code = await resp.text(); } catch (e) { this.print('load-local: file not found in VFS or at URL: ' + path, 'error'); return; } } try { const el = document.createElement('script'); el.textContent = code; el.onerror = () => this.print('load-local: failed to execute ' + path, 'error'); document.body.appendChild(el); this.print('Module loaded from ' + path, 'success'); } catch (e) { this.print('load-local: error: ' + e.message, 'error'); } } async saveCodeEditor() { const ta = document.getElementById('code-editor-textarea'); const mod = this._editingModule; if (!mod || !ta) return; const code = ta.value; this.fs.writeFile('_mods/' + mod.name + '.js', code); if (!this.fs.readFile('_mods/' + mod.name + '.orig.js')) { this.fs.writeFile('_mods/' + mod.name + '.orig.js', 'placeholder'); } this.fs.writeFile('_mods/' + mod.name + '.meta.json', JSON.stringify({ originalUrl: mod.url || '', modifiedAt: new Date().toISOString(), editCount: 1 })); try { const ok = await this.moduleManager.loadModuleFromCode(code, 'blob://_mods/' + mod.name + '.js'); if (ok) { this.print('✅ Module loaded live from _mods/' + mod.name + '.js', 'success'); } else { this.print('⚠️ Auto-test failed — edits saved but module not loaded', 'warning'); } } catch (e) { this.print('⚠️ Auto-test failed: ' + e.message + ' — edits saved but module not loaded', 'error'); } this.closeCodeEditor(); } _restoreFontSize() { try { const saved = localStorage.getItem('webx11_font_size'); if (saved) { const s = parseInt(saved); if (s >= 8 && s <= 32) { document.body.style.fontSize = s + 'px'; const tc = document.getElementById('terminal-container'); if (tc) tc.style.fontSize = s + 'px'; const inp = document.getElementById('command-input'); if (inp) inp.style.fontSize = s + 'px'; } } } catch (e) {} } _restoreTheme() { try { const savedTheme = localStorage.getItem('webx11_active_theme'); if (savedTheme) { const themesRaw = localStorage.getItem('webx11_themes'); const defaults = { 'dark': { '--bg-color': '#000000', '--bg-secondary': '#0a0a0a', '--bg-tertiary': '#111111', '--text-color': '#00ff00', '--text-muted': '#888888', '--accent-color': '#00aa00', '--border-color': '#005500' }, 'light': { '--bg-color': '#ffffff', '--bg-secondary': '#f5f5f5', '--bg-tertiary': '#eeeeee', '--text-color': '#222222', '--text-muted': '#888888', '--accent-color': '#0066cc', '--border-color': '#cccccc' }, 'amber': { '--bg-color': '#1a0f00', '--bg-secondary': '#2a1a00', '--bg-tertiary': '#332200', '--text-color': '#ffaa00', '--text-muted': '#886600', '--accent-color': '#ff8800', '--border-color': '#663300' }, 'cyber': { '--bg-color': '#000a14', '--bg-secondary': '#001a2e', '--bg-tertiary': '#002240', '--text-color': '#00ffff', '--text-muted': '#0088aa', '--accent-color': '#ff00ff', '--border-color': '#004466' }, 'ocean': { '--bg-color': '#0a1628', '--bg-secondary': '#0f2240', '--bg-tertiary': '#142e54', '--text-color': '#88ddff', '--text-muted': '#5599bb', '--accent-color': '#00aaff', '--border-color': '#1a4470' }, 'forest': { '--bg-color': '#0a1a0e', '--bg-secondary': '#0f2814', '--bg-tertiary': '#14361c', '--text-color': '#88dd88', '--text-muted': '#559955', '--accent-color': '#33cc33', '--border-color': '#1a4a22' }, 'monochrome': { '--bg-color': '#0a0a0a', '--bg-secondary': '#181818', '--bg-tertiary': '#222222', '--text-color': '#cccccc', '--text-muted': '#666666', '--accent-color': '#999999', '--border-color': '#333333' }, 'sunset': { '--bg-color': '#1a0a14', '--bg-secondary': '#2a1220', '--bg-tertiary': '#3a1a2c', '--text-color': '#ffbb99', '--text-muted': '#cc7755', '--accent-color': '#ff6644', '--border-color': '#552233' }, 'midnight': { '--bg-color': '#050510', '--bg-secondary': '#0a0a20', '--bg-tertiary': '#101030', '--text-color': '#aaaaee', '--text-muted': '#555588', '--accent-color': '#6666ff', '--border-color': '#1a1a3a' } }; if (themesRaw) { const themes = JSON.parse(themesRaw); Object.assign(defaults, themes); } const t = defaults[savedTheme]; if (t) { for (const [k, v] of Object.entries(t)) { document.documentElement.style.setProperty(k, v); } } } } catch (e) { console.warn('[Theme] restore error:', e); } } // ── First-run welcome ── _checkFirstRun() { if (localStorage.getItem('webx11_tutorial_complete') || localStorage.getItem('webx11_tutorial_skipped')) return; // Check if tutorial module is loaded — retry if ext modules not ready yet const tutMod = this.moduleManager.modules['TutorialModule']; if (!tutMod) { // Ext modules may still be loading; retry in 2s if (!this._welcomeRetry) { this._welcomeRetry = true; setTimeout(() => this._checkFirstRun(), 2000); } return; } this.tutorialModule = tutMod; // Only show welcome if user hasn't explicitly dismissed it if (!localStorage.getItem('webx11_welcome_dismissed')) { this.showWelcomeModal(); } } showWelcomeModal() { const modal = document.getElementById('welcome-modal'); if (modal) { modal.style.display = 'flex'; this._welcomeShown = true; } } dismissWelcome() { const modal = document.getElementById('welcome-modal'); if (modal) modal.style.display = 'none'; localStorage.setItem('webx11_welcome_dismissed', '1'); // Also mark tutorial as skipped so it doesn't reappear if (this.tutorialModule) this.tutorialModule.skipTutorial(); } toggleCmdCards() { const panel = document.getElementById('cmd-cards'); if (!panel) return; panel.classList.toggle('visible'); // If opening, focus the input field if (panel.classList.contains('visible')) { setTimeout(() => this.inputField.focus(), 50); } } log(level, source, message) { const d = new Date(); const ts = d.toISOString().split('T')[1].split('.')[0]; const entry = `[${ts}][${level}][${source}] ${message}`; console.log(entry); // Write to VFS log try { this.fs.mkdirRecursive('/logs'); let current = this.fs.readFile('/logs/current.log') || ''; current += entry + '\n'; const lines = current.split('\n').filter(l => l); if (lines.length > 200) { this.fs.writeFile('/logs/old.log', lines.slice(0, -100).join('\n')); this.fs.writeFile('/logs/current.log', lines.slice(-100).join('\n')); } else { this.fs.writeFile('/logs/current.log', current); } } catch (e) {} } setAiBusy(busy) { this._aiBusy = !!busy; if (this._aiBusyInterval) { clearInterval(this._aiBusyInterval); this._aiBusyInterval = null; } const cancelBtn = document.getElementById('ai-cancel-btn'); if (busy) { if (cancelBtn) cancelBtn.style.display = 'inline-block'; this._aiBusyStart = Date.now(); const updateTimer = () => { let elapsed = Math.round((Date.now() - this._aiBusyStart) / 1000); let sec = elapsed % 60; let min = Math.floor(elapsed / 60); this.statusBar.textContent = '🤖 AI Thinking... ⏱ ' + (min > 0 ? min + 'm ' : '') + sec + 's'; this.statusBar.className = 'loading'; }; updateTimer(); this._aiBusyInterval = setInterval(updateTimer, 1000); } else { if (cancelBtn) cancelBtn.style.display = 'none'; this.statusBar.textContent = 'Ready'; this.statusBar.className = ''; this._aiBusyStart = 0; } } printMemoryStats() { const pm = typeof performance !== 'undefined' && performance.memory; let heapInfo = ''; let domNodes = 0; try { domNodes = document.getElementsByTagName('*').length; } catch (e) {} if (pm) { const used = (pm.usedJSHeapSize / 1048576).toFixed(1); const total = (pm.totalJSHeapSize / 1048576).toFixed(1); const limit = (pm.jsHeapSizeLimit / 1048576).toFixed(1); heapInfo = `JS heap: ${used}MB / ${total}MB (limit ${limit}MB)`; } else { try { const lsSize = (new Blob([JSON.stringify(localStorage)]).size / 1024).toFixed(1); heapInfo = `localStorage: ${lsSize}KB`; } catch (e) { heapInfo = 'Memory API not available on this browser'; } } const output = [ `📊 Memory Stats`, ` ${heapInfo}`, ` DOM nodes: ${domNodes}`, ` Terminal lines in DOM: ${this.outputContainer.children.length}`, ` History buffer: ${this._historyBuffer.length} entries`, ` MAX_OUTPUT_LINES: ${this.MAX_OUTPUT_LINES}`, ` History-backed output lines: ${this._historyBuffer.length}`, ` Commands in history: ${this.history.length}` ].join('\n'); this.print(output, 'info'); } playNotificationSound() { if (localStorage.getItem('webx11_config_ai_sound') === 'off') return; try { const ctx = new (window.AudioContext || window.webkitAudioContext)(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.frequency.value = 880; osc.type = 'sine'; gain.gain.setValueAtTime(0.15, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.3); osc.start(ctx.currentTime); osc.stop(ctx.currentTime + 0.3); } catch (e) {} } toggleMicVisibility() { const voiceMod = this.moduleManager.modules['VoiceModule']; if (!voiceMod) { this.print('Voice module not loaded', 'warning'); return; } if (voiceMod.listening) { voiceMod.stopListening(); } else { voiceMod.handle([]); } } toggleDebugPanel() { const mod = this.moduleManager.modules['DebugPanel']; if (mod) { mod.terminal = this; mod.toggle(); } else { this.print('Debug panel not loaded', 'warning'); } } setFontSize(size) { const s = parseInt(size); if (isNaN(s) || s < 8 || s > 32) { this.print('Usage: font-size <8-32>', 'warning'); return; } document.body.style.fontSize = s + 'px'; document.getElementById('terminal-container').style.fontSize = s + 'px'; document.getElementById('command-input').style.fontSize = s + 'px'; try { localStorage.setItem('webx11_font_size', String(s)); } catch (e) {} this.print('Font size set to ' + s + 'px 👆', 'success'); } }