aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/core/core-bundle.js236
-rw-r--r--modules/core/module-aiassist.js46
-rw-r--r--modules/core/module-chatstudio.js131
-rw-r--r--modules/core/module-fs.js17
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/terminal.js42
-rw-r--r--modules/ext/console.js12
7 files changed, 380 insertions, 106 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) {
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) {