aboutsummaryrefslogtreecommitdiff
path: root/modules/ext/ui-cockpit.js
blob: ffb6c2af6e723d1b321200ed5981ebf2bcc7319c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// 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);
            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 () => {
            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;
            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;
            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); };
    }
}