anentrypoint-design 0.0.62 → 0.0.65

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anentrypoint-design",
3
- "version": "0.0.62",
3
+ "version": "0.0.65",
4
4
  "description": "247420 design system SDK — webjsx + modified ripple-ui, single-file ESM bundle for reproducible use of the AnEntrypoint design.",
5
5
  "type": "module",
6
6
  "main": "./dist/247420.js",
@@ -23,6 +23,7 @@
23
23
  "./desktop/icons.js": "./src/desktop/icons.js",
24
24
  "./desktop/shell.js": "./src/desktop/shell.js",
25
25
  "./desktop/freddie-dashboard.js": "./src/desktop/freddie-dashboard.js",
26
+ "./desktop/freddie-dashboard.css": "./src/desktop/freddie-dashboard.css",
26
27
  "./page-html": {
27
28
  "import": "./src/page-html.js",
28
29
  "default": "./src/page-html.js"
@@ -0,0 +1,16 @@
1
+ /* freddie-dashboard.css — minimal supplemental rule.
2
+ The dashboard now renders through bible components (AppShell/Panel/Kpi/
3
+ Table/Receipt/Hero/Row/EmptyState/Chip) that ship their own classes via
4
+ 247420.css. The only extra: a styled <pre> for raw JSON dumps. */
5
+ .app-fd .fd-pre {
6
+ font-family: var(--ff-mono, JetBrains Mono, monospace);
7
+ font-size: 12px;
8
+ white-space: pre-wrap;
9
+ word-break: break-all;
10
+ margin: 0;
11
+ max-height: 320px;
12
+ overflow: auto;
13
+ background: var(--panel-2, #DDD3BC);
14
+ padding: 8px 10px;
15
+ border-radius: var(--r-1, 6px);
16
+ }
@@ -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,130 +26,40 @@ 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
- }
30
-
31
- function ensureStyles() {
32
- if (document.getElementById('freddie-dashboard-style')) return;
33
- const s = document.createElement('style');
34
- s.id = 'freddie-dashboard-style';
35
- s.textContent = `
36
- .fdash { display: flex; height: 100%; font-family: var(--ff-ui, Nunito, sans-serif); color: var(--ink, #1F1B16); }
37
- .fdash-side { flex: 0 0 180px; background: var(--panel-1, #ECE6D5); border-right: 1px solid var(--panel-2, #DDD3BC); padding: 8px 0; overflow-y: auto; }
38
- .fdash-nav { display: flex; flex-direction: column; gap: 2px; }
39
- .fdash-nav button { text-align: left; padding: 6px 12px; background: transparent; color: inherit; border: 0; cursor: pointer; font-family: inherit; font-size: 12px; border-left: 4px solid transparent; }
40
- .fdash-nav button:hover { background: var(--panel-hover, rgba(0,0,0,0.04)); }
41
- .fdash-nav button.active { background: var(--panel-select, #C8E4CA); border-left-color: var(--panel-accent, #3F8A4A); font-weight: 600; }
42
- .fdash-nav .glyph { display: inline-block; width: 16px; opacity: 0.7; font-family: var(--ff-mono, JetBrains Mono, monospace); }
43
- .fdash-main { flex: 1; padding: 12px 16px; overflow: auto; min-width: 0; }
44
- .fdash-h { font-size: 14px; font-weight: 700; margin: 0 0 8px; }
45
- .fdash-kpi { display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
46
- .fdash-kpi .k { background: var(--panel-2, #DDD3BC); padding: 6px 12px; border-radius: var(--r-1, 6px); min-width: 80px; }
47
- .fdash-kpi .k .v { font-size: 18px; font-weight: 700; font-family: var(--ff-mono, JetBrains Mono, monospace); }
48
- .fdash-kpi .k .l { font-size: 10px; opacity: 0.7; text-transform: uppercase; }
49
- .fdash-panel { background: var(--panel-1, #ECE6D5); border-radius: var(--r-1, 6px); padding: 8px 12px; margin-bottom: 8px; }
50
- .fdash-panel h3 { font-size: 12px; font-weight: 700; margin: 0 0 6px; opacity: 0.8; text-transform: uppercase; }
51
- .fdash-panel pre { font-family: var(--ff-mono, JetBrains Mono, monospace); font-size: 11px; white-space: pre-wrap; word-break: break-all; margin: 0; max-height: 280px; overflow: auto; }
52
- .fdash-panel table { width: 100%; border-collapse: collapse; font-size: 12px; }
53
- .fdash-panel th, .fdash-panel td { padding: 4px 8px; text-align: left; border-bottom: 1px solid var(--panel-2, #DDD3BC); }
54
- .fdash-panel th { font-weight: 600; opacity: 0.7; font-size: 10px; text-transform: uppercase; }
55
- .fdash-row { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 12px; }
56
- .fdash-row .code { font-family: var(--ff-mono, JetBrains Mono, monospace); opacity: 0.6; }
57
- .fdash-row .meta { margin-left: auto; opacity: 0.5; font-size: 11px; }
58
- .fdash-form { display: flex; gap: 4px; flex-wrap: wrap; align-items: center; margin-bottom: 8px; }
59
- .fdash-form input, .fdash-form textarea, .fdash-form select { font: inherit; padding: 4px 8px; border: 1px solid var(--panel-2, #DDD3BC); border-radius: var(--r-1, 6px); background: var(--panel-0, #F5F0E4); color: inherit; }
60
- .fdash-form button { padding: 4px 12px; background: var(--panel-accent, #3F8A4A); color: #fff; border: 0; border-radius: var(--r-1, 6px); cursor: pointer; }
61
- .fdash-form button.danger { background: #c44; }
62
- .fdash-chip { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; margin: 2px; }
63
- .fdash-chip.ok { background: var(--panel-select, #C8E4CA); }
64
- .fdash-chip.miss { background: var(--panel-2, #DDD3BC); opacity: 0.6; }
65
- .fdash-empty { padding: 20px; text-align: center; opacity: 0.5; font-size: 12px; }
66
- @media (max-width: 600px) { .fdash-side { flex: 0 0 56px; } .fdash-nav button .label { display: none; } }
67
- `;
68
- document.head.appendChild(s);
69
- }
70
-
71
- function kpi(items) {
72
- const c = el('div', 'fdash-kpi');
73
- for (const [v, l] of items) {
74
- const k = el('div', 'k');
75
- k.appendChild(el('div', 'v', { text: String(v) }));
76
- k.appendChild(el('div', 'l', { text: String(l) }));
77
- c.appendChild(k);
78
- }
79
- return c;
80
- }
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
+ ];
81
35
 
82
- function panel(title, body, count) {
83
- const p = el('div', 'fdash-panel');
84
- const h = el('h3'); h.textContent = title + (count != null ? ' · ' + count : '');
85
- p.appendChild(h);
86
- if (body instanceof Node) p.appendChild(body);
87
- else if (Array.isArray(body)) for (const n of body) if (n) p.appendChild(n);
88
- else if (typeof body === 'string') { const pre = el('pre'); pre.textContent = body; p.appendChild(pre); }
89
- return p;
36
+ function pre(obj) {
37
+ return h('pre', { class: 'fd-pre' }, typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2));
90
38
  }
91
39
 
92
- function row(opts) {
93
- const r = el('div', 'fdash-row');
94
- if (opts.code) r.appendChild(el('span', 'code', { text: opts.code }));
95
- r.appendChild(el('span', 'title', { text: opts.title || '' }));
96
- if (opts.sub) r.appendChild(el('span', 'sub', { text: ' ' + opts.sub }));
97
- if (opts.meta) r.appendChild(el('span', 'meta', { text: opts.meta }));
98
- return r;
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));
99
47
  }
100
48
 
101
- function table(headers, rows) {
102
- const t = el('table');
103
- const thead = el('thead'); const trh = el('tr');
104
- for (const h of headers) trh.appendChild(el('th', null, { text: h }));
105
- thead.appendChild(trh); t.appendChild(thead);
106
- const tb = el('tbody');
107
- for (const r of rows) {
108
- const tr = el('tr');
109
- for (const c of r) tr.appendChild(el('td', null, { text: String(c) }));
110
- tb.appendChild(tr);
111
- }
112
- t.appendChild(tb);
113
- return t;
114
- }
49
+ export function createFreddieDashboard({ instance, bootHost, osSurfaces }) {
50
+ const root = document.createElement('div');
51
+ root.className = 'app-fd ds-247420';
52
+ root.style.cssText = 'height:100%;overflow:hidden;display:flex;flex-direction:column;';
115
53
 
116
- function pre(obj) { return el('pre', null, { text: typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2) }); }
117
-
118
- export function createFreddieDashboard({ instance, bootHost }) {
119
- ensureStyles();
120
- const root = el('div', 'fdash');
121
- const side = el('div', 'fdash-side');
122
- const nav = el('div', 'fdash-nav');
123
- const main = el('div', 'fdash-main');
124
- side.appendChild(nav);
125
- root.appendChild(side); root.appendChild(main);
126
-
127
- let active = 'home';
54
+ const state = {
55
+ active: 'home',
56
+ ts: new Date().toLocaleTimeString(),
57
+ body: null,
58
+ error: null,
59
+ };
128
60
  let host = instance.host || null;
129
61
 
130
- function setActive(p) {
131
- active = p;
132
- for (const b of nav.querySelectorAll('button')) b.classList.toggle('active', b.dataset.path === p);
133
- render();
134
- }
135
-
136
- for (const r of ROUTES) {
137
- const b = el('button', null, { 'data-path': r.path, on: { click: () => setActive(r.path) } });
138
- b.appendChild(el('span', 'glyph', { text: r.glyph }));
139
- b.appendChild(document.createTextNode(' '));
140
- b.appendChild(el('span', 'label', { text: r.label }));
141
- nav.appendChild(b);
142
- }
62
+ const allRoutes = osSurfaces ? [...ROUTES, ...OS_ROUTE_DEFS] : ROUTES;
143
63
 
144
64
  async function ensureHost() {
145
65
  if (host) return host;
@@ -148,247 +68,362 @@ export function createFreddieDashboard({ instance, bootHost }) {
148
68
  return host;
149
69
  }
150
70
 
151
- async function render() {
152
- main.innerHTML = '';
153
- main.appendChild(el('h2', 'fdash-h', { text: 'freddie · ' + instance.id + ' · ' + active }));
154
- const h = await ensureHost();
155
- const page = PAGES[active] || PAGES.home;
71
+ function setActive(p) {
72
+ state.active = p;
73
+ rerender();
74
+ }
75
+
76
+ function buildSide() {
77
+ const sections = [{
78
+ group: 'FREDDIE',
79
+ items: ROUTES.map(r => ({
80
+ glyph: r.glyph, label: r.label, href: '#fd-' + r.path,
81
+ active: state.active === r.path,
82
+ onClick: (ev) => { ev.preventDefault(); setActive(r.path); },
83
+ })),
84
+ }];
85
+ if (osSurfaces) sections.push({
86
+ group: 'OS',
87
+ items: OS_ROUTE_DEFS.map(r => ({
88
+ glyph: r.glyph, label: r.label, href: '#fd-' + r.path,
89
+ active: state.active === r.path,
90
+ onClick: (ev) => { ev.preventDefault(); setActive(r.path); },
91
+ })),
92
+ });
93
+ return Side({ sections });
94
+ }
95
+
96
+ function view() {
97
+ const route = allRoutes.find(r => r.path === state.active) || ROUTES[1];
98
+ return AppShell({
99
+ topbar: Topbar({ brand: 'freddie', leaf: 'dashboard', items: [], active: '' }),
100
+ crumb: Crumb({ trail: ['freddie', instance.id], leaf: route.path, right: state.error ? Chip({ tone: 'miss', children: 'error' }) : Chip({ tone: 'ok', children: 'live' }) }),
101
+ side: buildSide(),
102
+ main: state.body || EmptyState({ text: 'loading…', glyph: '◌' }),
103
+ status: Status({ left: ['ds-247420 · webjsx · ' + allRoutes.length + ' routes', 'instance=' + instance.id], right: [state.ts] }),
104
+ });
105
+ }
106
+
107
+ function rerender() {
108
+ webjsx.applyDiff(root, view());
109
+ loadActive();
110
+ }
111
+
112
+ async function loadActive() {
156
113
  try {
157
- const body = await page(h, instance);
158
- const arr = Array.isArray(body) ? body : [body];
159
- for (const n of arr) if (n) main.appendChild(n);
114
+ const h0 = await ensureHost();
115
+ const page = PAGES[state.active] || PAGES.home;
116
+ state.body = await page(h0, instance);
117
+ state.error = null;
160
118
  } catch (e) {
161
- main.appendChild(panel('error', el('pre', null, { text: String(e && e.stack || e) })));
119
+ state.error = String(e && e.stack || e);
120
+ state.body = Panel({ title: 'error', children: pre(state.error) });
162
121
  }
122
+ state.ts = new Date().toLocaleTimeString();
123
+ webjsx.applyDiff(root, view());
163
124
  }
164
125
 
165
126
  const PAGES = {
166
- async projects(h) {
167
- const list = h.pi.projects.list();
168
- const active = h.pi.projects.active();
169
- const form = el('form', 'fdash-form', { on: { submit: (ev) => {
170
- ev.preventDefault();
171
- try { h.pi.projects.create({ name: ev.target.elements.name.value, path: ev.target.elements.path.value }); render(); }
172
- catch (e) { alert(e.message); }
173
- } } });
174
- form.appendChild(el('input', null, { name: 'name', placeholder: 'project name', required: 'true' }));
175
- form.appendChild(el('input', null, { name: 'path', placeholder: '/path' }));
176
- form.appendChild(el('button', null, { type: 'submit', text: 'add' }));
177
- const rows = list.map(p => {
178
- const r = el('div', 'fdash-row');
179
- r.appendChild(el('span', 'code', { text: p.name === active?.name ? '●' : '○' }));
180
- r.appendChild(el('span', null, { text: p.name + (p.name === active?.name ? ' (active)' : '') }));
181
- r.appendChild(el('span', 'meta', { text: p.path }));
182
- if (p.name !== 'default') {
183
- const del = el('button', null, { type: 'button', text: 'remove', on: { click: () => { try { h.pi.projects.remove(p.name); render(); } catch (e) { alert(e.message); } } } });
184
- r.appendChild(del);
185
- }
186
- if (p.name !== active?.name) {
187
- const sw = el('button', null, { type: 'button', text: 'switch', on: { click: () => { h.pi.projects.setActive(p.name); render(); } } });
188
- r.appendChild(sw);
189
- }
190
- return r;
191
- });
127
+ async projects(h0) {
128
+ const list = h0.pi.projects.list();
129
+ const activeProj = (typeof h0.pi.projects.active === 'function') ? h0.pi.projects.active() : null;
130
+ const rows = list.map(p => Row({
131
+ key: p.name,
132
+ code: p.name === activeProj?.name ? '●' : '○',
133
+ title: p.name + (p.name === activeProj?.name ? ' (active)' : ''),
134
+ meta: p.path,
135
+ onClick: () => { if (p.name !== activeProj?.name) try { h0.pi.projects.setActive(p.name); rerender(); } catch (e) { alert(e.message); } },
136
+ }));
192
137
  return [
193
- kpi([[list.length, 'projects'], [active?.name || '—', 'active'], [active?.path || '', 'path']]),
194
- panel('add a project', form),
195
- panel('all projects', rows, list.length),
138
+ 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' }),
139
+ Kpi({ items: [[list.length, 'projects'], [activeProj?.name || '—', 'active'], [activeProj?.path?.length > 30 ? '…' + activeProj.path.slice(-28) : (activeProj?.path || '—'), 'path']] }),
140
+ Panel({ title: 'add a project', children: form({
141
+ fields: [{ name: 'name', placeholder: 'project name', required: true }, { name: 'path', placeholder: '/abs/path' }],
142
+ submit: 'add',
143
+ 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); } },
144
+ }) }),
145
+ Panel({ title: 'all projects', count: list.length, children: rows.length ? rows : EmptyState({ text: 'no projects', glyph: '◆' }) }),
146
+ Panel({ title: 'how encapsulation works', children: Receipt({ rows: [
147
+ ['sessions db', '<project>/sessions.db'],
148
+ ['config', '<project>/config.json'],
149
+ ['skills', '<project>/skills/'],
150
+ ['plugins', '<project>/plugins/'],
151
+ ['cron', '<project>/cron.db'],
152
+ ['batches', '<project>/batches/'],
153
+ ['logs', '<project>/logs/'],
154
+ ['auth', '<project>/auth.json'],
155
+ ] }) }),
196
156
  ];
197
157
  },
198
- async home(h) {
199
- const sessions = await h.pi.sessions.list();
200
- const tools = h.pi.tools.size;
201
- const skills = h.pi.skills.size;
202
- const health = h.pi.health();
158
+ async home(h0) {
159
+ const sessions = await h0.pi.sessions.list();
160
+ const tools = h0.pi.tools.size;
161
+ const skills = h0.pi.skills.size;
162
+ const health = (typeof h0.pi.health === 'function') ? h0.pi.health() : { ok: true };
203
163
  return [
204
- kpi([[sessions.length, 'sessions'], [tools, 'tools'], [skills, 'skills']]),
205
- panel('quick start', table(['action', 'how'], [
206
- ['open chat', "click 'chat' in sidebar"],
207
- ['list tools', '/tools in chat or → tools tab'],
208
- ['list skills', '/skills in chat or skills tab'],
209
- ['set api key', '→ keys tab → set ENV var'],
210
- ])),
211
- panel('host', table(['key', 'value'], Object.entries(health))),
164
+ Hero({ title: 'freddie', body: 'open js agent harness — pi-mono · xstate · floosie · anentrypoint-design.', accent: h0.version || 'web' }),
165
+ Kpi({ items: [[sessions.length, 'sessions'], [tools, 'tools'], [skills, 'skills']] }),
166
+ Panel({ title: 'quick start', children: Receipt({ rows: [
167
+ ['open chat', "click 'chat' in sidebar"],
168
+ ['list tools', '/tools in chat → tools tab'],
169
+ ['list skills', '/skills skills tab'],
170
+ ['set api key', 'keys tab → click chip'],
171
+ ['add cron', 'cron tab → form'],
172
+ ] }) }),
173
+ Panel({ title: 'host', children: Receipt({ rows: Object.entries(health).map(([k, v]) => [k, String(v)]) }) }),
212
174
  ];
213
175
  },
214
- async chat(h, instance) {
215
- const note = el('div', 'fdash-empty', { text: 'chat lives in its own thebird app — opening chat window…' });
216
- try {
217
- if (window.__debug?.shell?.openApp) window.__debug.shell.openApp('chat');
218
- } catch {}
219
- return [panel('chat', note), panel('cli surface', table(['command', 'description'], [...h.pi.cli.values()].map(c => [c.name, c.description])))];
176
+ async chat(h0) {
177
+ try { if (typeof window !== 'undefined' && window.__debug?.shell?.openApp) window.__debug.shell.openApp('chat'); } catch {}
178
+ return [
179
+ Panel({ title: 'chat', children: EmptyState({ text: 'opening chat window — chat lives in its own thebird app.', glyph: '⌨' }) }),
180
+ Panel({ title: 'cli surface', count: h0.pi.cli.size,
181
+ children: Table({ headers: ['command', 'description'], rows: [...h0.pi.cli.values()].map(c => [c.name, c.description || '']) }) }),
182
+ ];
220
183
  },
221
- async sessions(h) {
222
- const list = await h.pi.sessions.list();
184
+ async sessions(h0) {
185
+ const list = await h0.pi.sessions.list();
223
186
  return [
224
- kpi([[list.length, 'total sessions']]),
225
- panel('recent sessions', list.length === 0
226
- ? el('div', 'fdash-empty', { text: 'no sessions yet — start a chat' })
227
- : 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),
187
+ Kpi({ items: [[list.length, 'sessions']] }),
188
+ Panel({ title: 'recent sessions', count: list.length, children: list.length === 0
189
+ ? EmptyState({ text: 'no sessions yet — start a chat', glyph: '✉' })
190
+ : Table({ headers: ['id', 'title', 'platform', 'model', 'turns'],
191
+ rows: list.map(s => [(s.id || '').slice(0, 8), s.title || '—', s.platform || '—', s.model || '—', s.turn_count || 0]) }) }),
228
192
  ];
229
193
  },
230
- async agents(h) {
231
- const a = await h.pi.agents();
194
+ async agents(h0) {
195
+ const a = (typeof h0.pi.agents === 'function') ? await h0.pi.agents() : { count: 0, turns: 0, active: null };
232
196
  return [
233
- kpi([[a.count, 'active'], [a.turns, 'turns']]),
234
- panel('overview', table(['key', 'value'], [
235
- ['total turns', String(a.turns)],
197
+ Kpi({ items: [[a.count || 0, 'active'], [a.turns || 0, 'turns']] }),
198
+ Panel({ title: 'agent overview', children: Receipt({ rows: [
199
+ ['total turns', String(a.turns || 0)],
236
200
  ['active session', a.active || '(none)'],
237
201
  ['last activity', a.last_activity ? new Date(a.last_activity).toLocaleString() : '—'],
238
- ])),
202
+ ] }) }),
239
203
  ];
240
204
  },
241
- async analytics(h) {
242
- const list = await h.pi.sessions.list();
243
- const tools = [...h.pi.tools.values()];
205
+ async analytics(h0) {
206
+ const list = await h0.pi.sessions.list();
207
+ const tools = [...h0.pi.tools.values()];
244
208
  const byPlatform = list.reduce((a, s) => { const k = s.platform || '?'; a[k] = (a[k] || 0) + 1; return a; }, {});
245
209
  const byModel = list.reduce((a, s) => { const k = s.model || '?'; a[k] = (a[k] || 0) + 1; return a; }, {});
210
+ const byToolset = tools.reduce((a, t) => { (a[t.toolset || 'core'] = a[t.toolset || 'core'] || []).push(t.name); return a; }, {});
246
211
  return [
247
- kpi([[list.length, 'sessions'], [tools.length, 'tools']]),
248
- panel('sessions by platform', Object.keys(byPlatform).length === 0 ? el('div', 'fdash-empty', { text: 'no data' }) : table(['platform', 'count'], Object.entries(byPlatform))),
249
- panel('sessions by model', Object.keys(byModel).length === 0 ? el('div', 'fdash-empty', { text: 'no data' }) : table(['model', 'count'], Object.entries(byModel))),
250
- panel('tools', table(['name', 'description'], tools.map(t => [t.name, (t.description || '').slice(0, 80)]))),
212
+ Kpi({ items: [[list.length, 'sessions'], [tools.length, 'tools']] }),
213
+ Panel({ title: 'sessions by platform', children: Object.keys(byPlatform).length === 0
214
+ ? EmptyState({ text: 'no data', glyph: '' })
215
+ : Table({ headers: ['platform', 'count'], rows: Object.entries(byPlatform).sort((a, b) => b[1] - a[1]) }) }),
216
+ Panel({ title: 'sessions by model', children: Object.keys(byModel).length === 0
217
+ ? EmptyState({ text: 'no data', glyph: '◎' })
218
+ : Table({ headers: ['model', 'count'], rows: Object.entries(byModel).sort((a, b) => b[1] - a[1]) }) }),
219
+ Panel({ title: 'tool distribution', children: Table({ headers: ['toolset', 'count', 'tools'],
220
+ rows: Object.entries(byToolset).map(([k, v]) => [k, v.length, v.slice(0, 4).join(', ') + (v.length > 4 ? '…' : '')]) }) }),
251
221
  ];
252
222
  },
253
- async models(h) {
254
- const cfg = h.pi.config.load();
223
+ async models(h0) {
224
+ const cfg = (typeof h0.pi.config?.load === 'function') ? await h0.pi.config.load() : {};
255
225
  const agent = cfg.agent || {};
256
- const form = el('form', 'fdash-form', { on: { submit: (ev) => {
257
- ev.preventDefault();
258
- h.pi.config.saveValue('agent.provider', ev.target.elements.provider.value);
259
- h.pi.config.saveValue('agent.model', ev.target.elements.model.value);
260
- render();
261
- } } });
262
- form.appendChild(el('input', null, { name: 'provider', placeholder: 'provider', value: agent.provider || '' }));
263
- form.appendChild(el('input', null, { name: 'model', placeholder: 'model id', value: agent.model || '' }));
264
- form.appendChild(el('button', null, { type: 'submit', text: 'update' }));
265
226
  return [
266
- kpi([[agent.provider || '—', 'provider'], [agent.model || '—', 'model']]),
267
- panel('active model', table(['key', 'value'], [
227
+ Kpi({ items: [[agent.provider || '—', 'provider'], [agent.model || '—', 'model']] }),
228
+ Panel({ title: 'active model', children: Receipt({ rows: [
268
229
  ['provider', agent.provider || '(unset)'],
269
230
  ['model', agent.model || '(unset)'],
270
231
  ['max_iterations', String(agent.max_iterations || '—')],
271
- ])),
272
- panel('change model', form),
232
+ ['max_tokens', String(agent.max_tokens || '—')],
233
+ ['temperature', String(agent.temperature ?? '—')],
234
+ ] }) }),
235
+ Panel({ title: 'change model', children: form({
236
+ fields: [{ name: 'provider', placeholder: 'provider', value: agent.provider || '' }, { name: 'model', placeholder: 'model id', value: agent.model || '' }],
237
+ submit: 'update',
238
+ onSubmit: async (ev) => {
239
+ await h0.pi.config.saveValue('agent.provider', ev.target.elements.provider.value);
240
+ await h0.pi.config.saveValue('agent.model', ev.target.elements.model.value);
241
+ rerender();
242
+ },
243
+ }) }),
273
244
  ];
274
245
  },
275
- async logs(h) {
276
- const dbg = h.pi.debug();
277
- return [
278
- panel('host debug snapshot', pre(dbg)),
279
- ];
246
+ async logs(h0) {
247
+ const dbg = (typeof h0.pi.debug === 'function') ? h0.pi.debug() : { note: 'no debug surface' };
248
+ return [Panel({ title: 'host debug snapshot', children: pre(dbg) })];
280
249
  },
281
- async cron(h) {
282
- const list = await h.pi.cron.list();
283
- const form = el('form', 'fdash-form', { on: { submit: async (ev) => {
284
- ev.preventDefault();
285
- try { await h.pi.cron.create({ cron: ev.target.elements.cron.value, prompt: ev.target.elements.prompt.value }); render(); }
286
- catch (e) { alert(e.message); }
287
- } } });
288
- form.appendChild(el('input', null, { name: 'cron', placeholder: '* * * * *', required: 'true' }));
289
- form.appendChild(el('input', null, { name: 'prompt', placeholder: 'prompt', required: 'true' }));
290
- form.appendChild(el('button', null, { type: 'submit', text: 'create' }));
291
- const tbl = list.length === 0
292
- ? el('div', 'fdash-empty', { text: 'no cron jobs' })
293
- : table(['id', 'cron', 'prompt', 'enabled'], list.map(j => [j.id, j.cron, (j.prompt || '').slice(0, 40), j.enabled ? 'yes' : 'no']));
250
+ async cron(h0) {
251
+ const list = await h0.pi.cron.list();
294
252
  return [
295
- kpi([[list.length, 'cron jobs']]),
296
- panel('add job', form),
297
- panel('jobs', tbl, list.length),
253
+ Kpi({ items: [[list.length, 'cron jobs']] }),
254
+ Panel({ title: 'add job', children: form({
255
+ fields: [{ name: 'cron', placeholder: '* * * * *', required: true }, { name: 'prompt', placeholder: 'prompt', required: true }],
256
+ submit: 'create',
257
+ 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); } },
258
+ }) }),
259
+ Panel({ title: 'scheduled jobs', count: list.length, children: list.length === 0
260
+ ? EmptyState({ text: 'no cron jobs — add one above', glyph: '◷' })
261
+ : Table({ headers: ['id', 'cron', 'prompt', 'enabled'],
262
+ rows: list.map(j => [j.id, j.cron, (j.prompt || '').slice(0, 40), j.enabled ? 'yes' : 'no']) }) }),
298
263
  ];
299
264
  },
300
- async skills(h) {
301
- const list = [...h.pi.skills.values()];
265
+ async skills(h0) {
266
+ const list = [...h0.pi.skills.values()];
302
267
  const byCat = list.reduce((a, s) => { (a[s.category || 'other'] = a[s.category || 'other'] || []).push(s); return a; }, {});
303
- const out = [kpi([[list.length, 'skills'], [Object.keys(byCat).length, 'categories']])];
304
- for (const [cat, ss] of Object.entries(byCat)) {
305
- out.push(panel(cat, table(['name', 'description'], ss.map(s => [s.shortName || s.name, (s.description || '').slice(0, 80)])), ss.length));
306
- }
307
- return out;
268
+ return [
269
+ Kpi({ items: [[list.length, 'skills'], [Object.keys(byCat).length, 'categories']] }),
270
+ ...Object.entries(byCat).map(([cat, ss]) => Panel({ title: cat, count: ss.length,
271
+ children: ss.length === 0 ? EmptyState({ text: 'none', glyph: '◈' })
272
+ : Table({ headers: ['name', 'description'], rows: ss.map(s => [s.shortName || s.name, (s.description || '').slice(0, 100)]) }) })),
273
+ ];
308
274
  },
309
- async config(h) {
310
- const cfg = h.pi.config.load();
311
- const profiles = h.pi.profiles.list();
312
- const commands = h.pi.commands.list();
313
- const form = el('form', 'fdash-form', { on: { submit: (ev) => {
314
- ev.preventDefault();
315
- let v = ev.target.elements.value.value;
316
- try { v = JSON.parse(v); } catch {}
317
- h.pi.config.saveValue(ev.target.elements.key.value, v);
318
- render();
319
- } } });
320
- form.appendChild(el('input', null, { name: 'key', placeholder: 'dotted.key', required: 'true' }));
321
- form.appendChild(el('input', null, { name: 'value', placeholder: 'value (json or string)', required: 'true' }));
322
- form.appendChild(el('button', null, { type: 'submit', text: 'save' }));
275
+ async config(h0) {
276
+ const cfg = (typeof h0.pi.config?.load === 'function') ? await h0.pi.config.load() : {};
277
+ const profiles = (typeof h0.pi.profiles?.list === 'function') ? h0.pi.profiles.list() : [];
278
+ const commands = (typeof h0.pi.commands?.list === 'function') ? h0.pi.commands.list() : [];
323
279
  return [
324
- kpi([[profiles.length, 'profiles'], [commands.length, 'commands'], [cfg._config_version || 0, 'config version']]),
325
- panel('set value', form),
326
- panel('commands', table(['name', 'category', 'description'], commands.map(c => [c.name, c.category, c.description])), commands.length),
327
- panel('active config', pre(cfg)),
280
+ Kpi({ items: [[profiles.length, 'profiles'], [commands.length, 'commands'], [cfg._config_version || 0, 'config version']] }),
281
+ Panel({ title: 'set config value', children: form({
282
+ fields: [{ name: 'key', placeholder: 'dotted.key (e.g. agent.model)', required: true }, { name: 'value', placeholder: 'value (json or string)', required: true }],
283
+ submit: 'save',
284
+ onSubmit: async (ev) => {
285
+ let v = ev.target.elements.value.value;
286
+ try { v = JSON.parse(v); } catch {}
287
+ await h0.pi.config.saveValue(ev.target.elements.key.value, v);
288
+ rerender();
289
+ },
290
+ }) }),
291
+ Panel({ title: 'commands', count: commands.length,
292
+ children: Table({ headers: ['name', 'category', 'description'], rows: commands.map(c => [c.name, c.category || '', c.description || '']) }) }),
293
+ Panel({ title: 'active config', children: pre(cfg) }),
328
294
  ];
329
295
  },
330
- async env(h) {
331
- const list = h.pi.env.list();
296
+ async env(h0) {
297
+ const list = (typeof h0.pi.env?.list === 'function') ? h0.pi.env.list() : [];
332
298
  const setCount = list.filter(k => k.set).length;
333
- const chips = el('div');
334
- for (const k of list) {
335
- const c = el('span', 'fdash-chip ' + (k.set ? 'ok' : 'miss'), { text: k.key + (k.set ? ' ✓' : '') });
336
- c.addEventListener('click', () => {
337
- const v = prompt('set ' + k.key + ' (empty to unset):');
338
- if (v == null) return;
339
- h.pi.env.set(k.key, v);
340
- render();
341
- });
342
- chips.appendChild(c);
343
- }
299
+ const chipNodes = list.map(k => h(
300
+ 'span',
301
+ {
302
+ key: k.key,
303
+ onclick: () => {
304
+ const v = prompt('set ' + k.key + ' (empty to unset):');
305
+ if (v == null) return;
306
+ if (typeof h0.pi.env.set === 'function') { h0.pi.env.set(k.key, v); rerender(); }
307
+ },
308
+ style: 'cursor:pointer',
309
+ },
310
+ Chip({ tone: k.set ? 'ok' : 'miss', children: k.key + (k.set ? ' ✓' : ' ·') })
311
+ ));
312
+ return [
313
+ Kpi({ items: [[setCount, 'set'], [list.length - setCount, 'missing'], [list.length, 'total known']] }),
314
+ Panel({
315
+ title: 'environment variables',
316
+ right: h('span', {}, Chip({ tone: 'ok', children: setCount + ' set' }), ' ', Chip({ tone: 'miss', children: (list.length - setCount) + ' missing' })),
317
+ children: h('div', { style: 'padding:8px 4px;display:flex;flex-wrap:wrap;gap:6px' }, ...chipNodes),
318
+ }),
319
+ ];
320
+ },
321
+ async tools(h0) {
322
+ const list = [...h0.pi.tools.values()];
323
+ const byToolset = list.reduce((a, t) => { (a[t.toolset || 'core'] = a[t.toolset || 'core'] || []).push(t); return a; }, {});
324
+ return [
325
+ Kpi({ items: [[list.length, 'tools'], [Object.keys(byToolset).length, 'toolsets']] }),
326
+ ...Object.entries(byToolset).map(([ts, items]) => Panel({ title: 'toolset · ' + ts, count: items.length,
327
+ children: items.map(t => Row({ key: t.name, code: '⚒', title: t.name, sub: (t.description || (t.schema && t.schema.description) || '').slice(0, 80) })) })),
328
+ ];
329
+ },
330
+ async batch(h0) {
331
+ const out = h('div', { id: 'fd-batch-out' });
332
+ return [
333
+ Section({ title: '// batch runner', children: [
334
+ Panel({ title: 'run prompts', children: form({
335
+ fields: [{ name: 'prompts', kind: 'textarea', placeholder: 'one prompt per line' }, { name: 'concurrency', type: 'number', value: '4' }],
336
+ submit: 'run',
337
+ onSubmit: async (ev) => {
338
+ const prompts = ev.target.elements.prompts.value.split('\n').map(s => s.trim()).filter(Boolean);
339
+ if (!prompts.length) return;
340
+ const node = root.querySelector('#fd-batch-out');
341
+ if (node) node.textContent = 'running…';
342
+ try {
343
+ const r = await h0.pi.batch.run({ prompts, concurrency: Number(ev.target.elements.concurrency.value) || 4 });
344
+ if (node) { node.innerHTML = ''; node.appendChild(document.createTextNode(JSON.stringify(r, null, 2))); }
345
+ } catch (e) { if (node) node.textContent = 'error: ' + (e.message || e); }
346
+ },
347
+ }) }),
348
+ Panel({ title: 'results', children: out }),
349
+ Panel({ title: 'cli usage', children: Receipt({ rows: [
350
+ ['run batch file', 'freddie batch prompts.txt'],
351
+ ['set concurrency', 'freddie batch prompts.txt --concurrency 8'],
352
+ ['jsonl output', 'freddie batch prompts.txt > out.jsonl'],
353
+ ] }) }),
354
+ ] }),
355
+ ];
356
+ },
357
+ async gateway(h0) {
358
+ const platforms = (typeof h0.pi.gateway?.platforms === 'function') ? h0.pi.gateway.platforms() : [];
359
+ const active = platforms.filter(p => p.enabled);
360
+ return [
361
+ Kpi({ items: [[platforms.length, 'platforms'], [active.length, 'active']] }),
362
+ Panel({ title: 'platforms', count: platforms.length,
363
+ right: active.length > 0 ? Chip({ tone: 'ok', children: active.length + ' active' }) : Chip({ tone: 'miss', children: 'none active' }),
364
+ children: platforms.length === 0 ? EmptyState({ text: 'no platforms registered', glyph: '⇌' })
365
+ : platforms.map(p => Row({ key: p.name, code: p.enabled ? '●' : '○', title: p.name, sub: p.note || '', meta: p.enabled ? 'enabled' : '' })) }),
366
+ Panel({ title: 'start gateway', children: Receipt({ rows: [
367
+ ['webhook + api_server', 'freddie gateway --port 3000'],
368
+ ['specific platform', 'TELEGRAM_BOT_TOKEN=… freddie gateway'],
369
+ ['all platforms', 'set env vars per platform, then freddie gateway'],
370
+ ] }) }),
371
+ ];
372
+ },
373
+ async ['os-instances']() {
374
+ const list = (osSurfaces && osSurfaces.instances && osSurfaces.instances()) || [];
375
+ const activeId = osSurfaces && osSurfaces.activeInstanceId && osSurfaces.activeInstanceId();
344
376
  return [
345
- kpi([[setCount, 'set'], [list.length - setCount, 'missing'], [list.length, 'total known']]),
346
- panel('environment variables (click to set/unset)', chips),
377
+ Kpi({ items: [[list.length, 'instances'], [activeId || '—', 'active']] }),
378
+ Panel({ title: 'instances', count: list.length, children: list.length === 0
379
+ ? EmptyState({ text: 'no instances', glyph: '◫' })
380
+ : Table({ headers: ['id', 'active', 'shells', 'windows'],
381
+ rows: list.map(i => [i.id, i.id === activeId ? '●' : '', String((i.shells || []).length), String((i.windows || []).length)]) }) }),
347
382
  ];
348
383
  },
349
- async tools(h) {
350
- const list = [...h.pi.tools.values()];
384
+ async ['os-windows']() {
385
+ const wins = (osSurfaces && osSurfaces.wm && osSurfaces.wm.list && osSurfaces.wm.list()) || [];
386
+ const focused = osSurfaces && osSurfaces.wm && osSurfaces.wm.focused;
351
387
  return [
352
- kpi([[list.length, 'tools']]),
353
- panel('all tools', table(['name', 'description'], list.map(t => [t.name, (t.description || '').slice(0, 100)])), list.length),
388
+ Kpi({ items: [[wins.length, 'windows'], [focused ? (focused.id || focused.title || '?') : '—', 'focused']] }),
389
+ Panel({ title: 'windows', count: wins.length, children: wins.length === 0
390
+ ? EmptyState({ text: 'no windows open', glyph: '▭' })
391
+ : Table({ headers: ['id', 'title', 'min', 'max', 'pos'],
392
+ rows: wins.map(w => [w.id || '?', w.title || '', w.min ? '●' : '', w.max ? '●' : '',
393
+ (w.el ? `${w.el.offsetLeft},${w.el.offsetTop} ${w.el.offsetWidth}×${w.el.offsetHeight}` : '')]) }) }),
354
394
  ];
355
395
  },
356
- async batch(h) {
357
- const out = el('div');
358
- const form = el('form', 'fdash-form', { on: { submit: async (ev) => {
359
- ev.preventDefault();
360
- const prompts = ev.target.elements.prompts.value.split('\n').map(s => s.trim()).filter(Boolean);
361
- if (!prompts.length) return;
362
- out.textContent = 'running…';
363
- try { const r = await h.pi.batch.run({ prompts, concurrency: Number(ev.target.elements.conc.value) || 4 });
364
- out.innerHTML = ''; out.appendChild(pre(r));
365
- } catch (e) { out.textContent = 'error: ' + (e.message || e); }
366
- } } });
367
- const ta = el('textarea', null, { name: 'prompts', rows: '5', placeholder: 'one prompt per line' });
368
- form.appendChild(ta);
369
- form.appendChild(el('input', null, { name: 'conc', type: 'number', value: '4' }));
370
- form.appendChild(el('button', null, { type: 'submit', text: 'run' }));
396
+ async ['os-x']() {
397
+ const x = osSurfaces && osSurfaces.xServer && osSurfaces.xServer();
398
+ if (!x) return [Panel({ title: 'x-server', children: EmptyState({ text: 'x-server not running in this instance', glyph: '✕' }) })];
371
399
  return [
372
- panel('run prompts', form),
373
- panel('results', out),
400
+ Kpi({ items: [[x.windows, 'windows'], [x.pixmaps, 'pixmaps'], [x.gcs, 'gcs'], [x.atoms, 'atoms'], [x.cursors, 'cursors']] }),
401
+ Panel({ title: 'display', children: pre(x) }),
374
402
  ];
375
403
  },
376
- async gateway(h) {
377
- const platforms = h.pi.gateway.platforms();
404
+ async ['os-fs']() {
405
+ const list = await instance.fs.list('/');
378
406
  return [
379
- kpi([[platforms.length, 'platforms'], [platforms.filter(p => p.enabled).length, 'active']]),
380
- panel('platforms', table(['name', 'enabled', 'note'], platforms.map(p => [p.name, p.enabled ? 'yes' : 'no', p.note])), platforms.length),
407
+ Kpi({ items: [[list.length, 'paths'], [instance.id, 'instance']] }),
408
+ Panel({ title: 'paths', count: list.length, children: list.length === 0
409
+ ? EmptyState({ text: 'empty fs', glyph: '📁' })
410
+ : pre(list.join('\n')) }),
381
411
  ];
382
412
  },
383
413
  };
384
414
 
385
- setActive('home');
415
+ rerender();
386
416
 
387
417
  if (typeof window !== 'undefined') {
388
418
  window.__debug = window.__debug || {};
389
419
  window.__debug.instances = window.__debug.instances || {};
390
420
  window.__debug.instances[instance.id] = window.__debug.instances[instance.id] || {};
391
- window.__debug.instances[instance.id].dashboard = { root, routes: ROUTES.map(r => r.path), setActive, get active() { return active; } };
421
+ window.__debug.instances[instance.id].dashboard = {
422
+ root,
423
+ routes: allRoutes.map(r => r.path),
424
+ setActive,
425
+ get active() { return state.active; },
426
+ };
392
427
  }
393
428
 
394
429
  return { node: root, dispose() {} };