aboutsummaryrefslogtreecommitdiff
path: root/modules/ext
diff options
context:
space:
mode:
Diffstat (limited to 'modules/ext')
-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
4 files changed, 386 insertions, 9 deletions
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() {