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 /modules/core/core-bundle.js | |
| parent | fba6df22dae297c89fa0df065628dddded66c69c (diff) | |
auto: 2026-06-18_18:00 changes
Diffstat (limited to 'modules/core/core-bundle.js')
| -rw-r--r-- | modules/core/core-bundle.js | 236 |
1 files changed, 184 insertions, 52 deletions
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) { |