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
123
124
125
126
127
128
129
130
131
132
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); };
}
}
|