anentrypoint-design 0.0.63 → 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.63",
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",
@@ -1,32 +1,16 @@
1
- .fdash { display: flex; height: 100%; font-family: var(--ff-ui, Nunito, sans-serif); color: var(--ink, #1F1B16); }
2
- .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; }
3
- .fdash-nav { display: flex; flex-direction: column; gap: 2px; }
4
- .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; }
5
- .fdash-nav button:hover { background: var(--panel-hover, rgba(0,0,0,0.04)); }
6
- .fdash-nav button.active { background: var(--panel-select, #C8E4CA); border-left-color: var(--panel-accent, #3F8A4A); font-weight: 600; }
7
- .fdash-nav .glyph { display: inline-block; width: 16px; opacity: 0.7; font-family: var(--ff-mono, JetBrains Mono, monospace); }
8
- .fdash-nav .group-head { padding: 10px 12px 4px; font-size: 9px; opacity: 0.5; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 700; }
9
- .fdash-main { flex: 1; padding: 12px 16px; overflow: auto; min-width: 0; }
10
- .fdash-h { font-size: 14px; font-weight: 700; margin: 0 0 8px; }
11
- .fdash-kpi { display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
12
- .fdash-kpi .k { background: var(--panel-2, #DDD3BC); padding: 6px 12px; border-radius: var(--r-1, 6px); min-width: 80px; }
13
- .fdash-kpi .k .v { font-size: 18px; font-weight: 700; font-family: var(--ff-mono, JetBrains Mono, monospace); }
14
- .fdash-kpi .k .l { font-size: 10px; opacity: 0.7; text-transform: uppercase; }
15
- .fdash-panel { background: var(--panel-1, #ECE6D5); border-radius: var(--r-1, 6px); padding: 8px 12px; margin-bottom: 8px; }
16
- .fdash-panel h3 { font-size: 12px; font-weight: 700; margin: 0 0 6px; opacity: 0.8; text-transform: uppercase; }
17
- .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; }
18
- .fdash-panel table { width: 100%; border-collapse: collapse; font-size: 12px; }
19
- .fdash-panel th, .fdash-panel td { padding: 4px 8px; text-align: left; border-bottom: 1px solid var(--panel-2, #DDD3BC); }
20
- .fdash-panel th { font-weight: 600; opacity: 0.7; font-size: 10px; text-transform: uppercase; }
21
- .fdash-row { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 12px; }
22
- .fdash-row .code { font-family: var(--ff-mono, JetBrains Mono, monospace); opacity: 0.6; }
23
- .fdash-row .meta { margin-left: auto; opacity: 0.5; font-size: 11px; }
24
- .fdash-form { display: flex; gap: 4px; flex-wrap: wrap; align-items: center; margin-bottom: 8px; }
25
- .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; }
26
- .fdash-form button { padding: 4px 12px; background: var(--panel-accent, #3F8A4A); color: #fff; border: 0; border-radius: var(--r-1, 6px); cursor: pointer; }
27
- .fdash-form button.danger { background: #c44; }
28
- .fdash-chip { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; margin: 2px; }
29
- .fdash-chip.ok { background: var(--panel-select, #C8E4CA); }
30
- .fdash-chip.miss { background: var(--panel-2, #DDD3BC); opacity: 0.6; }
31
- .fdash-empty { padding: 20px; text-align: center; opacity: 0.5; font-size: 12px; }
32
- @media (max-width: 600px) { .fdash-side { flex: 0 0 56px; } .fdash-nav button .label { display: none; } }
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,106 +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 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
- }
41
-
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;
50
- }
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
+ ];
51
35
 
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;
36
+ function pre(obj) {
37
+ return h('pre', { class: 'fd-pre' }, typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2));
59
38
  }
60
39
 
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;
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));
74
47
  }
75
48
 
76
- function pre(obj) { return el('pre', null, { text: typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2) }); }
77
-
78
49
  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);
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;';
85
53
 
86
- let active = 'home';
54
+ const state = {
55
+ active: 'home',
56
+ ts: new Date().toLocaleTimeString(),
57
+ body: null,
58
+ error: null,
59
+ };
87
60
  let host = instance.host || null;
88
61
 
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
- }
62
+ const allRoutes = osSurfaces ? [...ROUTES, ...OS_ROUTE_DEFS] : ROUTES;
119
63
 
120
64
  async function ensureHost() {
121
65
  if (host) return host;
@@ -124,281 +68,362 @@ export function createFreddieDashboard({ instance, bootHost, osSurfaces }) {
124
68
  return host;
125
69
  }
126
70
 
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;
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() {
132
113
  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);
114
+ const h0 = await ensureHost();
115
+ const page = PAGES[state.active] || PAGES.home;
116
+ state.body = await page(h0, instance);
117
+ state.error = null;
136
118
  } catch (e) {
137
- 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) });
138
121
  }
122
+ state.ts = new Date().toLocaleTimeString();
123
+ webjsx.applyDiff(root, view());
139
124
  }
140
125
 
141
126
  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
- });
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
+ }));
168
137
  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),
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
+ ] }) }),
172
156
  ];
173
157
  },
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();
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 };
179
163
  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))),
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)]) }) }),
188
174
  ];
189
175
  },
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])))];
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
+ ];
196
183
  },
197
- async sessions(h) {
198
- const list = await h.pi.sessions.list();
184
+ async sessions(h0) {
185
+ const list = await h0.pi.sessions.list();
199
186
  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),
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]) }) }),
204
192
  ];
205
193
  },
206
- async agents(h) {
207
- 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 };
208
196
  return [
209
- kpi([[a.count, 'active'], [a.turns, 'turns']]),
210
- panel('overview', table(['key', 'value'], [
211
- ['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)],
212
200
  ['active session', a.active || '(none)'],
213
201
  ['last activity', a.last_activity ? new Date(a.last_activity).toLocaleString() : '—'],
214
- ])),
202
+ ] }) }),
215
203
  ];
216
204
  },
217
- async analytics(h) {
218
- const list = await h.pi.sessions.list();
219
- 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()];
220
208
  const byPlatform = list.reduce((a, s) => { const k = s.platform || '?'; a[k] = (a[k] || 0) + 1; return a; }, {});
221
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; }, {});
222
211
  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)]))),
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 ? '…' : '')]) }) }),
227
221
  ];
228
222
  },
229
- async models(h) {
230
- const cfg = h.pi.config.load();
223
+ async models(h0) {
224
+ const cfg = (typeof h0.pi.config?.load === 'function') ? await h0.pi.config.load() : {};
231
225
  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' }));
241
226
  return [
242
- kpi([[agent.provider || '—', 'provider'], [agent.model || '—', 'model']]),
243
- panel('active model', table(['key', 'value'], [
227
+ Kpi({ items: [[agent.provider || '—', 'provider'], [agent.model || '—', 'model']] }),
228
+ Panel({ title: 'active model', children: Receipt({ rows: [
244
229
  ['provider', agent.provider || '(unset)'],
245
230
  ['model', agent.model || '(unset)'],
246
231
  ['max_iterations', String(agent.max_iterations || '—')],
247
- ])),
248
- 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
+ }) }),
249
244
  ];
250
245
  },
251
- async logs(h) {
252
- const dbg = h.pi.debug();
253
- return [
254
- panel('host debug snapshot', pre(dbg)),
255
- ];
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) })];
256
249
  },
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']));
250
+ async cron(h0) {
251
+ const list = await h0.pi.cron.list();
270
252
  return [
271
- kpi([[list.length, 'cron jobs']]),
272
- panel('add job', form),
273
- 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']) }) }),
274
263
  ];
275
264
  },
276
- async skills(h) {
277
- const list = [...h.pi.skills.values()];
265
+ async skills(h0) {
266
+ const list = [...h0.pi.skills.values()];
278
267
  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;
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
+ ];
284
274
  },
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' }));
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() : [];
299
279
  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)),
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) }),
304
294
  ];
305
295
  },
306
- async env(h) {
307
- const list = h.pi.env.list();
296
+ async env(h0) {
297
+ const list = (typeof h0.pi.env?.list === 'function') ? h0.pi.env.list() : [];
308
298
  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
- }
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
+ ));
320
312
  return [
321
- kpi([[setCount, 'set'], [list.length - setCount, 'missing'], [list.length, 'total known']]),
322
- panel('environment variables (click to set/unset)', chips),
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
+ }),
323
319
  ];
324
320
  },
325
- async tools(h) {
326
- const list = [...h.pi.tools.values()];
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; }, {});
327
324
  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),
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) })) })),
330
328
  ];
331
329
  },
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' }));
330
+ async batch(h0) {
331
+ const out = h('div', { id: 'fd-batch-out' });
347
332
  return [
348
- panel('run prompts', form),
349
- panel('results', out),
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
+ ] }),
350
355
  ];
351
356
  },
352
- async gateway(h) {
353
- const platforms = h.pi.gateway.platforms();
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);
354
360
  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),
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
+ ] }) }),
357
371
  ];
358
372
  },
359
373
  async ['os-instances']() {
360
374
  const list = (osSurfaces && osSurfaces.instances && osSurfaces.instances()) || [];
361
375
  const activeId = osSurfaces && osSurfaces.activeInstanceId && osSurfaces.activeInstanceId();
362
376
  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),
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)]) }) }),
366
382
  ];
367
383
  },
368
384
  async ['os-windows']() {
369
385
  const wins = (osSurfaces && osSurfaces.wm && osSurfaces.wm.list && osSurfaces.wm.list()) || [];
370
386
  const focused = osSurfaces && osSurfaces.wm && osSurfaces.wm.focused;
371
387
  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),
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}` : '')]) }) }),
376
394
  ];
377
395
  },
378
396
  async ['os-x']() {
379
397
  const x = osSurfaces && osSurfaces.xServer && osSurfaces.xServer();
380
- if (!x) return [el('div', 'fdash-empty', { text: 'x-server not running in this instance' })];
398
+ if (!x) return [Panel({ title: 'x-server', children: EmptyState({ text: 'x-server not running in this instance', glyph: '✕' }) })];
381
399
  return [
382
- kpi([[x.windows, 'windows'], [x.pixmaps, 'pixmaps'], [x.gcs, 'gcs'], [x.atoms, 'atoms'], [x.cursors, 'cursors']]),
383
- panel('display', pre(x)),
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) }),
384
402
  ];
385
403
  },
386
404
  async ['os-fs']() {
387
405
  const list = await instance.fs.list('/');
388
406
  return [
389
- kpi([[list.length, 'paths'], [instance.id, 'instance']]),
390
- panel('paths', el('pre', null, { text: list.join('\n') }), list.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')) }),
391
411
  ];
392
412
  },
393
413
  };
394
414
 
395
- setActive('home');
415
+ rerender();
396
416
 
397
417
  if (typeof window !== 'undefined') {
398
418
  window.__debug = window.__debug || {};
399
419
  window.__debug.instances = window.__debug.instances || {};
400
420
  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; } };
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
+ };
402
427
  }
403
428
 
404
429
  return { node: root, dispose() {} };