anentrypoint-design 0.0.63 → 0.0.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,13 @@
1
+ import * as webjsx from '../../vendor/webjsx/index.js';
2
+ import * as components from '../components.js';
3
+
4
+ const h = webjsx.createElement;
5
+ const {
6
+ AppShell, Topbar, Side, Crumb, Status, Brand, Glyph,
7
+ Panel, Row, RowLink, Hero, Receipt, Kpi, Table, Section,
8
+ EmptyState, Chip,
9
+ } = components;
10
+
1
11
  const ROUTES = [
2
12
  { path: 'projects', label: 'projects', glyph: '◆' },
3
13
  { path: 'home', label: 'home', glyph: '⌂' },
@@ -16,106 +26,79 @@ const ROUTES = [
16
26
  { path: 'gateway', label: 'gateway', glyph: '⇌' },
17
27
  ];
18
28
 
19
- function el(tag, cls, attrs) {
20
- const e = document.createElement(tag);
21
- if (cls) e.className = cls;
22
- if (attrs) for (const k of Object.keys(attrs)) {
23
- if (k === 'on' && attrs.on) for (const ev of Object.keys(attrs.on)) e.addEventListener(ev, attrs.on[ev]);
24
- else if (k === 'html') e.innerHTML = attrs.html;
25
- else if (k === 'text') e.textContent = attrs.text;
26
- else e.setAttribute(k, attrs[k]);
27
- }
28
- return e;
29
+ const OS_ROUTE_DEFS = [
30
+ { path: 'os-instances', label: 'instances', glyph: '◫' },
31
+ { path: 'os-windows', label: 'windows', glyph: '▭' },
32
+ { path: 'os-x', label: 'x-server', glyph: '✕' },
33
+ { path: 'os-fs', label: 'fs', glyph: '📁' },
34
+ ];
35
+
36
+ function pre(obj) {
37
+ return h('pre', { class: 'fd-pre' }, typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2));
29
38
  }
30
39
 
31
- function kpi(items) {
32
- const c = el('div', 'fdash-kpi');
33
- for (const [v, l] of items) {
34
- const k = el('div', 'k');
35
- k.appendChild(el('div', 'v', { text: String(v) }));
36
- k.appendChild(el('div', 'l', { text: String(l) }));
37
- c.appendChild(k);
38
- }
39
- return c;
40
+ function form(opts) {
41
+ const { fields = [], submit = 'submit', onSubmit } = opts;
42
+ return h('form', { class: 'row-form', onsubmit: (ev) => { ev.preventDefault(); onSubmit && onSubmit(ev); } },
43
+ ...fields.map(f => f.kind === 'textarea'
44
+ ? h('textarea', { name: f.name, placeholder: f.placeholder || '', rows: f.rows || 4 })
45
+ : h('input', { name: f.name, type: f.type || 'text', placeholder: f.placeholder || '', value: f.value || '', required: f.required ? 'true' : null })),
46
+ h('button', { type: 'submit', class: 'btn-primary' }, submit));
40
47
  }
41
48
 
42
- function panel(title, body, count) {
43
- const p = el('div', 'fdash-panel');
44
- const h = el('h3'); h.textContent = title + (count != null ? ' · ' + count : '');
45
- p.appendChild(h);
46
- if (body instanceof Node) p.appendChild(body);
47
- else if (Array.isArray(body)) for (const n of body) if (n) p.appendChild(n);
48
- else if (typeof body === 'string') { const pre = el('pre'); pre.textContent = body; p.appendChild(pre); }
49
- return p;
49
+ function getRecentPaths() {
50
+ try { return JSON.parse(localStorage.getItem('fd_recent_cwds') || '[]'); } catch { return []; }
50
51
  }
51
52
 
52
- function row(opts) {
53
- const r = el('div', 'fdash-row');
54
- if (opts.code) r.appendChild(el('span', 'code', { text: opts.code }));
55
- r.appendChild(el('span', 'title', { text: opts.title || '' }));
56
- if (opts.sub) r.appendChild(el('span', 'sub', { text: ' — ' + opts.sub }));
57
- if (opts.meta) r.appendChild(el('span', 'meta', { text: opts.meta }));
58
- return r;
53
+ function saveRecentPath(p) {
54
+ if (!p) return;
55
+ try {
56
+ const prev = getRecentPaths().filter(x => x !== p);
57
+ localStorage.setItem('fd_recent_cwds', JSON.stringify([p, ...prev].slice(0, 5)));
58
+ } catch {}
59
59
  }
60
60
 
61
- function table(headers, rows) {
62
- const t = el('table');
63
- const thead = el('thead'); const trh = el('tr');
64
- for (const h of headers) trh.appendChild(el('th', null, { text: h }));
65
- thead.appendChild(trh); t.appendChild(thead);
66
- const tb = el('tbody');
67
- for (const r of rows) {
68
- const tr = el('tr');
69
- for (const c of r) tr.appendChild(el('td', null, { text: String(c) }));
70
- tb.appendChild(tr);
71
- }
72
- t.appendChild(tb);
73
- return t;
61
+ function skillLabel(s) {
62
+ if (s.shortName) return s.shortName;
63
+ const n = s.name || '';
64
+ return n.replace(/^gm:/, '').replace(/^software-development$/, 'software dev').replace(/-/g, ' ');
74
65
  }
75
66
 
76
- function pre(obj) { return el('pre', null, { text: typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2) }); }
67
+ function renderChatMessages(container, messages) {
68
+ if (!container) return;
69
+ container.innerHTML = '';
70
+ for (const m of messages) {
71
+ if (m.role === 'tool') {
72
+ const det = document.createElement('details');
73
+ det.style.cssText = 'margin:4px 0;padding:4px 8px;background:rgba(0,0,0,0.18);border-radius:4px;font-family:monospace;font-size:0.85em;';
74
+ const sum = document.createElement('summary');
75
+ sum.style.cssText = 'cursor:pointer;color:var(--color-warn,#fc9);padding:2px 0;';
76
+ sum.textContent = '⚒ ' + m.name + (m.argsSummary ? ' ' + m.argsSummary : '');
77
+ det.appendChild(sum);
78
+ const body = document.createElement('pre');
79
+ body.style.cssText = 'margin:4px 0 0;white-space:pre-wrap;word-break:break-all;max-height:200px;overflow-y:auto;';
80
+ body.textContent = m.content || '';
81
+ det.appendChild(body);
82
+ container.appendChild(det);
83
+ } else {
84
+ const el = document.createElement('div');
85
+ el.style.cssText = 'padding:6px 10px;border-bottom:1px solid rgba(128,128,128,0.15);white-space:pre-wrap;word-break:break-word;';
86
+ el.style.color = m.role === 'assistant' ? 'var(--color-accent,#7c9)' : 'inherit';
87
+ el.textContent = (m.role === 'assistant' ? '◈ ' : '▷ ') + (m.content || '');
88
+ container.appendChild(el);
89
+ }
90
+ }
91
+ container.scrollTop = container.scrollHeight;
92
+ }
77
93
 
78
94
  export function createFreddieDashboard({ instance, bootHost, osSurfaces }) {
79
- const root = el('div', 'fdash');
80
- const side = el('div', 'fdash-side');
81
- const nav = el('div', 'fdash-nav');
82
- const main = el('div', 'fdash-main');
83
- side.appendChild(nav);
84
- root.appendChild(side); root.appendChild(main);
95
+ const root = document.createElement('div');
96
+ root.className = 'app-fd ds-247420';
97
+ root.style.cssText = 'height:100%;overflow:hidden;display:flex;flex-direction:column;';
85
98
 
86
- let active = 'home';
99
+ const state = { active: 'home', ts: new Date().toLocaleTimeString(), body: null, error: null };
87
100
  let host = instance.host || null;
88
-
89
- function setActive(p) {
90
- active = p;
91
- for (const b of nav.querySelectorAll('button')) b.classList.toggle('active', b.dataset.path === p);
92
- render();
93
- }
94
-
95
- const OS_ROUTES = osSurfaces ? [
96
- { path: 'os-instances', label: 'instances', glyph: '◫' },
97
- { path: 'os-windows', label: 'windows', glyph: '▭' },
98
- { path: 'os-x', label: 'x-server', glyph: '✕' },
99
- { path: 'os-fs', label: 'fs', glyph: '📁' },
100
- ] : [];
101
-
102
- function navHead(text) {
103
- const h = el('div', 'group-head', { text });
104
- nav.appendChild(h);
105
- }
106
- function navBtn(r) {
107
- const b = el('button', null, { 'data-path': r.path, on: { click: () => setActive(r.path) } });
108
- b.appendChild(el('span', 'glyph', { text: r.glyph }));
109
- b.appendChild(document.createTextNode(' '));
110
- b.appendChild(el('span', 'label', { text: r.label }));
111
- nav.appendChild(b);
112
- }
113
- navHead('freddie');
114
- for (const r of ROUTES) navBtn(r);
115
- if (OS_ROUTES.length) {
116
- navHead('os');
117
- for (const r of OS_ROUTES) navBtn(r);
118
- }
101
+ const allRoutes = osSurfaces ? [...ROUTES, ...OS_ROUTE_DEFS] : ROUTES;
119
102
 
120
103
  async function ensureHost() {
121
104
  if (host) return host;
@@ -124,281 +107,547 @@ export function createFreddieDashboard({ instance, bootHost, osSurfaces }) {
124
107
  return host;
125
108
  }
126
109
 
127
- async function render() {
128
- main.innerHTML = '';
129
- main.appendChild(el('h2', 'fdash-h', { text: 'freddie · ' + instance.id + ' · ' + active }));
130
- const h = await ensureHost();
131
- const page = PAGES[active] || PAGES.home;
110
+ function setActive(p) { state.active = p; rerender(); }
111
+
112
+ if (typeof window !== 'undefined') window.__fd_nav = setActive;
113
+
114
+ function buildSide() {
115
+ const sections = [{
116
+ group: 'FREDDIE',
117
+ items: ROUTES.map(r => ({
118
+ glyph: r.glyph, label: r.label, href: '#fd-' + r.path,
119
+ active: state.active === r.path,
120
+ onClick: (ev) => { ev.preventDefault(); setActive(r.path); },
121
+ })),
122
+ }];
123
+ if (osSurfaces) sections.push({
124
+ group: 'OS',
125
+ items: OS_ROUTE_DEFS.map(r => ({
126
+ glyph: r.glyph, label: r.label, href: '#fd-' + r.path,
127
+ active: state.active === r.path,
128
+ onClick: (ev) => { ev.preventDefault(); setActive(r.path); },
129
+ })),
130
+ });
131
+ return Side({ sections });
132
+ }
133
+
134
+ function view() {
135
+ const route = allRoutes.find(r => r.path === state.active) || ROUTES[1];
136
+ return AppShell({
137
+ topbar: Topbar({ brand: 'freddie', leaf: 'dashboard', items: [], active: '' }),
138
+ crumb: Crumb({ trail: ['freddie', instance.id], leaf: route.path, right: state.error ? Chip({ tone: 'miss', children: 'error' }) : Chip({ tone: 'ok', children: 'live' }) }),
139
+ side: buildSide(),
140
+ main: state.body || EmptyState({ text: 'loading…', glyph: '◌' }),
141
+ status: Status({ left: ['ds-247420 · webjsx · ' + allRoutes.length + ' routes', 'instance=' + instance.id], right: [state.ts] }),
142
+ });
143
+ }
144
+
145
+ function rerender() { webjsx.applyDiff(root, view()); loadActive(); }
146
+
147
+ async function loadActive() {
132
148
  try {
133
- const body = await page(h, instance);
134
- const arr = Array.isArray(body) ? body : [body];
135
- for (const n of arr) if (n) main.appendChild(n);
149
+ const h0 = await ensureHost();
150
+ const page = PAGES[state.active] || PAGES.home;
151
+ state.body = await page(h0, instance);
152
+ state.error = null;
136
153
  } catch (e) {
137
- main.appendChild(panel('error', el('pre', null, { text: String(e && e.stack || e) })));
154
+ state.error = String(e && e.stack || e);
155
+ state.body = Panel({ title: 'error', children: pre(state.error) });
138
156
  }
157
+ state.ts = new Date().toLocaleTimeString();
158
+ webjsx.applyDiff(root, view());
139
159
  }
140
160
 
141
161
  const PAGES = {
142
- async projects(h) {
143
- const list = h.pi.projects.list();
144
- const active = h.pi.projects.active();
145
- const form = el('form', 'fdash-form', { on: { submit: (ev) => {
146
- ev.preventDefault();
147
- try { h.pi.projects.create({ name: ev.target.elements.name.value, path: ev.target.elements.path.value }); render(); }
148
- catch (e) { alert(e.message); }
149
- } } });
150
- form.appendChild(el('input', null, { name: 'name', placeholder: 'project name', required: 'true' }));
151
- form.appendChild(el('input', null, { name: 'path', placeholder: '/path' }));
152
- form.appendChild(el('button', null, { type: 'submit', text: 'add' }));
153
- const rows = list.map(p => {
154
- const r = el('div', 'fdash-row');
155
- r.appendChild(el('span', 'code', { text: p.name === active?.name ? '●' : '○' }));
156
- r.appendChild(el('span', null, { text: p.name + (p.name === active?.name ? ' (active)' : '') }));
157
- r.appendChild(el('span', 'meta', { text: p.path }));
158
- if (p.name !== 'default') {
159
- const del = el('button', null, { type: 'button', text: 'remove', on: { click: () => { try { h.pi.projects.remove(p.name); render(); } catch (e) { alert(e.message); } } } });
160
- r.appendChild(del);
161
- }
162
- if (p.name !== active?.name) {
163
- const sw = el('button', null, { type: 'button', text: 'switch', on: { click: () => { h.pi.projects.setActive(p.name); render(); } } });
164
- r.appendChild(sw);
165
- }
166
- return r;
167
- });
162
+ async projects(h0) {
163
+ const list = h0.pi.projects.list();
164
+ const activeProj = (typeof h0.pi.projects.active === 'function') ? h0.pi.projects.active() : null;
165
+ const rows = list.map(p => Row({
166
+ key: p.name,
167
+ code: p.name === activeProj?.name ? '●' : '○',
168
+ title: p.name + (p.name === activeProj?.name ? ' (active)' : ''),
169
+ meta: p.path,
170
+ onClick: () => { if (p.name !== activeProj?.name) try { h0.pi.projects.setActive(p.name); rerender(); } catch (e) { alert(e.message); } },
171
+ }));
168
172
  return [
169
- kpi([[list.length, 'projects'], [active?.name || '—', 'active'], [active?.path || '', 'path']]),
170
- panel('add a project', form),
171
- panel('all projects', rows, list.length),
173
+ Hero({ title: 'projects', body: 'each project is its own ~/.freddie home: separate sessions, agents, skills, config, env, cron, batches.', accent: activeProj ? 'active · ' + activeProj.name : 'no active project' }),
174
+ Kpi({ items: [[list.length, 'projects'], [activeProj?.name || '—', 'active'], [activeProj?.path?.length > 30 ? '…' + activeProj.path.slice(-28) : (activeProj?.path || '—'), 'path']] }),
175
+ Panel({ title: 'add a project', children: form({
176
+ fields: [{ name: 'name', placeholder: 'project name', required: true }, { name: 'path', placeholder: '/abs/path' }],
177
+ submit: 'add',
178
+ onSubmit: (ev) => { try { h0.pi.projects.create({ name: ev.target.elements.name.value, path: ev.target.elements.path.value }); rerender(); } catch (e) { alert(e.message); } },
179
+ }) }),
180
+ Panel({ title: 'all projects', count: list.length, children: rows.length ? rows : EmptyState({ text: 'no projects', glyph: '◆' }) }),
181
+ Panel({ title: 'how encapsulation works', children: Receipt({ rows: [
182
+ ['sessions db', '<project>/sessions.db'],
183
+ ['config', '<project>/config.json'],
184
+ ['skills', '<project>/skills/'],
185
+ ['plugins', '<project>/plugins/'],
186
+ ['cron', '<project>/cron.db'],
187
+ ['batches', '<project>/batches/'],
188
+ ['logs', '<project>/logs/'],
189
+ ['auth', '<project>/auth.json'],
190
+ ] }) }),
172
191
  ];
173
192
  },
174
- async home(h) {
175
- const sessions = await h.pi.sessions.list();
176
- const tools = h.pi.tools.size;
177
- const skills = h.pi.skills.size;
178
- const health = h.pi.health();
193
+ async home(h0) {
194
+ const sessions = await h0.pi.sessions.list();
195
+ const tools = h0.pi.tools.size;
196
+ const skills = h0.pi.skills.size;
197
+ const health = (typeof h0.pi.health === 'function') ? h0.pi.health() : { ok: true };
179
198
  return [
180
- kpi([[sessions.length, 'sessions'], [tools, 'tools'], [skills, 'skills']]),
181
- panel('quick start', table(['action', 'how'], [
182
- ['open chat', "click 'chat' in sidebar"],
183
- ['list tools', '/tools in chat or tools tab'],
184
- ['list skills', '/skills in chat or skills tab'],
185
- ['set api key', '→ keys tab set ENV var'],
186
- ])),
187
- panel('host', table(['key', 'value'], Object.entries(health))),
199
+ Hero({ title: 'freddie', body: 'open js agent harness — pi-mono · xstate · floosie · anentrypoint-design.', accent: h0.version || 'web' }),
200
+ Kpi({ items: [[sessions.length, 'sessions'], [tools, 'tools'], [skills, 'skills']] }),
201
+ Panel({ title: 'quick start', children: Receipt({ rows: [
202
+ ['open chat', "click 'chat' in sidebar set a working directory and pick a skill"],
203
+ ['pick skill', "software dev, research, planning shown with descriptions"],
204
+ ['pick model', "select a configured provider + model in the chat bar"],
205
+ ['list tools', '/tools in chat → tools tab'],
206
+ ['set api key', 'keys tab → click chip to set value'],
207
+ ['add cron', 'cron tab → form'],
208
+ ] }) }),
209
+ Panel({ title: 'host', children: Receipt({ rows: Object.entries(health).map(([k, v]) => [k, String(v)]) }) }),
188
210
  ];
189
211
  },
190
- async chat(h, instance) {
191
- const note = el('div', 'fdash-empty', { text: 'chat lives in its own thebird app — opening chat window…' });
192
- try {
193
- if (window.__debug?.shell?.openApp) window.__debug.shell.openApp('chat');
194
- } catch {}
195
- return [panel('chat', note), panel('cli surface', table(['command', 'description'], [...h.pi.cli.values()].map(c => [c.name, c.description])))];
212
+ async chat(h0) {
213
+ const skills = [...h0.pi.skills.values()];
214
+ const providers = await fetch('/api/providers').then(r => r.json()).catch(() => []);
215
+ const configuredProviders = providers.filter(p => p.configured);
216
+
217
+ const chatState = window.__fd_chatState = window.__fd_chatState || {
218
+ cwd: '', skill: '', provider: '', model: '', messages: [], busy: false, sessionId: null,
219
+ };
220
+ if (!chatState.cwd) chatState.cwd = (getRecentPaths()[0] || '');
221
+
222
+ function getMsgsContainer() { return root.querySelector('#fd-chat-msgs'); }
223
+
224
+ function newSession() {
225
+ if (chatState.busy) return;
226
+ chatState.messages = [];
227
+ chatState.sessionId = null;
228
+ renderChatMessages(getMsgsContainer(), chatState.messages);
229
+ }
230
+
231
+ const parseSseEvents = (text) => {
232
+ const events = [];
233
+ let curEvent = null, curData = '';
234
+ for (const line of text.split('\n')) {
235
+ if (line.startsWith('event: ')) { curEvent = line.slice(7).trim(); }
236
+ else if (line.startsWith('data: ')) { curData = line.slice(6).trim(); }
237
+ else if (line === '' && curEvent) {
238
+ try { events.push({ event: curEvent, data: JSON.parse(curData) }); } catch {}
239
+ curEvent = null; curData = '';
240
+ }
241
+ }
242
+ return events;
243
+ };
244
+
245
+ const sendChat = async (ev) => {
246
+ ev.preventDefault();
247
+ if (chatState.busy) return;
248
+ const promptEl = ev.target.elements.prompt;
249
+ const prompt = promptEl.value.trim();
250
+ if (!prompt) return;
251
+ chatState.messages.push({ role: 'user', content: prompt });
252
+ promptEl.value = '';
253
+ promptEl.style.height = 'auto';
254
+ chatState.busy = true;
255
+ saveRecentPath(chatState.cwd);
256
+ renderChatMessages(getMsgsContainer(), chatState.messages);
257
+ try {
258
+ const body = { prompt, cwd: chatState.cwd || undefined, skill: chatState.skill || undefined, provider: chatState.provider || undefined, model: chatState.model || undefined, sessionId: chatState.sessionId || undefined };
259
+ const resp = await fetch('/api/chat', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
260
+ const text = await resp.text();
261
+ const events = parseSseEvents(text);
262
+ let assistantContent = '';
263
+ for (const { event, data } of events) {
264
+ if (event === 'start' && data.sessionId) chatState.sessionId = data.sessionId;
265
+ if (event === 'done' && data.sessionId) chatState.sessionId = data.sessionId;
266
+ if (event === 'message') {
267
+ const role = data.role;
268
+ if (role === 'assistant') {
269
+ const content = Array.isArray(data.content) ? data.content : [{ type: 'text', text: String(data.content || '') }];
270
+ for (const block of content) {
271
+ if (block.type === 'text') assistantContent += block.text;
272
+ if (block.type === 'tool_use') {
273
+ if (assistantContent) { chatState.messages.push({ role: 'assistant', content: assistantContent }); assistantContent = ''; }
274
+ const argsSummary = JSON.stringify(block.input || {}).slice(0, 60);
275
+ chatState.messages.push({ role: 'tool', name: block.name, argsSummary, content: JSON.stringify(block.input || {}, null, 2) });
276
+ }
277
+ }
278
+ } else if (role === 'tool') {
279
+ const tc = Array.isArray(data.content) ? data.content[0] : data;
280
+ chatState.messages.push({ role: 'tool', name: 'result', argsSummary: '', content: String(tc?.content || tc?.text || JSON.stringify(tc)) });
281
+ }
282
+ }
283
+ if (event === 'done' && data.result) {
284
+ if (!assistantContent) assistantContent = data.result;
285
+ }
286
+ if (event === 'error') assistantContent = 'error: ' + (data.error || 'unknown');
287
+ }
288
+ if (assistantContent) chatState.messages.push({ role: 'assistant', content: assistantContent });
289
+ if (!events.length) chatState.messages.push({ role: 'assistant', content: '(no response)' });
290
+ } catch (e) {
291
+ chatState.messages.push({ role: 'assistant', content: 'error: ' + e.message });
292
+ }
293
+ chatState.busy = false;
294
+ renderChatMessages(getMsgsContainer(), chatState.messages);
295
+ };
296
+
297
+ const recentPaths = getRecentPaths();
298
+ const datalistId = 'fd-cwd-list';
299
+ const byCat = skills.reduce((a, s) => { const c = s.category || 'other'; (a[c] = a[c] || []).push(s); return a; }, {});
300
+
301
+ setTimeout(() => renderChatMessages(getMsgsContainer(), chatState.messages), 50);
302
+
303
+ return [
304
+ Panel({
305
+ title: 'chat',
306
+ right: h('button', {
307
+ class: 'btn-primary', style: 'padding:2px 10px;font-size:0.8em;',
308
+ onclick: (ev) => { ev.preventDefault(); newSession(); },
309
+ disabled: chatState.busy ? 'true' : null,
310
+ }, '+ new session'),
311
+ children: [
312
+ h('datalist', { id: datalistId }, ...recentPaths.map(p => h('option', { value: p }))),
313
+ h('form', { class: 'row-form', style: 'display:flex;flex-direction:column;gap:8px;', onsubmit: sendChat },
314
+ h('div', { style: 'display:flex;flex-direction:column;gap:4px;' },
315
+ h('label', { style: 'font-size:0.75em;opacity:0.7;letter-spacing:0.05em;' }, 'WORKING DIRECTORY'),
316
+ h('input', {
317
+ name: 'cwd', type: 'text', placeholder: 'e.g. C:/dev/myproject or /home/user/project',
318
+ value: chatState.cwd, list: datalistId,
319
+ style: 'width:100%;box-sizing:border-box;',
320
+ oninput: (ev) => { chatState.cwd = ev.target.value; },
321
+ })
322
+ ),
323
+ h('div', { style: 'display:flex;gap:8px;flex-wrap:wrap;' },
324
+ h('div', { style: 'display:flex;flex-direction:column;gap:4px;flex:2;min-width:160px;' },
325
+ h('label', { style: 'font-size:0.75em;opacity:0.7;letter-spacing:0.05em;' }, 'SKILL'),
326
+ h('select', { name: 'skill', onchange: (ev) => { chatState.skill = ev.target.value; } },
327
+ h('option', { value: '' }, '— no skill —'),
328
+ ...Object.entries(byCat).map(([cat, ss]) =>
329
+ h('optgroup', { label: cat },
330
+ ...ss.map(s => h('option', {
331
+ value: s.name,
332
+ selected: chatState.skill === s.name ? 'true' : null,
333
+ title: s.description || s.name,
334
+ }, skillLabel(s)))
335
+ )
336
+ )
337
+ )
338
+ ),
339
+ h('div', { style: 'display:flex;flex-direction:column;gap:4px;flex:2;min-width:140px;' },
340
+ h('label', { style: 'font-size:0.75em;opacity:0.7;letter-spacing:0.05em;' }, 'PROVIDER'),
341
+ h('select', { name: 'provider', onchange: (ev) => { chatState.provider = ev.target.value; } },
342
+ h('option', { value: '' }, configuredProviders.length ? '— auto —' : '— no providers configured —'),
343
+ ...configuredProviders.map(p => h('option', {
344
+ value: p.name,
345
+ selected: chatState.provider === p.name ? 'true' : null,
346
+ }, (p.available ? '● ' : '○ ') + p.name))
347
+ )
348
+ ),
349
+ h('div', { style: 'display:flex;flex-direction:column;gap:4px;flex:2;min-width:120px;' },
350
+ h('label', { style: 'font-size:0.75em;opacity:0.7;letter-spacing:0.05em;' }, 'MODEL (optional)'),
351
+ h('input', {
352
+ name: 'model', type: 'text',
353
+ placeholder: configuredProviders.find(p => p.name === chatState.provider)?.defaultModel || 'default',
354
+ value: chatState.model,
355
+ oninput: (ev) => { chatState.model = ev.target.value; },
356
+ })
357
+ )
358
+ ),
359
+ h('div', { style: 'display:flex;gap:8px;align-items:flex-end;' },
360
+ h('textarea', {
361
+ name: 'prompt', placeholder: 'describe what you want to do in the working directory…',
362
+ rows: 4, style: 'flex:1;resize:none;min-height:80px;',
363
+ oninput: (ev) => {
364
+ ev.target.style.height = 'auto';
365
+ ev.target.style.height = Math.min(ev.target.scrollHeight, 240) + 'px';
366
+ },
367
+ }),
368
+ h('button', {
369
+ type: 'submit', class: 'btn-primary', style: 'align-self:flex-end;',
370
+ disabled: chatState.busy ? 'true' : null,
371
+ }, chatState.busy ? '…' : 'send')
372
+ )
373
+ ),
374
+ h('div', { id: 'fd-chat-msgs', style: 'max-height:420px;overflow-y:auto;background:rgba(0,0,0,0.12);border-radius:4px;padding:4px;margin-top:8px;' }),
375
+ ],
376
+ }),
377
+ configuredProviders.length === 0
378
+ ? Panel({ title: 'no providers configured', children: Receipt({ rows: [
379
+ ['set API key', 'go to keys tab, click a provider chip to set its key'],
380
+ ['then reload', 'refresh this page to see providers here'],
381
+ ['or use acptoapi', 'run acptoapi server on localhost:4800 for local LLMs'],
382
+ ] }) })
383
+ : Panel({ title: 'configured providers', children: h('div', { style: 'display:flex;flex-wrap:wrap;gap:6px;padding:8px 4px;' },
384
+ ...providers.map(p => Chip({ tone: p.configured ? (p.available ? 'ok' : 'warn') : 'miss', children: p.name + (p.configured ? (p.available ? ' ●' : ' ○') : '') }))
385
+ ) }),
386
+ ];
196
387
  },
197
- async sessions(h) {
198
- const list = await h.pi.sessions.list();
388
+ async sessions(h0) {
389
+ const list = await h0.pi.sessions.list();
390
+ const rows = list.map(s => {
391
+ const cont = h('button', {
392
+ class: 'btn-primary', style: 'padding:2px 8px;font-size:0.8em;',
393
+ onclick: async () => {
394
+ const msgs = await h0.pi.sessions.getMessages(s.id);
395
+ const cs = window.__fd_chatState = window.__fd_chatState || { messages: [], busy: false, sessionId: null, cwd: '', skill: '', provider: '', model: '' };
396
+ cs.sessionId = s.id;
397
+ cs.messages = msgs.map(m => ({ role: m.role, content: String(m.content || '') }));
398
+ if (s.cwd) cs.cwd = s.cwd;
399
+ if (s.skill) cs.skill = s.skill;
400
+ if (typeof window.__fd_nav === 'function') window.__fd_nav('chat');
401
+ },
402
+ }, 'continue');
403
+ return [(s.id || '').slice(0, 8), s.title || '—', s.platform || '—', s.model || '—', s.cwd ? s.cwd.slice(-30) : '—', s.skill ? skillLabel({ name: s.skill }) : '—', cont];
404
+ });
199
405
  return [
200
- kpi([[list.length, 'total sessions']]),
201
- panel('recent sessions', list.length === 0
202
- ? el('div', 'fdash-empty', { text: 'no sessions yet — start a chat' })
203
- : table(['id', 'title', 'platform', 'model', 'turns'], list.map(s => [(s.id || '').slice(0, 8), s.title || '', s.platform, s.model || '—', s.turn_count])), list.length),
406
+ Kpi({ items: [[list.length, 'sessions']] }),
407
+ Panel({ title: 'recent sessions', count: list.length, children: list.length === 0
408
+ ? EmptyState({ text: 'no sessions yet — open chat and send a message', glyph: '✉' })
409
+ : Table({ headers: ['id', 'title', 'platform', 'model', 'cwd', 'skill', ''],
410
+ rows }) }),
204
411
  ];
205
412
  },
206
- async agents(h) {
207
- const a = await h.pi.agents();
413
+ async agents(h0) {
414
+ const a = (typeof h0.pi.agents === 'function') ? await h0.pi.agents() : { count: 0, turns: 0, active: null };
208
415
  return [
209
- kpi([[a.count, 'active'], [a.turns, 'turns']]),
210
- panel('overview', table(['key', 'value'], [
211
- ['total turns', String(a.turns)],
416
+ Kpi({ items: [[a.count || 0, 'active'], [a.turns || 0, 'turns']] }),
417
+ Panel({ title: 'agent overview', children: Receipt({ rows: [
418
+ ['total turns', String(a.turns || 0)],
212
419
  ['active session', a.active || '(none)'],
213
420
  ['last activity', a.last_activity ? new Date(a.last_activity).toLocaleString() : '—'],
214
- ])),
421
+ ] }) }),
215
422
  ];
216
423
  },
217
- async analytics(h) {
218
- const list = await h.pi.sessions.list();
219
- const tools = [...h.pi.tools.values()];
424
+ async analytics(h0) {
425
+ const list = await h0.pi.sessions.list();
426
+ const tools = [...h0.pi.tools.values()];
220
427
  const byPlatform = list.reduce((a, s) => { const k = s.platform || '?'; a[k] = (a[k] || 0) + 1; return a; }, {});
221
428
  const byModel = list.reduce((a, s) => { const k = s.model || '?'; a[k] = (a[k] || 0) + 1; return a; }, {});
429
+ const byToolset = tools.reduce((a, t) => { (a[t.toolset || 'core'] = a[t.toolset || 'core'] || []).push(t.name); return a; }, {});
222
430
  return [
223
- kpi([[list.length, 'sessions'], [tools.length, 'tools']]),
224
- panel('sessions by platform', Object.keys(byPlatform).length === 0 ? el('div', 'fdash-empty', { text: 'no data' }) : table(['platform', 'count'], Object.entries(byPlatform))),
225
- panel('sessions by model', Object.keys(byModel).length === 0 ? el('div', 'fdash-empty', { text: 'no data' }) : table(['model', 'count'], Object.entries(byModel))),
226
- panel('tools', table(['name', 'description'], tools.map(t => [t.name, (t.description || '').slice(0, 80)]))),
431
+ Kpi({ items: [[list.length, 'sessions'], [tools.length, 'tools']] }),
432
+ Panel({ title: 'sessions by platform', children: Object.keys(byPlatform).length === 0
433
+ ? EmptyState({ text: 'no data', glyph: '' })
434
+ : Table({ headers: ['platform', 'count'], rows: Object.entries(byPlatform).sort((a, b) => b[1] - a[1]) }) }),
435
+ Panel({ title: 'sessions by model', children: Object.keys(byModel).length === 0
436
+ ? EmptyState({ text: 'no data', glyph: '◎' })
437
+ : Table({ headers: ['model', 'count'], rows: Object.entries(byModel).sort((a, b) => b[1] - a[1]) }) }),
438
+ Panel({ title: 'tool distribution', children: Table({ headers: ['toolset', 'count', 'tools'],
439
+ rows: Object.entries(byToolset).map(([k, v]) => [k, v.length, v.slice(0, 4).join(', ') + (v.length > 4 ? '…' : '')]) }) }),
227
440
  ];
228
441
  },
229
- async models(h) {
230
- const cfg = h.pi.config.load();
442
+ async models(h0) {
443
+ const cfg = (typeof h0.pi.config?.load === 'function') ? await h0.pi.config.load() : {};
231
444
  const agent = cfg.agent || {};
232
- const form = el('form', 'fdash-form', { on: { submit: (ev) => {
233
- ev.preventDefault();
234
- h.pi.config.saveValue('agent.provider', ev.target.elements.provider.value);
235
- h.pi.config.saveValue('agent.model', ev.target.elements.model.value);
236
- render();
237
- } } });
238
- form.appendChild(el('input', null, { name: 'provider', placeholder: 'provider', value: agent.provider || '' }));
239
- form.appendChild(el('input', null, { name: 'model', placeholder: 'model id', value: agent.model || '' }));
240
- form.appendChild(el('button', null, { type: 'submit', text: 'update' }));
445
+ const providers = await fetch('/api/providers').then(r => r.json()).catch(() => []);
241
446
  return [
242
- kpi([[agent.provider || '—', 'provider'], [agent.model || '—', 'model']]),
243
- panel('active model', table(['key', 'value'], [
447
+ Kpi({ items: [[agent.provider || '—', 'provider'], [agent.model || '—', 'model']] }),
448
+ Panel({ title: 'active model', children: Receipt({ rows: [
244
449
  ['provider', agent.provider || '(unset)'],
245
450
  ['model', agent.model || '(unset)'],
246
451
  ['max_iterations', String(agent.max_iterations || '—')],
247
- ])),
248
- panel('change model', form),
452
+ ['max_tokens', String(agent.max_tokens || '—')],
453
+ ['temperature', String(agent.temperature ?? '—')],
454
+ ] }) }),
455
+ Panel({ title: 'change model', children: form({
456
+ fields: [{ name: 'provider', placeholder: 'provider', value: agent.provider || '' }, { name: 'model', placeholder: 'model id', value: agent.model || '' }],
457
+ submit: 'update',
458
+ onSubmit: async (ev) => {
459
+ await h0.pi.config.saveValue('agent.provider', ev.target.elements.provider.value);
460
+ await h0.pi.config.saveValue('agent.model', ev.target.elements.model.value);
461
+ rerender();
462
+ },
463
+ }) }),
464
+ Panel({ title: 'provider availability', children: h('div', { style: 'display:flex;flex-wrap:wrap;gap:6px;padding:8px 4px;' },
465
+ ...providers.map(p => Chip({ tone: p.configured ? (p.available ? 'ok' : 'warn') : 'miss', children: p.name + (p.configured ? (p.available ? ' ●' : ' ○') : ' ·') }))
466
+ ) }),
249
467
  ];
250
468
  },
251
- async logs(h) {
252
- const dbg = h.pi.debug();
253
- return [
254
- panel('host debug snapshot', pre(dbg)),
255
- ];
469
+ async logs(h0) {
470
+ const dbg = (typeof h0.pi.debug === 'function') ? h0.pi.debug() : { note: 'no debug surface' };
471
+ return [Panel({ title: 'host debug snapshot', children: pre(dbg) })];
256
472
  },
257
- async cron(h) {
258
- const list = await h.pi.cron.list();
259
- const form = el('form', 'fdash-form', { on: { submit: async (ev) => {
260
- ev.preventDefault();
261
- try { await h.pi.cron.create({ cron: ev.target.elements.cron.value, prompt: ev.target.elements.prompt.value }); render(); }
262
- catch (e) { alert(e.message); }
263
- } } });
264
- form.appendChild(el('input', null, { name: 'cron', placeholder: '* * * * *', required: 'true' }));
265
- form.appendChild(el('input', null, { name: 'prompt', placeholder: 'prompt', required: 'true' }));
266
- form.appendChild(el('button', null, { type: 'submit', text: 'create' }));
267
- const tbl = list.length === 0
268
- ? el('div', 'fdash-empty', { text: 'no cron jobs' })
269
- : table(['id', 'cron', 'prompt', 'enabled'], list.map(j => [j.id, j.cron, (j.prompt || '').slice(0, 40), j.enabled ? 'yes' : 'no']));
473
+ async cron(h0) {
474
+ const list = await h0.pi.cron.list();
270
475
  return [
271
- kpi([[list.length, 'cron jobs']]),
272
- panel('add job', form),
273
- panel('jobs', tbl, list.length),
476
+ Kpi({ items: [[list.length, 'cron jobs']] }),
477
+ Panel({ title: 'add job', children: form({
478
+ fields: [{ name: 'cron', placeholder: '* * * * *', required: true }, { name: 'prompt', placeholder: 'prompt', required: true }],
479
+ submit: 'create',
480
+ onSubmit: async (ev) => { try { await h0.pi.cron.create({ cron: ev.target.elements.cron.value, prompt: ev.target.elements.prompt.value }); rerender(); } catch (e) { alert(e.message); } },
481
+ }) }),
482
+ Panel({ title: 'scheduled jobs', count: list.length, children: list.length === 0
483
+ ? EmptyState({ text: 'no cron jobs — add one above', glyph: '◷' })
484
+ : Table({ headers: ['id', 'cron', 'prompt', 'enabled'],
485
+ rows: list.map(j => [j.id, j.cron, (j.prompt || '').slice(0, 40), j.enabled ? 'yes' : 'no']) }) }),
274
486
  ];
275
487
  },
276
- async skills(h) {
277
- const list = [...h.pi.skills.values()];
488
+ async skills(h0) {
489
+ const list = [...h0.pi.skills.values()];
278
490
  const byCat = list.reduce((a, s) => { (a[s.category || 'other'] = a[s.category || 'other'] || []).push(s); return a; }, {});
279
- const out = [kpi([[list.length, 'skills'], [Object.keys(byCat).length, 'categories']])];
280
- for (const [cat, ss] of Object.entries(byCat)) {
281
- out.push(panel(cat, table(['name', 'description'], ss.map(s => [s.shortName || s.name, (s.description || '').slice(0, 80)])), ss.length));
282
- }
283
- return out;
491
+ return [
492
+ Kpi({ items: [[list.length, 'skills'], [Object.keys(byCat).length, 'categories']] }),
493
+ list.length === 0 ? EmptyState({ text: 'no skills loaded add SKILL.md files to ~/.freddie/skills/', glyph: '◈' }) : null,
494
+ ...Object.entries(byCat).map(([cat, ss]) => Panel({ title: cat, count: ss.length,
495
+ children: ss.length === 0 ? EmptyState({ text: 'none', glyph: '◈' })
496
+ : Table({ headers: ['name', 'description'], rows: ss.map(s => [skillLabel(s), (s.description || '').slice(0, 120)]) }) })),
497
+ ].filter(Boolean);
284
498
  },
285
- async config(h) {
286
- const cfg = h.pi.config.load();
287
- const profiles = h.pi.profiles.list();
288
- const commands = h.pi.commands.list();
289
- const form = el('form', 'fdash-form', { on: { submit: (ev) => {
290
- ev.preventDefault();
291
- let v = ev.target.elements.value.value;
292
- try { v = JSON.parse(v); } catch {}
293
- h.pi.config.saveValue(ev.target.elements.key.value, v);
294
- render();
295
- } } });
296
- form.appendChild(el('input', null, { name: 'key', placeholder: 'dotted.key', required: 'true' }));
297
- form.appendChild(el('input', null, { name: 'value', placeholder: 'value (json or string)', required: 'true' }));
298
- form.appendChild(el('button', null, { type: 'submit', text: 'save' }));
499
+ async config(h0) {
500
+ const cfg = (typeof h0.pi.config?.load === 'function') ? await h0.pi.config.load() : {};
501
+ const profiles = (typeof h0.pi.profiles?.list === 'function') ? h0.pi.profiles.list() : [];
502
+ const commands = (typeof h0.pi.commands?.list === 'function') ? h0.pi.commands.list() : [];
299
503
  return [
300
- kpi([[profiles.length, 'profiles'], [commands.length, 'commands'], [cfg._config_version || 0, 'config version']]),
301
- panel('set value', form),
302
- panel('commands', table(['name', 'category', 'description'], commands.map(c => [c.name, c.category, c.description])), commands.length),
303
- panel('active config', pre(cfg)),
504
+ Kpi({ items: [[profiles.length, 'profiles'], [commands.length, 'commands'], [cfg._config_version || 0, 'config version']] }),
505
+ Panel({ title: 'set config value', children: form({
506
+ fields: [{ name: 'key', placeholder: 'dotted.key (e.g. agent.model)', required: true }, { name: 'value', placeholder: 'value (json or string)', required: true }],
507
+ submit: 'save',
508
+ onSubmit: async (ev) => {
509
+ let v = ev.target.elements.value.value;
510
+ try { v = JSON.parse(v); } catch {}
511
+ await h0.pi.config.saveValue(ev.target.elements.key.value, v);
512
+ rerender();
513
+ },
514
+ }) }),
515
+ Panel({ title: 'commands', count: commands.length,
516
+ children: Table({ headers: ['name', 'category', 'description'], rows: commands.map(c => [c.name, c.category || '', c.description || '']) }) }),
517
+ Panel({ title: 'active config', children: pre(cfg) }),
304
518
  ];
305
519
  },
306
- async env(h) {
307
- const list = h.pi.env.list();
520
+ async env(h0) {
521
+ const list = (typeof h0.pi.env?.list === 'function') ? h0.pi.env.list() : [];
308
522
  const setCount = list.filter(k => k.set).length;
309
- const chips = el('div');
310
- for (const k of list) {
311
- const c = el('span', 'fdash-chip ' + (k.set ? 'ok' : 'miss'), { text: k.key + (k.set ? ' ✓' : '') });
312
- c.addEventListener('click', () => {
313
- const v = prompt('set ' + k.key + ' (empty to unset):');
314
- if (v == null) return;
315
- h.pi.env.set(k.key, v);
316
- render();
317
- });
318
- chips.appendChild(c);
319
- }
523
+ const chipNodes = list.map(k => h(
524
+ 'span',
525
+ {
526
+ key: k.key,
527
+ onclick: () => {
528
+ const v = prompt('set ' + k.key + ' (empty to unset):');
529
+ if (v == null) return;
530
+ if (typeof h0.pi.env.set === 'function') { h0.pi.env.set(k.key, v); rerender(); }
531
+ },
532
+ style: 'cursor:pointer',
533
+ },
534
+ Chip({ tone: k.set ? 'ok' : 'miss', children: k.key + (k.set ? ' ✓' : ' ·') })
535
+ ));
320
536
  return [
321
- kpi([[setCount, 'set'], [list.length - setCount, 'missing'], [list.length, 'total known']]),
322
- panel('environment variables (click to set/unset)', chips),
537
+ Kpi({ items: [[setCount, 'set'], [list.length - setCount, 'missing'], [list.length, 'total known']] }),
538
+ Panel({
539
+ title: 'environment variables',
540
+ right: h('span', {}, Chip({ tone: 'ok', children: setCount + ' set' }), ' ', Chip({ tone: 'miss', children: (list.length - setCount) + ' missing' })),
541
+ children: h('div', { style: 'padding:8px 4px;display:flex;flex-wrap:wrap;gap:6px' }, ...chipNodes),
542
+ }),
323
543
  ];
324
544
  },
325
- async tools(h) {
326
- const list = [...h.pi.tools.values()];
545
+ async tools(h0) {
546
+ const list = [...h0.pi.tools.values()];
547
+ const byToolset = list.reduce((a, t) => { (a[t.toolset || 'core'] = a[t.toolset || 'core'] || []).push(t); return a; }, {});
327
548
  return [
328
- kpi([[list.length, 'tools']]),
329
- panel('all tools', table(['name', 'description'], list.map(t => [t.name, (t.description || '').slice(0, 100)])), list.length),
549
+ Kpi({ items: [[list.length, 'tools'], [Object.keys(byToolset).length, 'toolsets']] }),
550
+ ...Object.entries(byToolset).map(([ts, items]) => Panel({ title: 'toolset · ' + ts, count: items.length,
551
+ children: items.map(t => Row({ key: t.name, code: '⚒', title: t.name, sub: (t.description || (t.schema && t.schema.description) || '').slice(0, 80) })) })),
330
552
  ];
331
553
  },
332
- async batch(h) {
333
- const out = el('div');
334
- const form = el('form', 'fdash-form', { on: { submit: async (ev) => {
335
- ev.preventDefault();
336
- const prompts = ev.target.elements.prompts.value.split('\n').map(s => s.trim()).filter(Boolean);
337
- if (!prompts.length) return;
338
- out.textContent = 'running…';
339
- try { const r = await h.pi.batch.run({ prompts, concurrency: Number(ev.target.elements.conc.value) || 4 });
340
- out.innerHTML = ''; out.appendChild(pre(r));
341
- } catch (e) { out.textContent = 'error: ' + (e.message || e); }
342
- } } });
343
- const ta = el('textarea', null, { name: 'prompts', rows: '5', placeholder: 'one prompt per line' });
344
- form.appendChild(ta);
345
- form.appendChild(el('input', null, { name: 'conc', type: 'number', value: '4' }));
346
- form.appendChild(el('button', null, { type: 'submit', text: 'run' }));
554
+ async batch(h0) {
555
+ const out = h('div', { id: 'fd-batch-out' });
347
556
  return [
348
- panel('run prompts', form),
349
- panel('results', out),
557
+ Section({ title: '// batch runner', children: [
558
+ Panel({ title: 'run prompts', children: form({
559
+ fields: [{ name: 'prompts', kind: 'textarea', placeholder: 'one prompt per line' }, { name: 'concurrency', type: 'number', value: '4' }],
560
+ submit: 'run',
561
+ onSubmit: async (ev) => {
562
+ const prompts = ev.target.elements.prompts.value.split('\n').map(s => s.trim()).filter(Boolean);
563
+ if (!prompts.length) return;
564
+ const node = root.querySelector('#fd-batch-out');
565
+ if (node) node.textContent = 'running…';
566
+ try {
567
+ const r = await h0.pi.batch.run({ prompts, concurrency: Number(ev.target.elements.concurrency.value) || 4 });
568
+ if (node) { node.innerHTML = ''; node.appendChild(document.createTextNode(JSON.stringify(r, null, 2))); }
569
+ } catch (e) { if (node) node.textContent = 'error: ' + (e.message || e); }
570
+ },
571
+ }) }),
572
+ Panel({ title: 'results', children: out }),
573
+ Panel({ title: 'cli usage', children: Receipt({ rows: [
574
+ ['run batch file', 'freddie batch prompts.txt'],
575
+ ['set concurrency', 'freddie batch prompts.txt --concurrency 8'],
576
+ ['jsonl output', 'freddie batch prompts.txt > out.jsonl'],
577
+ ] }) }),
578
+ ] }),
350
579
  ];
351
580
  },
352
- async gateway(h) {
353
- const platforms = h.pi.gateway.platforms();
581
+ async gateway(h0) {
582
+ const platforms = (typeof h0.pi.gateway?.platforms === 'function') ? h0.pi.gateway.platforms() : [];
583
+ const active = platforms.filter(p => p.enabled);
354
584
  return [
355
- kpi([[platforms.length, 'platforms'], [platforms.filter(p => p.enabled).length, 'active']]),
356
- panel('platforms', table(['name', 'enabled', 'note'], platforms.map(p => [p.name, p.enabled ? 'yes' : 'no', p.note])), platforms.length),
585
+ Kpi({ items: [[platforms.length, 'platforms'], [active.length, 'active']] }),
586
+ Panel({ title: 'platforms', count: platforms.length,
587
+ right: active.length > 0 ? Chip({ tone: 'ok', children: active.length + ' active' }) : Chip({ tone: 'miss', children: 'none active' }),
588
+ children: platforms.length === 0 ? EmptyState({ text: 'no platforms registered', glyph: '⇌' })
589
+ : platforms.map(p => Row({ key: p.name, code: p.enabled ? '●' : '○', title: p.name, sub: p.note || '', meta: p.enabled ? 'enabled' : '' })) }),
590
+ Panel({ title: 'start gateway', children: Receipt({ rows: [
591
+ ['webhook + api_server', 'freddie gateway --port 3000'],
592
+ ['specific platform', 'TELEGRAM_BOT_TOKEN=… freddie gateway'],
593
+ ['all platforms', 'set env vars per platform, then freddie gateway'],
594
+ ] }) }),
357
595
  ];
358
596
  },
359
597
  async ['os-instances']() {
360
598
  const list = (osSurfaces && osSurfaces.instances && osSurfaces.instances()) || [];
361
599
  const activeId = osSurfaces && osSurfaces.activeInstanceId && osSurfaces.activeInstanceId();
362
600
  return [
363
- kpi([[list.length, 'instances'], [activeId || '—', 'active']]),
364
- panel('instances', table(['id', 'active', 'shells', 'windows'],
365
- list.map(i => [i.id, i.id === activeId ? '●' : '', String((i.shells || []).length), String((i.windows || []).length)])), list.length),
601
+ Kpi({ items: [[list.length, 'instances'], [activeId || '—', 'active']] }),
602
+ Panel({ title: 'instances', count: list.length, children: list.length === 0
603
+ ? EmptyState({ text: 'no instances', glyph: '' })
604
+ : Table({ headers: ['id', 'active', 'shells', 'windows'],
605
+ rows: list.map(i => [i.id, i.id === activeId ? '●' : '', String((i.shells || []).length), String((i.windows || []).length)]) }) }),
366
606
  ];
367
607
  },
368
608
  async ['os-windows']() {
369
609
  const wins = (osSurfaces && osSurfaces.wm && osSurfaces.wm.list && osSurfaces.wm.list()) || [];
370
610
  const focused = osSurfaces && osSurfaces.wm && osSurfaces.wm.focused;
371
611
  return [
372
- kpi([[wins.length, 'windows'], [focused ? (focused.id || focused.title || '?') : '—', 'focused']]),
373
- panel('windows', table(['id', 'title', 'min', 'max', 'pos'],
374
- wins.map(w => [w.id || '?', w.title || '', w.min ? '●' : '', w.max ? '●' : '',
375
- (w.el ? `${w.el.offsetLeft},${w.el.offsetTop} ${w.el.offsetWidth}×${w.el.offsetHeight}` : '')])), wins.length),
612
+ Kpi({ items: [[wins.length, 'windows'], [focused ? (focused.id || focused.title || '?') : '—', 'focused']] }),
613
+ Panel({ title: 'windows', count: wins.length, children: wins.length === 0
614
+ ? EmptyState({ text: 'no windows open', glyph: '' })
615
+ : Table({ headers: ['id', 'title', 'min', 'max', 'pos'],
616
+ rows: wins.map(w => [w.id || '?', w.title || '', w.min ? '●' : '', w.max ? '●' : '',
617
+ (w.el ? `${w.el.offsetLeft},${w.el.offsetTop} ${w.el.offsetWidth}×${w.el.offsetHeight}` : '')]) }) }),
376
618
  ];
377
619
  },
378
620
  async ['os-x']() {
379
621
  const x = osSurfaces && osSurfaces.xServer && osSurfaces.xServer();
380
- if (!x) return [el('div', 'fdash-empty', { text: 'x-server not running in this instance' })];
622
+ if (!x) return [Panel({ title: 'x-server', children: EmptyState({ text: 'x-server not running in this instance', glyph: '✕' }) })];
381
623
  return [
382
- kpi([[x.windows, 'windows'], [x.pixmaps, 'pixmaps'], [x.gcs, 'gcs'], [x.atoms, 'atoms'], [x.cursors, 'cursors']]),
383
- panel('display', pre(x)),
624
+ Kpi({ items: [[x.windows, 'windows'], [x.pixmaps, 'pixmaps'], [x.gcs, 'gcs'], [x.atoms, 'atoms'], [x.cursors, 'cursors']] }),
625
+ Panel({ title: 'display', children: pre(x) }),
384
626
  ];
385
627
  },
386
628
  async ['os-fs']() {
387
629
  const list = await instance.fs.list('/');
388
630
  return [
389
- kpi([[list.length, 'paths'], [instance.id, 'instance']]),
390
- panel('paths', el('pre', null, { text: list.join('\n') }), list.length),
631
+ Kpi({ items: [[list.length, 'paths'], [instance.id, 'instance']] }),
632
+ Panel({ title: 'paths', count: list.length, children: list.length === 0
633
+ ? EmptyState({ text: 'empty fs', glyph: '📁' })
634
+ : pre(list.join('\n')) }),
391
635
  ];
392
636
  },
393
637
  };
394
638
 
395
- setActive('home');
639
+ rerender();
396
640
 
397
641
  if (typeof window !== 'undefined') {
398
642
  window.__debug = window.__debug || {};
399
643
  window.__debug.instances = window.__debug.instances || {};
400
644
  window.__debug.instances[instance.id] = window.__debug.instances[instance.id] || {};
401
- window.__debug.instances[instance.id].dashboard = { root, routes: [...ROUTES, ...OS_ROUTES].map(r => r.path), setActive, get active() { return active; } };
645
+ window.__debug.instances[instance.id].dashboard = {
646
+ root,
647
+ routes: allRoutes.map(r => r.path),
648
+ setActive,
649
+ get active() { return state.active; },
650
+ };
402
651
  }
403
652
 
404
653
  return { node: root, dispose() {} };