diff options
| author | Agando <roduccomm@gmail.com> | 2026-06-10 06:00:30 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-06-10 06:00:30 +0200 |
| commit | 39b2df36ee5ccea52558d0537bfee87058558e94 (patch) | |
| tree | 7cac66cf09b128c07fa0300da0a46d016d4459e5 /modules/core/virtual-fs.js | |
| parent | e8d4985bbb60797936d31b65604a7f94c76bf623 (diff) | |
auto: 2026-06-10_06:00 changes
Diffstat (limited to 'modules/core/virtual-fs.js')
| -rw-r--r-- | modules/core/virtual-fs.js | 354 |
1 files changed, 345 insertions, 9 deletions
diff --git a/modules/core/virtual-fs.js b/modules/core/virtual-fs.js index f6e2826..9d3ea79 100644 --- a/modules/core/virtual-fs.js +++ b/modules/core/virtual-fs.js @@ -3,6 +3,308 @@ class VirtualFileSystem { this.root = { type: 'dir', content: {} }; this.currentPath = []; this._ready = this.loadFromStorage(); + this._virtualMounts = null; // lazy-init on first /sys/ access + // Register /sys/ in emit-change exclusion (also added below) + } + // ── Virtual /sys/ filesystem (live runtime state, never persisted) ── + _initVirtualMounts() { + if (this._virtualMounts) return; + const vm = (name, factory) => ({ _factory: factory, _name: name }); + const dir = () => ({ _dir: true, content: {} }); + const vmCache = {}; // factories can cache costly results within same tick + const mm = () => typeof window !== 'undefined' ? window.webx11?.moduleManager?.modules : null; + const ai = () => mm()?.WebX11AIAssistantModule || null; + const heap = () => typeof performance !== 'undefined' && performance.memory ? performance.memory : null; + const fmt = (bytes) => { + if (!bytes && bytes !== 0) return 'N/A'; + const kb = bytes / 1024; + if (kb < 1024) return kb.toFixed(1) + ' KB'; + return (kb / 1024).toFixed(1) + ' MB'; + }; + const fmtBytes = (bytes) => { + if (!bytes && bytes !== 0) return 'N/A'; + if (bytes < 1024) return bytes + ' B'; + const kb = bytes / 1024; + if (kb < 1024) return kb.toFixed(1) + ' KB'; + return (kb / 1024).toFixed(1) + ' MB'; + }; + // Walk VFS root to count files/dirs and find largest + const vfsWalk = (dir, prefix) => { + let files = 0, dirs = 0, total = 0, largest = []; + const walk = (node, p) => { + if (!node) return; + if (node._factory || node._dir) return; // skip virtual + if (node.type === 'file') { + files++; + const size = node.content ? node.content.length : 0; + total += size; + largest.push({ path: p, size }); + } else if (node.type === 'dir') { + dirs++; + for (const [name, child] of Object.entries(node.content || {})) { + walk(child, p + '/' + name); + } + } + }; + walk(dir, prefix); + largest.sort((a, b) => b.size - a.size); + return { files, dirs, total, largest: largest.slice(0, 20) }; + }; + this._virtualMounts = { + // /sys/info + info: vm('info', () => [ + '=== newOS4 Runtime Debug (/sys/) ===', + `Version: ${window.NEWOS4_VERSION || '?'}`, + `JS heap: ${fmt(heap()?.usedJSHeapSize)} / ${fmt(heap()?.jsHeapSizeLimit)}`, + `DOM nodes: ${typeof document !== 'undefined' ? document.querySelectorAll('*').length : 'N/A'}`, + `Loaded modules: ${mm() ? Object.keys(mm()).length : 'N/A'}`, + '', + ' cat /sys/heap/stats — Memory overview', + ' cat /sys/heap/breakdown — What consumes what', + ' cat /sys/logs/tool-calls — AI tool call log', + ' cat /sys/logs/errors — AI error log', + ' cat /sys/vfs/stats — VFS file/dir counts', + ' cat /sys/vfs/largest — Top 20 largest files', + ' ls /sys/modules/ — Loaded modules', + ' cat /sys/config/ai — AI config parameters', + ' cat /sys/dom/node-count — DOM element breakdown', + ].join('\n')), + // /sys/heap/ + heap: { _dir: true, content: { + stats: vm('stats', () => { + const h = heap(); + const lsSize = (k) => { try { return (localStorage.getItem(k) || '').length / 1024; } catch(e) { return 0; } }; + return [ + '=== Heap Stats ===', + ` JS heap used: ${fmt(h?.usedJSHeapSize)}`, + ` JS heap limit: ${fmt(h?.jsHeapSizeLimit)}`, + ` JS heap % used: ${h ? ((h.usedJSHeapSize / h.jsHeapSizeLimit) * 100).toFixed(1) + '%' : 'N/A'}`, + '', + ` VFS localStorage: ${lsSize('webx11_filesystem_v2').toFixed(1)} KB`, + ` AI localStorage: ${lsSize('webx11_ai_context').toFixed(1)} KB`, + ` Config localStorage: ${lsSize('webx11_ai_config').toFixed(1)} KB`, + ].join('\n'); + }), + breakdown: vm('breakdown', () => { + const a = ai(); + const cCount = a?.context?.length || 0; + const lCount = a?._toolCallLog?.length || 0; + const eCount = a?._errorLog?.length || 0; + const modules = mm() ? Object.keys(mm()).length : 0; + const domNodes = typeof document !== 'undefined' ? document.querySelectorAll('*').length : 0; + return [ + '=== Heap Breakdown (estimates) ===', + ` Module code + state: ~${(modules * 1.2).toFixed(1)} MB (${modules} modules × ~1.2MB)`, + ` VFS tree (in-memory): varies with file count`, + ` AI context messages: ${cCount} (max: ${a?.maxContext || '?'})`, + ` AI tool call log: ${lCount} (cap: 50)`, + ` AI error log: ${eCount} (cap: 20)`, + ` DOM nodes: ${domNodes}`, + ].join('\n'); + }) + }}, + // /sys/modules/ + modules: { _dir: true, content: { + list: vm('list', () => { + const m = mm(); + if (!m) return '(moduleManager not available)'; + return Object.keys(m).sort().join('\n'); + }) + // module subdirectories are dynamic — handled in _virtualLs + }}, + // /sys/vfs/ + vfs: { _dir: true, content: { + stats: vm('stats', () => { + const w = vfsWalk(this.root, ''); + return [ + '=== VFS Stats ===', + ` Files: ${w.files}`, + ` Directories: ${w.dirs}`, + ` Total content: ${fmtBytes(w.total)}`, + ` localStorage: ${((localStorage.getItem('webx11_filesystem_v2') || '').length / 1024).toFixed(1)} KB (compressed)`, + ].join('\n'); + }), + largest: vm('largest', () => { + const w = vfsWalk(this.root, ''); + if (w.largest.length === 0) return '(empty VFS)'; + const maxW = Math.max(...w.largest.map(f => f.path.length), 8); + let out = '=== Top 20 Largest Files ===\n'; + w.largest.forEach((f, i) => { + out += ` ${(i + 1).toString().padStart(2)}. ${f.path.padEnd(maxW)} ${fmtBytes(f.size)}\n`; + }); + return out; + }) + }}, + // /sys/logs/ + logs: { _dir: true, content: { + 'tool-calls': vm('tool-calls', () => { + const a = ai(); + if (!a || !a._toolCallLog || a._toolCallLog.length === 0) return '(empty)'; + return a._toolCallLog.map((e, i) => `[${i}] ${JSON.stringify(e)}`).join('\n'); + }), + 'errors': vm('errors', () => { + const a = ai(); + if (!a || !a._errorLog || a._errorLog.length === 0) return '(empty)'; + return a._errorLog.map((e, i) => `[${i}] ${JSON.stringify(e)}`).join('\n'); + }), + 'commands': vm('commands', () => { + const a = ai(); + if (!a || !a._commandFreq || Object.keys(a._commandFreq).length === 0) return '(empty)'; + const sorted = Object.entries(a._commandFreq).sort((a,b) => b[1] - a[1]); + return sorted.map(([cmd, n]) => `${cmd.padEnd(20)} ${n}x`).join('\n'); + }) + }}, + // /sys/dom/ + dom: { _dir: true, content: { + 'node-count': vm('node-count', () => { + if (typeof document === 'undefined') return 'N/A'; + const all = document.querySelectorAll('*'); + const byTag = {}; + all.forEach(el => { const t = el.tagName.toLowerCase(); byTag[t] = (byTag[t] || 0) + 1; }); + const sorted = Object.entries(byTag).sort((a,b) => b[1] - a[1]); + let out = `Total DOM nodes: ${all.length}\n\n=== By Tag ===\n`; + sorted.forEach(([tag, count]) => { + out += ` ${tag.padEnd(15)} ${count}\n`; + }); + return out; + }) + }}, + // /sys/config/ + config: { _dir: true, content: { + version: vm('version', () => { + return `NEWOS4_VERSION: ${window.NEWOS4_VERSION || '?'}\n`; + }), + ai: vm('ai', () => { + const a = ai(); + if (!a) return '(AI module not loaded)'; + return [ + '=== AI Config ===', + ` maxContext: ${a.maxContext}`, + ` MAX_TOOL_TURNS: ${a.MAX_TOOL_TURNS}`, + ` maxContextTokens: ${a.maxContextTokens}`, + ` confirmTimeoutMs: ${a._confirmTimeoutMs}`, + ` skillDebounceMs: ${a._skillDebounceMs}`, + ` context messages: ${a.context?.length || 0}`, + ` tool call log cap: 50 (auto-capped)`, + ` error log cap: 20 (auto-capped)`, + ].join('\n'); + }) + }} + }; + } + // Check if a path is under /sys/ + _isVirtualPath(path) { + if (!path) return false; + const normalized = path.startsWith('/') ? path : '/' + path; + return normalized === '/sys' || normalized.startsWith('/sys/'); + } + // Resolve a virtual path: returns {dir: virtualNode, name: lastSegment} or null + _resolveVirtualPath(path) { + this._initVirtualMounts(); + const normalized = path.startsWith('/') ? path : '/' + path; + let segments = normalized.split('/').filter(s => s); + // Remove leading 'sys' + if (segments[0] === 'sys') segments = segments.slice(1); + if (segments.length === 0) { + // /sys itself — return the root virtual mounts node as a dir + const allContent = {}; + for (const [k, v] of Object.entries(this._virtualMounts)) { + if (v._dir) allContent[k] = { type: 'dir', content: v.content, _virtual: true }; + else allContent[k] = { type: 'file', content: null, _virtual: true, _factory: v._factory }; + } + // Add dynamic module subdirs to modules listing + if (this._virtualMounts.modules?._dir) { + const mm = typeof window !== 'undefined' ? window.webx11?.moduleManager?.modules : null; + if (mm) { + for (const modName of Object.keys(mm)) { + if (!allContent[modName]) { + allContent[modName] = { type: 'dir', content: {}, _virtual: true }; + } + } + } + } + return { dir: { content: allContent }, name: null }; + } + // Walk the tree + let node = this._virtualMounts; + for (let i = 0; i < segments.length - 1; i++) { + const seg = segments[i]; + if (!node[seg]) { + // Check dynamic module subdir under /sys/modules/ + const mods = typeof window !== 'undefined' ? window.webx11?.moduleManager?.modules : null; + if (node === this._virtualMounts?.modules?.content && mods && mods[seg]) { + // Dynamic module subdir — create virtual node + node = { + _dir: true, + content: { + info: { type: 'file', content: null, _virtual: true, _factory: () => { + const mod = mods[seg]; + const cmdKeys = mod.commands ? Object.keys(mod.commands) : []; + return [ + `Module: ${seg}`, + ` Type: ${typeof mod}`, + ` Commands: ${cmdKeys.join(', ') || '(none)'}`, + ` Has help: ${mod.getHelpInfo ? 'yes' : 'no'}`, + ` Constructor: ${mod.constructor?.name || 'anonymous'}`, + ].join('\n'); + }} + } + }; + continue; + } + return null; + } + node = node[seg]; + if (node && node._dir) node = node.content || {}; + } + const lastName = segments[segments.length - 1]; + if (node && node[lastName]) { + const entry = node[lastName]; + if (entry._dir) { + // Static subdir with known content + const allContent = {}; + for (const [k, v] of Object.entries(entry.content || {})) { + if (v._dir) allContent[k] = { type: 'dir', content: v.content, _virtual: true }; + else allContent[k] = { type: 'file', content: null, _virtual: true, _factory: v._factory }; + } + // Add dynamic module subdirs + if (segments[0] === 'modules' || (segments.length >= 2 && segments[segments.length - 2] === 'modules')) { + const mm = typeof window !== 'undefined' ? window.webx11?.moduleManager?.modules : null; + if (mm) { + for (const modName of Object.keys(mm)) { + if (!allContent[modName]) { + allContent[modName] = { type: 'dir', content: {}, _virtual: true }; + } + } + } + } + return { dir: { content: allContent }, name: null }; + } else { + return { dir: { content: node }, name: lastName }; + } + } + // Check if it's a module subdirectory itself (e.g. /sys/modules/calc) + const mods = typeof window !== 'undefined' ? window.webx11?.moduleManager?.modules : null; + if (node && mods && mods[lastName] && segments.length >= 1) { + // It's a module name — return its info dir + return { + dir: { content: { + info: { type: 'file', content: null, _virtual: true, _factory: () => { + const mod = mods[lastName]; + const cmdKeys = mod.commands ? Object.keys(mod.commands) : []; + return [ + `Module: ${lastName}`, + ` Type: ${typeof mod}`, + ` Commands: ${cmdKeys.join(', ') || '(none)'}`, + ` Has help: ${mod.getHelpInfo ? 'yes' : 'no'}`, + ` Constructor: ${mod.constructor?.name || 'anonymous'}`, + ].join('\n'); + }} + }}, + name: null + }; + } + return null; } _u8ToBase64(u8) { let s = ''; @@ -70,7 +372,7 @@ class VirtualFileSystem { // Skip internal index/system files to prevent re-trigger loops if (!path) return; const normalized = path.startsWith('/') ? path : '/' + path; - const sysPaths = ['/.semantic_index', '/.memory_graph', '/.trash', '/_snapshots', '/_sessions', '/logs', '/memory', '/system', '/_mods', '/backups']; + const sysPaths = ['/.semantic_index', '/.memory_graph', '/.trash', '/_snapshots', '/_sessions', '/logs', '/memory', '/system', '/_mods', '/backups', '/sys']; if (sysPaths.some(p => normalized.startsWith(p))) return; if (typeof window !== 'undefined' && window.__nvfsMarkDirty) { window.__nvfsMarkDirty(path); @@ -119,6 +421,7 @@ class VirtualFileSystem { return result; } _resolvePath(path) { + if (this._isVirtualPath(path)) return this._resolveVirtualPath(path); const segs = this._toAbsoluteSegments(path); if (segs === null) return null; if (segs.length === 0) return { dir: this.root, name: null }; @@ -130,6 +433,10 @@ class VirtualFileSystem { return { dir, name: segs[segs.length - 1] }; } _resolveDir(path) { + if (this._isVirtualPath(path)) { + const r = this._resolveVirtualPath(path); + return r ? r.dir : null; + } const segs = this._toAbsoluteSegments(path); if (segs === null) return null; if (segs.length === 0) return this.root; @@ -156,12 +463,19 @@ class VirtualFileSystem { dir = this.getCurrentDir(); } if (!dir) return []; - return Object.keys(dir.content).map(name => ({ + const entries = Object.keys(dir.content).map(name => ({ name, type: dir.content[name].type, - size: dir.content[name].content ? dir.content[name].content.length : 0 + size: dir.content[name]._factory ? 0 : (dir.content[name].content ? dir.content[name].content.length : 0), + _virtual: !!dir.content[name]._virtual })); + // Inject virtual /sys/ at root level + if ((!path || path === '/' || path === '') && !entries.some(e => e.name === 'sys' && e._virtual)) { + entries.push({ name: 'sys', type: 'dir', size: 0, _virtual: true }); + } + return entries; } writeFile(path, content) { + if (this._isVirtualPath(path)) return false; const resolved = this._resolvePath(path); if (!resolved || !resolved.name) return false; const name = resolved.name; @@ -176,10 +490,14 @@ class VirtualFileSystem { const resolved = this._resolvePath(path); if (!resolved || !resolved.name) return null; const node = resolved.dir.content[resolved.name]; + if (node && node._factory) { + return node._factory(); + } if (node && node.type === 'file') return node.content; return null; } deleteFile(path) { + if (this._isVirtualPath(path)) return false; const resolved = this._resolvePath(path); if (!resolved || !resolved.name) return false; const node = resolved.dir.content[resolved.name]; @@ -243,6 +561,7 @@ class VirtualFileSystem { return this.trashDir(path); } mkdirRecursive(path) { + if (this._isVirtualPath(path)) return false; const segs = this._toAbsoluteSegments(path); if (!segs) return false; let dir = this.root; @@ -259,6 +578,7 @@ class VirtualFileSystem { return true; } rename(oldPath, newPath) { + if (this._isVirtualPath(oldPath) || this._isVirtualPath(newPath)) return false; const src = this._resolvePath(oldPath); if (!src || !src.name) return false; const dst = this._resolvePath(newPath); @@ -273,6 +593,7 @@ class VirtualFileSystem { return true; } copy(srcPath, dstPath) { + if (this._isVirtualPath(srcPath) || this._isVirtualPath(dstPath)) return false; const src = this._resolvePath(srcPath); if (!src || !src.name) return false; const dst = this._resolvePath(dstPath); @@ -294,6 +615,7 @@ class VirtualFileSystem { return this.rename(srcPath, dstPath); } rm(path) { + if (this._isVirtualPath(path)) return false; const resolved = this._resolvePath(path); if (!resolved || !resolved.name) return false; const node = resolved.dir.content[resolved.name]; @@ -309,6 +631,7 @@ class VirtualFileSystem { return false; } rmdir(path) { + if (this._isVirtualPath(path)) return false; const resolved = this._resolvePath(path); if (!resolved || !resolved.name) return false; const node = resolved.dir.content[resolved.name]; @@ -322,6 +645,7 @@ class VirtualFileSystem { return false; } touch(path) { + if (this._isVirtualPath(path)) return false; return this.writeFile(path, ''); } readDir(path) { @@ -330,16 +654,22 @@ class VirtualFileSystem { let dir; if (resolved.name) { const node = resolved.dir.content[resolved.name]; - if (node && node.type === 'dir') dir = node; + if (node && (node.type === 'dir' || node._virtual)) dir = node; else return null; } else { dir = resolved.dir; } - return Object.keys(dir.content).map(name => ({ + const entries = Object.keys(dir.content).map(name => ({ name, - type: dir.content[name].type, - size: dir.content[name].content ? dir.content[name].content.length : 0 + type: dir.content[name].type || (dir.content[name]._virtual ? 'file' : '?'), + size: dir.content[name]._factory ? 0 : (dir.content[name].content ? dir.content[name].content.length : 0), + _virtual: !!dir.content[name]._virtual })); + // Inject virtual /sys/ at root level + if ((!path || path === '/' || path === '') && !entries.some(e => e.name === 'sys' && e._virtual)) { + entries.push({ name: 'sys', type: 'dir', size: 0, _virtual: true }); + } + return entries; } fsWriteJSON(path, obj) { return this.writeFile(path, JSON.stringify(obj)); @@ -354,7 +684,7 @@ class VirtualFileSystem { } } tree(path = '', prefix = '') { - const resolved = this._resolvePath(path); + const resolved = this._resolvePath(path || '/'); if (!resolved) return ''; let dir; if (resolved.name) { @@ -366,12 +696,15 @@ class VirtualFileSystem { } let result = ''; const entries = Object.keys(dir.content).sort(); + // Inject /sys/ at root level in tree view + const atRoot = !path || path === '' || path === '/'; + if (atRoot && !entries.includes('sys')) entries.push('sys'); for (let i = 0; i < entries.length; i++) { const name = entries[i]; const isLast = i === entries.length - 1; const connector = isLast ? '└── ' : '├── '; result += prefix + connector + name + '\n'; - if (dir.content[name].type === 'dir') { + if (dir.content[name].type === 'dir' || dir.content[name]._virtual) { const ext = isLast ? ' ' : '│ '; result += this.tree(path + '/' + name, prefix + ext); } @@ -379,6 +712,7 @@ class VirtualFileSystem { return result; } find(pattern, path = '') { + if (this._isVirtualPath(path || '/')) return []; const resolved = this._resolvePath(path); if (!resolved) return []; let dir; @@ -415,6 +749,7 @@ class VirtualFileSystem { return results; } chmod(path, mode) { + if (this._isVirtualPath(path)) return false; const resolved = this._resolvePath(path); if (!resolved || !resolved.name) return false; const node = resolved.dir.content[resolved.name]; @@ -424,6 +759,7 @@ class VirtualFileSystem { return true; } du(path = '') { + if (this._isVirtualPath(path || '/')) return 0; const resolved = this._resolvePath(path); if (!resolved) return 0; let dir; |