aboutsummaryrefslogtreecommitdiff
path: root/modules/ext/ui-workspace.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/ext/ui-workspace.js')
-rw-r--r--modules/ext/ui-workspace.js139
1 files changed, 139 insertions, 0 deletions
diff --git a/modules/ext/ui-workspace.js b/modules/ext/ui-workspace.js
new file mode 100644
index 0000000..d660283
--- /dev/null
+++ b/modules/ext/ui-workspace.js
@@ -0,0 +1,139 @@
+// 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' },
+ // `modules` OPENS the module-manager panel โ€” capturing it re-opened the panel on every
+ // 15 s refresh ("the modules tab keeps opening", reported 2026-11-07). `listmods` prints
+ // the same inventory as text.
+ { id: 'modules', ico: '๐Ÿงฉ', title: 'Modules', cmd: 'listmods' },
+ { 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 () => {
+ // Do NOT bump the generation on every tick: a capture slower than the 15 s interval
+ // invalidated the in-flight run before it reached the later cards, so those stayed at
+ // "โ€ฆ" forever under slow storage (found by review). Teardown still bumps gen to stop it.
+ 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); };
+ }
+}