// 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); }; } }