aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-06-08 18:01:06 +0200
committerAgando <roduccomm@gmail.com>2026-06-08 18:01:06 +0200
commitb562e1491513163921593985f3542d4ee8ae0e75 (patch)
treef768a2378d3c09b3685a7bf42a9fc4dec71ef31c
parentb9485bae5c2e62fa9c1b1c56d0f75be67cb47c37 (diff)
auto: 2026-06-08_18:00 changes
-rw-r--r--cheatsheet.html15
-rw-r--r--help.html55
-rw-r--r--main.html133
-rw-r--r--modules/core/module-aiassist.js10
-rw-r--r--modules/core/module-loader.js4
-rw-r--r--modules/core/module-orchestrator.js660
-rw-r--r--modules/core/module-system.js2
-rw-r--r--modules/core/terminal.js6
-rw-r--r--modules/core/virtual-fs.js109
-rw-r--r--modules/ext/alias.js26
-rw-r--r--modules/ext/floating.js331
-rw-r--r--modules/ext/shell.js2
-rw-r--r--modules/ext/skill-pack-core.js36
-rw-r--r--modules/test/test-suite.js54
-rw-r--r--packages.json15
-rw-r--r--review-newos4.html408
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 &lt;file&gt;</span><span class="desc">Create empty file</span></div>
<div class="item"><span class="cmd">mkdir &lt;dir&gt;</span><span class="desc">Create directory</span></div>
<div class="item"><span class="cmd">rm &lt;file&gt;</span><span class="desc">Remove file</span></div>
+<div class="item"><span class="cmd">rmdir &lt;dir&gt;</span><span class="desc">Remove directory</span></div>
<div class="item"><span class="cmd">cp &lt;src&gt; &lt;dst&gt;</span><span class="desc">Copy file</span></div>
<div class="item"><span class="cmd">mv &lt;old&gt; &lt;new&gt;</span><span class="desc">Move/rename</span></div>
<div class="item"><span class="cmd">edit &lt;file&gt;</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 &gt; 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 &lt;id&gt; &lt;lbl&gt; &lt;cmd&gt;</span><span class="desc">Create a floating button</span></div>
+<div class="item"><span class="cmd">floating-add menu &lt;id&gt; &lt;lbl&gt; btn:X|cmd ...</span><span class="desc">Create a floating menu</span></div>
+<div class="item"><span class="cmd">floating-add panel &lt;id&gt; &lt;title&gt; &lt;text&gt;</span><span class="desc">Create a floating panel</span></div>
+<div class="item"><span class="cmd">floating-remove &lt;id&gt;</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 &lt;id&gt;</span><span class="desc">Show/hide a floating element</span></div>
+<div class="item"><span class="cmd">floating-pos &lt;id&gt; &lt;x&gt; &lt;y&gt;</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>
diff --git a/help.html b/help.html
index 5a827a0..76109e2 100644
--- a/help.html
+++ b/help.html
@@ -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' &gt; greet.sh
echo 'echo "Hello, $NAME!"' &gt;&gt; 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 &lt;var&gt;</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 &lt;path.sh&gt;</code></td><td>Run a VFS script file</td></tr>
+ <tr><td><code>sh &lt;path.sh&gt;</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 &lt;name&gt;</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 &lt;id&gt; &lt;label&gt; &lt;command&gt;</code></td><td>Spawn a floating button that runs a terminal command</td></tr>
+ <tr><td><code>floating-add menu &lt;id&gt; &lt;label&gt; btn:Sub|cmd ...</code></td><td>Spawn a collapsible floating menu with sub-buttons</td></tr>
+ <tr><td><code>floating-add panel &lt;id&gt; &lt;title&gt; &lt;content&gt;</code></td><td>Spawn a floating text panel</td></tr>
+ <tr><td><code>floating-remove &lt;id&gt;</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 &lt;id&gt; &lt;x|auto&gt; &lt;y|auto&gt;</code></td><td>Reposition a floating element</td></tr>
+ <tr><td><code>floating-toggle &lt;id&gt;</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 &lt;id&gt;</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 &lt;task&gt;</code></td><td>Delegate multi-step task to agent orchestrator</td></tr>
+ <tr><td><code>orchestrate-parallel &lt;task&gt;</code></td><td>Decompose and run sub-tasks in parallel (auto-splits on "and"/"then")</td></tr>
+ <tr><td><code>orchestrate-decompose &lt;task&gt;</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 &lt;role&gt; [caps...]</code></td><td>Spawn a new worker agent</td></tr>
<tr><td><code>orchestrator-sleep &lt;id&gt;</code></td><td>Suspend agent to VFS (survives reload)</td></tr>
<tr><td><code>orchestrator-wake &lt;id&gt;</code></td><td>Restore agent from VFS sleep state</td></tr>
<tr><td><code>orchestrator-kill &lt;id&gt;</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 &lt;key&gt; &lt;value&gt;</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 (&gt;60s silent)</td></tr>
+ <tr><td><code>orchestrator-config &lt;key&gt; &lt;value&gt;</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 &lt;id&gt; &lt;policy&gt;</code></td><td>Set per-agent restart policy: on-failure, always, never</td></tr>
+ <tr><td><code>consensus &lt;question&gt;</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 &lt;test|deploy|verify|history|config&gt;</code></td><td>Autonomous CI/CD: test agents/modules, deploy (test→auto-fix→verify), history, config</td></tr>
<tr><td><code>lease-create &lt;agent-id&gt; [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 &lt;id&gt;</code></td><td>Revoke an active lease</td></tr>
diff --git a/main.html b/main.html
index cf1b33a..5c7a69c 100644
--- a/main.html
+++ b/main.html
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
+ .replace(/"/g, '&quot;').replace(/'/g, '&#039;');
+ }
+
+ // ── 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">&larr; Back to Versions</a>
+
+ <div class="version-header">
+ <span class="version-icon">🧩</span>
+ <span class="version-title">newOS4 v4.5.22 &mdash; 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 (>&gt; 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 &amp; 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