diff options
| author | Agando <roduccomm@gmail.com> | 2026-06-08 18:01:06 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-06-08 18:01:06 +0200 |
| commit | b562e1491513163921593985f3542d4ee8ae0e75 (patch) | |
| tree | f768a2378d3c09b3685a7bf42a9fc4dec71ef31c /modules/ext | |
| parent | b9485bae5c2e62fa9c1b1c56d0f75be67cb47c37 (diff) | |
auto: 2026-06-08_18:00 changes
Diffstat (limited to 'modules/ext')
| -rw-r--r-- | modules/ext/alias.js | 26 | ||||
| -rw-r--r-- | modules/ext/floating.js | 331 | ||||
| -rw-r--r-- | modules/ext/shell.js | 2 | ||||
| -rw-r--r-- | modules/ext/skill-pack-core.js | 36 |
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, '&').replace(/</g, '<').replace(/>/g, '>') + .replace(/"/g, '"').replace(/'/g, '''); + } + + // ── Commands ── + + _cmdAdd(args) { + if (args.length < 3) { + this.terminal.print('Usage: floating-add btn|menu|panel <id> <label> [command|btn:Label|cmd...]', 'warning'); + return; + } + const type = args[0]; + const id = args[1]; + const label = args[2]; + + if (this._elements[id]) { + this.terminal.print(`Element '${id}' already exists`, 'warning'); + return; + } + + const el = { id, type, label, x: null, y: null, visible: true }; + + switch (type) { + case 'btn': + case 'button': + if (args.length < 4) { + this.terminal.print('Usage: floating-add btn <id> <label> <command>', 'warning'); + return; + } + el.type = 'button'; + el.command = args.slice(3).join(' '); + break; + case 'menu': + el.type = 'menu'; + el.collapsed = true; + el.buttons = []; + for (let i = 3; i < args.length; i++) { + const m = args[i].match(/^btn:(.+?)\|(.+)$/); + if (m) { + el.buttons.push({ label: m[1], command: m[2] }); + } + } + if (el.buttons.length === 0) { + this.terminal.print('No valid sub-buttons. Use btn:Label|command format', 'warning'); + return; + } + break; + case 'panel': + el.type = 'panel'; + el.content = args.slice(3).join(' ') || ''; + break; + default: + this.terminal.print(`Unknown type '${type}'. Use: btn, menu, panel`, 'warning'); + return; + } + + this._elements[id] = el; + this._save(); + this._renderAll(); + this.terminal.print(`Floating '${type}' '${label}' created (id: ${id})`, 'success'); + } + + _cmdRemove(args) { + if (!args[0]) { + this.terminal.print('Usage: floating-remove <id>', 'warning'); + return; + } + if (!this._elements[args[0]]) { + this.terminal.print(`Element '${args[0]}' not found`, 'warning'); + return; + } + delete this._elements[args[0]]; + this._save(); + this._renderAll(); + this.terminal.print(`Element '${args[0]}' removed`, 'success'); + } + + _cmdList() { + const ids = Object.keys(this._elements); + if (ids.length === 0) { + this.terminal.print('No floating elements', 'info'); + return; + } + this.terminal.print(`Floating elements (${ids.length}):`, 'info'); + for (const id of ids) { + const el = this._elements[id]; + const pos = (el.x != null) ? `${el.x},${el.y}` : 'auto'; + const vis = el.visible ? 'visible' : 'hidden'; + this.terminal.print(` ${id}: ${el.type} "${el.label}" @${pos} [${vis}]`, 'text-muted'); + if (el.type === 'menu' && el.buttons) { + el.buttons.forEach(b => this.terminal.print(` → btn:${b.label}|${b.command}`, 'text-muted')); + } + } + } + + _cmdPos(args) { + if (args.length < 3) { + this.terminal.print('Usage: floating-pos <id> <x|auto> <y|auto>', 'warning'); + return; + } + const id = args[0]; + if (!this._elements[id]) { + this.terminal.print(`Element '${id}' not found`, 'warning'); + return; + } + this._elements[id].x = args[1] === 'auto' ? null : parseInt(args[1]); + this._elements[id].y = args[2] === 'auto' ? null : parseInt(args[2]); + this._save(); + this._renderAll(); + this.terminal.print(`Element '${id}' repositioned`, 'success'); + } + + _cmdToggle(args) { + if (!args[0]) { + this.terminal.print('Usage: floating-toggle <id>', 'warning'); + return; + } + const id = args[0]; + if (!this._elements[id]) { + this.terminal.print(`Element '${id}' not found`, 'warning'); + return; + } + this._elements[id].visible = !this._elements[id].visible; + this._save(); + this._renderAll(); + this.terminal.print(`Element '${id}' now ${this._elements[id].visible ? 'visible' : 'hidden'}`, 'success'); + } + + _cmdClear() { + this._elements = {}; + this._save(); + this._renderAll(); + this.terminal.print('All floating elements cleared', 'success'); + } +}
\ No newline at end of file diff --git a/modules/ext/shell.js b/modules/ext/shell.js index e87c37c..55d66de 100644 --- a/modules/ext/shell.js +++ b/modules/ext/shell.js @@ -23,6 +23,7 @@ class ShellModule { 'unset': 'unset <var> — Remove a shell variable', 'script': 'script <path.sh> — Run a shell script from VFS', '$': '$ <command> — Execute with shell parsing (default: auto-detected)', + 'sh': 'sh <path.sh> — Run a shell script from VFS (alias for script)', 'set': 'set — List all shell variables', }; } @@ -31,6 +32,7 @@ class ShellModule { this.commands['export'] = (args) => this._cmdExport(args); this.commands['unset'] = (args) => this._cmdUnset(args); this.commands['script'] = (args) => this._cmdScript(args); + this.commands['sh'] = (args) => this._cmdScript(args); this.commands['set'] = () => this._cmdSet(); } diff --git a/modules/ext/skill-pack-core.js b/modules/ext/skill-pack-core.js index 6f7ad4c..abbc7ab 100644 --- a/modules/ext/skill-pack-core.js +++ b/modules/ext/skill-pack-core.js @@ -471,7 +471,41 @@ class SkillPackCoreModule { ## When to use - User wants to brainstorm, organize ideas, plan features - User says "map out", "diagram", "brainstorm", "organize thoughts" -- Visualizing project structure or task decomposition`) +- Visualizing project structure or task decomposition`), + + sk('floating-ui', 'How to create, manage, and remove floating UI elements — buttons, collapsible menus, and status panels that persist across page reloads', ['floating', 'ui', 'buttons', 'menu', 'gui'], 'ui', +`## Commands + +1. **Create a floating button:** + - floating-add btn <id> <label> <command> + - Example: floating-add btn files "📁 Files" ls + - Button appears at bottom-right, persists across reloads + +2. **Create a collapsible menu:** + - floating-add menu <id> <label> btn:SubLabel|command btn:Sub2|cmd2 ... + - Example: floating-add menu tools "🔧 Tools" btn:Help|help btn:Clear|clear + - Click header to expand/collapse. State persists. + +3. **Create a text panel:** + - floating-add panel <id> <title> <content> + - Example: floating-add panel status "📊 Status" "CPU: 2%" + +4. **Manage elements:** + - floating-list — show all elements with IDs and positions + - floating-remove <id> — delete by ID + - floating-toggle <id> — show/hide without deleting + - floating-pos <id> x|auto y|auto — reposition or reset to auto + - floating-clear — remove everything + +5. **Persistence:** + - All elements auto-save to localStorage (webx11_floating_elements) + - Survive page reload, browser restart + - Run floating-clear to start fresh + +## When to use +- User wants quick-access buttons instead of typing commands +- User says "add a button for X", "make a shortcut", "floating menu" +- Creating a dashboard with status panels and action buttons`) ]; } registerCommands() { |