freddie 0.0.75 → 0.0.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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
- 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; }
@@ -1,187 +0,0 @@
1
- import { icons } from './icons.js';
2
-
3
- const THEME_CSS_URL = new URL('./theme.css', import.meta.url).href;
4
-
5
- function ensureCss(href) {
6
- if (document.querySelector('link[data-os-theme]')) return;
7
- const l = document.createElement('link');
8
- l.rel = 'stylesheet';
9
- l.href = href || THEME_CSS_URL;
10
- l.dataset.osTheme = '1';
11
- document.head.appendChild(l);
12
- }
13
-
14
- function ic(svg) {
15
- const s = document.createElement('span');
16
- s.className = 'ic';
17
- s.innerHTML = svg;
18
- return s;
19
- }
20
-
21
- function makeBtn(svg, label, role) {
22
- const b = document.createElement('button');
23
- b.className = 'os-btn';
24
- b.type = 'button';
25
- if (role) b.dataset.role = role;
26
- if (svg) b.append(ic(svg));
27
- if (label) b.append(Object.assign(document.createElement('span'), { textContent: label }));
28
- return b;
29
- }
30
-
31
- export function createDesktopShell({ root = document.body, wm, registry, brand = 'desktop', themeUrl, onNewInstance, autoBoot = false } = {}) {
32
- if (!wm) throw new Error('createDesktopShell: wm is required');
33
- if (!registry) throw new Error('createDesktopShell: registry is required');
34
- ensureCss(themeUrl);
35
-
36
- const osRoot = document.createElement('div');
37
- osRoot.className = 'os-root';
38
- root.appendChild(osRoot);
39
-
40
- const menubar = document.createElement('div');
41
- menubar.className = 'os-menubar';
42
-
43
- const homeBtn = makeBtn(icons.home, '', 'home');
44
- homeBtn.title = 'apps';
45
-
46
- const brandEl = document.createElement('span');
47
- brandEl.className = 'os-brand';
48
- brandEl.textContent = brand;
49
-
50
- const appsBtn = makeBtn(icons.apps, 'apps', 'apps');
51
- const newInstBtn = onNewInstance ? makeBtn(icons.plus, 'instance', 'add') : null;
52
-
53
- const instSwitch = document.createElement('div');
54
- instSwitch.className = 'os-instances';
55
-
56
- const spacer = document.createElement('div');
57
- spacer.className = 'os-spacer';
58
-
59
- const tray = document.createElement('div');
60
- tray.className = 'os-tray';
61
- const clock = document.createElement('span');
62
- clock.className = 'os-clock';
63
- tray.appendChild(clock);
64
-
65
- menubar.append(homeBtn, brandEl, appsBtn);
66
- if (newInstBtn) menubar.append(newInstBtn);
67
- menubar.append(instSwitch, spacer, tray);
68
-
69
- const appsMenu = document.createElement('div');
70
- appsMenu.className = 'os-menu';
71
-
72
- const sideRail = document.createElement('div');
73
- sideRail.className = 'os-side-rail';
74
-
75
- const drawer = document.createElement('div');
76
- drawer.className = 'os-drawer';
77
- drawer.setAttribute('aria-hidden', 'true');
78
- const drawerHeader = document.createElement('div');
79
- drawerHeader.className = 'os-drawer-head';
80
- const drawerTitle = document.createElement('span');
81
- drawerTitle.className = 'os-drawer-title';
82
- drawerTitle.textContent = 'apps';
83
- const drawerClose = document.createElement('button');
84
- drawerClose.className = 'os-drawer-close';
85
- drawerClose.type = 'button';
86
- drawerClose.append(ic(icons.close));
87
- drawerHeader.append(drawerTitle, drawerClose);
88
- const drawerGrid = document.createElement('div');
89
- drawerGrid.className = 'os-drawer-grid';
90
- drawer.append(drawerHeader, drawerGrid);
91
-
92
- const apps = typeof registry.list === 'function' ? registry.list() : [...registry.values()];
93
-
94
- for (const app of apps) {
95
- const iconSvg = app.icon || icons[app.id] || '';
96
- const menuBtn = makeBtn(iconSvg, app.name);
97
- menuBtn.addEventListener('click', () => { closeMenu(); openApp(app.id); });
98
- appsMenu.appendChild(menuBtn);
99
-
100
- const railBtn = document.createElement('button');
101
- railBtn.className = 'os-rail-btn';
102
- railBtn.type = 'button';
103
- railBtn.title = app.name;
104
- railBtn.append(ic(iconSvg));
105
- railBtn.addEventListener('click', () => openApp(app.id));
106
- sideRail.appendChild(railBtn);
107
-
108
- const tile = document.createElement('button');
109
- tile.className = 'os-drawer-tile';
110
- tile.type = 'button';
111
- tile.append(ic(iconSvg), Object.assign(document.createElement('span'), { className: 'lbl', textContent: app.name }));
112
- tile.addEventListener('click', () => { closeDrawer(); openApp(app.id); });
113
- drawerGrid.appendChild(tile);
114
- }
115
-
116
- const taskbar = document.createElement('div');
117
- taskbar.className = 'os-taskbar';
118
-
119
- osRoot.append(menubar, appsMenu, taskbar);
120
- document.body.append(sideRail, drawer);
121
-
122
- function openMenu() { appsMenu.classList.add('open'); }
123
- function closeMenu() { appsMenu.classList.remove('open'); }
124
- function openDrawer() { drawer.classList.add('open'); drawer.setAttribute('aria-hidden', 'false'); }
125
- function closeDrawer() { drawer.classList.remove('open'); drawer.setAttribute('aria-hidden', 'true'); }
126
-
127
- appsBtn.addEventListener('click', e => { e.stopPropagation(); appsMenu.classList.toggle('open'); });
128
- homeBtn.addEventListener('click', e => { e.stopPropagation(); drawer.classList.contains('open') ? closeDrawer() : openDrawer(); });
129
- drawerClose.addEventListener('click', closeDrawer);
130
- drawer.addEventListener('click', e => { if (e.target === drawer) closeDrawer(); });
131
- document.addEventListener('click', e => {
132
- if (!appsMenu.contains(e.target) && !appsBtn.contains(e.target)) closeMenu();
133
- });
134
- document.addEventListener('keydown', e => {
135
- if (e.key === 'Escape') { closeMenu(); closeDrawer(); }
136
- });
137
-
138
- function tickClock() { clock.textContent = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }
139
- tickClock();
140
- const clockTimer = setInterval(tickClock, 30000);
141
-
142
- let activeContext = null;
143
- function setContext(ctx) { activeContext = ctx; }
144
-
145
- function refreshTaskbar() {
146
- taskbar.innerHTML = '';
147
- for (const w of wm.list()) {
148
- const t = document.createElement('button');
149
- t.className = 'os-task' + (w.focused ? ' focused' : '');
150
- t.type = 'button';
151
- t.textContent = w.title;
152
- t.dataset.winId = w.id;
153
- t.addEventListener('click', () => wm.focus(w.id));
154
- taskbar.appendChild(t);
155
- }
156
- }
157
-
158
- function openApp(appId) {
159
- const app = (typeof registry.get === 'function') ? registry.get(appId) : registry[appId];
160
- if (!app) throw new Error('unknown app: ' + appId);
161
- const ctx = { ...(activeContext || {}), registry, openApp, wm };
162
- const result = app.factory(ctx);
163
- const finish = (r) => {
164
- const sz = app.defaultSize || { w: 520, h: 360 };
165
- const titlePrefix = (activeContext && activeContext.titlePrefix) ? activeContext.titlePrefix + ' · ' : '';
166
- const win = wm.open({ title: titlePrefix + app.name, body: r.node, kind: appId, width: sz.w, height: sz.h, x: 100 + (wm.count * 28) % 240, y: 80 + (wm.count * 22) % 180 });
167
- win._app = { id: appId, dispose: r.dispose };
168
- refreshTaskbar();
169
- return win;
170
- };
171
- return (result && typeof result.then === 'function') ? result.then(finish) : finish(result);
172
- }
173
-
174
- if (newInstBtn) newInstBtn.addEventListener('click', () => onNewInstance && onNewInstance({ instSwitch, setContext, openApp }));
175
-
176
- const taskTimer = setInterval(refreshTaskbar, 500);
177
-
178
- const api = {
179
- wm, registry, openApp, setContext, refreshTaskbar,
180
- openDrawer, closeDrawer, openMenu, closeMenu,
181
- elements: { osRoot, menubar, taskbar, appsMenu, sideRail, drawer, instSwitch, homeBtn, appsBtn },
182
- dispose() { clearInterval(clockTimer); clearInterval(taskTimer); osRoot.remove(); sideRail.remove(); drawer.remove(); },
183
- };
184
-
185
- if (autoBoot && typeof autoBoot === 'string') openApp(autoBoot);
186
- return api;
187
- }