diff options
Diffstat (limited to 'modules/ext/ui-workspace.js')
| -rw-r--r-- | modules/ext/ui-workspace.js | 133 |
1 files changed, 133 insertions, 0 deletions
diff --git a/modules/ext/ui-workspace.js b/modules/ext/ui-workspace.js new file mode 100644 index 0000000..be7a5e0 --- /dev/null +++ b/modules/ext/ui-workspace.js @@ -0,0 +1,133 @@ +// UI mode: workspace (level 4). Card grid; each card is the captured text of a real command. +// api.capture() is for these fixed strings only โ user input must use api.exec(). +class UIWorkspaceMode { + constructor() { this.commands = {}; this.terminal = null; } + requirements() { return { met: true, missing: [] }; } + getHelpInfo() { return {}; } + registerCommands() {} + init(terminal) { + this.terminal = terminal; + if (!terminal.uiShell) return; + terminal.uiShell.registerMode({ + id: 'workspace', label: 'Workspace', level: 4, icon: '๐', + tagline: 'Everything at a glance.', + mount: (api) => this.mount(api) + }); + } + mount(api) { + api.injectCSS('workspace', ` +body[data-ui-mode="workspace"] #ui-root { display: flex; flex-direction: column; } +#ws-bar { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; padding: 8px 12px; + border-bottom: 1px solid var(--border-color); background: var(--bg-secondary); } +#ws-bar .title { flex: 1 1 auto; font-weight: 600; font-size: .95em; } +#ws-bar .stamp { color: var(--text-muted); font-size: .75em; } +#ws-grid { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 12px; + display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); gap: 10px; align-content: start; } +#ws-grid .wx11-card { display: flex; flex-direction: column; min-height: 150px; max-height: 260px; } +#ws-grid h3 { margin: 0 0 8px; font-size: .85em; text-transform: uppercase; letter-spacing: .06em; + color: var(--text-muted); display: flex; gap: 6px; align-items: center; } +#ws-grid h3 .run { margin-left: auto; font-size: 1.1em; cursor: pointer; padding: 4px 8px; } +#ws-out { display: none; flex: 0 0 auto; height: 38vh; border-top: 1px solid var(--border-color); + background: var(--bg-color); } +#ws-out.open { display: flex; flex-direction: column; } +#ws-out #terminal-container { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 6px 10px; font-size: .85em; } +#ws-composer { flex: 0 0 auto; border-top: 1px solid var(--border-color); padding: 8px 12px; } +#ws-composer #input-area { display: flex; gap: 6px; align-items: flex-end; } +#ws-composer .prompt-label, #ws-composer #autocomplete-dropdown { display: none; } +#ws-composer #command-input { width: 100%; font-family: inherit; } +@media (max-width: 600px) { #ws-grid { grid-template-columns: 1fr; } #ws-grid .wx11-card { max-height: none; } } +`); + + const root = api.root; + root.innerHTML = ''; + const bar = api.el('div', { id: 'ws-bar' }); + bar.appendChild(api.el('span', { class: 'title', text: 'Workspace' })); + bar.appendChild(api.el('span', { class: 'stamp', id: 'ws-stamp' })); + + const styleBtn = api.el('button', { class: 'wx11-btn', text: 'โ Style' }); + styleBtn.addEventListener('click', () => api.exec('ui picker')); + const refreshBtn = api.el('button', { class: 'wx11-btn', text: 'โณ Refresh' }); + const outBtn = api.el('button', { class: 'wx11-btn', text: 'โจ Output' }); + let drawerOpen = false; + outBtn.addEventListener('click', () => { + drawerOpen = !drawerOpen; + const d = document.getElementById('ws-out'); + if (d) d.classList.toggle('open', drawerOpen); + outBtn.textContent = drawerOpen ? 'โจ Hide' : 'โจ Output'; + }); + bar.appendChild(refreshBtn); + bar.appendChild(outBtn); + bar.appendChild(styleBtn); + + const grid = api.el('div', { id: 'ws-grid' }); + const drawer = api.el('div', { id: 'ws-out' }); + // Output visible by default: a card grid with the terminal collapsed reads as "where did my + // answer go?" โ and the probe (correctly) measures a 0x0 pane as invisible. + drawer.classList.add('open'); + drawerOpen = true; + outBtn.textContent = 'โจ Hide'; + const composer = api.el('div', { id: 'ws-composer' }); + + const cards = [ + { id: 'files', ico: '๐', title: 'Files', cmd: 'ls -la' }, + { id: 'sys', ico: '๐ฅ', title: 'Heap', cmd: 'cat /sys/heap/stats' }, + { id: 'modules', ico: '๐งฉ', title: 'Modules', cmd: 'modules' }, + { id: 'agents', ico: '๐ค', title: 'Agents', cmd: 'agent-list' }, + { id: 'cron', ico: 'โฑ', title: 'Scheduled', cmd: 'cron-list' }, + { id: 'errors', ico: '๐', title: 'Recent errors', cmd: 'cat /sys/logs/errors' } + ]; + const pres = {}; + cards.forEach(c => { + const card = api.el('div', { class: 'wx11-card' }); + const h = api.el('h3'); + h.appendChild(api.el('span', { text: c.ico })); + h.appendChild(api.el('span', { text: c.title })); + const run = api.el('span', { class: 'run', text: 'โธ' }); + run.addEventListener('click', () => { + const d = document.getElementById('ws-out'); + if (d && !d.classList.contains('open')) d.classList.add('open'); + api.exec(c.cmd); + api.toast('Running: ' + c.cmd); + }); + h.appendChild(run); + const pre = api.el('pre', { class: 'wx11-pre', text: 'โฆ' }); + pres[c.id] = pre; + card.appendChild(h); card.appendChild(pre); + grid.appendChild(card); + }); + + root.appendChild(bar); + root.appendChild(grid); + root.appendChild(drawer); + root.appendChild(composer); + api.attach(composer, api.stock.input); + api.attach(drawer, api.stock.output); // the run-in-terminal output needs somewhere to land + + // Generation guard: capture() is async, so a 15s tick can land AFTER teardown and write into + // a detached tree (invisible work, retained DOM). Every refresh claims a generation. + let gen = 0; + const refresh = async () => { + const my = ++gen; + let changed = 0; + for (const c of cards) { + try { + const out = await api.capture(c.cmd); + if (my !== gen) return; + pres[c.id].textContent = (out && out.trim()) ? out : '(empty)'; + changed++; + } catch (e) { + if (my !== gen) return; + pres[c.id].textContent = 'unavailable: ' + e.message; + } + } + if (my !== gen) return; + const stamp = document.getElementById('ws-stamp'); + if (stamp) stamp.textContent = changed + '/' + cards.length + ' ยท ' + + new Date().toLocaleTimeString(); + }; + refreshBtn.addEventListener('click', refresh); + refresh(); + const timer = api.interval(refresh, 15000); + return () => { gen++; api.clearInterval(timer); }; + } +} |