diff options
| author | Agando <roduccomm@gmail.com> | 2026-06-08 18:01:06 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-06-08 18:01:06 +0200 |
| commit | b562e1491513163921593985f3542d4ee8ae0e75 (patch) | |
| tree | f768a2378d3c09b3685a7bf42a9fc4dec71ef31c | |
| parent | b9485bae5c2e62fa9c1b1c56d0f75be67cb47c37 (diff) | |
auto: 2026-06-08_18:00 changes
| -rw-r--r-- | cheatsheet.html | 15 | ||||
| -rw-r--r-- | help.html | 55 | ||||
| -rw-r--r-- | main.html | 133 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 10 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 4 | ||||
| -rw-r--r-- | modules/core/module-orchestrator.js | 660 | ||||
| -rw-r--r-- | modules/core/module-system.js | 2 | ||||
| -rw-r--r-- | modules/core/terminal.js | 6 | ||||
| -rw-r--r-- | modules/core/virtual-fs.js | 109 | ||||
| -rw-r--r-- | modules/ext/alias.js | 26 | ||||
| -rw-r--r-- | modules/ext/floating.js | 331 | ||||
| -rw-r--r-- | modules/ext/shell.js | 2 | ||||
| -rw-r--r-- | modules/ext/skill-pack-core.js | 36 | ||||
| -rw-r--r-- | modules/test/test-suite.js | 54 | ||||
| -rw-r--r-- | packages.json | 15 | ||||
| -rw-r--r-- | review-newos4.html | 408 |
16 files changed, 1713 insertions, 153 deletions
diff --git a/cheatsheet.html b/cheatsheet.html index 79601f9..aecb84e 100644 --- a/cheatsheet.html +++ b/cheatsheet.html @@ -34,7 +34,7 @@ pre{background:var(--bg2);border:1px solid var(--border);border-radius:var(--rad <body> <h1>⚡ newOS4 Quick Reference</h1> -<div class="sub">WebX11 v4.4 · <a href="help.html">Full docs →</a> · <span class="no-print">Ctrl+P to print</span></div> +<div class="sub">WebX11 v4.5.22 · <a href="help.html">Full docs →</a> · <span class="no-print">Ctrl+P to print</span></div> <div class="cols"> @@ -51,6 +51,7 @@ pre{background:var(--bg2);border:1px solid var(--border);border-radius:var(--rad <div class="item"><span class="cmd">touch <file></span><span class="desc">Create empty file</span></div> <div class="item"><span class="cmd">mkdir <dir></span><span class="desc">Create directory</span></div> <div class="item"><span class="cmd">rm <file></span><span class="desc">Remove file</span></div> +<div class="item"><span class="cmd">rmdir <dir></span><span class="desc">Remove directory</span></div> <div class="item"><span class="cmd">cp <src> <dst></span><span class="desc">Copy file</span></div> <div class="item"><span class="cmd">mv <old> <new></span><span class="desc">Move/rename</span></div> <div class="item"><span class="cmd">edit <file></span><span class="desc">Open code editor</span></div> @@ -151,6 +152,18 @@ pre{background:var(--bg2);border:1px solid var(--border);border-radius:var(--rad <div class="item"><span class="cmd">cmd > file</span><span class="desc">Redirect output</span></div> </div> +<h2>🪄 Floating</h2> +<div class="grid"> +<div class="item"><span class="cmd">floating-add btn <id> <lbl> <cmd></span><span class="desc">Create a floating button</span></div> +<div class="item"><span class="cmd">floating-add menu <id> <lbl> btn:X|cmd ...</span><span class="desc">Create a floating menu</span></div> +<div class="item"><span class="cmd">floating-add panel <id> <title> <text></span><span class="desc">Create a floating panel</span></div> +<div class="item"><span class="cmd">floating-remove <id></span><span class="desc">Remove a floating element</span></div> +<div class="item"><span class="cmd">floating-list</span><span class="desc">List all floating elements</span></div> +<div class="item"><span class="cmd">floating-toggle <id></span><span class="desc">Show/hide a floating element</span></div> +<div class="item"><span class="cmd">floating-pos <id> <x> <y></span><span class="desc">Reposition element</span></div> +<div class="item"><span class="cmd">floating-clear</span><span class="desc">Remove all floating elements</span></div> +</div> + <h2>🖥️ System</h2> <div class="grid"> <div class="item"><span class="cmd">status</span><span class="desc">System status overview</span></div> @@ -231,8 +231,8 @@ footer .tagline{color:var(--accent2);margin-top:10px;font-weight:500} <h1>WebX11 <span>Docs</span></h1> </div> <div class="header-meta"> - <span class="badge badge-v">v4.5.11</span> - <span class="badge badge-new">28 core · 17 ext</span> + <span class="badge badge-v">v4.5.22</span> + <span class="badge badge-new">38 core · ext</span> <span style="color:var(--text2)">160+ commands</span> </div> </div> @@ -1012,7 +1012,8 @@ echo 'export NAME=WebX11' > greet.sh echo 'echo "Hello, $NAME!"' >> greet.sh <span class="cmt"># Run it</span> -script greet.sh</pre> +script greet.sh +sh greet.sh</pre> <h3>Shell Commands</h3> <div class="table-wrap"><table> @@ -1021,9 +1022,46 @@ script greet.sh</pre> <tr><td><code>unset <var></code></td><td>Remove a shell variable</td></tr> <tr><td><code>set</code></td><td>List all shell variables</td></tr> <tr><td><code>script <path.sh></code></td><td>Run a VFS script file</td></tr> + <tr><td><code>sh <path.sh></code></td><td>Alias for <code>script</code> — run a VFS script file</td></tr> + <tr><td><code>alias [name=value]</code></td><td>List aliases or set a custom alias name=command</td></tr> + <tr><td><code>unalias <name></code></td><td>Remove a custom alias</td></tr> </table></div> </section> + <!-- ── Floating UI ── --> + <section id="floating"> + <h2>🪄 Floating UI</h2> + <div class="callout callout-success"> + <span class="callout-title">Remixable floating buttons · Menus · Panels</span> + The Floating Module lets you or the AI spawn persistent floating UI elements that survive page reloads. All state is kept in localStorage. + </div> + <div class="table-wrap"><table> + <tr><th>Command</th><th>Description</th></tr> + <tr><td><code>floating-add btn <id> <label> <command></code></td><td>Spawn a floating button that runs a terminal command</td></tr> + <tr><td><code>floating-add menu <id> <label> btn:Sub|cmd ...</code></td><td>Spawn a collapsible floating menu with sub-buttons</td></tr> + <tr><td><code>floating-add panel <id> <title> <content></code></td><td>Spawn a floating text panel</td></tr> + <tr><td><code>floating-remove <id></code></td><td>Remove a floating element</td></tr> + <tr><td><code>floating-list</code></td><td>Show all floating elements with IDs and positions</td></tr> + <tr><td><code>floating-pos <id> <x|auto> <y|auto></code></td><td>Reposition a floating element</td></tr> + <tr><td><code>floating-toggle <id></code></td><td>Show/hide a floating element</td></tr> + <tr><td><code>floating-clear</code></td><td>Remove all floating elements</td></tr> + </table></div> + <pre><span class="cmt"># Create a button</span> +floating-add btn files "📁 Files" ls + +<span class="cmt"># Create a collapsible menu</span> +floating-add menu tools "🔧 Tools" btn:Help|help btn:Clear|clear + +<span class="cmt"># Create a status panel</span> +floating-add panel status "📊 Status" "Ready" + +<span class="cmt"># List all elements</span> +floating-list + +<span class="cmt"># Hide without removing</span> +floating-toggle files</pre> + </section> + <!-- ── Security ── --> <section id="security"> <h2>🔒 Security & Guardrails</h2> @@ -1135,15 +1173,22 @@ vfs-import</pre> <tr><td><code>agent-unregister <id></code></td><td>Remove an agent from the registry</td></tr> <tr><td><code>lease-list</code></td><td>Show active leases — agent tasks with status and TTL</td></tr> <tr><td><code>orchestrate <task></code></td><td>Delegate multi-step task to agent orchestrator</td></tr> + <tr><td><code>orchestrate-parallel <task></code></td><td>Decompose and run sub-tasks in parallel (auto-splits on "and"/"then")</td></tr> + <tr><td><code>orchestrate-decompose <task></code></td><td>Preview how a task would be split into sub-tasks</td></tr> + <tr><td><code>orchestrate-result</code></td><td>Show results of the last parallel batch</td></tr> <tr><td><code>orchestrator-status</code></td><td>Show orchestrator agent pool status</td></tr> <tr><td><code>orchestrator-list</code></td><td>List all spawned agents with full details</td></tr> <tr><td><code>orchestrator-spawn <role> [caps...]</code></td><td>Spawn a new worker agent</td></tr> <tr><td><code>orchestrator-sleep <id></code></td><td>Suspend agent to VFS (survives reload)</td></tr> <tr><td><code>orchestrator-wake <id></code></td><td>Restore agent from VFS sleep state</td></tr> <tr><td><code>orchestrator-kill <id></code></td><td>Terminate an agent and revoke leases</td></tr> - <tr><td><code>orchestrator-purge</code></td><td>Remove dead agents (>60s silent)</td></tr> - <tr><td><code>orchestrator-config <key> <value></code></td><td>Tune: spawn-limit, spawn-strategy, heartbeat-timeout, idle-timeout, restart-policy, a2a-polling, a2a-poll-interval, heartbeat-check, idle-sweep-interval, dead-purge-timeout (persisted)</td></tr> + <tr><td><code>orchestrator-purge</code></td><td>Remove dead agents (>60s silent)</td></tr> + <tr><td><code>orchestrator-config <key> <value></code></td><td>Tune: spawn-limit, spawn-strategy, heartbeat-timeout, idle-timeout, restart-policy, a2a-polling (on/off), a2a-poll-interval, heartbeat-check, idle-sweep-interval, dead-purge-timeout, consensus-size, consensus-strategy, consensus-timeout, pipeline-auto-fix, pipeline-stage-timeout (persisted)</td></tr> <tr><td><code>orchestrator-restart-policy <id> <policy></code></td><td>Set per-agent restart policy: on-failure, always, never</td></tr> + <tr><td><code>consensus <question></code></td><td>Consensus: spawn N agents, debate, resolve by majority vote</td></tr> + <tr><td><code>consensus-status</code></td><td>Show current/active consensus round status</td></tr> + <tr><td><code>consensus-history</code></td><td>Show past consensus rounds and resolutions</td></tr> + <tr><td><code>pipeline <test|deploy|verify|history|config></code></td><td>Autonomous CI/CD: test agents/modules, deploy (test→auto-fix→verify), history, config</td></tr> <tr><td><code>lease-create <agent-id> [duration] [task]</code></td><td>Create a time-bound lease for a worker agent</td></tr> <tr><td><code>lease-list</code></td><td>Show active leases — agent tasks with status and TTL</td></tr> <tr><td><code>lease-revoke <id></code></td><td>Revoke an active lease</td></tr> @@ -666,6 +666,132 @@ .cmd-card .cmd { font-family: var(--font-primary); font-weight: bold; font-size: 0.85em; color: var(--accent-color); flex-shrink: 0; min-width: 60px; } .cmd-card .desc { font-size: 0.78em; color: var(--text-muted); } #cmd-cards-toggle { cursor: pointer; font-size: 1.1em; line-height: 1; } + + /* ── Floating UI Module ── */ + #floating-container { + position: fixed; + bottom: calc(50px + 25px + 8px); + right: 12px; + z-index: 500; + display: flex; + flex-direction: column-reverse; + align-items: flex-end; + gap: 6px; + pointer-events: none; + max-width: 280px; + } + .floating-btn { + pointer-events: auto; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + color: var(--text-color); + padding: 8px 14px; + border-radius: 8px; + font-family: inherit; + font-size: 0.9em; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; + white-space: nowrap; + touch-action: manipulation; + min-height: 44px; + box-shadow: 0 2px 8px rgba(0,0,0,0.4); + transition: border-color 0.15s, background 0.15s; + user-select: none; + } + .floating-btn:hover, + .floating-btn:active { + border-color: var(--accent-color); + background: #0a1a0a; + } + .floating-menu { + pointer-events: auto; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 8px; + overflow: hidden; + min-width: 140px; + box-shadow: 0 2px 12px rgba(0,0,0,0.5); + } + .floating-menu-header { + padding: 8px 14px; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + font-size: 0.9em; + min-height: 44px; + touch-action: manipulation; + user-select: none; + border-bottom: 1px solid var(--border-color); + } + .floating-menu-header:hover { + background: #0a1a0a; + } + .floating-menu-header .arrow { + transition: transform 0.2s; + font-size: 0.7em; + } + .floating-menu-header .arrow.collapsed { + transform: rotate(-90deg); + } + .floating-menu-body { + overflow: hidden; + transition: max-height 0.2s ease-out; + max-height: 0; + } + .floating-menu-body.expanded { + max-height: 300px; + } + .floating-menu-btn { + display: block; + width: 100%; + text-align: left; + padding: 8px 14px 8px 24px; + background: transparent; + border: none; + color: var(--text-muted); + font-family: inherit; + font-size: 0.85em; + cursor: pointer; + touch-action: manipulation; + min-height: 40px; + } + .floating-menu-btn:hover, + .floating-menu-btn:active { + background: #0a1a0a; + color: var(--text-color); + } + .floating-panel { + pointer-events: auto; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 8px 14px; + font-size: 0.8em; + min-width: 120px; + box-shadow: 0 2px 8px rgba(0,0,0,0.4); + } + .floating-panel-title { + font-weight: bold; + color: var(--text-muted); + margin-bottom: 4px; + font-size: 0.9em; + } + .floating-panel-content { + color: var(--text-color); + line-height: 1.4; + } + @keyframes float-in { + from { opacity: 0; transform: translateY(8px) scale(0.95); } + to { opacity: 1; transform: translateY(0) scale(1); } + } + .floating-btn, + .floating-menu, + .floating-panel { + animation: float-in 0.15s ease-out; + } </style> </head> <body> @@ -718,6 +844,9 @@ <div class="output-line info loading" id="loading-indicator">Loading modules...</div> </div> + <!-- Floating UI Container — populated by FloatingModule --> + <div id="floating-container"></div> + <!-- AI Confirm Area — fixed outside scrollable output, always visible --> <div id="ai-confirm-area" class="hidden"></div> @@ -1058,7 +1187,7 @@ var mods = [ ['Foundation (always loaded)', ['virtual-fs.js','module-manager.js','ollama-manager.js','terminal.js'], true], ['Built-in modules', ['module-system.js','module-fs.js','module-modmgr.js','module-ollama.js','module-my.js','module-searxng.js','module-servermon.js','module-ollamamodels.js','module-config.js','module-simplegui.js','module-io.js','module-dynamicprompts.js','module-aiassist.js','module-aiasm.js','module-smartctx.js','module-duckduckgo.js','module-fetcher.js','module-skills.js','module-chatstudio.js'], false], - ['Extensions', ['calc.js','voice.js','cron.js','theme.js','wpm.js','mindmap.js','webdav.js','skill-pack-core.js','editor.js','diff.js','alias.js','finder.js','filemgr.js','cryptogzip.js','tutorial.js','shell.js','busybox.js'], false] + ['Extensions', ['calc.js','voice.js','cron.js','theme.js','wpm.js','mindmap.js','webdav.js','skill-pack-core.js','editor.js','diff.js','alias.js','finder.js','filemgr.js','cryptogzip.js','tutorial.js','shell.js','busybox.js','floating.js'], false] ]; var isSel = function(name) { return true; }; // default: all selected mods.forEach(function(section) { @@ -1095,7 +1224,7 @@ </script> <!-- Async dependency-ordered module loader (replaces 26 synchronous script tags) --> - <script src="modules/core/module-loader.js?v=2026-06-07-v4.5.13"></script> + <script src="modules/core/module-loader.js?v=2026-06-08-v4.5.22"></script> <script> // ═══ ONE TRUE VERSION SOURCE ═══ diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index ed76a8a..7f99386 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -93,6 +93,15 @@ SKILLS (PERSISTENT PROCEDURAL MEMORY): - create_skill(name, description, content, tags, category) — Save a new stored procedure. Call after completing complex multi-step tasks (3+ tool calls). - patch_skill(name, content) — Update an existing skill when the approach changes +FLOATING UI (PERSISTENT UI ELEMENTS): Use 'run_command' tool with these terminal commands: +- 'floating-add btn <id> <label> <command>' — Create a floating button that runs a terminal command when clicked +- 'floating-add menu <id> <label> <btn1>,<btn2>...' — Create a collapsible floating menu (buttons: label:command pairs) +- 'floating-add panel <id> <title> <content>' — Create a floating text/status panel +- 'floating-remove <id>' — Remove a floating element by ID +- 'floating-toggle <id>' — Show/hide a floating element without deleting +- 'floating-list' — List all floating elements +- 'floating-clear' — Remove all floating elements +- NOTE: These are terminal commands, run via 'run_command'. Use SPACES not hyphens between subcommands. MODULE SELF-IMPROVEMENT: - view_module(name) — View source code of any loaded module - edit_module(name) — Copy module into editor. Opens editor for modifications. @@ -1733,6 +1742,7 @@ RULES: const orch = this.terminal.moduleManager.modules['OrchestratorModule']; if (!orch) return JSON.stringify({ error: 'Orchestrator not available' }); const agent = orch.spawnAgent(role, caps); + if (!agent) return JSON.stringify({ error: 'Agent spawn failed — limit reached or concurrent spawn conflict' }); return JSON.stringify({ message: `Agent spawned: ${agent.id}`, agent }); } case 'load_skills': { diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 1d1cbda..96fee00 100644 --- a/modules/core/module-loader.js +++ b/modules/core/module-loader.js @@ -105,7 +105,7 @@ window.__moduleLoaderReload = reloadWithSelection; -const LATEST_VER = '2026-06-06-v4.5.12'; +const LATEST_VER = '2026-06-08-v4.5.22'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { @@ -164,7 +164,7 @@ const CACHE = '?v=' + LATEST_VER; // Phase 4: Ext modules (filtered) if (typeof webx11._loadExtModules === 'function') { - const extKeys = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'tutorial', 'shell', 'busybox']; + const extKeys = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'tutorial', 'shell', 'busybox', 'floating']; const extSelected = selected ? extKeys.filter(k => isSelected(k + '.js', selected)) : extKeys; diff --git a/modules/core/module-orchestrator.js b/modules/core/module-orchestrator.js index 05e1598..bcbd00d 100644 --- a/modules/core/module-orchestrator.js +++ b/modules/core/module-orchestrator.js @@ -23,10 +23,18 @@ class OrchestratorModule { this._restartBackoffs = {}; this._eventBus = {}; this._idleSweepInterval = null; + this._spawnLock = false; + this._CONSENSUS_SIZE = 3; + this._CONSENSUS_STRATEGY = 'majority'; + this._CONSENSUS_TIMEOUT = 60000; + this._consensusRounds = []; + this._activeConsensus = null; + this._pipelineHistory = []; + this._PIPELINE_CONFIG = { autoFix: false, stageTimeout: 30000 }; this._loadConfig(); } _loadConfig() { - try { const c = localStorage.getItem('webx11_orchestrator_config'); if (c) { const p = JSON.parse(c); if (p.spawnLimit) this._SPAWN_LIMIT = p.spawnLimit; if (p.heartbeatTimeout) this._HEARTBEAT_TIMEOUT = p.heartbeatTimeout; if (p.idleTimeout) this._IDLE_TIMEOUT = p.idleTimeout; if (p.restartPolicy) this._RESTART_POLICY = p.restartPolicy; if (p.a2aPollInterval) this._A2A_POLL_INTERVAL = p.a2aPollInterval; if (p.heartbeatCheck) this._HEARTBEAT_CHECK = p.heartbeatCheck; if (p.idleSweepInterval) this._IDLE_SWEEP_INTERVAL = p.idleSweepInterval; if (p.deadPurgeTimeout) this._DEAD_PURGE_TIMEOUT = p.deadPurgeTimeout; } } catch (e) {} + try { const c = localStorage.getItem('webx11_orchestrator_config'); if (c) { const p = JSON.parse(c); if (p.spawnLimit) this._SPAWN_LIMIT = p.spawnLimit; if (p.heartbeatTimeout) this._HEARTBEAT_TIMEOUT = p.heartbeatTimeout; if (p.idleTimeout) this._IDLE_TIMEOUT = p.idleTimeout; if (p.restartPolicy) this._RESTART_POLICY = p.restartPolicy; if (p.a2aPollInterval) this._A2A_POLL_INTERVAL = p.a2aPollInterval; if (p.heartbeatCheck) this._HEARTBEAT_CHECK = p.heartbeatCheck; if (p.idleSweepInterval) this._IDLE_SWEEP_INTERVAL = p.idleSweepInterval; if (p.deadPurgeTimeout) this._DEAD_PURGE_TIMEOUT = p.deadPurgeTimeout; if (p.consensusSize) this._CONSENSUS_SIZE = p.consensusSize; if (p.consensusStrategy) this._CONSENSUS_STRATEGY = p.consensusStrategy; if (p.consensusTimeout) this._CONSENSUS_TIMEOUT = p.consensusTimeout; if (p.pipelineAutoFix !== undefined) this._PIPELINE_CONFIG.autoFix = p.pipelineAutoFix; if (p.pipelineStageTimeout) this._PIPELINE_CONFIG.stageTimeout = p.pipelineStageTimeout; } } catch (e) {} } _saveConfig() { localStorage.setItem('webx11_orchestrator_config', JSON.stringify({ @@ -37,7 +45,12 @@ class OrchestratorModule { a2aPollInterval: this._A2A_POLL_INTERVAL, heartbeatCheck: this._HEARTBEAT_CHECK, idleSweepInterval: this._IDLE_SWEEP_INTERVAL, - deadPurgeTimeout: this._DEAD_PURGE_TIMEOUT + deadPurgeTimeout: this._DEAD_PURGE_TIMEOUT, + consensusSize: this._CONSENSUS_SIZE, + consensusStrategy: this._CONSENSUS_STRATEGY, + consensusTimeout: this._CONSENSUS_TIMEOUT, + pipelineAutoFix: this._PIPELINE_CONFIG.autoFix, + pipelineStageTimeout: this._PIPELINE_CONFIG.stageTimeout })); } registerCommands() { @@ -53,10 +66,75 @@ class OrchestratorModule { this.commands['orchestrator-restart-policy'] = (args) => this.setRestartPolicy(args); this.commands['a2a-send'] = (args) => this.sendA2AMessage(args); this.commands['a2a-list'] = () => this.listA2AMessages(); + this.commands['orchestrate-parallel'] = (args) => this.delegateTask(args.join(' '), { parallel: true }); + this.commands['orchestrate-decompose'] = (args) => this.showDecomposition(args.join(' ')); + this.commands['orchestrate-result'] = () => this.showParallelResult(); + this.commands['consensus'] = (args) => this.handleConsensus(args); + this.commands['consensus-status'] = () => this.consensusStatus(); + this.commands['consensus-history'] = () => this.consensusHistory(); + this.commands['pipeline'] = (args) => this.runPipeline(args); + } + // ─── Task Decomposition Engine ─── + _decomposeGoal(goal) { + if (!goal || typeof goal !== 'string') return [{ goal }]; + const lower = goal; + // Heuristic: only split at conjunctions when both sides have distinct action verbs + // Action verb patterns from _deriveCapabilities + const ACTION_VERBS = [ + 'search', 'find', 'research', 'lookup', 'query', + 'write', 'create', 'generate', 'compose', 'draft', 'save', + 'delete', 'remove', 'trash', 'rm', + 'read', 'view', 'cat', 'show', 'display', 'inspect', 'preview', + 'edit', 'modify', 'update', 'patch', 'change', 'fix', + 'install', 'package', 'setup', + 'list', 'status', 'health', 'audit', 'report', 'check', + 'fetch', 'scrape', 'download', + 'build', 'develop', 'compile', + 'analyze', 'analyse', 'summarize', 'review', 'investigate', + 'deploy', 'publish', 'upload', + 'monitor', 'watch', 'poll', 'track', 'observe', + 'test', 'verify', 'cmp', 'diff', 'compare' + ]; + const hasActionVerb = (s) => ACTION_VERBS.some(v => { + const re = new RegExp('\\b' + v + '\\b', 'i'); + return re.test(s); + }); + // Try splitting on " and " / " then " / " also " / "; " / ", then" + const separators = [ + /\s+and\s+/i, + /\s+then\s+/i, + /\s+also\s+/i, + /;\s+/, + /,\s+then\s+/i, + /\s+followed by\s+/i, + ]; + let bestSplit = null; + for (const sep of separators) { + const parts = goal.split(sep); + if (parts.length >= 2 && parts.every(p => hasActionVerb(p.trim()))) { + bestSplit = parts.map(p => p.trim()).filter(p => p.length > 3); + break; // first matching separator wins + } + } + if (!bestSplit || bestSplit.length < 2) { + return [{ goal }]; // atomic + } + return bestSplit.map(sub => ({ + goal: sub, + role: this._deriveRole(sub), + caps: this._deriveCapabilities(sub) + })); } getHelpInfo() { return { 'orchestrate': 'orchestrate <task> — Delegate a task to an available agent (auto-spawns if needed)', + 'orchestrate-parallel': 'orchestrate-parallel <task> — Decompose and delegate sub-tasks in parallel (auto-splits on "and"/"then")', + 'orchestrate-decompose': 'orchestrate-decompose <task> — Preview how a task would be decomposed into sub-tasks', + 'orchestrate-result': 'orchestrate-result — Show the last parallel batch results (stored in VFS)', + 'consensus': 'consensus <question> — Start a consensus round: spawn 3 agents, debate the question, resolve by majority', + 'consensus-status': 'consensus-status — Show current/active consensus round status', + 'consensus-history': 'consensus-history — Show past consensus rounds and their resolutions', + 'pipeline': 'pipeline <test|deploy|verify|history|config> — Autonomous CI/CD: test agents/queue/modules, deploy (test→auto-fix→verify), history (show runs), config (show config)', 'orchestrator-status': 'orchestrator-status — Show agent pool status and config', 'orchestrator-spawn': 'orchestrator-spawn [role] [capabilities...] — Manually spawn a new agent', 'orchestrator-kill': 'orchestrator-kill <agent-id> — Terminate an agent and revoke leases', @@ -234,35 +312,44 @@ class OrchestratorModule { return (name || 'worker').replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').substring(0, 32) || 'worker'; } spawnAgent(role, capabilities) { - const safeRole = this._sanitizeId(role); - const id = `agent-${Object.keys(this._agents).length + 1}-${safeRole}`; - if (Object.keys(this._agents).length >= this._SPAWN_LIMIT) { - this.terminal.print(`❌ Agent spawn limit (${this._SPAWN_LIMIT}) reached`, 'error'); + if (this._spawnLock) { + this.terminal.print('⚠️ Spawn in progress — queued', 'warning'); return null; } - const agent = { - id, - role: role || 'worker', - capabilities: capabilities.length ? capabilities : ['list_files', 'read_file', 'web_search'], - createdAt: new Date().toISOString(), - lastHeartbeat: Date.now(), - status: 'idle', - currentTask: null, - leaseId: null, - restartPolicy: this._RESTART_POLICY, - failureCount: 0, - restartCount: 0, - nextRestartAt: null - }; - this._agents[id] = agent; - // Create VFS agent directory with status - if (!this.terminal.fs.readFile(`/agents/${id}/`)) this.terminal.fs.mkdir(`agents/${id}`); - this.terminal.fs.writeFile(`agents/${id}/status.json`, JSON.stringify(agent)); - // Register in agent registry - const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; - if (registry) registry.registerAgent(['register', id, ...agent.capabilities]); - this.terminal.print(`🤖 Agent '${id}' spawned (role: ${role || 'worker'}, ${agent.capabilities.length} caps)`, 'success'); - return agent; + this._spawnLock = true; + try { + const safeRole = this._sanitizeId(role); + const id = `agent-${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 4)}-${safeRole}`; + if (Object.keys(this._agents).length >= this._SPAWN_LIMIT) { + this.terminal.print(`❌ Agent spawn limit (${this._SPAWN_LIMIT}) reached`, 'error'); + return null; + } + const agent = { + id, + role: role || 'worker', + capabilities: capabilities.length ? capabilities : ['list_files', 'read_file', 'web_search'], + createdAt: new Date().toISOString(), + lastHeartbeat: Date.now(), + status: 'idle', + currentTask: null, + leaseId: null, + restartPolicy: this._RESTART_POLICY, + failureCount: 0, + restartCount: 0, + nextRestartAt: null + }; + this._agents[id] = agent; + // Create VFS agent directory with status + if (!this.terminal.fs.readFile(`/agents/${id}/`)) this.terminal.fs.mkdir(`agents/${id}`); + this.terminal.fs.writeFile(`agents/${id}/status.json`, JSON.stringify(agent)); + // Register in agent registry + const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; + if (registry) registry.registerAgent(['register', id, ...agent.capabilities]); + this.terminal.print(`🤖 Agent '${id}' spawned (role: ${role || 'worker'}, ${agent.capabilities.length} caps)`, 'success'); + return agent; + } finally { + this._spawnLock = false; + } } killAgent(id) { @@ -460,7 +547,12 @@ class OrchestratorModule { 'a2a-poll-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 50 || n > 30000) return 'Must be 50-30000ms'; clearInterval(this._a2aPollTimer); this._a2aPollTimer = null; this._A2A_POLL_INTERVAL = n; if (this._A2A_POLLING) { this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), n); } return `a2a-poll-interval set to ${n}ms`; }, 'heartbeat-check': () => { const n = parseInt(val); if (isNaN(n) || n < 500 || n > 60000) return 'Must be 500-60000ms'; clearInterval(this._heartbeatInterval); this._HEARTBEAT_CHECK = n; this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), n); return `heartbeat-check set to ${n}ms`; }, 'idle-sweep-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; clearInterval(this._idleSweepInterval); this._IDLE_SWEEP_INTERVAL = n; this._idleSweepInterval = setInterval(() => this._sweepIdle(), n); return `idle-sweep-interval set to ${n}ms`; }, - 'dead-purge-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; this._DEAD_PURGE_TIMEOUT = n; return `dead-purge-timeout set to ${n}ms`; } + 'dead-purge-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; this._DEAD_PURGE_TIMEOUT = n; return `dead-purge-timeout set to ${n}ms`; }, + 'consensus-size': () => { const n = parseInt(val); if (isNaN(n) || n < 2 || n > 10) return 'Must be 2-10'; this._CONSENSUS_SIZE = n; return `consensus-size set to ${n}`; }, + 'consensus-strategy': () => { if (!['majority', 'unanimous', 'weighted'].includes(val)) return 'Must be majority, unanimous, or weighted'; this._CONSENSUS_STRATEGY = val; return `consensus-strategy set to ${val}`; }, + 'consensus-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 300000) return 'Must be 5000-300000ms'; this._CONSENSUS_TIMEOUT = n; return `consensus-timeout set to ${n}ms`; }, + 'pipeline-auto-fix': () => { this._PIPELINE_CONFIG.autoFix = val === 'on' || val === 'true'; return `auto-fix ${this._PIPELINE_CONFIG.autoFix ? 'on' : 'off'}`; }, + 'pipeline-stage-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 120000) return 'Must be 5000-120000ms'; this._PIPELINE_CONFIG.stageTimeout = n; return `stage-timeout set to ${n}ms`; } }; if (!configMap[key]) { @@ -471,25 +563,514 @@ class OrchestratorModule { this._saveConfig(); } + // ─── Show Decomposition Preview ─── + + showDecomposition(goal) { + if (!goal) return this.terminal.print('Usage: orchestrate-decompose <goal>', 'error'); + const parts = this._decomposeGoal(goal); + this.terminal.print('🔍 Decomposition of "' + goal.substring(0, 60) + '":', 'header'); + if (parts.length === 1 && parts[0].goal === goal) { + this.terminal.print(' → Atomic (no split detected)', 'info'); + return; + } + for (let i = 0; i < parts.length; i++) { + const p = parts[i]; + this.terminal.print(' [' + (i + 1) + '] ' + p.goal.substring(0, 50), 'success'); + this.terminal.print(' Role: ' + p.role + ' | Caps: ' + (p.caps || []).join(', '), 'text-muted'); + } + this.terminal.print('→ ' + parts.length + ' sub-task(s) — run with: orchestrate-parallel "' + goal + '"', 'info'); + } + + // ─── Show Last Parallel Result ─── + + showParallelResult() { + const data = this.terminal.fs.fsReadJSON('orchestrator/parallel-results.json'); + if (!data) return this.terminal.print('No parallel results saved yet', 'info'); + this.terminal.print('📊 Last Parallel Batch Results', 'header'); + this.terminal.print(' Task: ' + (data.parent_task || '').substring(0, 60), 'info'); + this.terminal.print(' ' + data.successes + '/' + data.total + ' succeeded' + (data.failures > 0 ? ', ' + data.failures + ' failed' : ''), 'info'); + this.terminal.print(' Timestamp: ' + data.timestamp, 'text-muted'); + for (const s of (data.summaries || [])) { + const icon = s.status === 'ok' ? '✅' : '❌'; + const snippet = s.result ? s.result.replace(/\n/g, ' ').substring(0, 80) : (s.error || ''); + this.terminal.print(' ' + icon + ' ' + s.task.substring(0, 40) + ' → ' + snippet, 'info'); + } + } + + // ─── Consensus Protocol ─── + + async handleConsensus(args) { + if (!args || args.length === 0) return this.terminal.print('Usage: consensus <question>', 'error'); + const question = args.join(' '); + if (this._activeConsensus) return this.terminal.print('❌ A consensus round is already in progress — wait for it to resolve', 'error'); + + const roundId = `consensus-${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 4)}`; + const size = this._CONSENSUS_SIZE; + this.terminal.print(`🤝 Starting consensus round ${roundId} (${size} agents, strategy: ${this._CONSENSUS_STRATEGY})`, 'header'); + this.terminal.print(` Question: ${question.substring(0, 80)}`, 'info'); + + this._activeConsensus = { + id: roundId, + question, + strategy: this._CONSENSUS_STRATEGY, + size, + votes: [], + startedAt: new Date().toISOString(), + resolved: false, + result: null + }; + + // Spawn N agents with same question + const tasks = []; + for (let i = 0; i < size; i++) { + tasks.push(`analyze and answer: ${question}. Provide your reasoning then your final answer as "VOTE: <answer>"`); + } + + const timeout = setTimeout(() => { + if (this._activeConsensus && !this._activeConsensus.resolved) { + this._resolveConsensus('timeout'); + } + }, this._CONSENSUS_TIMEOUT); + + try { + const results = await Promise.allSettled(tasks.map(t => this.delegateTask(t))); + + for (let i = 0; i < results.length; i++) { + const r = results[i]; + const voteText = r.status === 'fulfilled' ? (r.value || '') : 'FAILED'; + // Extract VOTE: marker or use full text + let answer = 'no vote'; + const voteMatch = voteText.match(/VOTE:\s*(.+?)(?:\n|$)/i); + if (voteMatch) answer = voteMatch[1].trim(); + else if (voteText.length > 0 && voteText !== 'FAILED') answer = voteText.substring(0, 60).trim(); + + this._activeConsensus.votes.push({ + agentIdx: i, + answer, + raw: voteText.substring(0, 200), + status: r.status === 'fulfilled' ? 'voted' : 'failed' + }); + } + } catch (e) { + this.terminal.print(`⚠️ Consensus error: ${e.message}`, 'warning'); + } finally { + clearTimeout(timeout); + } + + this._resolveConsensus('complete'); + return this._activeConsensus.result; + } + + _resolveConsensus(reason) { + const round = this._activeConsensus; + if (!round || round.resolved) return; + round.resolved = true; + round.resolvedAt = new Date().toISOString(); + round.resolvedBy = reason; + + // Count votes + const voteCounts = {}; + for (const v of round.votes) { + if (v.status === 'failed') continue; + voteCounts[v.answer] = (voteCounts[v.answer] || 0) + 1; + } + + let winner = null; + let maxVotes = 0; + let totalVoted = 0; + for (const [ans, count] of Object.entries(voteCounts)) { + totalVoted += count; + if (count > maxVotes) { + maxVotes = count; + winner = ans; + } + } + + if (this._CONSENSUS_STRATEGY === 'majority') { + const needed = Math.ceil(round.size / 2); + if (maxVotes >= needed) { + round.result = { consensus: 'reached', answer: winner, votes: maxVotes, total: round.size }; + } else { + round.result = { consensus: 'hung', votes: voteCounts, total: round.size }; + } + } else if (this._CONSENSUS_STRATEGY === 'unanimous') { + if (totalVoted === round.size && Object.keys(voteCounts).length === 1) { + round.result = { consensus: 'reached', answer: winner, votes: maxVotes, total: round.size }; + } else { + round.result = { consensus: 'hung', votes: voteCounts, total: round.size }; + } + } else { + // weighted — fallback to majority + const needed = Math.ceil(round.size / 2); + round.result = maxVotes >= needed + ? { consensus: 'reached', answer: winner, votes: maxVotes, total: round.size } + : { consensus: 'hung', votes: voteCounts, total: round.size }; + } + + // Store the round + this._consensusRounds.push(round); + if (!this.terminal.fs.readDir('orchestrator')) this.terminal.fs.mkdir('orchestrator'); + this.terminal.fs.writeFile(`orchestrator/consensus-round-${round.id}.json`, JSON.stringify(round, null, 2)); + + // Print result + const r = round.result; + if (r.consensus === 'reached') { + this.terminal.print(`✅ Consensus reached: "${r.answer}" (${r.votes}/${r.total} votes, ${round.strategy})`, 'success'); + } else { + this.terminal.print(`⚖️ Consensus hung — no clear winner. Votes: ${JSON.stringify(r.votes)}`, 'warning'); + } + + this._activeConsensus = null; + } + + consensusStatus() { + if (this._activeConsensus) { + const r = this._activeConsensus; + this.terminal.print('🤝 Active Consensus Round', 'header'); + this.terminal.print(` ID: ${r.id}`, 'info'); + this.terminal.print(` Question: ${r.question.substring(0, 60)}`, 'info'); + this.terminal.print(` Strategy: ${r.strategy} | Size: ${r.size}`, 'info'); + this.terminal.print(` Votes collected: ${r.votes.length}/${r.size}`, 'info'); + for (const v of r.votes) { + this.terminal.print(` ${v.status === 'voted' ? '✅' : '❌'} Agent ${v.agentIdx}: ${v.answer}`, 'info'); + } + } else { + this.terminal.print('No active consensus round', 'info'); + } + } + + consensusHistory() { + const rounds = this._consensusRounds; + if (rounds.length === 0) { + this.terminal.print('No consensus rounds in memory — checking VFS...', 'info'); + // Try VFS + const dir = this.terminal.fs.readDir('orchestrator/'); + if (dir) { + const files = dir.filter(e => e.name && e.name.startsWith('consensus-round-')); + if (files.length > 0) { + this.terminal.print(`📜 ${files.length} round(s) in VFS`, 'header'); + for (const f of files.slice(-5)) { + const data = this.terminal.fs.fsReadJSON(`orchestrator/${f.name}`); + if (data) { + const r = data.result; + const status = r?.consensus === 'reached' ? '✅' : '⚖️'; + this.terminal.print(` ${status} ${data.id}: "${data.question.substring(0, 40)}" → ${r?.answer || 'no result'}`, 'info'); + } + } + return; + } + } + this.terminal.print('No past consensus rounds found', 'info'); + return; + } + this.terminal.print('📜 Consensus History', 'header'); + for (const round of rounds.slice(-10)) { + const r = round.result; + const status = r?.consensus === 'reached' ? '✅' : '⚖️'; + this.terminal.print(` ${status} ${round.id}: "${round.question.substring(0, 40)}"`, 'info'); + this.terminal.print(` Strategy: ${round.strategy} | Votes: ${round.votes.length}/${round.size} | Result: ${r?.answer || 'hung'}`, 'text-muted'); + } + } + + // ─── Autonomous CI/CD Pipeline ─── + + async runPipeline(args) { + if (!args || args.length === 0) return this.terminal.print('Usage: pipeline <command> [options]', 'error'); + const cmd = args[0].toLowerCase(); + const opts = args.slice(1); + + const run = { + id: `pipeline-${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 4)}`, + command: cmd, + startedAt: new Date().toISOString(), + stages: [], + status: 'running' + }; + + try { + if (cmd === 'test') { + await this._pipelineStage(run, 'agent-health', () => this._testAgentHealth()); + await this._pipelineStage(run, 'a2a-mailbox', () => this._testA2AMailbox()); + await this._pipelineStage(run, 'module-check', () => this._testModules()); + run.status = 'passed'; + this.terminal.print('✅ All pipeline tests passed', 'success'); + } else if (cmd === 'deploy') { + const deployTask = opts.join(' ') || 'run all checks'; + await this._pipelineStage(run, 'test', () => this.runPipeline(['test'])); + if (run.stages.find(s => s.name === 'test' && s.status === 'failed')) { + if (this._PIPELINE_CONFIG.autoFix) { + await this._pipelineStage(run, 'auto-fix', () => this._autoFix()); + } else { + run.status = 'blocked'; + this.terminal.print('❌ Pipeline blocked — tests failed. Set pipeline-auto-fix on to auto-repair', 'error'); + return; + } + } + await this._pipelineStage(run, 'verify', () => this._pipelineVerify()); + run.status = 'passed'; + this.terminal.print('✅ Pipeline deploy passed all stages', 'success'); + } else if (cmd === 'verify') { + await this._pipelineStage(run, 'verify', () => this._pipelineVerify()); + run.status = 'passed'; + } else if (cmd === 'history') { + this._pipelineHistory.slice(-10).forEach(p => { + const icon = p.status === 'passed' ? '✅' : (p.status === 'blocked' ? '🚫' : '❌'); + this.terminal.print(` ${icon} ${p.id}: ${p.command} (${p.startedAt.slice(11, 19)})`, 'info'); + p.stages.forEach(s => { + this.terminal.print(` ${s.status === 'passed' ? '✅' : '❌'} ${s.name} (${(s.duration || 0).toFixed(0)}ms)`, 'text-muted'); + }); + }); + if (this._pipelineHistory.length === 0) this.terminal.print('No pipeline runs yet', 'info'); + return; + } else if (cmd === 'config') { + this.terminal.print('Pipeline Config:', 'header'); + this.terminal.print(` auto-fix: ${this._PIPELINE_CONFIG.autoFix ? 'on' : 'off'}`, 'info'); + this.terminal.print(` stage-timeout: ${this._PIPELINE_CONFIG.stageTimeout}ms`, 'info'); + return; + } else { + return this.terminal.print('Unknown pipeline command. Usage: test, deploy, verify, history, config', 'error'); + } + } catch (e) { + run.status = 'failed'; + run.error = e.message; + this.terminal.print(`❌ Pipeline failed: ${e.message}`, 'error'); + } + + run.completedAt = new Date().toISOString(); + this._pipelineHistory.push(run); + if (!this.terminal.fs.readDir('orchestrator')) this.terminal.fs.mkdir('orchestrator'); + this.terminal.fs.writeFile(`orchestrator/pipeline-${run.id}.json`, JSON.stringify(run, null, 2)); + return run; + } + + async _pipelineStage(run, name, fn) { + const stage = { name, status: 'running', startedAt: Date.now() }; + run.stages.push(stage); + this.terminal.print(`⏳ Pipeline stage: ${name}...`, 'info'); + + try { + const timeout = this._PIPELINE_CONFIG.stageTimeout; + const result = await Promise.race([ + fn(), + new Promise((_, reject) => setTimeout(() => reject(new Error(`${name} timed out (${timeout}ms)`)), timeout)) + ]); + stage.status = 'passed'; + stage.duration = Date.now() - stage.startedAt; + stage.result = typeof result === 'string' ? result.substring(0, 200) : 'ok'; + this.terminal.print(`✅ Stage ${name} passed (${stage.duration}ms)`, 'success'); + return result; + } catch (e) { + stage.status = 'failed'; + stage.duration = Date.now() - stage.startedAt; + stage.error = e.message; + this.terminal.print(`❌ Stage ${name} failed: ${e.message}`, 'error'); + throw e; + } + } + + _testAgentHealth() { + const total = Object.keys(this._agents).length; + const busy = Object.values(this._agents).filter(a => a.status === 'busy').length; + const failed = Object.values(this._agents).filter(a => a.status === 'failed').length; + + const issues = []; + if (failed > 0) issues.push(`${failed} agent(s) in failed state`); + if (total > 10) issues.push(`high agent count: ${total}`); + if (issues.length > 0) throw new Error(issues.join('; ')); + this.terminal.print(` Agents: ${total} (${busy} busy, ${failed} failed) — healthy`, 'info'); + return `agents_ok:${total}:${busy}:${failed}`; + } + + _testA2AMailbox() { + // Verify A2A polling is running + if (!this._A2A_POLLING) throw new Error('A2A polling is disabled'); + if (!this._a2aPollTimer) throw new Error('A2A polling timer not running'); + // Verify each agent has a VFS directory + for (const [id, agent] of Object.entries(this._agents)) { + const dir = this.terminal.fs.readDir(`/agents/${id}/`); + if (!dir) throw new Error(`Agent ${id} missing VFS directory`); + } + this.terminal.print(` A2A: ${Object.keys(this._agents).length} agent mailboxes OK`, 'info'); + return `a2a_ok:${Object.keys(this._agents).length}`; + } + + _testModules() { + const mm = this.terminal.moduleManager; + if (!mm || !mm.modules) throw new Error('ModuleManager not available'); + const required = ['OrchestratorModule', 'AgentRegistryModule', 'LeaseModule', 'MemoryGraphModule']; + const missing = required.filter(n => !mm.modules[n]); + if (missing.length > 0) throw new Error(`Missing modules: ${missing.join(', ')}`); + // Check orchestrator is in registry + const registry = mm.modules['AgentRegistryModule']; + if (registry && !registry.agents?.find(a => a.id === 'orchestrator-0')) { + this.terminal.print(' ⚠️ orchestrator-0 not in registry (will register on next init)', 'warning'); + } + this.terminal.print(` Modules: ${required.length}/${required.length} required loaded`, 'info'); + return `modules_ok:${required.length}`; + } + + _pipelineVerify() { + // Verify running state — internal checks + const issues = []; + + // Check orchestrator status + const total = Object.keys(this._agents).length; + const failed = Object.values(this._agents).filter(a => a.status === 'failed').length; + if (failed > 0) issues.push(`${failed} failed agents`); + if (total + failed > 0 && failed === total) issues.push('all agents failed'); + + // Verify last parallel result exists + const lastResult = this.terminal.fs.fsReadJSON('orchestrator/parallel-results.json'); + if (!lastResult) this.terminal.print(' ℹ️ No parallel results yet (first deploy)', 'info'); + + // Verify consensus history + const consensusDir = this.terminal.fs.readDir('orchestrator/'); + if (consensusDir) { + const rounds = consensusDir.filter(e => e.name && e.name.startsWith('consensus-round-')); + this.terminal.print(` VFS: ${rounds.length} consensus rounds archived`, 'info'); + } + + // Check memory graph + const memgraph = this.terminal.moduleManager?.modules['MemoryGraphModule']; + if (memgraph && typeof memgraph.getStats === 'function') { + const stats = memgraph.getStats(); + this.terminal.print(` Memory graph: ${stats?.edges || 0} edges, ${stats?.nodes || 0} nodes`, 'info'); + } + + if (issues.length > 0) throw new Error(issues.join('; ')); + return `verify_ok:${total} agents, ${issues.length} issues`; + } + + async _autoFix() { + this.terminal.print('🔄 Auto-fix running...', 'warning'); + let fixed = 0; + + // Fix 1: Purge failed agents + const failedIds = Object.entries(this._agents) + .filter(([, a]) => a.status === 'failed') + .map(([id]) => id); + for (const id of failedIds) { + this.killAgent(id); + fixed++; + } + if (fixed > 0) this.terminal.print(` Purged ${fixed} failed agent(s)`, 'warning'); + + // Fix 2: Restart A2A poller if stopped + if (!this._A2A_POLLING) { + this._A2A_POLLING = true; + this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL); + this.terminal.print(' Restarted A2A polling', 'warning'); + fixed++; + } + + // Fix 3: Ensure VFS orchestrator dir exists + if (!this.terminal.fs.readDir('orchestrator')) { + this.terminal.fs.mkdir('orchestrator'); + fixed++; + } + + if (fixed === 0) this.terminal.print(' No auto-fix actions needed', 'info'); + return `fixed ${fixed} issues`; + } + // ─── Task Delegation ─── - async delegateTask(task) { + async delegateTask(task, opts = {}) { if (!task) return this.terminal.print('Usage: orchestrate <task description>', 'error'); - // Find an idle agent + // Parallel fan-out: decompose and run sub-tasks concurrently + if (opts.parallel) { + const parts = this._decomposeGoal(task); + if (parts.length > 1) { + this.terminal.print('🧩 Decomposed into ' + parts.length + ' sub-tasks — running in parallel', 'info'); + + // Auto-scale: ensure enough idle agents for N sub-tasks + const idleCount = Object.values(this._agents).filter(a => a.status === 'idle').length; + const totalSlots = this._SPAWN_LIMIT; + const usedSlots = Object.keys(this._agents).length; + const availableSlots = totalSlots - usedSlots; + const needed = parts.length - idleCount; + if (needed > 0 && availableSlots > 0) { + const toSpawn = Math.min(needed, availableSlots); + this.terminal.print('📈 Auto-scaling: spawning ' + toSpawn + ' pre-warm agent(s)', 'info'); + for (let i = 0; i < toSpawn; i++) { + this.spawnAgent('worker', ['list_files', 'read_file', 'write_file', 'web_search']); + } + } + + const results = await Promise.allSettled(parts.map(sub => this.delegateTask(sub.goal))); + const successes = results.filter(r => r.status === 'fulfilled').length; + const failures = results.filter(r => r.status === 'rejected').length; + + // Result synthesis: collect outbox results from each sub-task's agent + const summaries = []; + for (let i = 0; i < parts.length; i++) { + const sub = parts[i]; + const r = results[i]; + if (r.status === 'fulfilled') { + // Find the agent that handled this sub-task + const agent = Object.values(this._agents).find(a => a.currentTask === sub.goal || (a.lastTask === sub.goal)); + if (agent) { + const outbox = this.terminal.fs.fsReadJSON(`/agents/${agent.id}/outbox.json`); + const snippet = outbox?.result ? outbox.result.substring(0, 200) : 'completed (no outbox)'; + summaries.push({ task: sub.goal, agent: agent.id, status: 'ok', result: snippet }); + } else { + summaries.push({ task: sub.goal, status: 'ok', result: 'completed (agent recycled)' }); + } + } else { + summaries.push({ task: sub.goal, status: 'failed', error: r.reason?.message || 'unknown error' }); + } + } + + // Store merged result to VFS for persistence + const merged = { + type: 'parallel_result', + parent_task: task, + timestamp: new Date().toISOString(), + total: parts.length, + successes, + failures, + summaries + }; + if (!this.terminal.fs.readDir('orchestrator')) this.terminal.fs.mkdir('orchestrator'); + this.terminal.fs.writeFile('orchestrator/parallel-results.json', JSON.stringify(merged, null, 2)); + + // Print synthesis to terminal + this.terminal.print('📊 Parallel Batch Results:', 'header'); + for (const s of summaries) { + const icon = s.status === 'ok' ? '✅' : '❌'; + const agentTag = s.agent ? ' [' + s.agent.substring(0, 20) + ']' : ''; + const snippet = s.result ? s.result.replace(/\n/g, ' ').substring(0, 60) : ''; + this.terminal.print(' ' + icon + ' ' + s.task.substring(0, 35) + agentTag + ' → ' + snippet, 'info'); + } + this.terminal.print('✅ Parallel batch: ' + successes + '/' + parts.length + ' succeeded' + (failures > 0 ? ', ' + failures + ' failed' : ''), 'header'); + return merged; + } + // Single sub-task — fall through to normal path + } + + // Find an idle agent and immediately stamp it busy (atomic claim) let agent = Object.values(this._agents).find(a => a.status === 'idle'); + if (agent) { + agent.status = 'busy'; + } // Auto-spawn with role-derived capabilities if (!agent) { if (Object.keys(this._agents).length >= this._SPAWN_LIMIT) { // Try finding any non-failed agent to reuse - agent = Object.values(this._agents).find(a => a.status !== 'failed' && a.status !== 'sleeping'); + const reuse = Object.values(this._agents).find(a => a.status !== 'failed' && a.status !== 'sleeping'); + if (reuse) { + reuse.status = 'busy'; + agent = reuse; + } } if (!agent) { const role = this._deriveRole(task); const caps = this._deriveCapabilities(task); - this.spawnAgent(role, caps); - agent = Object.values(this._agents).find(a => a.status === 'idle'); + agent = this.spawnAgent(role, caps); + if (!agent) return this.terminal.print('❌ No available agents — spawn limit reached', 'error'); + agent.status = 'busy'; // claim immediately after spawn } } if (!agent) return this.terminal.print('❌ No available agents', 'error'); @@ -548,8 +1129,12 @@ class OrchestratorModule { this.terminal.print(`✅ ${agent.id} completed task`, 'success'); + // Return result for callers that await delegateTask + const outboxResult = typeof result === 'string' ? result : (result?.content || 'completed'); + // Check restart policy — if always, set back to idle for next task if (agent.restartPolicy === 'always') { + agent.lastTask = agent.currentTask; agent.status = 'idle'; agent.currentTask = null; agent.lastHeartbeat = Date.now(); @@ -564,11 +1149,14 @@ class OrchestratorModule { // Return to idle pool (unless failed/restarting) if (agent.status === 'failed') { // handled by _handleAgentFailure + throw new Error(agent.lastError || 'agent failed'); } else if (agent.status !== 'busy') { - agent.status = 'idle'; + agent.lastTask = agent.currentTask; agent.currentTask = null; + agent.status = 'idle'; agent.lastHeartbeat = Date.now(); } + return outboxResult || 'completed'; } // ─── Monitoring ─── diff --git a/modules/core/module-system.js b/modules/core/module-system.js index c2115c1..77525c7 100644 --- a/modules/core/module-system.js +++ b/modules/core/module-system.js @@ -284,7 +284,7 @@ search: ['search','s','ddg','duck','fetch','web','extract'], tools: ['calc','cron-every','cron-list','cron-stop','cron-clear','wpm','mindmap','theme','voice','alias','unalias', 'which','env','date','cal','yes','seq','base64','printf','cut','uniq','hostname','whoami','basename','dirname','sleep','expand','tutorial','gzip','gunzip','encrypt','decrypt','files','session-save','session-load','session-list','session-delete','session-export','session-import'], - system: ['help','clear','cls','echo','mem','status','font-size','setfont','smart-prompt','logs','preview','factory-reset','reset-vfs','export','unset','script','set','test','logrotate','health-check','memory-map-export','commands','check','checkpoint','checkpoint-list','rollback','checkpoint-del'], + system: ['help','clear','cls','echo','mem','status','font-size','setfont','smart-prompt','logs','preview','factory-reset','reset-vfs','export','unset','script','sh','set','test','logrotate','health-check','memory-map-export','commands','check','checkpoint','checkpoint-list','rollback','checkpoint-del'], net: ['server','servers','server-check','server-remove','monitor'], mods: ['modules','listmods','loadmod','removemod','load-local','loadlocal','startup-modules'], ollama: ['ollama','ollamaconnect','ollamadisconnect','ollamamodels','ollamasetmodel','ollamasetproxy','models','model-info','model-pull','model-delete','model-run'], diff --git a/modules/core/terminal.js b/modules/core/terminal.js index 2825340..1fb44aa 100644 --- a/modules/core/terminal.js +++ b/modules/core/terminal.js @@ -1160,7 +1160,7 @@ } catch (e) {} } async _loadExtModules(filterKeys) { - const autoLoadKeys = filterKeys || ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox']; + const autoLoadKeys = filterKeys || ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating']; const cacheVer = localStorage.getItem('webx11_cache_ver') || '2026-05-27-14'; try { const res = await fetch('packages.json?v=' + cacheVer); @@ -1179,7 +1179,7 @@ } async reloadPersistedModules() { const registry = this.moduleManager.moduleRegistry; - const skipList = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox']; + const skipList = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating']; for (const mod of registry) { if (mod.enabled && mod.url) { // Skip ext modules — they load via _loadExtModules @@ -2055,7 +2055,7 @@ 'module-fetcher.js','module-skills.js','module-chatstudio.js' ]; const extFiles = [ - 'calc.js','voice.js','cron.js','theme.js','wpm.js','mindmap.js','webdav.js','skill-pack-core.js','editor.js','diff.js','alias.js','finder.js','filemgr.js','cryptogzip.js','tutorial.js','shell.js','busybox.js' + 'calc.js','voice.js','cron.js','theme.js','wpm.js','mindmap.js','webdav.js','skill-pack-core.js','editor.js','diff.js','alias.js','finder.js','filemgr.js','cryptogzip.js','tutorial.js','shell.js','busybox.js','floating.js' ]; const addSection = (title) => { diff --git a/modules/core/virtual-fs.js b/modules/core/virtual-fs.js index 95b031e..cb8020a 100644 --- a/modules/core/virtual-fs.js +++ b/modules/core/virtual-fs.js @@ -3,7 +3,6 @@ class VirtualFileSystem { this.root = { type: 'dir', content: {} }; this.currentPath = []; this._ready = this.loadFromStorage(); - this._conceptCache = null; } _u8ToBase64(u8) { let s = ''; @@ -66,6 +65,17 @@ class VirtualFileSystem { } return false; } + _emitChange(path) { + // Notify NeuralVFS via window event — zero coupling + // 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']; + if (sysPaths.some(p => normalized.startsWith(p))) return; + if (typeof window !== 'undefined' && window.__nvfsMarkDirty) { + window.__nvfsMarkDirty(path); + } + } saveToStorage() { if (this._storageSavePending) return; this._storageSavePending = true; @@ -83,38 +93,6 @@ class VirtualFileSystem { } catch (e) { console.error('FS save error:', e); } }, 2000); } - _emitChange(path) { - // Notify NeuralVFS via window event — zero coupling - // 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']; - if (sysPaths.some(p => normalized.startsWith(p))) return; - if (typeof window !== 'undefined' && window.__nvfsMarkDirty) { - window.__nvfsMarkDirty(path); - } - } - getCurrentPath() { - return '/' + this.currentPath.join('/'); - } - setCurrentPath(path) { - const segs = this._toAbsoluteSegments(path); - if (segs === null) return false; - this.currentPath = segs; - return true; - } - _loadConceptIndex() { - if (this._conceptCache) return this._conceptCache; - try { - const raw = this.readFile('/.semantic_index/map.json'); - if (raw) { - this._conceptCache = JSON.parse(raw); - return this._conceptCache; - } - } catch (e) {} - return null; - } - _clearConceptCache() { this._conceptCache = null; } getCurrentDir() { let dir = this.root; for (let part of this.currentPath) { @@ -163,18 +141,6 @@ class VirtualFileSystem { return dir; } ls(path) { - // concept:// listing - if (path && path.startsWith('concept://')) { - const concept = path.replace('concept://', '').replace(/\/.*$/, '').toLowerCase(); - const index = this._loadConceptIndex(); - if (!index) return []; - let matches = index[concept] || []; - if (matches.length === 0) { - const fuzzy = Object.keys(index).filter(k => k.includes(concept) || concept.includes(k)); - if (fuzzy.length > 0) matches = index[fuzzy[0]]; - } - return (matches || []).map(m => ({ name: m.path.split('/').pop(), type: 'file', path: m.path, size: 0, concept: true })); - } let dir; if (path) { const resolved = this._resolvePath(path); @@ -207,24 +173,6 @@ class VirtualFileSystem { return true; } readFile(path) { - // concept:// URI resolution - if (path && path.startsWith('concept://')) { - const concept = path.replace('concept://', '').replace(/\/.*$/, '').toLowerCase(); - try { - const index = this._loadConceptIndex(); - if (!index) return 'No semantic index found. Run nvfs-index first.'; - let matches = index[concept]; - if (!matches) { - const fuzzy = Object.keys(index).filter(k => k.includes(concept) || concept.includes(k)); - if (fuzzy.length > 0) matches = index[fuzzy[0]]; - } - if (!matches || matches.length === 0) return `No files found for concept "${concept}"`; - const first = matches[0]; - const content = this.readFile(first.path); - if (content) return `--- ${first.path} (concept://${concept}) ---\n${content}`; - return `Concept "${concept}" → ${matches.length} file(s):\n${matches.map(m => ` ${m.path} — ${m.summary || ''}`).join('\n')}`; - } catch (e) { return `concept:// error: ${e.message}`; } - } const resolved = this._resolvePath(path); if (!resolved || !resolved.name) return null; const node = resolved.dir.content[resolved.name]; @@ -291,6 +239,9 @@ class VirtualFileSystem { } return false; } + deleteDirRecursive(path) { + return this.trashDir(path); + } mkdirRecursive(path) { const segs = this._toAbsoluteSegments(path); if (!segs) return false; @@ -539,25 +490,15 @@ class VirtualFileSystem { checkpoint(name) { if (!name) name = new Date().toISOString(); const createdAt = new Date().toISOString(); - // Strip system dirs to avoid fractal bloat — snapshots/sessions/trash - // don't belong inside a checkpoint of user data - const root = JSON.parse(JSON.stringify(this.root)); - if (root && root.content) { - delete root.content['_snapshots']; - delete root.content['_sessions']; - delete root.content['.trash']; - } const data = { version: 1, createdAt, - root, + root: JSON.parse(JSON.stringify(this.root)), currentPath: [...this.currentPath] }; if (!this.readDir('/_snapshots')) { this.mkdirRecursive('/_snapshots'); } - // Strip _snapshots from the serialized data too (redundant safety) - data.root = root; this.fsWriteJSON('/_snapshots/checkpoint-' + name + '.json', data); this.saveToStorage(); // Auto-manage: keep max 10 checkpoints, delete oldest @@ -597,26 +538,6 @@ class VirtualFileSystem { } return false; } - // Reset VFS — wipes everything, removes localStorage keys - reset() { - this.root = { type: 'dir', content: {} }; - this.currentPath = []; - this._conceptCache = null; - try { - localStorage.removeItem('webx11_filesystem_v2'); - localStorage.removeItem('webx11_filesystem'); - } catch (e) {} - return true; - } - // Empty trash — permanently delete all trashed files - emptyTrash() { - if (this.root.content['.trash']) { - delete this.root.content['.trash']; - this.saveToStorage(); - return true; - } - return false; - } // Old-compat aliases for agent tools and SkillsModule read(path) { return this.readFile(path); } write(path, content) { return this.writeFile(path, content); } diff --git a/modules/ext/alias.js b/modules/ext/alias.js index 80663ac..06a5d47 100644 --- a/modules/ext/alias.js +++ b/modules/ext/alias.js @@ -3,8 +3,10 @@ class AliasModule { this.commands = {}; this.terminal = null; this.key = 'webx11_aliases'; - this.defaults = { 'll': 'ls', 'h': 'help', 'cls': 'clear' }; + this.defaults = { 'll': 'ls', 'h': 'help', 'cls': 'clear', 'sh': 'script' }; this.aliases = this.load(); + this._aliasDepth = 0; + this._aliasMaxDepth = 10; } getHelpInfo() { @@ -37,7 +39,10 @@ class AliasModule { if (name === 'alias' || name === 'unalias') return; const target = this.aliases[name]; this.commands[name] = (args) => { - this.terminal.executeCommand(target + (args.length ? ' ' + args.join(' ') : '')); + if (this._aliasDepth >= this._aliasMaxDepth) { this.terminal.print(`alias: max depth (${this._aliasMaxDepth}) reached`, 'error'); return; } + this._aliasDepth++; + try { this.terminal.executeCommand(target + (args.length ? ' ' + args.join(' ') : '')); } + finally { this._aliasDepth--; } }; }); } @@ -64,7 +69,8 @@ class AliasModule { // Show specific alias value const name = args[0]; if (this.aliases[name]) { - this.terminal.print(`${name}=${this.aliases[name]}`, 'info'); + const mark = this.defaults[name] ? ' (built-in)' : ''; + this.terminal.print(`${name}=${this.aliases[name]}${mark}`, 'info'); } else { this.terminal.print(`Alias '${name}' not found`, 'error'); } @@ -77,15 +83,19 @@ class AliasModule { if (name === 'alias' || name === 'unalias') { return this.terminal.print('Cannot alias reserved command name', 'error'); } + if (this.defaults[name]) { + return this.terminal.print(`Cannot alias built-in alias '${name}'`, 'warning'); + } this.aliases[name] = value; this.save(); // Register the new alias as a command immediately - if (!this.defaults[name]) { - const target = value; - this.commands[name] = (args) => { - this.terminal.executeCommand(target + (args.length ? ' ' + args.join(' ') : '')); + const target = value; + this.commands[name] = (args) => { + if (this._aliasDepth >= this._aliasMaxDepth) { this.terminal.print(`alias: max depth (${this._aliasMaxDepth}) reached`, 'error'); return; } + this._aliasDepth++; + try { this.terminal.executeCommand(target + (args.length ? ' ' + args.join(' ') : '')); } + finally { this._aliasDepth--; } }; - } this.terminal.print(`Alias set: ${name}=${value}`, 'success'); } handleUnalias(args) { diff --git a/modules/ext/floating.js b/modules/ext/floating.js new file mode 100644 index 0000000..b6f5a93 --- /dev/null +++ b/modules/ext/floating.js @@ -0,0 +1,331 @@ +// FloatingModule — Remixable floating UI elements (buttons, menus, panels) +// Persisted in localStorage. Created/modified/removed via terminal commands. +// Accessible to AI via normal run_command path. + +class FloatingModule { + constructor() { + this.commands = {}; + this.terminal = null; + this._elements = {}; + this._storageKey = 'webx11_floating_elements'; + } + + requirements() { return { met: true, missing: [] }; } + + getHelpInfo() { + return { + 'floating-add': 'floating-add btn|menu|panel <id> <label> [command|buttons...] — Add a floating UI element', + 'floating-remove': 'floating-remove <id> — Remove a floating element', + 'floating-list': 'floating-list — Show all floating elements', + 'floating-pos': 'floating-pos <id> <x|auto> <y|auto> — Reposition a floating element', + 'floating-clear': 'floating-clear — Remove all floating elements', + 'floating-toggle': 'floating-toggle <id> — Show/hide a floating element', + }; + } + + registerCommands() { + this.commands['floating-add'] = (args) => this._cmdAdd(args); + this.commands['floating-remove'] = (args) => this._cmdRemove(args); + this.commands['floating-list'] = () => this._cmdList(); + this.commands['floating-pos'] = (args) => this._cmdPos(args); + this.commands['floating-clear'] = () => this._cmdClear(); + this.commands['floating-toggle'] = (args) => this._cmdToggle(args); + } + + seed() { + this._restore(); + } + + // ── Persistence ── + + _save() { + const data = { version: 1, elements: {} }; + for (const [id, el] of Object.entries(this._elements)) { + data.elements[id] = { + type: el.type, + label: el.label, + command: el.command, + buttons: el.buttons, + content: el.content, + x: el.x, + y: el.y, + visible: el.visible !== false, + collapsed: el.collapsed !== false + }; + } + try { + localStorage.setItem(this._storageKey, JSON.stringify(data)); + } catch (e) { + console.warn('FloatingModule: save failed', e); + } + } + + _restore() { + try { + const raw = localStorage.getItem(this._storageKey); + if (!raw) return; + const data = JSON.parse(raw); + if (!data.elements) return; + for (const [id, cfg] of Object.entries(data.elements)) { + this._elements[id] = { + id, + type: cfg.type, + label: cfg.label, + command: cfg.command || '', + buttons: cfg.buttons || [], + content: cfg.content || '', + x: cfg.x != null ? cfg.x : null, + y: cfg.y != null ? cfg.y : null, + visible: cfg.visible !== false, + collapsed: cfg.collapsed !== false + }; + } + } catch (e) { + console.warn('FloatingModule: failed to restore', e); + } + this._renderAll(); + } + + // ── Rendering ── + + _getContainer() { + let container = document.getElementById('floating-container'); + if (!container) { + container = document.createElement('div'); + container.id = 'floating-container'; + const ref = document.getElementById('ai-confirm-area') || document.getElementById('input-area'); + const parent = document.body; + if (ref && ref.parentNode) { + parent.insertBefore(container, ref); + } else { + parent.appendChild(container); + } + } + return container; + } + + _renderAll() { + const container = this._getContainer(); + container.innerHTML = ''; + for (const [id, el] of Object.entries(this._elements)) { + if (!el.visible) continue; + const node = this._renderElement(el); + if (!node) continue; + node.dataset.floatingId = id; + if (el.x != null && el.y != null) { + node.style.position = 'fixed'; + node.style.left = el.x + 'px'; + node.style.top = el.y + 'px'; + node.style.bottom = 'auto'; + node.style.right = 'auto'; + } + container.appendChild(node); + } + } + + _renderElement(el) { + switch (el.type) { + case 'button': return this._renderButton(el); + case 'menu': return this._renderMenu(el); + case 'panel': return this._renderPanel(el); + default: return null; + } + } + + _renderButton(el) { + const btn = document.createElement('button'); + btn.className = 'floating-btn'; + btn.innerHTML = el.label; + btn.onclick = (e) => { + e.stopPropagation(); + if (el.command && this.terminal) { + this.terminal.executeCommand(el.command); + } + }; + return btn; + } + + _renderMenu(el) { + const wrapper = document.createElement('div'); + wrapper.className = 'floating-menu'; + + const header = document.createElement('div'); + header.className = 'floating-menu-header'; + header.innerHTML = `<span class="arrow ${el.collapsed ? 'collapsed' : ''}">▼</span> ${el.label}`; + + const body = document.createElement('div'); + body.className = 'floating-menu-body' + (el.collapsed ? '' : ' expanded'); + + (el.buttons || []).forEach(sub => { + const subBtn = document.createElement('button'); + subBtn.className = 'floating-menu-btn'; + subBtn.textContent = sub.label; + subBtn.onclick = (e) => { + e.stopPropagation(); + if (sub.command && this.terminal) { + this.terminal.executeCommand(sub.command); + } + }; + body.appendChild(subBtn); + }); + + header.onclick = () => { + el.collapsed = !el.collapsed; + const arrow = header.querySelector('.arrow'); + if (arrow) arrow.classList.toggle('collapsed', el.collapsed); + body.classList.toggle('expanded', !el.collapsed); + this._save(); + }; + + wrapper.appendChild(header); + wrapper.appendChild(body); + return wrapper; + } + + _renderPanel(el) { + const panel = document.createElement('div'); + panel.className = 'floating-panel'; + panel.innerHTML = ` + <div class="floating-panel-title">${this._escapeHtml(el.label)}</div> + <div class="floating-panel-content">${this._escapeHtml(el.content)}</div> + `; + return panel; + } + + _escapeHtml(str) { + if (!str) return ''; + return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + .replace(/"/g, '"').replace(/'/g, '''); + } + + // ── Commands ── + + _cmdAdd(args) { + if (args.length < 3) { + this.terminal.print('Usage: floating-add btn|menu|panel <id> <label> [command|btn:Label|cmd...]', 'warning'); + return; + } + const type = args[0]; + const id = args[1]; + const label = args[2]; + + if (this._elements[id]) { + this.terminal.print(`Element '${id}' already exists`, 'warning'); + return; + } + + const el = { id, type, label, x: null, y: null, visible: true }; + + switch (type) { + case 'btn': + case 'button': + if (args.length < 4) { + this.terminal.print('Usage: floating-add btn <id> <label> <command>', 'warning'); + return; + } + el.type = 'button'; + el.command = args.slice(3).join(' '); + break; + case 'menu': + el.type = 'menu'; + el.collapsed = true; + el.buttons = []; + for (let i = 3; i < args.length; i++) { + const m = args[i].match(/^btn:(.+?)\|(.+)$/); + if (m) { + el.buttons.push({ label: m[1], command: m[2] }); + } + } + if (el.buttons.length === 0) { + this.terminal.print('No valid sub-buttons. Use btn:Label|command format', 'warning'); + return; + } + break; + case 'panel': + el.type = 'panel'; + el.content = args.slice(3).join(' ') || ''; + break; + default: + this.terminal.print(`Unknown type '${type}'. Use: btn, menu, panel`, 'warning'); + return; + } + + this._elements[id] = el; + this._save(); + this._renderAll(); + this.terminal.print(`Floating '${type}' '${label}' created (id: ${id})`, 'success'); + } + + _cmdRemove(args) { + if (!args[0]) { + this.terminal.print('Usage: floating-remove <id>', 'warning'); + return; + } + if (!this._elements[args[0]]) { + this.terminal.print(`Element '${args[0]}' not found`, 'warning'); + return; + } + delete this._elements[args[0]]; + this._save(); + this._renderAll(); + this.terminal.print(`Element '${args[0]}' removed`, 'success'); + } + + _cmdList() { + const ids = Object.keys(this._elements); + if (ids.length === 0) { + this.terminal.print('No floating elements', 'info'); + return; + } + this.terminal.print(`Floating elements (${ids.length}):`, 'info'); + for (const id of ids) { + const el = this._elements[id]; + const pos = (el.x != null) ? `${el.x},${el.y}` : 'auto'; + const vis = el.visible ? 'visible' : 'hidden'; + this.terminal.print(` ${id}: ${el.type} "${el.label}" @${pos} [${vis}]`, 'text-muted'); + if (el.type === 'menu' && el.buttons) { + el.buttons.forEach(b => this.terminal.print(` → btn:${b.label}|${b.command}`, 'text-muted')); + } + } + } + + _cmdPos(args) { + if (args.length < 3) { + this.terminal.print('Usage: floating-pos <id> <x|auto> <y|auto>', 'warning'); + return; + } + const id = args[0]; + if (!this._elements[id]) { + this.terminal.print(`Element '${id}' not found`, 'warning'); + return; + } + this._elements[id].x = args[1] === 'auto' ? null : parseInt(args[1]); + this._elements[id].y = args[2] === 'auto' ? null : parseInt(args[2]); + this._save(); + this._renderAll(); + this.terminal.print(`Element '${id}' repositioned`, 'success'); + } + + _cmdToggle(args) { + if (!args[0]) { + this.terminal.print('Usage: floating-toggle <id>', 'warning'); + return; + } + const id = args[0]; + if (!this._elements[id]) { + this.terminal.print(`Element '${id}' not found`, 'warning'); + return; + } + this._elements[id].visible = !this._elements[id].visible; + this._save(); + this._renderAll(); + this.terminal.print(`Element '${id}' now ${this._elements[id].visible ? 'visible' : 'hidden'}`, 'success'); + } + + _cmdClear() { + this._elements = {}; + this._save(); + this._renderAll(); + this.terminal.print('All floating elements cleared', 'success'); + } +}
\ No newline at end of file diff --git a/modules/ext/shell.js b/modules/ext/shell.js index e87c37c..55d66de 100644 --- a/modules/ext/shell.js +++ b/modules/ext/shell.js @@ -23,6 +23,7 @@ class ShellModule { 'unset': 'unset <var> — Remove a shell variable', 'script': 'script <path.sh> — Run a shell script from VFS', '$': '$ <command> — Execute with shell parsing (default: auto-detected)', + 'sh': 'sh <path.sh> — Run a shell script from VFS (alias for script)', 'set': 'set — List all shell variables', }; } @@ -31,6 +32,7 @@ class ShellModule { this.commands['export'] = (args) => this._cmdExport(args); this.commands['unset'] = (args) => this._cmdUnset(args); this.commands['script'] = (args) => this._cmdScript(args); + this.commands['sh'] = (args) => this._cmdScript(args); this.commands['set'] = () => this._cmdSet(); } diff --git a/modules/ext/skill-pack-core.js b/modules/ext/skill-pack-core.js index 6f7ad4c..abbc7ab 100644 --- a/modules/ext/skill-pack-core.js +++ b/modules/ext/skill-pack-core.js @@ -471,7 +471,41 @@ class SkillPackCoreModule { ## When to use - User wants to brainstorm, organize ideas, plan features - User says "map out", "diagram", "brainstorm", "organize thoughts" -- Visualizing project structure or task decomposition`) +- Visualizing project structure or task decomposition`), + + sk('floating-ui', 'How to create, manage, and remove floating UI elements — buttons, collapsible menus, and status panels that persist across page reloads', ['floating', 'ui', 'buttons', 'menu', 'gui'], 'ui', +`## Commands + +1. **Create a floating button:** + - floating-add btn <id> <label> <command> + - Example: floating-add btn files "📁 Files" ls + - Button appears at bottom-right, persists across reloads + +2. **Create a collapsible menu:** + - floating-add menu <id> <label> btn:SubLabel|command btn:Sub2|cmd2 ... + - Example: floating-add menu tools "🔧 Tools" btn:Help|help btn:Clear|clear + - Click header to expand/collapse. State persists. + +3. **Create a text panel:** + - floating-add panel <id> <title> <content> + - Example: floating-add panel status "📊 Status" "CPU: 2%" + +4. **Manage elements:** + - floating-list — show all elements with IDs and positions + - floating-remove <id> — delete by ID + - floating-toggle <id> — show/hide without deleting + - floating-pos <id> x|auto y|auto — reposition or reset to auto + - floating-clear — remove everything + +5. **Persistence:** + - All elements auto-save to localStorage (webx11_floating_elements) + - Survive page reload, browser restart + - Run floating-clear to start fresh + +## When to use +- User wants quick-access buttons instead of typing commands +- User says "add a button for X", "make a shortcut", "floating menu" +- Creating a dashboard with status panels and action buttons`) ]; } registerCommands() { diff --git a/modules/test/test-suite.js b/modules/test/test-suite.js index 760259d..925a83e 100644 --- a/modules/test/test-suite.js +++ b/modules/test/test-suite.js @@ -242,6 +242,60 @@ class TestSuiteModule { destructive: false }, + // ── Aliases ── + 'alias-list': { + desc: 'alias shows built-in defaults', + cmd: 'alias', + expect: (out) => out.includes('ll=ls') && out.includes('h=help') && out.includes('cls=clear') && out.includes('sh=script'), + destructive: false + }, + 'alias-show-specific': { + desc: 'alias <name> shows a specific alias value', + cmd: 'alias ll', + expect: (out) => out.includes('ll=ls') && out.includes('built-in'), + destructive: false + }, + 'alias-missing-unknown': { + desc: 'alias <unknown> shows not found', + cmd: 'alias non_existent_alias_xyz', + expect: (out) => out.includes('not found'), + destructive: false + }, + 'unalias-builtin-refused': { + desc: 'unalias refuses to remove built-in alias', + cmd: 'unalias ll', + expect: (out) => out.includes('Cannot unalias built-in'), + destructive: false + }, + 'alias-create-and-run': { + desc: 'create custom alias and execute it', + cmd_parts: [ + { cmd: 'alias greet=echo hello from alias' }, + { cmd: 'greet', expect: (out) => out.includes('hello from alias') }, + { cmd: 'unalias greet' }, + ], + destructive: true + }, + 'alias-overwrite': { + desc: 'create, then overwrite, execute updated alias', + cmd_parts: [ + { cmd: 'alias greet2=echo first' }, + { cmd: 'alias greet2=echo second' }, + { cmd: 'greet2', expect: (out) => out.includes('second') }, + { cmd: 'unalias greet2' }, + ], + destructive: true + }, + 'unalias-custom': { + desc: 'unalias removes a custom alias', + cmd_parts: [ + { cmd: 'alias tempcmd=echo temp' }, + { cmd: 'unalias tempcmd' }, + { cmd: 'alias tempcmd', expect: (out) => out.includes('not found') }, + ], + destructive: true + }, + // ── Modules ── 'commands-help': { desc: 'help lists multiple command categories', diff --git a/packages.json b/packages.json index 72876e8..c1fa543 100644 --- a/packages.json +++ b/packages.json @@ -200,6 +200,21 @@ ], "category": "system" }, + "floating": { + "name": "Floating UI", + "version": "1.0.0", + "desc": "Remixable floating UI elements — buttons, collapsible menus, panels. Persisted in localStorage.", + "url": "modules/ext/floating.js", + "commands": [ + "floating-add", + "floating-remove", + "floating-list", + "floating-pos", + "floating-clear", + "floating-toggle" + ], + "category": "ui" + }, "tutorial": { "name": "Tutorial", "version": "1.0.0", diff --git a/review-newos4.html b/review-newos4.html new file mode 100644 index 0000000..f59b0fc --- /dev/null +++ b/review-newos4.html @@ -0,0 +1,408 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="version" content="4.2.0-newos4"> + <title>ChatStudio newOS4 - Review</title> + <style> + :root { + --bg-dark: #000000; + --bg-card: #1a1a2e; + --text: #e5e7eb; + --text-muted: #9ca3af; + --accent: #6366f1; + --accent-hover: #818cf8; + --border: #2d2d44; + --success: #22c55e; + --warning: #f59e0b; + } + * { box-sizing: border-box; margin: 0; padding: 0; } + body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background: var(--bg-dark); + color: var(--text); + line-height: 1.6; + padding: 20px; + max-width: 800px; + margin: 0 auto; + } + .back-link { + display: inline-block; + margin-bottom: 1.5rem; + color: var(--accent); + text-decoration: none; + font-size: 0.95rem; + } + .back-link:hover { text-decoration: underline; } + .version-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 1.5rem; + flex-wrap: wrap; + } + .version-icon { font-size: 2.5rem; } + .version-title { font-size: 1.8rem; color: var(--accent); } + .version-badge { + color: #fff; + padding: 4px 12px; + border-radius: 12px; + font-size: 0.75rem; + font-weight: bold; + } + .section { margin-bottom: 2rem; } + .section h2 { + font-size: 1.2rem; + color: var(--accent-hover); + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border); + } + .features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1rem; + } + .feature-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1rem; + } + .feature-card h3 { + font-size: 1rem; + margin-bottom: 0.5rem; + display: flex; + align-items: center; + gap: 8px; + } + .feature-card p { + font-size: 0.85rem; + color: var(--text-muted); + margin: 0; + } + .pros-cons { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1rem; + } + .pros, .cons { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1rem; + } + .pros h3 { color: var(--success); margin-bottom: 0.75rem; } + .cons h3 { color: var(--warning); margin-bottom: 0.75rem; } + .pros ul, .cons ul { margin: 0; padding-left: 1.25rem; } + .pros li, .cons li { margin-bottom: 0.35rem; color: var(--text-muted); font-size: 0.9rem; } + .launch-btn { + display: inline-flex; + align-items: center; + gap: 8px; + background: var(--accent); + color: #fff; + padding: 14px 28px; + border-radius: 10px; + text-decoration: none; + font-weight: 600; + font-size: 1rem; + transition: all 0.2s; + } + .launch-btn:hover { + background: var(--accent-hover); + transform: translateY(-2px); + box-shadow: 0 4px 20px rgba(99, 102, 241, 0.4); + } + .meta-info { + display: flex; + gap: 1rem; + flex-wrap: wrap; + margin-top: 1rem; + font-size: 0.85rem; + color: var(--text-muted); + } + .meta-info span { + background: var(--bg-card); + padding: 4px 10px; + border-radius: 6px; + } + .rating { + font-size: 1.5rem; + margin-top: 1rem; + } + .arch-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 0.75rem; + margin-top: 0.75rem; + } + .arch-item { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 8px; + padding: 0.75rem; + } + .arch-item h4 { + font-size: 0.9rem; + color: var(--accent-hover); + margin-bottom: 0.4rem; + } + .arch-item p { + font-size: 0.8rem; + color: var(--text-muted); + margin: 0; + } + @media (min-width: 600px) { + body { padding: 40px 20px; } + .version-title { font-size: 2rem; } + } + </style> +</head> +<body> + <a href="/spaces/chatstudio/latest.html" class="back-link">← Back to Versions</a> + + <div class="version-header"> + <span class="version-icon">🧩</span> + <span class="version-title">newOS4 v4.5.22 — Terminal OS</span> + <span class="version-badge" style="background:#a855f7;">Most Advanced</span> + </div> + + <div class="meta-info"> + <span>v4.2.0</span> + <span>1,122 lines (main.html skeleton)</span> + <span>49 module files (32 core + 17 ext)</span> + <span>13 AI-managed skills</span> + <span>~1MB total code</span> + </div> + + <div class="section"> + <h2>Architecture</h2> + <p style="color: var(--text-muted); margin-bottom: 1rem;"> + A full in-browser terminal OS with class-based modular system. The main.html (56KB) is a lean skeleton — all functionality lives in 49 module files. + </p> + <div class="arch-grid"> + <div class="arch-item"> + <h4>🧠 Core Module Classes</h4> + <p>27 module-*.js files: System, FS, AI Assistant (agentic loop, 19 tools), Ollama, SearXNG, Config, Smart Context, Skills, Chat Overlay, and more.</p> + </div> + <div class="arch-item"> + <h4>🔧 Core Infrastructure</h4> + <p>5 files: VirtualFileSystem, ModuleManager, OllamaManager, SecretVault, WebX11Terminal, Sandbox Worker.</p> + </div> + <div class="arch-item"> + <h4>🧩 External Modules</h4> + <p>17 self-registering scripts: alias, busybox, calc, cron, cryptogzip, diff, editor, filemgr, finder, mindmap, shell, skill-pack-core, theme, tutorial, voice, webdav, wpm.</p> + </div> + <div class="arch-item"> + <h4>📚 Skills System</h4> + <p>13 skills in VFS /skills/: agent-orchestration, backup-restore, debug-panel, memory-profile, mindmap, module-management, navigation, security, system-maintenance, webdav, web-search, and more.</p> + </div> + <div class="arch-item"> + <h4>🎨 Theme Presets</h4> + <p>9 themes (dark, light, amber, cyber, ocean, forest, monochrome, sunset, midnight) + custom theme creation, export/import JSON.</p> + </div> + <div class="arch-item"> + <h4>💬 Chat Overlay</h4> + <p>4 files (chat-ui.css, chat-ui.html, chat-commands.js, chat-modules.js) for the ChatStudio Lite overlay inside terminal.</p> + </div> + </div> + </div> + + <div class="section"> + <h2>Features</h2> + <div class="features-grid"> + <div class="feature-card"> + <h3>🤖 Agentic AI</h3> + <p>5-turn tool-calling loop with 19 tool definitions. Direct VFS reads for reflection. Falls back to text-only for non-tool models. Date/time injected into system prompt.</p> + </div> + <div class="feature-card"> + <h3>🔧 19 Unix Commands</h3> + <p>ls (flags), cd, pwd, cat, echo (>> redirect), put, rm/rmdir/mkdir/mv/cp/touch/tree/find/head/tail/sort/wc/du/upload/download/grep.</p> + </div> + <div class="feature-card"> + <h3>🧠 Skills System</h3> + <p>13 AI-managed skills in VFS. Agent can read, suggest, and apply skills automatically. Self-improving loop.</p> + </div> + <div class="feature-card"> + <h3>📁 Virtual File System</h3> + <p>Full VFS with session save/load, auto-save (60s interval with dirty flag), crash survival, dual localStorage+VFS persist.</p> + </div> + <div class="feature-card"> + <h3>🎤 Voice Command</h3> + <p>Clean voice input (command-only, text-to-command, no FAB spam). Togglable mic button. SpeechRecognition API.</p> + </div> + <div class="feature-card"> + <h3>🔍 Web Search & Fetch</h3> + <p>SearXNG search + DuckDuckGo fallback. URL fetch via API proxy. /images and /videos search support.</p> + </div> + <div class="feature-card"> + <h3>🎨 9 Theme Presets</h3> + <p>Dark, light, amber, cyber, ocean, forest, monochrome, sunset, midnight. Custom themes with export/import.</p> + </div> + <div class="feature-card"> + <h3>🔄 Autocomplete & Sessions</h3> + <p>3-tier autocomplete (commands, files, sessions). Session save/load/list/delete/export/import. Auto-save on 60s cycle.</p> + </div> + <div class="feature-card"> + <h3>🔊 Sound Notifications</h3> + <p>Toggleable AI response "ding" (880Hz sine, 0.3s). Quick-actions bar for sound, mic, and settings.</p> + </div> + <div class="feature-card"> + <h3>⚡ Tool Cache & Optimizations</h3> + <p>Cached tool definitions, parallel safe-tool execution, skip skill loading for trivial queries, batch VFS reflection.</p> + </div> + <div class="feature-card"> + <h3>✅ Inline Confirm Bar</h3> + <p>Destructive action confirmation inline (no popups). 120s auto-deny timer with visual escalation.</p> + </div> + <div class="feature-card"> + <h3>💬 Chat Overlay</h3> + <p>Full ChatStudio Lite embedded as toggleable chat command. Settings modal, module CRUD, slash commands inside overlay.</p> + </div> + </div> + </div> + + <div class="section"> + <h2>Pros & Cons</h2> + <div class="pros-cons"> + <div class="pros"> + <h3>Pros</h3> + <ul> + <li>Most feature-rich ChatStudio variant — agentic AI with tool calling</li> + <li>Virtual File System with persistent sessions and auto-save</li> + <li>Self-improving skills system (AI can create/update skills)</li> + <li>19 Unix commands + grep, 19 AI tool definitions</li> + <li>9 theme presets + custom theme creation</li> + <li>Voice command input (SpeechRecognition)</li> + <li>Inline confirm bar with timer (no intrusive popups)</li> + <li>Tool definition caching, parallel tool execution, trivial query skipping</li> + <li>ChatStudio Lite overlay inside terminal</li> + <li>Sound notifications with toggle</li> + <li>Autocomplete (3 tiers: commands, files, sessions)</li> + <li>Date/time injected into AI system prompt</li> + </ul> + </div> + <div class="cons"> + <h3>Cons</h3> + <ul> + <li>Ollama dependency — requires Ollama backend for AI features</li> + <li>Modular architecture = many files to maintain (49 module files)</li> + <li>No streaming AI responses (request/response cycle only)</li> + <li>No PDF support</li> + <li>Single-language (English only)</li> + <li>No Plan Mode (Stable variant has this)</li> + <li>Heavy total size (~1MB across all modules)</li> + <li>Limited mobile optimization (desktop-first design)</li> + </ul> + </div> + </div> + </div> + + <div class="section"> + <h2>Verdict</h2> + <p style="color: var(--text-muted); margin-bottom: 0.5rem;"> + <strong>newOS4 v4.2</strong> is the most advanced ChatStudio variant — an in-browser terminal OS with agentic AI, a virtual file system, and self-improving skills. It's designed for power users, developers, and AI enthusiasts who want maximum control and capability. The modular architecture (49 files) makes it maintainable but heavy. + </p> + <div class="rating">🧩🧩🧩🧩 — <strong>4/5</strong> — Most advanced, trimmed to essentials</div> + </div> + + <div class="section"> + <h2>Comparison</h2> + <div style="overflow-x:auto;"> + <table style="width:100%;border-collapse:collapse;font-size:0.85rem;color:var(--text-muted);"> + <tr style="border-bottom:1px solid var(--border);"> + <th style="text-align:left;padding:6px 8px;color:var(--text);">Feature</th> + <th style="text-align:center;padding:6px 8px;color:var(--text);">Main</th> + <th style="text-align:center;padding:6px 8px;color:var(--text);">Lite</th> + <th style="text-align:center;padding:6px 8px;color:var(--text);">Stable</th> + <th style="text-align:center;padding:6px 8px;color:var(--text);">Plus</th> + <th style="text-align:center;padding:6px 8px;color:var(--accent);">newOS4</th> + </tr> + <tr style="border-bottom:1px solid var(--border);"> + <td style="padding:6px 8px;">AI Chat</td> + <td style="text-align:center;padding:6px 8px;">❌</td><td style="text-align:center;padding:6px 8px;">✅ streaming</td> + <td style="text-align:center;padding:6px 8px;">✅ no stream</td><td style="text-align:center;padding:6px 8px;">✅</td> + <td style="text-align:center;padding:6px 8px;">✅ Agentic</td> + </tr> + <tr style="border-bottom:1px solid var(--border);"> + <td style="padding:6px 8px;">Skills System</td> + <td style="text-align:center;padding:6px 8px;">❌</td><td style="text-align:center;padding:6px 8px;">❌</td> + <td style="text-align:center;padding:6px 8px;">❌</td><td style="text-align:center;padding:6px 8px;">❌</td> + <td style="text-align:center;padding:6px 8px;">✅ 13 skills</td> + </tr> + <tr style="border-bottom:1px solid var(--border);"> + <td style="padding:6px 8px;">AI Tool Calling</td> + <td style="text-align:center;padding:6px 8px;">❌</td><td style="text-align:center;padding:6px 8px;">❌</td> + <td style="text-align:center;padding:6px 8px;">❌</td><td style="text-align:center;padding:6px 8px;">❌</td> + <td style="text-align:center;padding:6px 8px;">✅ 19 tools</td> + </tr> + <tr style="border-bottom:1px solid var(--border);"> + <td style="padding:6px 8px;">AI Memory</td> + <td style="text-align:center;padding:6px 8px;">❌</td><td style="text-align:center;padding:6px 8px;">❌</td> + <td style="text-align:center;padding:6px 8px;">❌</td><td style="text-align:center;padding:6px 8px;">❌</td> + <td style="text-align:center;padding:6px 8px;">✅</td> + </tr> + <tr style="border-bottom:1px solid var(--border);"> + <td style="padding:6px 8px;">Self-improving</td> + <td style="text-align:center;padding:6px 8px;">❌</td><td style="text-align:center;padding:6px 8px;">❌</td> + <td style="text-align:center;padding:6px 8px;">❌</td><td style="text-align:center;padding:6px 8px;">❌</td> + <td style="text-align:center;padding:6px 8px;">✅ Skills CRUD</td> + </tr> + <tr style="border-bottom:1px solid var(--border);"> + <td style="padding:6px 8px;">File Manager</td> + <td style="text-align:center;padding:6px 8px;">❌</td><td style="text-align:center;padding:6px 8px;">❌</td> + <td style="text-align:center;padding:6px 8px;">❌</td><td style="text-align:center;padding:6px 8px;">❌</td> + <td style="text-align:center;padding:6px 8px;">✅ Virtual FS</td> + </tr> + <tr style="border-bottom:1px solid var(--border);"> + <td style="padding:6px 8px;">Theme Presets</td> + <td style="text-align:center;padding:6px 8px;">✅</td><td style="text-align:center;padding:6px 8px;">✅</td> + <td style="text-align:center;padding:6px 8px;">✅</td><td style="text-align:center;padding:6px 8px;">✅ Custom</td> + <td style="text-align:center;padding:6px 8px;">✅ 9 presets</td> + </tr> + <tr style="border-bottom:1px solid var(--border);"> + <td style="padding:6px 8px;">Voice Input</td> + <td style="text-align:center;padding:6px 8px;">❌</td><td style="text-align:center;padding:6px 8px;">❌</td> + <td style="text-align:center;padding:6px 8px;">❌</td><td style="text-align:center;padding:6px 8px;">❌</td> + <td style="text-align:center;padding:6px 8px;">✅</td> + </tr> + <tr style="border-bottom:1px solid var(--border);"> + <td style="padding:6px 8px;">i18n</td> + <td style="text-align:center;padding:6px 8px;">✅ 7 langs</td><td style="text-align:center;padding:6px 8px;">❌</td> + <td style="text-align:center;padding:6px 8px;">❌</td><td style="text-align:center;padding:6px 8px;">❌</td> + <td style="text-align:center;padding:6px 8px;">❌</td> + </tr> + <tr style="border-bottom:1px solid var(--border);"> + <td style="padding:6px 8px;">Plan Mode</td> + <td style="text-align:center;padding:6px 8px;">❌</td><td style="text-align:center;padding:6px 8px;">❌</td> + <td style="text-align:center;padding:6px 8px;">✅</td><td style="text-align:center;padding:6px 8px;">❌</td> + <td style="text-align:center;padding:6px 8px;">❌</td> + </tr> + <tr style="border-bottom:1px solid var(--border);"> + <td style="padding:6px 8px;">Session Export</td> + <td style="text-align:center;padding:6px 8px;">❌</td><td style="text-align:center;padding:6px 8px;">❌</td> + <td style="text-align:center;padding:6px 8px;">❌</td><td style="text-align:center;padding:6px 8px;">✅</td> + <td style="text-align:center;padding:6px 8px;">✅ Session save/load</td> + </tr> + <tr> + <td style="padding:6px 8px;">File Upload</td> + <td style="text-align:center;padding:6px 8px;">❌</td><td style="text-align:center;padding:6px 8px;">❌</td> + <td style="text-align:center;padding:6px 8px;">❌</td><td style="text-align:center;padding:6px 8px;">✅</td> + <td style="text-align:center;padding:6px 8px;">✅ file attach</td> + </tr> + </table> + </div> + </div> + + <div class="section"> + <h2>Best For</h2> + <p style="color: var(--text-muted);">Power users, developers, and AI enthusiasts who want an in-browser terminal OS with agentic AI capabilities. Ideal for those who need a virtual file system, self-improving skills, tool-calling AI, voice input, and don't mind a heavier codebase or Ollama dependency.</p> + </div> + + <div class="section"> + <a href="/spaces/newos4/main.html" class="launch-btn">Launch newOS4</a> + </div> +</body> +</html>
\ No newline at end of file |