diff options
| author | Agando <roduccomm@gmail.com> | 2026-06-18 18:00:54 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-06-18 18:00:54 +0200 |
| commit | 0eca39021874a673933ebb82b973002b682e7a4b (patch) | |
| tree | dbfbb685e40d10e6255e68629b72194b23709854 | |
| parent | fba6df22dae297c89fa0df065628dddded66c69c (diff) | |
auto: 2026-06-18_18:00 changes
| -rw-r--r-- | chat/chat-commands.js | 28 | ||||
| -rw-r--r-- | chat/chat-modules.js | 20 | ||||
| -rw-r--r-- | chat/chat-ui.html | 2 | ||||
| -rw-r--r-- | docs/audit-2026-06-18_chatstudio-overlay.md | 47 | ||||
| -rw-r--r-- | main.html | 35 | ||||
| -rw-r--r-- | modules/core/core-bundle.js | 236 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 46 | ||||
| -rw-r--r-- | modules/core/module-chatstudio.js | 131 | ||||
| -rw-r--r-- | modules/core/module-fs.js | 17 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/core/terminal.js | 42 | ||||
| -rw-r--r-- | modules/ext/console.js | 12 |
12 files changed, 492 insertions, 126 deletions
diff --git a/chat/chat-commands.js b/chat/chat-commands.js index d5cfdd9..c0c77ee 100644 --- a/chat/chat-commands.js +++ b/chat/chat-commands.js @@ -4,6 +4,16 @@ * Handles /search, /fetch, /explain, /help, /status, /clear, /modules, etc. */ +// Fetch with timeout — AbortController-based, 30s default +function _csFetch(url, opts, timeout) { + timeout = timeout || 30000; + opts = opts || {}; + var ctrl = new AbortController(); + opts.signal = ctrl.signal; + var timer = setTimeout(function() { ctrl.abort(); }, timeout); + return fetch(url, opts).finally(function() { clearTimeout(timer); }); +} + async function handleCommand(cmd) { var parts = cmd.split(' '); var command = parts[0].toLowerCase(); @@ -67,7 +77,7 @@ async function handleCommand(cmd) { if (typeof OLLAMA_API_KEY !== 'undefined' && OLLAMA_API_KEY) { headers['Authorization'] = 'Bearer ' + OLLAMA_API_KEY; } - var response = await fetch((typeof OLLAMA_URL !== 'undefined' ? OLLAMA_URL : '/ollama') + '/api/generate', { + var response = await _csFetch((typeof OLLAMA_URL !== 'undefined' ? OLLAMA_URL : '/ollama') + '/api/generate', { method: 'POST', headers: headers, body: JSON.stringify({ @@ -95,7 +105,7 @@ async function handleCommand(cmd) { } showTyping(); try { - var response = await fetch('/api/search?q=' + encodeURIComponent(query) + '&summarize=true'); + var response = await _csFetch('/api/search?q=' + encodeURIComponent(query) + '&summarize=true'); var data = await response.json(); hideTyping(); if (data.error) { @@ -140,11 +150,11 @@ async function handleCommand(cmd) { } showTyping(); try { - var extractResp = await fetch('/api/extract?url=' + encodeURIComponent(url)); + var extractResp = await _csFetch('/api/extract?url=' + encodeURIComponent(url)); var extractOk = extractResp.ok; var extractData = extractOk ? await extractResp.json() : null; - var htmlResp = await fetch('/api/fetch?url=' + encodeURIComponent(url)); + var htmlResp = await _csFetch('/api/fetch?url=' + encodeURIComponent(url)); if (!htmlResp.ok) throw new Error('HTTP ' + htmlResp.status); var html = await htmlResp.text(); var htmlPreview = html.substring(0, 262144) + (html.length > 262144 ? '\n\n... (truncated)' : ''); @@ -178,7 +188,7 @@ async function handleCommand(cmd) { } showTyping(); try { - var response = await fetch('/api/summarize?url=' + encodeURIComponent(url)); + var response = await _csFetch('/api/summarize?url=' + encodeURIComponent(url)); if (!response.ok) throw new Error('HTTP ' + response.status); var data = await response.json(); hideTyping(); @@ -204,7 +214,7 @@ async function handleCommand(cmd) { } showTyping(); try { - var response = await fetch('/api/extract?url=' + encodeURIComponent(url)); + var response = await _csFetch('/api/extract?url=' + encodeURIComponent(url)); if (!response.ok) throw new Error('HTTP ' + response.status); var data = await response.json(); hideTyping(); @@ -225,7 +235,7 @@ async function handleCommand(cmd) { // === /status === if (command === '/status') { try { - var response = await fetch((typeof OLLAMA_URL !== 'undefined' ? OLLAMA_URL : '/ollama') + '/api/tags'); + var response = await _csFetch((typeof OLLAMA_URL !== 'undefined' ? OLLAMA_URL : '/ollama') + '/api/tags'); var data = await response.json(); var modelCount = data.models ? data.models.length : 0; var current = document.getElementById('modelSelect').value; @@ -241,7 +251,7 @@ async function handleCommand(cmd) { showTyping(); try { var start = Date.now(); - var response = await fetch((typeof OLLAMA_URL !== 'undefined' ? OLLAMA_URL : '/ollama') + '/api/tags'); + var response = await _csFetch((typeof OLLAMA_URL !== 'undefined' ? OLLAMA_URL : '/ollama') + '/api/tags'); var ms = Date.now() - start; hideTyping(); addMessage('ai', '**Connection Test**\n\n- Response time: ' + ms + 'ms\n- Status: OK'); @@ -255,7 +265,7 @@ async function handleCommand(cmd) { // === /models or /list models === if (command === '/models') { try { - var response = await fetch((typeof OLLAMA_URL !== 'undefined' ? OLLAMA_URL : '/ollama') + '/api/tags'); + var response = await _csFetch((typeof OLLAMA_URL !== 'undefined' ? OLLAMA_URL : '/ollama') + '/api/tags'); var data = await response.json(); var list = data.models ? data.models.map(function (m) { return '- ' + m.name; }).join('\n') : 'No models'; addMessage('ai', '**Available Models:**\n\n' + list); diff --git a/chat/chat-modules.js b/chat/chat-modules.js index 140e379..295c7c9 100644 --- a/chat/chat-modules.js +++ b/chat/chat-modules.js @@ -10,9 +10,9 @@ const DEFAULT_MODULES = [ id: 'default', name: 'AI Chat', desc: 'General purpose AI assistant', - prompt: 'You are a helpful AI assistant on WebX11. You have access to these tools:\n\n1. **Search:** /api/search?q=QUERY - Searches the web via DuckDuckGo. Returns HTML results. Use this for current events, facts, and web information.\n\n2. **Fetch:** /api/fetch?url=URL - Fetches content from any URL. Use this to read web pages, APIs, or documents.\n\n3. **Commands:** Users can type /help, /search [query], /fetch [url], /explain [code], /status, /clear directly.\n\nWhen a user asks about current events, recent info, or something you dont know, suggest they use /search or tell them to ask you to search the web. If they provide a URL, offer to fetch it.\n\nBe concise, helpful, and use the available tools when appropriate.', - help: 'Just type any question! I can help with:\n- Current events (I can search the web)\n- Web pages (I can fetch URLs)\n- Coding questions\n- General knowledge\n- Problem solving\n\nTry: /search latest news', - commands: '/help, /search, /fetch, /explain, /status, /clear' + prompt: 'You are a helpful AI assistant on WebX11 newOS4. Current date is injected dynamically. You have access to these tools:\n\n1. **Search:** /api/search?q=QUERY - Searches the web via DuckDuckGo/SearXNG. Returns results with titles, URLs, and snippets. Use this for current events, facts, and web information.\n2. **Fetch:** /api/fetch?url=URL - Fetches raw HTML from any URL. Use this to read web pages, APIs, or documents.\n3. **Extract:** /api/extract?url=URL - Extracts readable content (title, word count, excerpt, full text) from a URL. Prefer this over /fetch for reading articles.\n4. **Summarize:** /api/summarize?url=URL - AI summarization of a URL.\n\n**Tool-Use Rules:**\n- NEVER announce that you are about to use a tool (no "Hold on...", "Let me search...", "Fetching...", or waiting indicators). Just use the tool and present the result immediately.\n- If a /search returns generic/portal results (e.g., google.com homepages), automatically retry with a more specific query using site: operators or exact phrases in quotes.\n- If a user types a malformed command (e.g., /extraxt instead of /extract, /serch instead of /search), recognize their intent and suggest the correct command immediately: "Did you mean /extract [url]? Here are the available commands: /search, /fetch, /extract, /summarize"\n- When a user shares a URL, proactively suggest /extract or /fetch with the URL pre-filled.\n- Be concise. Deliver results, not commentary about the process.\n\n**Available slash commands:** /help, /search [query], /fetch [url], /extract [url], /summarize [url], /explain [code], /status, /models, /clear', + help: 'Just type any question! I can help with:\n- Current events (I can search the web)\n- Web pages (I can extract/fetch URLs)\n- Coding questions\n- General knowledge\n- Problem solving\n\nCommands: /search, /fetch, /extract, /summarize, /explain, /status, /clear\n\nTry: /search latest news', + commands: '/help, /search, /fetch, /extract, /summarize, /explain, /status, /models, /clear' }, { id: 'search', @@ -72,7 +72,8 @@ const DEFAULT_MODULES = [ } ]; -var modules = []; +// modules array lives on window.modules (set by _initLiteEngine in module-chatstudio.js) +// Do NOT redeclare here — just reference the global var currentModule = 'default'; var editingModuleId = null; @@ -266,14 +267,19 @@ function saveModule() { // === AI Module Generator === -async function generateModuleWithAI() { +async function generateModuleWithAI(evt) { + var e = evt || window.event; var desc = document.getElementById('aiModuleDesc').value.trim(); if (!desc) { showToast('Please describe what the module should do'); return; } - var btn = event.target; + var btn = e ? e.target : null; + if (!btn) { + var modal = document.getElementById('moduleModal'); + btn = modal ? modal.querySelector('button[onclick*="generateModule"]') : null; + } btn.disabled = true; btn.textContent = 'Generating...'; @@ -283,7 +289,7 @@ async function generateModuleWithAI() { headers['Authorization'] = 'Bearer ' + OLLAMA_API_KEY; } - var response = await fetch((typeof OLLAMA_URL !== 'undefined' ? OLLAMA_URL : '/ollama') + '/api/generate', { + var response = await _csFetch((typeof OLLAMA_URL !== 'undefined' ? OLLAMA_URL : '/ollama') + '/api/generate', { method: 'POST', headers: headers, body: JSON.stringify({ diff --git a/chat/chat-ui.html b/chat/chat-ui.html index efe0347..5b8e421 100644 --- a/chat/chat-ui.html +++ b/chat/chat-ui.html @@ -62,7 +62,7 @@ </div> <div class="input-area"> - <textarea id="chatInput" placeholder="Type your message..." autocomplete="off" rows="1" onfocus="this.style.height='80px'" oninput="autoExpand(this);if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMessage();}"></textarea> + <textarea id="chatInput" placeholder="Type your message..." autocomplete="off" rows="1" onfocus="this.style.height='80px'" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMessage();}" oninput="autoExpand(this)"></textarea> <button class="btn btn-primary" onclick="sendMessage()">Send</button> </div> </main> diff --git a/docs/audit-2026-06-18_chatstudio-overlay.md b/docs/audit-2026-06-18_chatstudio-overlay.md new file mode 100644 index 0000000..8134caf --- /dev/null +++ b/docs/audit-2026-06-18_chatstudio-overlay.md @@ -0,0 +1,47 @@ +# ChatStudio Overlay Audit — v4.5.70 +Date: 2026-06-18 +Auditor: Hermes Agent +Files reviewed: module-chatstudio.js (1,064 lines), chat-commands.js (276 lines), chat-modules.js (375 lines), chat-ui.html (160 lines), chat-ui.css (747 lines) + +## P0 — Critical Security (3 issues) — FIXED in v4.5.70 + +| # | Issue | File:Line | Status | +|---|-------|-----------|--------| +| 1 | XSS via single-quote injection — `escapeHtml()` didn't escape `'`, module IDs injected into `onclick` with single-quote delimiters | module-chatstudio.js:254, chat-modules.js:113-117 | ✅ Fixed — added `'` escaping | +| 2 | Iframe preview without sandbox — `showHtmlPreview()`/`previewCodeBlock()` write AI content into unsandboxed iframe with full parent access | chat-ui.html:157 | ✅ Fixed — `sandbox="allow-same-origin"` | +| 3 | `importModules()` accepts unvalidated JSON — zero schema check, no ID sanitization, one-click XSS via crafted import file | chat-modules.js:339-367 | ✅ Fixed — 1MB limit, schema validation, ID sanitization | + +## P1 — Bugs & Reliability (7 issues) — NOT YET FIXED + +| # | Issue | File:Line | Impact | +|---|-------|-----------|--------| +| 4 | No fetch timeouts — 11 bare `fetch()` calls with no AbortController | chat-commands.js:98,143,147,181,207,228,244,258 + module-chatstudio.js:446,465,773,860 | Slow/hung backend freezes chat indefinitely | +| 5 | No streaming cancel — once `isStreaming=true`, no abort button or timeout | module-chatstudio.js:751-856 | Model hang = page reload required | +| 6 | CSS load race condition — `resolve()` fires before CSS `<link>` onload | module-chatstudio.js:39-46 | FOUC on first overlay open | +| 7 | `oninput` for Enter key — unreliable across browsers, should be `onkeydown` | chat-ui.html:65 | Enter key may not submit on some browsers | +| 8 | Duplicate `modules` array — `chat-modules.js:75` declares local `var modules` shadowing `window.modules` | chat-modules.js:75 vs module-chatstudio.js:960 | If load order changes, `handleCommand` sees empty array | +| 9 | `_csGlobalsInited` stuck true after init failure — placeholder stubs remain active on retry | module-chatstudio.js:94 vs 1054 | Retry silently does nothing | +| 10 | `generateModuleWithAI` uses deprecated global `event` | chat-modules.js:276 | Fails in strict mode | + +## P2 — UX & Architecture (8 issues) — NOT YET FIXED + +| # | Issue | File:Line | Notes | +|---|-------|-----------|-------| +| 11 | API key in base64 localStorage — not encryption, trivially decodable | module-chatstudio.js:419 | Should use secret-vault.js | +| 12 | Inconsistent storage encoding — messages plain JSON, logs/settings base64 | module-chatstudio.js:505 | Should be consistent | +| 13 | Chat history no max size — long conversations overflow localStorage | module-chatstudio.js:502-508 | Cap at N messages | +| 14 | Static `?v=5` cache-busting — doesn't use NEWOS4_VERSION | module-chatstudio.js:10489,10550 | Manual version bump needed | +| 15 | `buildChatContext` naive HTML strip — regex-based, easily fooled | module-chatstudio.js:888 | Use DOMParser | +| 16 | User messages get no markdown formatting — asymmetric UX | module-chatstudio.js:548 | Apply formatContent to user msgs too | +| 17 | Activity log hardcoded 50 limit | module-chatstudio.js:325 | Not configurable | +| 18 | `CHATSTUDIO_VERSION` not synced to `NEWOS4_VERSION` | module-chatstudio.js:231 | Stale version info | + +## Tool-Use Optimization — Applied in v4.5.70 + +From AI self-report: +- Search retry strategy: system prompt instructs AI to retry with `site:` operators if first search returns generic portals +- No "waiting" hallucinations: system prompt bans announcing tool use ("Hold on...", "Fetching...") +- Typo tolerance: system prompt instructs AI to recognize malformed commands and suggest correct one +- Date injection: ChatStudio overlay now injects current date into every AI request +- Updated default module prompt: lists all 4 tools (search, fetch, extract, summarize) with tool-use rules +- Updated help text: shows all available commands
\ No newline at end of file @@ -849,6 +849,35 @@ animation: float-in 0.15s ease-out; } </style> + <!-- Early persistence restore — apply saved theme + font-size BEFORE bundle loads to prevent FOUC --> + <script> + (function() { + try { + // Restore font size + var fs = localStorage.getItem('webx11_font_size'); + if (fs) { document.documentElement.style.fontSize = fs + 'px'; } + // Restore theme + var theme = localStorage.getItem('webx11_active_theme'); + if (theme) { + var themesRaw = localStorage.getItem('webx11_themes'); + var 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) { try { Object.assign(defaults, JSON.parse(themesRaw)); } catch(e){} } + var t = defaults[theme]; + if (t) { for (var k in t) { document.documentElement.style.setProperty(k, t[k]); } } + } + } catch(e) {} + })(); + </script> </head> <body> <header> @@ -1281,11 +1310,11 @@ </script> <!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix --> -<script src="modules/core/core-bundle.js?v=2026-06-18-v4.5.70"></script> -<script src="modules/ext/critical-ext-bundle.js?v=2026-06-18-v4.5.70"></script> +<script src="modules/core/core-bundle.js?v=2026-06-18-v4.5.74"></script> +<script src="modules/ext/critical-ext-bundle.js?v=2026-06-18-v4.5.74"></script> <!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules --> -<script src="modules/core/module-loader.js?v=2026-06-18-v4.5.70"></script> +<script src="modules/core/module-loader.js?v=2026-06-18-v4.5.74"></script> <script> // ═══ ONE TRUE VERSION SOURCE ═══ diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index cb3471a..af59d06 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -2237,7 +2237,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm this.destructiveCmds = new Set(['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']); this.systemCmds = new Set(['reboot','shutdown','format','kill','halt','poweroff']); this.suspiciousFlags = new Set(['--dump','--execute','--eval','--exec','--shell','--script']); - this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','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','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']); + this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','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','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']); // Load persisted overrides from localStorage try { const d = localStorage.getItem('webx11_security_destructive'); if(d){JSON.parse(d).forEach(c=>this.destructiveCmds.add(c));} }catch(e){} try { const s = localStorage.getItem('webx11_security_system'); if(s){JSON.parse(s).forEach(c=>this.systemCmds.add(c));} }catch(e){} @@ -3254,7 +3254,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm // 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']; + 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 @@ -3440,15 +3440,37 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm } } _safeRegister(className, label) { - let cls; - try { - cls = eval(className); - } catch (e) { - console.log(`[SafeRegister] Skipped ${label}: class not loaded (skipped in boot selection)`); - return; - } + // 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, + }; + const cls = registry[className]; if (typeof cls === 'undefined') { - console.log(`[SafeRegister] Skipped ${label}: class not defined`); + console.log(`[SafeRegister] Skipped ${label}: class not loaded (skipped in boot selection)`); return; } try { this.moduleManager.registerModule(new cls()); } @@ -5591,6 +5613,11 @@ class FileSystemModule { 'download': 'download <file> — Download a file to your device', 'vfs-export': 'vfs-export — Export the entire virtual filesystem as JSON', 'vfs-import': 'vfs-import — Import a previously exported VFS JSON file', + 'session-save': 'session-save <name> — Save current session (path + terminal history)', + 'session-load': 'session-load <name> — Load a saved session', + 'session-list': 'session-list — List all saved sessions', + 'session-new': 'session-new — Start a fresh session (auto-saves current, clears AI context)', + 'session-delete': 'session-delete <name> — Delete a saved session', }; } registerCommands() { @@ -6044,6 +6071,18 @@ class FileSystemModule { this.terminal.print('Session not found: ' + args[0], 'error'); } }; + this.commands['session-new'] = () => { + // Auto-save current session, clear AI context, fresh terminal + this.terminal._autoSaveSession(); + try { localStorage.removeItem('webx11_ai_context'); } catch(e) {} + // Clear terminal output + if (this.terminal.outputContainer) this.terminal.outputContainer.innerHTML = ''; + if (this.terminal._historyBuffer) this.terminal._historyBuffer = []; + // Clear AI conversation context + const aiMod = this.terminal.moduleManager.modules['WebX11AIAssistantModule']; + if (aiMod && aiMod.clearContext) aiMod.clearContext(); + this.terminal.print('✨ New session started — context cleared', 'success'); + }; this.commands['session-export'] = (args) => { if (!args[0]) return this.terminal.print('Usage: session-export <name>', 'warning'); const filePath = '_sessions/' + args[0] + '.json'; @@ -7302,7 +7341,8 @@ buildSystemPrompt() { + 'VFS→Modules→AI. nvfs-index for commands. Tool→Observe→Answer. nvfs-resolve. Destructive cmds need confirm.\n' + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind: vaultmind_connect(network) — connect wallet, vaultmind_status — check session, vaultmind_query(ep,msg,agent) — Hyperliquid prices/portfolio/chat | wallet_lookup(address) — read-only portfolio for any wallet\n' + 'web_fetch tool: GET for normal pages (returns title+content+word_count), POST for JSON APIs like Hyperliquid /info (pass method="POST" and body as JSON string).\n' - + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, guardrail, wget, tar, secret-* — discoverable via help'; + + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, guardrail, wget, tar, secret-* — discoverable via help. ' + + 'JS: node <file.js> executes JS from VFS (alias for js-run), node -e "expr" for inline eval. No real Node.js runtime — browser JS only.'; if (profilePrompt) { base = profilePrompt + '\n\n' + base; } @@ -7727,6 +7767,8 @@ buildSystemPrompt() { 'ai-stop': 'ai-stop — Cancel current AI operation', 'ai-model': 'ai-model [model] — Show or set AI model', 'ai-config': 'ai-config [key] [value] — View or set AI loop config (max-turns, max-context, confirm-timeout(0=disabled))', + 'debug': 'debug — Toggle system debug console (logs, fetch, errors)', + 'debug-ai': 'debug-ai — Toggle AI tool-call debug panel', 'mem-debug': 'mem-debug — Show memory breakdown by component', 'reset-mem': 'reset-mem — Reset AI memory (logs, context, profile, guardrails) without losing VFS or config', 'vaultmind-connect': 'vaultmind-connect [testnet|mainnet] — Connect VaultMind wallet using hyperliquid_key from SecretVault', @@ -7746,7 +7788,8 @@ buildSystemPrompt() { this.commands['ai-model'] = (args) => this.showModel(args); this.commands['ai-compress'] = () => this._manualCompress(); this.commands['ai-config'] = (args) => this.aiConfig(args); - this.commands['debug'] = () => this.toggleDebug(); + this.commands['debug'] = () => this.toggleDebugPanel(); + this.commands['debug-ai'] = () => this.toggleDebug(); this.commands['guardrail'] = (args) => this.guardrailCmd(args); this.commands['guardrail-add'] = (args) => this.guardrailCmd(['add', ...args]); this.commands['guardrail-remove'] = (args) => this.guardrailCmd(['remove', ...args]); @@ -7912,7 +7955,7 @@ buildSystemPrompt() { } _updateDebugPanel() { if (!this._debugPanelEnabled) return; - const panel = document.getElementById('debug-panel'); + const panel = document.getElementById('debug-ai-panel'); if (!panel) return; const recent = this._toolCallLog.slice(-20); let html = ''; @@ -7933,7 +7976,7 @@ buildSystemPrompt() { '<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var t=m._toolCallLog.map(function(e){return\'#\'+e.turn+\' \'+e.name+\' (\'+e.duration+\'s)\'+(e.error?\' ⚠️\':\'\')+\'\\n\'+JSON.stringify(e.args).substring(0,80)}).join(\'\\n\\n\');navigator.clipboard.writeText(t).then(function(){m.terminal&&m.terminal.print(\'📋 Debug log copied to clipboard\',\'success\')}).catch(function(){prompt(\'Copy debug log:\',t)})}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">📋</button>' + '<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var json=JSON.stringify(m._toolCallLog,null,2);var b=new Blob([json],{type:\'application/json\'});var u=URL.createObjectURL(b);var a=document.createElement(\'a\');a.href=u;a.download=\'debug-log-\'+new Date().toISOString().slice(0,19)+\'.json\';document.body.appendChild(a);a.click();setTimeout(function(){document.body.removeChild(a);URL.revokeObjectURL(u)},2000);m.terminal&&m.terminal.print(\'📥 Debug log exported\',\'success\')}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">📥</button>' + '<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var existing=[];var raw=m.terminal.fs.readFile(\'/debug.log\');if(raw){try{existing=JSON.parse(raw);}catch(e){}}var log=m._toolCallLog;var seen={};existing.forEach(function(e){seen[e.turn]=true;});log.forEach(function(e){if(!seen[e.turn]){existing.push(e);seen[e.turn]=true;}});var json=JSON.stringify(existing,null,2);m.terminal.fs.writeFile(\'/debug.log\',json);m.terminal.print(\'💾 Debug log appended — \'+existing.length+\' entries total\',\'success\')}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">💾</button>' + - '<span style="cursor:pointer;color:var(--text-muted);font-size:1.1em;touch-action:manipulation;" onclick="var p=document.getElementById(\'debug-panel\');if(p)p.style.display=\'none\';var a=window.webx11&&webx11.moduleManager&&webx11.moduleManager.modules&&webx11.moduleManager.modules.WebX11AIAssistantModule;if(a)a._debugPanelEnabled=false;">✕</span>' + + '<span style="cursor:pointer;color:var(--text-muted);font-size:1.1em;touch-action:manipulation;" onclick="var p=document.getElementById(\'debug-ai-panel\');if(p)p.style.display=\'none\';var a=window.webx11&&webx11.moduleManager&&webx11.moduleManager.modules&&webx11.moduleManager.modules.WebX11AIAssistantModule;if(a)a._debugPanelEnabled=false;">✕</span>' + '</span></div>' + html; } _isToolError(result) { @@ -8365,24 +8408,32 @@ buildSystemPrompt() { // Immediately reset UI so user knows it stopped this.terminal.setAiBusy(false); } + toggleDebugPanel() { + const mod = this.terminal.moduleManager.modules['DebugPanel']; + if (mod) { + mod.terminal = this.terminal; + if (!mod._seeded) { + mod._seeded = true; + mod.seed(); + } + mod.toggle(); + } else { + this.terminal.print('Debug panel not loaded. Run: module load debug-panel', 'warning'); + } + } toggleDebug() { this._debugPanelEnabled = !this._debugPanelEnabled; - let panel = document.getElementById('debug-panel'); + let panel = document.getElementById('debug-ai-panel'); if (!panel) { - // Create the debug panel panel = document.createElement('div'); - panel.id = 'debug-panel'; + panel.id = 'debug-ai-panel'; panel.style.cssText = 'position:fixed;right:10px;top:50px;width:340px;max-height:350px;overflow-y:auto;background:#0a0a0a;border:1px solid #333;border-radius:6px;z-index:100;font-family:monospace;font-size:0.75em;display:none;'; document.body.appendChild(panel); - // Add close button handler - panel.addEventListener('click', function() { - // Toggle collapsed/expanded state - }); } panel.style.display = this._debugPanelEnabled ? 'block' : 'none'; if (this._debugPanelEnabled) { this._updateDebugPanel(); - this.terminal.print('🔧 Debug panel enabled — shows tool call log in bottom-right corner. Close with: debug', 'info'); + this.terminal.print('🔧 AI debug panel enabled — shows tool call log. Close with: debug-ai', 'info'); } else { this.terminal.print('🔧 Debug panel disabled', 'info'); } @@ -8664,7 +8715,15 @@ buildSystemPrompt() { 'system-health','modules','listmods','modules-list','get-help-info','alias', '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']); + 'debug','debug-ai','session-new','mem-debug','reset-mem', + // Node bridge — alias for js-run + 'node', + // Synced from terminal whitelist — read-only orchestration/A2A/agent commands + 'orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result', + 'orchestrator-status','orchestrator-list','orchestrator-config','orchestrator-restart-policy', + 'a2a-list','a2a-status','a2a-poll', + 'consensus','consensus-status','consensus-history', + 'pipeline','agent-list','agent-status','contract-list']); return !safeRead.has(cmd); } return ['write_file','delete_file','make_directory','edit_module','delegate_task','spawn_agent','create_skill','patch_skill','wpm_install','user_set','session_save'].includes(name); @@ -10482,14 +10541,17 @@ class ChatStudioModule { _loadOverlayAssets() { return new Promise((resolve, reject) => { - // Load CSS + // Load CSS — gate resolve on onload to prevent FOUC + var cssReady = Promise.resolve(); if (!this._cssLoaded) { - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = 'chat/chat-ui.css?v=5'; - link.onload = () => { this._cssLoaded = true; }; - link.onerror = () => reject(new Error('Failed to load chat/chat-ui.css')); - document.head.appendChild(link); + cssReady = new Promise((res, rej) => { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'chat/chat-ui.css?v=' + (window.NEWOS4_VERSION || '5'); + link.onload = () => { this._cssLoaded = true; res(); }; + link.onerror = () => rej(new Error('Failed to load chat/chat-ui.css')); + document.head.appendChild(link); + }); } // Expose ALL Lite globals BEFORE scripts load so chat-modules.js & chat-commands.js can use them @@ -10503,7 +10565,7 @@ class ChatStudioModule { window.logs = []; window.messages = []; window.isStreaming = false; - window.CHATSTUDIO_VERSION = '1.0.0-lite-inline'; + window.CHATSTUDIO_VERSION = window.NEWOS4_VERSION || '1.0.0-lite-inline'; window.modules = []; window.currentChatModule = 'default'; @@ -10541,13 +10603,14 @@ class ChatStudioModule { window._csGlobalsInited = true; } - // Fetch HTML template (cached after first load) + // Fetch HTML template (cached after first load) — wait for CSS first if (this._overlayHtml) { - // Overlay div already exists (just hidden). No-op. - resolve(); + // Overlay div already exists (just hidden). Wait for CSS then resolve. + cssReady.then(() => resolve()); return; } - fetch('chat/chat-ui.html?v=5') + cssReady + .then(() => fetch('chat/chat-ui.html?v=' + (window.NEWOS4_VERSION || '5'))) .then(r => r.text()) .then(html => { this._overlayHtml = html; // cache for re-open @@ -10561,7 +10624,7 @@ class ChatStudioModule { const loadScript = (src) => { return new Promise((res, rej) => { const s = document.createElement('script'); - s.src = src + '?v=5'; + s.src = src + '?v=' + (window.NEWOS4_VERSION || '5'); s.onload = () => res(); s.onerror = () => rej(new Error('Failed to load ' + src)); document.body.appendChild(s); @@ -10675,11 +10738,13 @@ class ChatStudioModule { window.closeChatOverlay = () => this._closeOverlay(); // ── Constants ── - const CHATSTUDIO_VERSION = '1.0.0-lite-inline'; + const CHATSTUDIO_VERSION = window.NEWOS4_VERSION || '1.0.0-lite-inline'; const API_KEY = 'webx11_chat_api'; const MESSAGES_KEY = 'cslite_messages'; const SETTINGS_KEY = 'webx11_chat_settings'; const LOGS_KEY = 'webx11_chat_logs'; + // P2#11: API key is now kept in-memory only (not persisted to localStorage) + // It's loaded into OLLAMA_API_KEY during session, cleared on page reload // ── State ── let OLLAMA_URL = '/ollama'; @@ -10720,6 +10785,17 @@ class ChatStudioModule { return Date.now().toString(36) + '_' + Math.random().toString(36).substr(2, 9); } + // Fetch with timeout — AbortController-based, 30s default + var _streamAbort = null; // AbortController for streaming, null when not active + function fetchWithTimeout(url, opts, timeout) { + timeout = timeout || 30000; + opts = opts || {}; + var ctrl = new AbortController(); + opts.signal = ctrl.signal; + var timer = setTimeout(function() { ctrl.abort(); }, timeout); + return fetch(url, opts).finally(function() { clearTimeout(timer); }); + } + function showToast(msg) { var t = document.getElementById('toast'); if (!t) return; @@ -10767,13 +10843,15 @@ class ChatStudioModule { } // ── Log System ── + var MAX_LOGS = 200; // P2#17: Configurable log limit (was hardcoded 50) function addLog(action, detail) { var time = new Date().toLocaleTimeString(); logs.unshift({ time: time, action: action, detail: detail }); - if (logs.length > 50) logs.pop(); + if (logs.length > MAX_LOGS) logs.length = MAX_LOGS; renderLogs(); try { - localStorage.setItem(LOGS_KEY, utf8_to_b64(JSON.stringify(logs))); + // P2#12: Use plain JSON instead of base64 (consistent with messages storage) + localStorage.setItem(LOGS_KEY, JSON.stringify(logs)); } catch (e) { /* ignore */ } } @@ -10811,7 +10889,14 @@ class ChatStudioModule { function loadLogs() { try { var saved = localStorage.getItem(LOGS_KEY); - if (saved) logs = JSON.parse(b64_to_utf8(saved)); + if (saved) { + // P2#12: Handle both old base64 and new plain JSON + try { + logs = JSON.parse(saved); + } catch (e1) { + logs = JSON.parse(b64_to_utf8(saved)); + } + } } catch (e) { logs = []; } @@ -10823,7 +10908,10 @@ class ChatStudioModule { try { var saved = localStorage.getItem(SETTINGS_KEY); if (saved) { - var settings = JSON.parse(b64_to_utf8(saved)); + // P2#12: Handle both old base64 and new plain JSON + var settings; + try { settings = JSON.parse(saved); } + catch (e1) { settings = JSON.parse(b64_to_utf8(saved)); } OLLAMA_URL = settings.ollamaUrl || '/ollama'; OLLAMA_API_KEY = settings.ollamaApiKey || ''; DEFAULT_MODEL = settings.defaultModel || 'llama3.2'; @@ -10859,12 +10947,13 @@ class ChatStudioModule { try { var settings = { ollamaUrl: OLLAMA_URL, - ollamaApiKey: OLLAMA_API_KEY, + // P2#11: Don't persist API key — keep in memory only defaultModel: DEFAULT_MODEL, useStreaming: USE_STREAMING, apiBase: API_BASE }; - localStorage.setItem(SETTINGS_KEY, utf8_to_b64(JSON.stringify(settings))); + // P2#12: Use plain JSON instead of base64 (consistent storage) + localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); } catch (e) { console.warn('Failed to save settings:', e); } @@ -10891,7 +10980,7 @@ class ChatStudioModule { try { var headers = { 'Content-Type': 'application/json' }; if (OLLAMA_API_KEY) headers['Authorization'] = 'Bearer ' + OLLAMA_API_KEY; - var response = await fetch(OLLAMA_URL + '/api/tags', { headers: headers }); + var response = await fetchWithTimeout(OLLAMA_URL + '/api/tags', { headers: headers }); if (response.ok) { var data = await response.json(); var modelCount = data.models ? data.models.length : 0; @@ -10910,7 +10999,7 @@ class ChatStudioModule { // ── Models ── async function loadModels() { try { - var response = await fetch(OLLAMA_URL + '/api/tags'); + var response = await fetchWithTimeout(OLLAMA_URL + '/api/tags'); if (!response.ok) throw new Error('Failed to fetch models'); var data = await response.json(); var select = document.getElementById('modelSelect'); @@ -10947,8 +11036,15 @@ class ChatStudioModule { } // ── Message History ── + var MAX_MESSAGES = 100; // P2#13: Cap chat history to prevent localStorage overflow function saveMessages(msgs) { msgs = msgs || messages; + // P2#13: Trim to last MAX_MESSAGES before saving + if (msgs.length > MAX_MESSAGES) { + msgs = msgs.slice(-MAX_MESSAGES); + messages.length = 0; + messages.push.apply(messages, msgs); + } try { localStorage.setItem(MESSAGES_KEY, JSON.stringify(msgs)); } catch (e) { @@ -10993,7 +11089,8 @@ class ChatStudioModule { ? '<button class="msg-copy" onclick="showHtmlPreview()" title="Preview HTML">👁️</button>' : ''; if (role === 'ai') latestAiContent = content; - var renderedContent = role === 'user' ? escapeHtml(content) : formatContent(content); + // P2#16: Apply markdown formatting to user messages too (was plain escapeHtml only) + var renderedContent = role === 'user' ? formatContent(escapeHtml(content)) : formatContent(content); div.innerHTML = '<div class="role">' + '<span>' + (role === 'user' ? 'You' : 'AI') + '</span>' + '<div style="display:flex;gap:4px;">' + @@ -11174,6 +11271,12 @@ class ChatStudioModule { var model = modelSelect ? modelSelect.value : DEFAULT_MODEL; var mod = typeof getCurrentModule === 'function' ? getCurrentModule() : null; var systemPrompt = mod && mod.prompt ? mod.prompt : ''; + // Inject current date into system prompt + var now = new Date(); + var dateStr = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0'); + var days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; + var timeStr = String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0'); + systemPrompt = 'Current date: ' + dateStr + ' (' + days[now.getDay()] + ') ' + timeStr + '\n\n' + systemPrompt; var chatHistory = buildChatContext(); try { @@ -11209,6 +11312,7 @@ class ChatStudioModule { div.innerHTML = '<div class="role"><span>AI</span><div style="display:flex;gap:4px;">' + '<button class="msg-copy" onclick="copyMessage(this, \'ai\')" title="Copy">📋</button>' + '<button class="msg-copy" onclick="showHtmlPreview()" title="Preview HTML">👁️</button>' + + '<button id="streamStopBtn" class="msg-copy" onclick="stopStreaming()" title="Stop" style="color:var(--danger);">⏹</button>' + '</div></div><div class="msg-content"><span class="streaming-cursor"></span></div>'; div.dataset.content = ''; container.appendChild(div); @@ -11218,6 +11322,7 @@ class ChatStudioModule { var fullContent = ''; try { + _streamAbort = new AbortController(); var response = await fetch(OLLAMA_URL + '/api/generate', { method: 'POST', headers: headers, @@ -11225,7 +11330,8 @@ class ChatStudioModule { model: model, prompt: prompt, stream: true - }) + }), + signal: _streamAbort.signal }); if (!response.ok) { @@ -11296,16 +11402,32 @@ class ChatStudioModule { addLog('AI', fullContent.substring(0, 50) + '...'); } catch (e) { - if (contentEl) contentEl.textContent = 'Error: ' + e.message; - console.error('Stream error:', e); + if (e.name === 'AbortError') { + if (contentEl && !fullContent) contentEl.textContent = '⏹ Stopped by user.'; + } else { + if (contentEl) contentEl.textContent = 'Error: ' + e.message; + console.error('Stream error:', e); + } } isStreaming = false; + _streamAbort = null; + // Remove stop button if present + var stopBtn = document.getElementById('streamStopBtn'); + if (stopBtn) stopBtn.remove(); + } + + function stopStreaming() { + if (_streamAbort) { + _streamAbort.abort(); + _streamAbort = null; + } + isStreaming = false; } async function sendNonStreamingRequest(headers, model, prompt) { try { - var response = await fetch(OLLAMA_URL + '/api/generate', { + var response = await fetchWithTimeout(OLLAMA_URL + '/api/generate', { method: 'POST', headers: headers, body: JSON.stringify({ @@ -11333,7 +11455,16 @@ class ChatStudioModule { for (var i = 0; i < messages.length; i++) { var m = messages[i]; var role = m.role === 'user' ? 'User' : 'AI'; - var content = m.content.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); + // P2#15: Use DOMParser instead of naive regex HTML strip + var content = m.content; + try { + var doc = new DOMParser().parseFromString(content, 'text/html'); + content = doc.body.textContent || doc.body.innerText || ''; + } catch (e) { + // Fallback to regex if DOMParser fails + content = content.replace(/<[^>]+>/g, ' '); + } + content = content.replace(/\s+/g, ' ').trim(); context += role + ': ' + content + '\n'; } return context; @@ -11498,8 +11629,9 @@ class ChatStudioModule { renderMessagesFromData(); addLog('Init', 'Chat overlay initialized'); } catch (err) { - // Reset initialization flag so user can retry + // Reset initialization flags so user can retry this._initialized = false; + window._csGlobalsInited = false; // Print error to terminal if (this.terminal && this.terminal.print) { diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index ee36789..c58bab3 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -82,7 +82,8 @@ buildSystemPrompt() { + 'VFS→Modules→AI. nvfs-index for commands. Tool→Observe→Answer. nvfs-resolve. Destructive cmds need confirm.\n' + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind: vaultmind_connect(network) — connect wallet, vaultmind_status — check session, vaultmind_query(ep,msg,agent) — Hyperliquid prices/portfolio/chat | wallet_lookup(address) — read-only portfolio for any wallet\n' + 'web_fetch tool: GET for normal pages (returns title+content+word_count), POST for JSON APIs like Hyperliquid /info (pass method="POST" and body as JSON string).\n' - + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, guardrail, wget, tar, secret-* — discoverable via help'; + + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, guardrail, wget, tar, secret-* — discoverable via help. ' + + 'JS: node <file.js> executes JS from VFS (alias for js-run), node -e "expr" for inline eval. No real Node.js runtime — browser JS only.'; if (profilePrompt) { base = profilePrompt + '\n\n' + base; } @@ -507,6 +508,8 @@ buildSystemPrompt() { 'ai-stop': 'ai-stop — Cancel current AI operation', 'ai-model': 'ai-model [model] — Show or set AI model', 'ai-config': 'ai-config [key] [value] — View or set AI loop config (max-turns, max-context, confirm-timeout(0=disabled))', + 'debug': 'debug — Toggle system debug console (logs, fetch, errors)', + 'debug-ai': 'debug-ai — Toggle AI tool-call debug panel', 'mem-debug': 'mem-debug — Show memory breakdown by component', 'reset-mem': 'reset-mem — Reset AI memory (logs, context, profile, guardrails) without losing VFS or config', 'vaultmind-connect': 'vaultmind-connect [testnet|mainnet] — Connect VaultMind wallet using hyperliquid_key from SecretVault', @@ -526,7 +529,8 @@ buildSystemPrompt() { this.commands['ai-model'] = (args) => this.showModel(args); this.commands['ai-compress'] = () => this._manualCompress(); this.commands['ai-config'] = (args) => this.aiConfig(args); - this.commands['debug'] = () => this.toggleDebug(); + this.commands['debug'] = () => this.toggleDebugPanel(); + this.commands['debug-ai'] = () => this.toggleDebug(); this.commands['guardrail'] = (args) => this.guardrailCmd(args); this.commands['guardrail-add'] = (args) => this.guardrailCmd(['add', ...args]); this.commands['guardrail-remove'] = (args) => this.guardrailCmd(['remove', ...args]); @@ -692,7 +696,7 @@ buildSystemPrompt() { } _updateDebugPanel() { if (!this._debugPanelEnabled) return; - const panel = document.getElementById('debug-panel'); + const panel = document.getElementById('debug-ai-panel'); if (!panel) return; const recent = this._toolCallLog.slice(-20); let html = ''; @@ -713,7 +717,7 @@ buildSystemPrompt() { '<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var t=m._toolCallLog.map(function(e){return\'#\'+e.turn+\' \'+e.name+\' (\'+e.duration+\'s)\'+(e.error?\' ⚠️\':\'\')+\'\\n\'+JSON.stringify(e.args).substring(0,80)}).join(\'\\n\\n\');navigator.clipboard.writeText(t).then(function(){m.terminal&&m.terminal.print(\'📋 Debug log copied to clipboard\',\'success\')}).catch(function(){prompt(\'Copy debug log:\',t)})}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">📋</button>' + '<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var json=JSON.stringify(m._toolCallLog,null,2);var b=new Blob([json],{type:\'application/json\'});var u=URL.createObjectURL(b);var a=document.createElement(\'a\');a.href=u;a.download=\'debug-log-\'+new Date().toISOString().slice(0,19)+\'.json\';document.body.appendChild(a);a.click();setTimeout(function(){document.body.removeChild(a);URL.revokeObjectURL(u)},2000);m.terminal&&m.terminal.print(\'📥 Debug log exported\',\'success\')}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">📥</button>' + '<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var existing=[];var raw=m.terminal.fs.readFile(\'/debug.log\');if(raw){try{existing=JSON.parse(raw);}catch(e){}}var log=m._toolCallLog;var seen={};existing.forEach(function(e){seen[e.turn]=true;});log.forEach(function(e){if(!seen[e.turn]){existing.push(e);seen[e.turn]=true;}});var json=JSON.stringify(existing,null,2);m.terminal.fs.writeFile(\'/debug.log\',json);m.terminal.print(\'💾 Debug log appended — \'+existing.length+\' entries total\',\'success\')}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">💾</button>' + - '<span style="cursor:pointer;color:var(--text-muted);font-size:1.1em;touch-action:manipulation;" onclick="var p=document.getElementById(\'debug-panel\');if(p)p.style.display=\'none\';var a=window.webx11&&webx11.moduleManager&&webx11.moduleManager.modules&&webx11.moduleManager.modules.WebX11AIAssistantModule;if(a)a._debugPanelEnabled=false;">✕</span>' + + '<span style="cursor:pointer;color:var(--text-muted);font-size:1.1em;touch-action:manipulation;" onclick="var p=document.getElementById(\'debug-ai-panel\');if(p)p.style.display=\'none\';var a=window.webx11&&webx11.moduleManager&&webx11.moduleManager.modules&&webx11.moduleManager.modules.WebX11AIAssistantModule;if(a)a._debugPanelEnabled=false;">✕</span>' + '</span></div>' + html; } _isToolError(result) { @@ -1145,24 +1149,32 @@ buildSystemPrompt() { // Immediately reset UI so user knows it stopped this.terminal.setAiBusy(false); } + toggleDebugPanel() { + const mod = this.terminal.moduleManager.modules['DebugPanel']; + if (mod) { + mod.terminal = this.terminal; + if (!mod._seeded) { + mod._seeded = true; + mod.seed(); + } + mod.toggle(); + } else { + this.terminal.print('Debug panel not loaded. Run: module load debug-panel', 'warning'); + } + } toggleDebug() { this._debugPanelEnabled = !this._debugPanelEnabled; - let panel = document.getElementById('debug-panel'); + let panel = document.getElementById('debug-ai-panel'); if (!panel) { - // Create the debug panel panel = document.createElement('div'); - panel.id = 'debug-panel'; + panel.id = 'debug-ai-panel'; panel.style.cssText = 'position:fixed;right:10px;top:50px;width:340px;max-height:350px;overflow-y:auto;background:#0a0a0a;border:1px solid #333;border-radius:6px;z-index:100;font-family:monospace;font-size:0.75em;display:none;'; document.body.appendChild(panel); - // Add close button handler - panel.addEventListener('click', function() { - // Toggle collapsed/expanded state - }); } panel.style.display = this._debugPanelEnabled ? 'block' : 'none'; if (this._debugPanelEnabled) { this._updateDebugPanel(); - this.terminal.print('🔧 Debug panel enabled — shows tool call log in bottom-right corner. Close with: debug', 'info'); + this.terminal.print('🔧 AI debug panel enabled — shows tool call log. Close with: debug-ai', 'info'); } else { this.terminal.print('🔧 Debug panel disabled', 'info'); } @@ -1444,7 +1456,15 @@ buildSystemPrompt() { 'system-health','modules','listmods','modules-list','get-help-info','alias', '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']); + 'debug','debug-ai','session-new','mem-debug','reset-mem', + // Node bridge — alias for js-run + 'node', + // Synced from terminal whitelist — read-only orchestration/A2A/agent commands + 'orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result', + 'orchestrator-status','orchestrator-list','orchestrator-config','orchestrator-restart-policy', + 'a2a-list','a2a-status','a2a-poll', + 'consensus','consensus-status','consensus-history', + 'pipeline','agent-list','agent-status','contract-list']); return !safeRead.has(cmd); } return ['write_file','delete_file','make_directory','edit_module','delegate_task','spawn_agent','create_skill','patch_skill','wpm_install','user_set','session_save'].includes(name); diff --git a/modules/core/module-chatstudio.js b/modules/core/module-chatstudio.js index d558445..8e9d208 100644 --- a/modules/core/module-chatstudio.js +++ b/modules/core/module-chatstudio.js @@ -35,14 +35,17 @@ class ChatStudioModule { _loadOverlayAssets() { return new Promise((resolve, reject) => { - // Load CSS + // Load CSS — gate resolve on onload to prevent FOUC + var cssReady = Promise.resolve(); if (!this._cssLoaded) { - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = 'chat/chat-ui.css?v=5'; - link.onload = () => { this._cssLoaded = true; }; - link.onerror = () => reject(new Error('Failed to load chat/chat-ui.css')); - document.head.appendChild(link); + cssReady = new Promise((res, rej) => { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'chat/chat-ui.css?v=' + (window.NEWOS4_VERSION || '5'); + link.onload = () => { this._cssLoaded = true; res(); }; + link.onerror = () => rej(new Error('Failed to load chat/chat-ui.css')); + document.head.appendChild(link); + }); } // Expose ALL Lite globals BEFORE scripts load so chat-modules.js & chat-commands.js can use them @@ -56,7 +59,7 @@ class ChatStudioModule { window.logs = []; window.messages = []; window.isStreaming = false; - window.CHATSTUDIO_VERSION = '1.0.0-lite-inline'; + window.CHATSTUDIO_VERSION = window.NEWOS4_VERSION || '1.0.0-lite-inline'; window.modules = []; window.currentChatModule = 'default'; @@ -94,13 +97,14 @@ class ChatStudioModule { window._csGlobalsInited = true; } - // Fetch HTML template (cached after first load) + // Fetch HTML template (cached after first load) — wait for CSS first if (this._overlayHtml) { - // Overlay div already exists (just hidden). No-op. - resolve(); + // Overlay div already exists (just hidden). Wait for CSS then resolve. + cssReady.then(() => resolve()); return; } - fetch('chat/chat-ui.html?v=5') + cssReady + .then(() => fetch('chat/chat-ui.html?v=' + (window.NEWOS4_VERSION || '5'))) .then(r => r.text()) .then(html => { this._overlayHtml = html; // cache for re-open @@ -114,7 +118,7 @@ class ChatStudioModule { const loadScript = (src) => { return new Promise((res, rej) => { const s = document.createElement('script'); - s.src = src + '?v=5'; + s.src = src + '?v=' + (window.NEWOS4_VERSION || '5'); s.onload = () => res(); s.onerror = () => rej(new Error('Failed to load ' + src)); document.body.appendChild(s); @@ -228,11 +232,13 @@ class ChatStudioModule { window.closeChatOverlay = () => this._closeOverlay(); // ── Constants ── - const CHATSTUDIO_VERSION = '1.0.0-lite-inline'; + const CHATSTUDIO_VERSION = window.NEWOS4_VERSION || '1.0.0-lite-inline'; const API_KEY = 'webx11_chat_api'; const MESSAGES_KEY = 'cslite_messages'; const SETTINGS_KEY = 'webx11_chat_settings'; const LOGS_KEY = 'webx11_chat_logs'; + // P2#11: API key is now kept in-memory only (not persisted to localStorage) + // It's loaded into OLLAMA_API_KEY during session, cleared on page reload // ── State ── let OLLAMA_URL = '/ollama'; @@ -273,6 +279,17 @@ class ChatStudioModule { return Date.now().toString(36) + '_' + Math.random().toString(36).substr(2, 9); } + // Fetch with timeout — AbortController-based, 30s default + var _streamAbort = null; // AbortController for streaming, null when not active + function fetchWithTimeout(url, opts, timeout) { + timeout = timeout || 30000; + opts = opts || {}; + var ctrl = new AbortController(); + opts.signal = ctrl.signal; + var timer = setTimeout(function() { ctrl.abort(); }, timeout); + return fetch(url, opts).finally(function() { clearTimeout(timer); }); + } + function showToast(msg) { var t = document.getElementById('toast'); if (!t) return; @@ -320,13 +337,15 @@ class ChatStudioModule { } // ── Log System ── + var MAX_LOGS = 200; // P2#17: Configurable log limit (was hardcoded 50) function addLog(action, detail) { var time = new Date().toLocaleTimeString(); logs.unshift({ time: time, action: action, detail: detail }); - if (logs.length > 50) logs.pop(); + if (logs.length > MAX_LOGS) logs.length = MAX_LOGS; renderLogs(); try { - localStorage.setItem(LOGS_KEY, utf8_to_b64(JSON.stringify(logs))); + // P2#12: Use plain JSON instead of base64 (consistent with messages storage) + localStorage.setItem(LOGS_KEY, JSON.stringify(logs)); } catch (e) { /* ignore */ } } @@ -364,7 +383,14 @@ class ChatStudioModule { function loadLogs() { try { var saved = localStorage.getItem(LOGS_KEY); - if (saved) logs = JSON.parse(b64_to_utf8(saved)); + if (saved) { + // P2#12: Handle both old base64 and new plain JSON + try { + logs = JSON.parse(saved); + } catch (e1) { + logs = JSON.parse(b64_to_utf8(saved)); + } + } } catch (e) { logs = []; } @@ -376,7 +402,10 @@ class ChatStudioModule { try { var saved = localStorage.getItem(SETTINGS_KEY); if (saved) { - var settings = JSON.parse(b64_to_utf8(saved)); + // P2#12: Handle both old base64 and new plain JSON + var settings; + try { settings = JSON.parse(saved); } + catch (e1) { settings = JSON.parse(b64_to_utf8(saved)); } OLLAMA_URL = settings.ollamaUrl || '/ollama'; OLLAMA_API_KEY = settings.ollamaApiKey || ''; DEFAULT_MODEL = settings.defaultModel || 'llama3.2'; @@ -412,12 +441,13 @@ class ChatStudioModule { try { var settings = { ollamaUrl: OLLAMA_URL, - ollamaApiKey: OLLAMA_API_KEY, + // P2#11: Don't persist API key — keep in memory only defaultModel: DEFAULT_MODEL, useStreaming: USE_STREAMING, apiBase: API_BASE }; - localStorage.setItem(SETTINGS_KEY, utf8_to_b64(JSON.stringify(settings))); + // P2#12: Use plain JSON instead of base64 (consistent storage) + localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); } catch (e) { console.warn('Failed to save settings:', e); } @@ -444,7 +474,7 @@ class ChatStudioModule { try { var headers = { 'Content-Type': 'application/json' }; if (OLLAMA_API_KEY) headers['Authorization'] = 'Bearer ' + OLLAMA_API_KEY; - var response = await fetch(OLLAMA_URL + '/api/tags', { headers: headers }); + var response = await fetchWithTimeout(OLLAMA_URL + '/api/tags', { headers: headers }); if (response.ok) { var data = await response.json(); var modelCount = data.models ? data.models.length : 0; @@ -463,7 +493,7 @@ class ChatStudioModule { // ── Models ── async function loadModels() { try { - var response = await fetch(OLLAMA_URL + '/api/tags'); + var response = await fetchWithTimeout(OLLAMA_URL + '/api/tags'); if (!response.ok) throw new Error('Failed to fetch models'); var data = await response.json(); var select = document.getElementById('modelSelect'); @@ -500,8 +530,15 @@ class ChatStudioModule { } // ── Message History ── + var MAX_MESSAGES = 100; // P2#13: Cap chat history to prevent localStorage overflow function saveMessages(msgs) { msgs = msgs || messages; + // P2#13: Trim to last MAX_MESSAGES before saving + if (msgs.length > MAX_MESSAGES) { + msgs = msgs.slice(-MAX_MESSAGES); + messages.length = 0; + messages.push.apply(messages, msgs); + } try { localStorage.setItem(MESSAGES_KEY, JSON.stringify(msgs)); } catch (e) { @@ -546,7 +583,8 @@ class ChatStudioModule { ? '<button class="msg-copy" onclick="showHtmlPreview()" title="Preview HTML">👁️</button>' : ''; if (role === 'ai') latestAiContent = content; - var renderedContent = role === 'user' ? escapeHtml(content) : formatContent(content); + // P2#16: Apply markdown formatting to user messages too (was plain escapeHtml only) + var renderedContent = role === 'user' ? formatContent(escapeHtml(content)) : formatContent(content); div.innerHTML = '<div class="role">' + '<span>' + (role === 'user' ? 'You' : 'AI') + '</span>' + '<div style="display:flex;gap:4px;">' + @@ -727,6 +765,12 @@ class ChatStudioModule { var model = modelSelect ? modelSelect.value : DEFAULT_MODEL; var mod = typeof getCurrentModule === 'function' ? getCurrentModule() : null; var systemPrompt = mod && mod.prompt ? mod.prompt : ''; + // Inject current date into system prompt + var now = new Date(); + var dateStr = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0'); + var days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; + var timeStr = String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0'); + systemPrompt = 'Current date: ' + dateStr + ' (' + days[now.getDay()] + ') ' + timeStr + '\n\n' + systemPrompt; var chatHistory = buildChatContext(); try { @@ -762,6 +806,7 @@ class ChatStudioModule { div.innerHTML = '<div class="role"><span>AI</span><div style="display:flex;gap:4px;">' + '<button class="msg-copy" onclick="copyMessage(this, \'ai\')" title="Copy">📋</button>' + '<button class="msg-copy" onclick="showHtmlPreview()" title="Preview HTML">👁️</button>' + + '<button id="streamStopBtn" class="msg-copy" onclick="stopStreaming()" title="Stop" style="color:var(--danger);">⏹</button>' + '</div></div><div class="msg-content"><span class="streaming-cursor"></span></div>'; div.dataset.content = ''; container.appendChild(div); @@ -771,6 +816,7 @@ class ChatStudioModule { var fullContent = ''; try { + _streamAbort = new AbortController(); var response = await fetch(OLLAMA_URL + '/api/generate', { method: 'POST', headers: headers, @@ -778,7 +824,8 @@ class ChatStudioModule { model: model, prompt: prompt, stream: true - }) + }), + signal: _streamAbort.signal }); if (!response.ok) { @@ -849,16 +896,32 @@ class ChatStudioModule { addLog('AI', fullContent.substring(0, 50) + '...'); } catch (e) { - if (contentEl) contentEl.textContent = 'Error: ' + e.message; - console.error('Stream error:', e); + if (e.name === 'AbortError') { + if (contentEl && !fullContent) contentEl.textContent = '⏹ Stopped by user.'; + } else { + if (contentEl) contentEl.textContent = 'Error: ' + e.message; + console.error('Stream error:', e); + } } isStreaming = false; + _streamAbort = null; + // Remove stop button if present + var stopBtn = document.getElementById('streamStopBtn'); + if (stopBtn) stopBtn.remove(); + } + + function stopStreaming() { + if (_streamAbort) { + _streamAbort.abort(); + _streamAbort = null; + } + isStreaming = false; } async function sendNonStreamingRequest(headers, model, prompt) { try { - var response = await fetch(OLLAMA_URL + '/api/generate', { + var response = await fetchWithTimeout(OLLAMA_URL + '/api/generate', { method: 'POST', headers: headers, body: JSON.stringify({ @@ -886,7 +949,16 @@ class ChatStudioModule { for (var i = 0; i < messages.length; i++) { var m = messages[i]; var role = m.role === 'user' ? 'User' : 'AI'; - var content = m.content.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); + // P2#15: Use DOMParser instead of naive regex HTML strip + var content = m.content; + try { + var doc = new DOMParser().parseFromString(content, 'text/html'); + content = doc.body.textContent || doc.body.innerText || ''; + } catch (e) { + // Fallback to regex if DOMParser fails + content = content.replace(/<[^>]+>/g, ' '); + } + content = content.replace(/\s+/g, ' ').trim(); context += role + ': ' + content + '\n'; } return context; @@ -1051,8 +1123,9 @@ class ChatStudioModule { renderMessagesFromData(); addLog('Init', 'Chat overlay initialized'); } catch (err) { - // Reset initialization flag so user can retry + // Reset initialization flags so user can retry this._initialized = false; + window._csGlobalsInited = false; // Print error to terminal if (this.terminal && this.terminal.print) { diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js index ab8fd99..25442eb 100644 --- a/modules/core/module-fs.js +++ b/modules/core/module-fs.js @@ -33,6 +33,11 @@ class FileSystemModule { 'download': 'download <file> — Download a file to your device', 'vfs-export': 'vfs-export — Export the entire virtual filesystem as JSON', 'vfs-import': 'vfs-import — Import a previously exported VFS JSON file', + 'session-save': 'session-save <name> — Save current session (path + terminal history)', + 'session-load': 'session-load <name> — Load a saved session', + 'session-list': 'session-list — List all saved sessions', + 'session-new': 'session-new — Start a fresh session (auto-saves current, clears AI context)', + 'session-delete': 'session-delete <name> — Delete a saved session', }; } registerCommands() { @@ -486,6 +491,18 @@ class FileSystemModule { this.terminal.print('Session not found: ' + args[0], 'error'); } }; + this.commands['session-new'] = () => { + // Auto-save current session, clear AI context, fresh terminal + this.terminal._autoSaveSession(); + try { localStorage.removeItem('webx11_ai_context'); } catch(e) {} + // Clear terminal output + if (this.terminal.outputContainer) this.terminal.outputContainer.innerHTML = ''; + if (this.terminal._historyBuffer) this.terminal._historyBuffer = []; + // Clear AI conversation context + const aiMod = this.terminal.moduleManager.modules['WebX11AIAssistantModule']; + if (aiMod && aiMod.clearContext) aiMod.clearContext(); + this.terminal.print('✨ New session started — context cleared', 'success'); + }; this.commands['session-export'] = (args) => { if (!args[0]) return this.terminal.print('Usage: session-export <name>', 'warning'); const filePath = '_sessions/' + args[0] + '.json'; diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 3b33a95..f1608ed 100644 --- a/modules/core/module-loader.js +++ b/modules/core/module-loader.js @@ -4,7 +4,7 @@ // Core module bundle loaded synchronously in main.html. // This loader only handles version cache-busting, splash, terminal init, and optional ext modules. - const LATEST_VER = '2026-06-18-v4.5.70'; + const LATEST_VER = '2026-06-18-v4.5.74'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { diff --git a/modules/core/terminal.js b/modules/core/terminal.js index 468670b..5954bfb 100644 --- a/modules/core/terminal.js +++ b/modules/core/terminal.js @@ -10,7 +10,7 @@ this.destructiveCmds = new Set(['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']); this.systemCmds = new Set(['reboot','shutdown','format','kill','halt','poweroff']); this.suspiciousFlags = new Set(['--dump','--execute','--eval','--exec','--shell','--script']); - this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','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','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']); + this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','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','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']); // Load persisted overrides from localStorage try { const d = localStorage.getItem('webx11_security_destructive'); if(d){JSON.parse(d).forEach(c=>this.destructiveCmds.add(c));} }catch(e){} try { const s = localStorage.getItem('webx11_security_system'); if(s){JSON.parse(s).forEach(c=>this.systemCmds.add(c));} }catch(e){} @@ -1027,7 +1027,7 @@ // 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']; + 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 @@ -1213,15 +1213,37 @@ } } _safeRegister(className, label) { - let cls; - try { - cls = eval(className); - } catch (e) { - console.log(`[SafeRegister] Skipped ${label}: class not loaded (skipped in boot selection)`); - return; - } + // 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, + }; + const cls = registry[className]; if (typeof cls === 'undefined') { - console.log(`[SafeRegister] Skipped ${label}: class not defined`); + console.log(`[SafeRegister] Skipped ${label}: class not loaded (skipped in boot selection)`); return; } try { this.moduleManager.registerModule(new cls()); } diff --git a/modules/ext/console.js b/modules/ext/console.js index f041ad0..5d3007a 100644 --- a/modules/ext/console.js +++ b/modules/ext/console.js @@ -8,13 +8,23 @@ class JSConsoleModule { getHelpInfo() { return { 'js': 'js <expression> — Evaluate JavaScript in page context', - 'js-run': 'js-run <path> — Load and execute a JS file from VFS' + 'js-run': 'js-run <path> — Load and execute a JS file from VFS', + 'node': 'node <path> — Alias for js-run (executes a JS file from VFS)' }; } registerCommands() { this.commands['js'] = (args) => this.evalJS(args); this.commands['js-run'] = (args) => this.evalJSFile(args); + this.commands['node'] = (args) => { + // Bridge: AI hallucinates `node script.js` — map to js-run + if (!args[0]) return this.terminal.print('Usage: node <script.js> (alias for js-run)', 'warning'); + // Handle `node -e "expr"` — inline eval like Node.js + if (args[0] === '-e') { + return this.evalJS(args.slice(1)); + } + return this.evalJSFile(args); + }; } evalJS(args) { |