diff options
Diffstat (limited to 'modules/ext/ui-cockpit.js')
| -rw-r--r-- | modules/ext/ui-cockpit.js | 132 |
1 files changed, 132 insertions, 0 deletions
diff --git a/modules/ext/ui-cockpit.js b/modules/ext/ui-cockpit.js new file mode 100644 index 0000000..73fbe9f --- /dev/null +++ b/modules/ext/ui-cockpit.js @@ -0,0 +1,132 @@ +// UI mode: cockpit (level 6). Eight live panes straight from /sys/ + real commands. +class UICockpitMode { + 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: 'cockpit', label: 'Cockpit', level: 6, icon: '🛰', + tagline: 'Live system telemetry.', + mount: (api) => this.mount(api) + }); + } + mount(api) { + api.injectCSS('cockpit', ` +body[data-ui-mode="cockpit"] { font-size: 13px; } +body[data-ui-mode="cockpit"] #ui-root { display: grid; grid-template-rows: 1fr 190px; min-height: 0; } +#ck-panes { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; + background: var(--border-color); min-height: 0; overflow-y: auto; } +#ck-panes .pane { background: var(--bg-color); display: flex; flex-direction: column; + min-height: 130px; min-width: 0; transition: outline-color .2s; } +#ck-panes .pane h5 { margin: 0; padding: 5px 8px; font-size: .72em; letter-spacing: .1em; + text-transform: uppercase; color: var(--text-muted); border-bottom: 1px solid var(--border-color); + display: flex; gap: 8px; align-items: center; } +#ck-panes .pane h5 .n { margin-left: auto; color: var(--accent-color); } +#ck-panes pre { flex: 1 1 auto; margin: 0; padding: 6px 8px; overflow: auto; white-space: pre-wrap; + word-break: break-word; font-family: var(--font-primary); font-size: .72em; line-height: 1.35; } +#ck-term { display: flex; flex-direction: column; min-height: 0; border-top: 1px solid var(--border-color); } +#ck-term-bar { flex: 0 0 auto; display: flex; gap: 6px; align-items: center; padding: 4px 8px; + background: var(--bg-secondary); font-size: .78em; } +#ck-term-bar .title { flex: 1 1 auto; color: var(--text-muted); } +#ck-out { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 4px 8px; font-size: .82em; } +#ck-in { flex: 0 0 auto; padding: 4px 8px 8px; } +#ck-in #input-area { display: flex; gap: 6px; align-items: flex-end; } +#ck-in .prompt-label, #ck-in #autocomplete-dropdown { display: none; } +#ck-in #command-input { width: 100%; font-family: var(--font-primary); font-size: .85em; } +@media (max-width: 900px) { + body[data-ui-mode="cockpit"] #ui-root { grid-template-rows: 1fr 44vh; } + #ck-panes { grid-template-columns: 1fr 1fr; } +} +`); + + const root = api.root; + root.innerHTML = ''; + const panes = api.el('div', { id: 'ck-panes' }); + const termWrap = api.el('div', { id: 'ck-term' }); + root.appendChild(panes); root.appendChild(termWrap); + + const defs = [ + { id: 'heap', label: 'heap', cmd: 'cat /sys/heap/stats' }, + { id: 'breakdown', label: 'breakdown', cmd: 'cat /sys/heap/breakdown' }, + { id: 'vfs', label: 'vfs', cmd: 'cat /sys/vfs/stats' }, + { id: 'tools', label: 'tool calls', cmd: 'cat /sys/logs/tool-calls' }, + { id: 'errors', label: 'errors', cmd: 'cat /sys/logs/errors' }, + { id: 'dom', label: 'dom', cmd: 'cat /sys/dom/node-count' }, + { id: 'agents', label: 'agents', cmd: 'agent-list' }, + { id: 'leases', label: 'leases', cmd: 'lease-list' } + ]; + const slots = {}; + defs.forEach((d, i) => { + const pane = api.el('div', { class: 'pane' }); + pane.dataset.pane = String(i + 1); + pane.tabIndex = -1; // so F1-F8 can actually move focus, not just scroll + const h = api.el('h5'); + h.appendChild(api.el('span', { text: 'F' + (i + 1) + ' ' + d.label })); + h.appendChild(api.el('span', { class: 'n' })); + const pre = api.el('pre', { text: '…' }); + pane.appendChild(h); pane.appendChild(pre); + slots[d.id] = { pre, n: h.querySelector('.n') }; + panes.appendChild(pane); + }); + + const bar = api.el('div', { id: 'ck-term-bar' }); + bar.appendChild(api.el('span', { class: 'title', text: 'terminal · F1-F8 focus · F5 refresh' })); + const styleBtn = api.el('button', { class: 'wx11-btn', text: '⇄' }); + styleBtn.addEventListener('click', () => api.exec('ui picker')); + bar.appendChild(styleBtn); + const out = api.el('div', { id: 'ck-out' }); + const inp = api.el('div', { id: 'ck-in' }); + termWrap.appendChild(bar); termWrap.appendChild(out); termWrap.appendChild(inp); + api.attach(out, api.stock.output); + api.attach(inp, api.stock.input); + + // Generation guard: eight async captures per tick at 5s — a tick can outlive teardown. + let gen = 0; + const refresh = async () => { + // Same as workspace: bumping per tick cancelled a slow run before the later panes were + // written, leaving them at "…" indefinitely. + const my = gen; + for (const d of defs) { + try { + const text = await api.capture(d.cmd); + if (my !== gen) return; + const lines = (text || '').trim() ? String(text).trim().split('\n') : ['(empty)']; + slots[d.id].pre.textContent = lines.join('\n'); + slots[d.id].n.textContent = lines.length + 'L'; + } catch (e) { + if (my !== gen) return; + slots[d.id].pre.textContent = 'unavailable: ' + e.message; + slots[d.id].n.textContent = 'ERR'; + } + } + }; + refresh(); + const timer = api.interval(refresh, 5000); + + const onKey = (ev) => { + const m = /^F([1-8])$/.exec(String(ev.key)); + if (!m) return; + const t = ev.target; + // Never swallow keys aimed at a text field, and leave F1/F12 to the browser: the handler + // used to preventDefault on all eight, so help, the address bar and devtools were dead + // while the cockpit was open (found by review). + if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return; + if (ev.key === 'F1' || ev.key === 'F12') return; + ev.preventDefault(); + const idx = parseInt(m[1], 10); + if (idx === 5) { refresh(); return; } // F5 = refresh, as labelled + const pane = panes.querySelector('[data-pane="' + idx + '"]'); + if (!pane) return; + if (pane.focus) pane.focus(); + pane.scrollIntoView({ block: 'nearest' }); + pane.style.outline = '1px solid var(--accent-color)'; + setTimeout(() => { pane.style.outline = ''; }, 900); + }; + document.addEventListener('keydown', onKey); + + return () => { gen++; document.removeEventListener('keydown', onKey); api.clearInterval(timer); }; + } +} |