freddie 0.0.76 → 0.0.78

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/src/web/server.js CHANGED
@@ -10,15 +10,10 @@ export async function createDashboard({ port = 0 } = {}) {
10
10
  const app = express()
11
11
  app.use(express.json())
12
12
  app.use(express.static(__dirname))
13
- const vendored = path.join(__dirname, 'vendor', 'anentrypoint-design', 'dist')
14
13
  const fromNodeModules = path.join(__dirname, '..', '..', 'node_modules', 'anentrypoint-design', 'dist')
15
- const fs = await import('node:fs')
16
- const designDist = fs.existsSync(vendored) ? vendored : fromNodeModules
17
- app.use('/vendor/anentrypoint-design', express.static(designDist))
18
- const vendoredDesktop = path.join(__dirname, 'vendor', 'anentrypoint-design', 'desktop')
14
+ app.use('/vendor/anentrypoint-design', express.static(fromNodeModules))
19
15
  const nmDesktop = path.join(__dirname, '..', '..', 'node_modules', 'anentrypoint-design', 'src', 'desktop')
20
- const desktopSrc = fs.existsSync(vendoredDesktop) ? vendoredDesktop : nmDesktop
21
- app.use('/vendor/anentrypoint-design/desktop', express.static(desktopSrc))
16
+ app.use('/vendor/anentrypoint-design/desktop', express.static(nmDesktop))
22
17
  for (const r of host.gui.routes.list()) {
23
18
  const verb = r.method.toLowerCase()
24
19
  if (typeof app[verb] === 'function') app[verb](r.path, r.handler)
@@ -0,0 +1,84 @@
1
+ import { h } from 'anentrypoint-design';
2
+
3
+ export const j = async (u, opts) => { const r = await fetch(u, opts); if (!r.ok) throw new Error(r.status + ' ' + r.statusText); return r.json(); };
4
+ export const post = (u, b) => j(u, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(b) });
5
+
6
+ export function reg(arr, keyFn = x => x.name) {
7
+ const m = new Map();
8
+ for (const it of arr || []) m.set(keyFn(it), it);
9
+ return { get: k => m.get(k), has: k => m.has(k), list: () => [...m.values()], values: () => m.values(), get size() { return m.size; } };
10
+ }
11
+
12
+ export async function fetchHost() {
13
+ const [tools, skillsR, cron, projR, env, gateway, health, commands] = await Promise.all([
14
+ j('/api/tools/detail').catch(() => []),
15
+ j('/api/skills').catch(() => ({ home: [], bundled: [] })),
16
+ j('/api/cron').catch(() => []),
17
+ j('/api/projects').catch(() => ({ active: null, projects: [] })),
18
+ j('/api/env').catch(() => []),
19
+ j('/api/gateway').catch(() => ({ platforms: [] })),
20
+ j('/api/health').catch(() => ({ ok: false })),
21
+ j('/api/commands').catch(() => []),
22
+ ]);
23
+ const skillList = [...(skillsR.home || []), ...(skillsR.bundled || [])];
24
+ const projList = projR.projects || [];
25
+ return {
26
+ kind: 'freddie-web', version: 'web',
27
+ pi: {
28
+ tools: reg(tools),
29
+ skills: reg(skillList, s => s.name || s.id),
30
+ cli: reg(commands),
31
+ projects: {
32
+ list: () => projList,
33
+ active: () => projR.active,
34
+ create: ({ name, path }) => post('/api/projects', { name, path }).then(() => location.reload()),
35
+ remove: name => fetch('/api/projects/' + encodeURIComponent(name), { method: 'DELETE' }).then(() => location.reload()),
36
+ setActive: name => post('/api/projects/active', { name }).then(() => location.reload()),
37
+ },
38
+ sessions: {
39
+ list: () => j('/api/sessions').catch(() => []),
40
+ getMessages: id => j('/api/sessions/' + encodeURIComponent(id) + '/messages').catch(() => []),
41
+ search: q => j('/api/search?q=' + encodeURIComponent(q)).catch(() => []),
42
+ },
43
+ cron: {
44
+ list: () => Promise.resolve(cron),
45
+ create: job => post('/api/cron', job),
46
+ delete: id => fetch('/api/cron/' + id, { method: 'DELETE' }),
47
+ },
48
+ env: { list: () => env, isSet: k => (env.find(e => e.key === k) || {}).set || false },
49
+ gateway: { platforms: () => gateway.platforms || [] },
50
+ agents: () => j('/api/agents').catch(() => ({ count: 0, turns: 0, active: null })),
51
+ health: () => health,
52
+ config: {
53
+ load: () => j('/api/config').catch(() => ({})),
54
+ saveValue: (path, value) => post('/api/config', { path, value }),
55
+ },
56
+ chat: { send: text => post('/api/chat', { text }) },
57
+ batch: { run: (prompts, conc) => post('/api/batch', { prompts, concurrency: conc }) },
58
+ hooks: {},
59
+ },
60
+ };
61
+ }
62
+
63
+ export const ROUTES = [
64
+ { path: 'home', label: 'home', glyph: '⌂' },
65
+ { path: 'chat', label: 'chat', glyph: '⌨' },
66
+ { path: 'sessions', label: 'sessions', glyph: '✉' },
67
+ { path: 'projects', label: 'projects', glyph: '◆' },
68
+ { path: 'agents', label: 'agents', glyph: '◈' },
69
+ { path: 'analytics', label: 'analytics', glyph: '◉' },
70
+ { path: 'models', label: 'models', glyph: '◎' },
71
+ { path: 'cron', label: 'cron', glyph: '◷' },
72
+ { path: 'skills', label: 'skills', glyph: '◈' },
73
+ { path: 'config', label: 'config', glyph: '⚙' },
74
+ { path: 'env', label: 'keys', glyph: '⚿' },
75
+ { path: 'tools', label: 'tools', glyph: '⚒' },
76
+ { path: 'batch', label: 'batch', glyph: '⊞' },
77
+ { path: 'gateway', label: 'gateway', glyph: '⇌' },
78
+ ];
79
+
80
+ export function pre(obj) {
81
+ return h('pre', { class: 'fd-pre' }, typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2));
82
+ }
83
+
84
+ export { skillLabel, getRecentPaths, saveRecentPath, renderChatMessages } from 'anentrypoint-design';
@@ -1,32 +0,0 @@
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,405 +0,0 @@
1
- const ROUTES = [
2
- { path: 'projects', label: 'projects', glyph: '◆' },
3
- { path: 'home', label: 'home', glyph: '⌂' },
4
- { path: 'chat', label: 'chat', glyph: '⌨' },
5
- { path: 'sessions', label: 'sessions', glyph: '✉' },
6
- { path: 'agents', label: 'agents', glyph: '◈' },
7
- { path: 'analytics', label: 'analytics', glyph: '◉' },
8
- { path: 'models', label: 'models', glyph: '◎' },
9
- { path: 'logs', label: 'logs', glyph: '☰' },
10
- { path: 'cron', label: 'cron', glyph: '◷' },
11
- { path: 'skills', label: 'skills', glyph: '◈' },
12
- { path: 'config', label: 'config', glyph: '⚙' },
13
- { path: 'env', label: 'keys', glyph: '⚿' },
14
- { path: 'tools', label: 'tools', glyph: '⚒' },
15
- { path: 'batch', label: 'batch', glyph: '⊞' },
16
- { path: 'gateway', label: 'gateway', glyph: '⇌' },
17
- ];
18
-
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
- }
51
-
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;
59
- }
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;
74
- }
75
-
76
- function pre(obj) { return el('pre', null, { text: typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2) }); }
77
-
78
- 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);
85
-
86
- let active = 'home';
87
- 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
- }
119
-
120
- async function ensureHost() {
121
- if (host) return host;
122
- if (typeof bootHost !== 'function') throw new Error('createFreddieDashboard: instance.host or bootHost required');
123
- host = instance.host = await bootHost({ fs: instance.fs });
124
- return host;
125
- }
126
-
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;
132
- 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);
136
- } catch (e) {
137
- main.appendChild(panel('error', el('pre', null, { text: String(e && e.stack || e) })));
138
- }
139
- }
140
-
141
- 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
- });
168
- 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),
172
- ];
173
- },
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();
179
- 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))),
188
- ];
189
- },
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])))];
196
- },
197
- async sessions(h) {
198
- const list = await h.pi.sessions.list();
199
- 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),
204
- ];
205
- },
206
- async agents(h) {
207
- const a = await h.pi.agents();
208
- return [
209
- kpi([[a.count, 'active'], [a.turns, 'turns']]),
210
- panel('overview', table(['key', 'value'], [
211
- ['total turns', String(a.turns)],
212
- ['active session', a.active || '(none)'],
213
- ['last activity', a.last_activity ? new Date(a.last_activity).toLocaleString() : '—'],
214
- ])),
215
- ];
216
- },
217
- async analytics(h) {
218
- const list = await h.pi.sessions.list();
219
- const tools = [...h.pi.tools.values()];
220
- const byPlatform = list.reduce((a, s) => { const k = s.platform || '?'; a[k] = (a[k] || 0) + 1; return a; }, {});
221
- const byModel = list.reduce((a, s) => { const k = s.model || '?'; a[k] = (a[k] || 0) + 1; return a; }, {});
222
- 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)]))),
227
- ];
228
- },
229
- async models(h) {
230
- const cfg = h.pi.config.load();
231
- 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
- return [
242
- kpi([[agent.provider || '—', 'provider'], [agent.model || '—', 'model']]),
243
- panel('active model', table(['key', 'value'], [
244
- ['provider', agent.provider || '(unset)'],
245
- ['model', agent.model || '(unset)'],
246
- ['max_iterations', String(agent.max_iterations || '—')],
247
- ])),
248
- panel('change model', form),
249
- ];
250
- },
251
- async logs(h) {
252
- const dbg = h.pi.debug();
253
- return [
254
- panel('host debug snapshot', pre(dbg)),
255
- ];
256
- },
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']));
270
- return [
271
- kpi([[list.length, 'cron jobs']]),
272
- panel('add job', form),
273
- panel('jobs', tbl, list.length),
274
- ];
275
- },
276
- async skills(h) {
277
- const list = [...h.pi.skills.values()];
278
- 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;
284
- },
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' }));
299
- 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)),
304
- ];
305
- },
306
- async env(h) {
307
- const list = h.pi.env.list();
308
- 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
- }
320
- return [
321
- kpi([[setCount, 'set'], [list.length - setCount, 'missing'], [list.length, 'total known']]),
322
- panel('environment variables (click to set/unset)', chips),
323
- ];
324
- },
325
- async tools(h) {
326
- const list = [...h.pi.tools.values()];
327
- 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),
330
- ];
331
- },
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' }));
347
- return [
348
- panel('run prompts', form),
349
- panel('results', out),
350
- ];
351
- },
352
- async gateway(h) {
353
- const platforms = h.pi.gateway.platforms();
354
- 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),
357
- ];
358
- },
359
- async ['os-instances']() {
360
- const list = (osSurfaces && osSurfaces.instances && osSurfaces.instances()) || [];
361
- const activeId = osSurfaces && osSurfaces.activeInstanceId && osSurfaces.activeInstanceId();
362
- 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),
366
- ];
367
- },
368
- async ['os-windows']() {
369
- const wins = (osSurfaces && osSurfaces.wm && osSurfaces.wm.list && osSurfaces.wm.list()) || [];
370
- const focused = osSurfaces && osSurfaces.wm && osSurfaces.wm.focused;
371
- 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),
376
- ];
377
- },
378
- async ['os-x']() {
379
- const x = osSurfaces && osSurfaces.xServer && osSurfaces.xServer();
380
- if (!x) return [el('div', 'fdash-empty', { text: 'x-server not running in this instance' })];
381
- return [
382
- kpi([[x.windows, 'windows'], [x.pixmaps, 'pixmaps'], [x.gcs, 'gcs'], [x.atoms, 'atoms'], [x.cursors, 'cursors']]),
383
- panel('display', pre(x)),
384
- ];
385
- },
386
- async ['os-fs']() {
387
- const list = await instance.fs.list('/');
388
- return [
389
- kpi([[list.length, 'paths'], [instance.id, 'instance']]),
390
- panel('paths', el('pre', null, { text: list.join('\n') }), list.length),
391
- ];
392
- },
393
- };
394
-
395
- setActive('home');
396
-
397
- if (typeof window !== 'undefined') {
398
- if (!window.__debug) window.__debug = {};
399
- window.__debug.instances = window.__debug.instances || {};
400
- 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; } };
402
- }
403
-
404
- return { node: root, dispose() {} };
405
- }
@@ -1,17 +0,0 @@
1
- export const icons = {
2
- terminal: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9l3 3-3 3M13 15h4"/></svg>',
3
- browser: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 010 18M12 3a14 14 0 000 18"/></svg>',
4
- canvas: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="14" rx="2"/><path d="M3 17l6-5 4 3 5-4 3 2"/></svg>',
5
- files: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6a2 2 0 012-2h4l2 2h8a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2z"/></svg>',
6
- monitor: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12l4-8 4 14 4-10 4 8 2-3"/></svg>',
7
- validator: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l4 4L19 6"/></svg>',
8
- about: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v.01M11 12h1v5h1"/></svg>',
9
- apps: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg>',
10
- plus: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>',
11
- home: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h16M4 18h16"/></svg>',
12
- xdisplay: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="13" rx="1.5"/><path d="M8 21h8M12 17v4M9 9l6 4M15 9l-6 4"/></svg>',
13
- close: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M18 6l-12 12"/></svg>',
14
- chat: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h16v11H8l-4 4z"/><path d="M8 9h8M8 12h6"/></svg>',
15
- tools: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 7l3-3 3 3-3 3-3-3zM7 14l3 3-7 7-3-3 7-7zM5 7l3-3M14 14l6 6"/></svg>',
16
- freddie: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2M8 12a4 4 0 008 0"/></svg>',
17
- };
@@ -1,3 +0,0 @@
1
- export { icons } from './icons.js';
2
- export { createDesktopShell } from './shell.js';
3
- export const themeUrl = new URL('./theme.css', import.meta.url).href;
@@ -1,44 +0,0 @@
1
- /* ============================================================
2
- Launcher dock — paired with createLauncher() (validation harness path)
3
- Bible: tonal step from canvas, no borders, pill controls, lowercase.
4
- ============================================================ */
5
-
6
- .launcher-dock {
7
- position: fixed;
8
- left: 0; top: 0; bottom: 0;
9
- width: 56px;
10
- background: var(--os-bg-2);
11
- border: none;
12
- display: flex;
13
- flex-direction: column;
14
- align-items: center;
15
- padding: 8px 0;
16
- gap: 8px;
17
- z-index: 10000;
18
- pointer-events: auto;
19
- font: 11px var(--os-mono);
20
- color: var(--os-fg);
21
- }
22
-
23
- .launcher-btn {
24
- width: 40px; height: 40px;
25
- background: transparent;
26
- color: inherit;
27
- cursor: pointer;
28
- border-radius: var(--os-radius-sm);
29
- display: flex;
30
- align-items: center;
31
- justify-content: center;
32
- padding: 0;
33
- font: inherit;
34
- transition: background 80ms ease, color 80ms ease, box-shadow 80ms ease;
35
- }
36
- .launcher-btn:hover { background: var(--panel-hover, var(--os-bg-1)); color: var(--os-fg); }
37
- .launcher-btn.active {
38
- background: var(--panel-select, var(--os-accent-soft));
39
- color: var(--os-fg);
40
- box-shadow: inset 4px 0 0 var(--os-accent);
41
- }
42
- .launcher-add { font-size: 20px; }
43
-
44
- .launcher-dock + .wm-root { left: 56px !important; }