freddie 0.0.76 → 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.
@@ -4,16 +4,34 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Freddie Dashboard</title>
7
+ <link rel="stylesheet" href="/vendor/anentrypoint-design/247420.css">
7
8
  <script type="importmap">
8
9
  { "imports": { "anentrypoint-design": "/vendor/anentrypoint-design/247420.js" } }
9
10
  </script>
10
11
  <style>
11
- /* Freddie-only overrides all generic primitives ship via SDK installStyles() */
12
- html, body { margin: 0; padding: 0; min-height: 100vh; }
13
- #app { padding: 16px 20px; box-sizing: border-box; }
12
+ html, body { margin: 0; padding: 0; height: 100%; }
13
+ #app { height: 100%; }
14
+ .fd-label { font-size: 0.75em; opacity: 0.7; letter-spacing: 0.05em; display: block; margin-bottom: 2px; }
15
+ .fd-row { display: flex; gap: 8px; flex-wrap: wrap; }
16
+ .fd-row-send { align-items: flex-end; }
17
+ .fd-col { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 120px; }
18
+ .fd-chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 4px; }
19
+ .fd-chip-wrap { cursor: pointer; }
20
+ .fd-chat-thread { max-height: 50vh; overflow-y: auto; border-radius: 10px; padding: 4px; margin-top: 8px; background: rgba(0,0,0,0.08); }
21
+ .fd-msg { padding: 6px 10px; border-bottom: 1px solid rgba(128,128,128,0.1); white-space: pre-wrap; word-break: break-word; }
22
+ .fd-msg-assistant { opacity: 0.9; }
23
+ .fd-tool-call { margin: 4px 0; padding: 4px 8px; background: rgba(0,0,0,0.12); border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-size: 0.85em; }
24
+ .fd-tool-call summary { cursor: pointer; padding: 2px 0; }
25
+ .fd-tool-body { margin: 4px 0 0; white-space: pre-wrap; word-break: break-all; max-height: 200px; overflow-y: auto; }
26
+ .fd-pre { white-space: pre-wrap; word-break: break-all; font-family: 'JetBrains Mono', monospace; font-size: 0.85em; }
27
+ .row-form { display: flex; flex-direction: column; gap: 8px; }
28
+ .fd-chat-form { display: flex; flex-direction: column; gap: 8px; }
29
+ .fd-chat-send-row { display: flex; gap: 8px; align-items: flex-end; }
30
+ .fd-chat-send-row textarea { flex: 1; resize: vertical; }
31
+ .fd-chat-send-row .btn-primary { flex-shrink: 0; align-self: flex-end; padding: 10px 20px; }
14
32
  </style>
15
33
  </head>
16
- <body data-theme="dark">
34
+ <body data-theme="light">
17
35
  <div id="app"></div>
18
36
  <script type="module" src="./app.js"></script>
19
37
  </body>
@@ -0,0 +1,321 @@
1
+ import { h, components } from 'anentrypoint-design';
2
+ import { j, post, pre, getRecentPaths, saveRecentPath, skillLabel, renderChatMessages } from './state.js';
3
+
4
+ const { Panel, Row, Hero, Receipt, Kpi, Table, EmptyState, Chip, Form } = components;
5
+
6
+ export const PAGES = {
7
+ async home(h0) {
8
+ const sessions = await h0.pi.sessions.list();
9
+ const health = h0.pi.health();
10
+ return [
11
+ Hero({ title: 'freddie', body: 'open js agent harness — pi-mono · xstate · floosie · anentrypoint-design.', accent: h0.version || 'web' }),
12
+ Kpi({ items: [[sessions.length, 'sessions'], [h0.pi.tools.size, 'tools'], [h0.pi.skills.size, 'skills']] }),
13
+ Panel({ title: 'quick start', children: Receipt({ rows: [
14
+ ['open chat', "click 'chat' — set a working directory and pick a skill"],
15
+ ['pick skill', 'software dev, research, planning — shown with descriptions'],
16
+ ['pick model', 'select a configured provider + model in the chat bar'],
17
+ ['set api key', 'keys tab → click chip to set value'],
18
+ ['add cron', 'cron tab → form'],
19
+ ] }) }),
20
+ Panel({ title: 'host', children: Receipt({ rows: Object.entries(health).map(([k, v]) => [k, String(v)]) }) }),
21
+ ];
22
+ },
23
+ async chat(h0) {
24
+ const skills = [...h0.pi.skills.values()];
25
+ const providers = await fetch('/api/providers').then(r => r.json()).catch(() => []);
26
+ const configured = providers.filter(p => p.configured);
27
+ const cs = window.__fd_chatState = window.__fd_chatState || {
28
+ cwd: '', skill: '', provider: '', model: '', messages: [], busy: false, sessionId: null,
29
+ };
30
+ if (!cs.cwd) cs.cwd = (getRecentPaths()[0] || '');
31
+ const root = document.getElementById('app');
32
+ const getMsgs = () => root.querySelector('#fd-chat-msgs');
33
+ function newSession() {
34
+ if (cs.busy) return;
35
+ cs.messages = []; cs.sessionId = null;
36
+ renderChatMessages(getMsgs(), cs.messages);
37
+ }
38
+ const parseSse = text => {
39
+ const evs = []; let ev = null, data = '';
40
+ for (const line of text.split('\n')) {
41
+ if (line.startsWith('event: ')) ev = line.slice(7).trim();
42
+ else if (line.startsWith('data: ')) data = line.slice(6).trim();
43
+ else if (line === '' && ev) { try { evs.push({ event: ev, data: JSON.parse(data) }); } catch {} ev = null; data = ''; }
44
+ }
45
+ return evs;
46
+ };
47
+ const sendChat = async ev => {
48
+ ev.preventDefault();
49
+ if (cs.busy) return;
50
+ const promptEl = ev.target.elements.prompt;
51
+ const prompt = promptEl.value.trim();
52
+ if (!prompt) return;
53
+ cs.messages.push({ role: 'user', content: prompt });
54
+ promptEl.value = ''; promptEl.style.height = 'auto'; cs.busy = true;
55
+ saveRecentPath(cs.cwd);
56
+ renderChatMessages(getMsgs(), cs.messages);
57
+ try {
58
+ const body = { prompt, cwd: cs.cwd || undefined, skill: cs.skill || undefined, provider: cs.provider || undefined, model: cs.model || undefined, sessionId: cs.sessionId || undefined };
59
+ const resp = await fetch('/api/chat', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
60
+ const text = await resp.text();
61
+ const events = parseSse(text);
62
+ let assistantContent = '';
63
+ for (const { event, data } of events) {
64
+ if (event === 'start' && data.sessionId) cs.sessionId = data.sessionId;
65
+ if (event === 'done' && data.sessionId) cs.sessionId = data.sessionId;
66
+ if (event === 'message') {
67
+ if (data.role === 'assistant') {
68
+ const content = Array.isArray(data.content) ? data.content : [{ type: 'text', text: String(data.content || '') }];
69
+ for (const block of content) {
70
+ if (block.type === 'text') assistantContent += block.text;
71
+ if (block.type === 'tool_use') {
72
+ if (assistantContent) { cs.messages.push({ role: 'assistant', content: assistantContent }); assistantContent = ''; }
73
+ cs.messages.push({ role: 'tool', name: block.name, argsSummary: JSON.stringify(block.input || {}).slice(0, 60), content: JSON.stringify(block.input || {}, null, 2) });
74
+ }
75
+ }
76
+ } else if (data.role === 'tool') {
77
+ const tc = Array.isArray(data.content) ? data.content[0] : data;
78
+ cs.messages.push({ role: 'tool', name: 'result', argsSummary: '', content: String(tc?.content || tc?.text || JSON.stringify(tc)) });
79
+ }
80
+ }
81
+ if (event === 'done' && data.result && !assistantContent) assistantContent = data.result;
82
+ if (event === 'error') assistantContent = 'error: ' + (data.error || 'unknown');
83
+ }
84
+ if (assistantContent) cs.messages.push({ role: 'assistant', content: assistantContent });
85
+ if (!events.length) cs.messages.push({ role: 'assistant', content: '(no response)' });
86
+ } catch (e) { cs.messages.push({ role: 'assistant', content: 'error: ' + e.message }); }
87
+ cs.busy = false;
88
+ renderChatMessages(getMsgs(), cs.messages);
89
+ };
90
+ const byCat = skills.reduce((a, s) => { const c = s.category || 'other'; (a[c] = a[c] || []).push(s); return a; }, {});
91
+ setTimeout(() => renderChatMessages(getMsgs(), cs.messages), 50);
92
+ return [
93
+ Panel({
94
+ title: 'chat',
95
+ right: h('button', { class: 'btn-primary', onclick: ev => { ev.preventDefault(); newSession(); } }, '+ new'),
96
+ children: [
97
+ h('form', { class: 'fd-chat-form', onsubmit: sendChat },
98
+ h('label', { class: 'fd-label' }, 'WORKING DIRECTORY'),
99
+ h('input', { name: 'cwd', type: 'text', placeholder: 'e.g. C:/dev/myproject', value: cs.cwd, oninput: ev => { cs.cwd = ev.target.value; } }),
100
+ h('div', { class: 'fd-row' },
101
+ h('div', { class: 'fd-col' },
102
+ h('label', { class: 'fd-label' }, 'SKILL'),
103
+ h('select', { name: 'skill', onchange: ev => { cs.skill = ev.target.value; } },
104
+ h('option', { value: '' }, '— no skill —'),
105
+ ...Object.entries(byCat).map(([cat, ss]) =>
106
+ h('optgroup', { label: cat }, ...ss.map(s => h('option', { value: s.name, selected: cs.skill === s.name ? 'true' : null }, skillLabel(s))))
107
+ )
108
+ )
109
+ ),
110
+ h('div', { class: 'fd-col' },
111
+ h('label', { class: 'fd-label' }, 'PROVIDER'),
112
+ h('select', { name: 'provider', onchange: ev => { cs.provider = ev.target.value; } },
113
+ h('option', { value: '' }, configured.length ? '— auto —' : '— none configured —'),
114
+ ...configured.map(p => h('option', { value: p.name, selected: cs.provider === p.name ? 'true' : null }, (p.available ? '● ' : '○ ') + p.name))
115
+ )
116
+ ),
117
+ h('div', { class: 'fd-col' },
118
+ h('label', { class: 'fd-label' }, 'MODEL'),
119
+ h('input', { name: 'model', type: 'text', placeholder: 'default', value: cs.model, oninput: ev => { cs.model = ev.target.value; } })
120
+ )
121
+ ),
122
+ h('div', { class: 'fd-chat-send-row' },
123
+ h('textarea', { name: 'prompt', placeholder: 'describe what you want…', rows: 4,
124
+ oninput: ev => { ev.target.style.height = 'auto'; ev.target.style.height = Math.min(ev.target.scrollHeight, 240) + 'px'; } }),
125
+ h('button', { type: 'submit', class: 'btn-primary', disabled: cs.busy ? 'true' : null }, cs.busy ? '…' : 'send')
126
+ )
127
+ ),
128
+ h('div', { id: 'fd-chat-msgs', class: 'fd-chat-thread' }),
129
+ ],
130
+ }),
131
+ configured.length === 0
132
+ ? Panel({ title: 'no providers configured', children: Receipt({ rows: [
133
+ ['set API key', 'keys tab → click a chip to set its key'],
134
+ ['or use acptoapi', 'run acptoapi server on localhost:4800 for local LLMs'],
135
+ ] }) })
136
+ : Panel({ title: 'providers', children: h('div', { class: 'fd-chips' },
137
+ ...providers.map(p => Chip({ tone: p.configured ? (p.available ? 'ok' : 'warn') : 'miss', children: p.name + (p.configured ? (p.available ? ' ●' : ' ○') : '') }))
138
+ ) }),
139
+ ];
140
+ },
141
+ async sessions(h0) {
142
+ const list = await h0.pi.sessions.list();
143
+ const rows = list.map(s => {
144
+ const cont = h('button', { class: 'btn-primary', onclick: async () => {
145
+ const msgs = await h0.pi.sessions.getMessages(s.id);
146
+ const cs = window.__fd_chatState = window.__fd_chatState || { messages: [], busy: false, sessionId: null, cwd: '', skill: '', provider: '', model: '' };
147
+ cs.sessionId = s.id; cs.messages = msgs.map(m => ({ role: m.role, content: String(m.content || '') }));
148
+ if (s.cwd) cs.cwd = s.cwd;
149
+ if (s.skill) cs.skill = s.skill;
150
+ if (typeof window.__fd_nav === 'function') window.__fd_nav('chat');
151
+ } }, 'continue');
152
+ return [(s.id || '').slice(0, 8), s.title || '—', s.platform || '—', s.model || '—', s.cwd ? s.cwd.slice(-30) : '—', s.skill ? skillLabel({ name: s.skill }) : '—', cont];
153
+ });
154
+ return [
155
+ Kpi({ items: [[list.length, 'sessions']] }),
156
+ Panel({ title: 'sessions', count: list.length, children: list.length === 0
157
+ ? EmptyState({ text: 'no sessions yet', glyph: '✉' })
158
+ : Table({ headers: ['id', 'title', 'platform', 'model', 'cwd', 'skill', ''], rows }) }),
159
+ ];
160
+ },
161
+ async projects(h0) {
162
+ const list = h0.pi.projects.list();
163
+ const active = h0.pi.projects.active();
164
+ const rows = list.map(p => Row({ key: p.name, code: p.name === active?.name ? '●' : '○', title: p.name + (p.name === active?.name ? ' (active)' : ''), meta: p.path,
165
+ onClick: () => { if (p.name !== active?.name) h0.pi.projects.setActive(p.name); } }));
166
+ return [
167
+ Hero({ title: 'projects', body: 'each project is its own ~/.freddie home.', accent: active ? 'active · ' + active.name : 'no active project' }),
168
+ Kpi({ items: [[list.length, 'projects'], [active?.name || '—', 'active']] }),
169
+ Panel({ title: 'add project', children: Form({ fields: [{ name: 'name', placeholder: 'name', required: true }, { name: 'path', placeholder: '/abs/path' }], submit: 'add',
170
+ onSubmit: ev => { h0.pi.projects.create({ name: ev.target.elements.name.value, path: ev.target.elements.path.value }); } }) }),
171
+ Panel({ title: 'all projects', count: list.length, children: rows.length ? rows : EmptyState({ text: 'no projects', glyph: '◆' }) }),
172
+ ];
173
+ },
174
+ async agents(h0) {
175
+ const a = typeof h0.pi.agents === 'function' ? await h0.pi.agents() : { count: 0, turns: 0, active: null };
176
+ return [
177
+ Kpi({ items: [[a.count || 0, 'active'], [a.turns || 0, 'turns']] }),
178
+ Panel({ title: 'agents', children: Receipt({ rows: [['active session', a.active || '(none)'], ['total turns', String(a.turns || 0)]] }) }),
179
+ ];
180
+ },
181
+ async analytics(h0) {
182
+ const list = await h0.pi.sessions.list();
183
+ const tools = [...h0.pi.tools.values()];
184
+ const byPlatform = list.reduce((a, s) => { const k = s.platform || '?'; a[k] = (a[k] || 0) + 1; return a; }, {});
185
+ const byModel = list.reduce((a, s) => { const k = s.model || '?'; a[k] = (a[k] || 0) + 1; return a; }, {});
186
+ return [
187
+ Kpi({ items: [[list.length, 'sessions'], [tools.length, 'tools']] }),
188
+ Panel({ title: 'by platform', children: Object.keys(byPlatform).length === 0 ? EmptyState({ text: 'no data', glyph: '◉' }) : Table({ headers: ['platform', 'count'], rows: Object.entries(byPlatform).sort((a, b) => b[1] - a[1]) }) }),
189
+ Panel({ title: 'by model', children: Object.keys(byModel).length === 0 ? EmptyState({ text: 'no data', glyph: '◎' }) : Table({ headers: ['model', 'count'], rows: Object.entries(byModel).sort((a, b) => b[1] - a[1]) }) }),
190
+ ];
191
+ },
192
+ async models(h0) {
193
+ const cfg = typeof h0.pi.config?.load === 'function' ? await h0.pi.config.load() : {};
194
+ const agent = cfg.agent || {};
195
+ const providers = await fetch('/api/providers').then(r => r.json()).catch(() => []);
196
+ const configured = providers.filter(p => p.configured);
197
+ const probeState = window.__fd_probeState = window.__fd_probeState || {};
198
+ async function probeAll() {
199
+ await Promise.allSettled(configured.map(async p => {
200
+ probeState[p.name] = 'loading';
201
+ try {
202
+ const r = await fetch('/api/providers/' + p.name + '/probe', { method: 'POST' }).then(x => x.json());
203
+ probeState[p.name] = r.models || r.error || '?';
204
+ } catch (e) { probeState[p.name] = 'error: ' + e.message; }
205
+ }));
206
+ if (typeof window.__fd_nav === 'function') window.__fd_nav('models');
207
+ }
208
+ const modelPanels = configured.map(p => {
209
+ const cached = p.models;
210
+ const err = p.modelsError;
211
+ const loading = probeState[p.name] === 'loading';
212
+ const probed = Array.isArray(probeState[p.name]) ? probeState[p.name] : null;
213
+ const models = probed || cached;
214
+ const children = loading
215
+ ? h('span', {}, 'probing…')
216
+ : models && models.length > 0
217
+ ? Table({ headers: ['model id'], rows: models.map(m => [m]) })
218
+ : h('span', { class: 'fd-muted' }, err ? ('error: ' + err) : 'not probed — click "probe all"');
219
+ return Panel({ title: p.name + (p.available ? ' ●' : ' ○'), children });
220
+ });
221
+ return [
222
+ Kpi({ items: [[configured.length, 'configured'], [providers.filter(p => p.available).length, 'available']] }),
223
+ Panel({ title: 'change active model', children: Form({ fields: [{ name: 'provider', placeholder: 'provider', value: agent.provider || '' }, { name: 'model', placeholder: 'model id', value: agent.model || '' }], submit: 'update',
224
+ onSubmit: async ev => { await h0.pi.config.saveValue('agent.provider', ev.target.elements.provider.value); await h0.pi.config.saveValue('agent.model', ev.target.elements.model.value); } }) }),
225
+ Panel({ title: 'providers', right: h('button', { class: 'btn-primary', onclick: ev => { ev.preventDefault(); probeAll(); } }, 'probe all'),
226
+ children: h('div', { class: 'fd-chips' }, ...providers.map(p => Chip({ tone: p.configured ? (p.available ? 'ok' : 'warn') : 'miss', children: p.name + (p.configured ? (p.available ? ' ●' : ' ○') : ' ·') }))) }),
227
+ ...modelPanels,
228
+ ];
229
+ },
230
+ async cron(h0) {
231
+ const list = await h0.pi.cron.list();
232
+ return [
233
+ Kpi({ items: [[list.length, 'jobs']] }),
234
+ Panel({ title: 'add job', children: Form({ fields: [{ name: 'cron', placeholder: '* * * * *', required: true }, { name: 'prompt', placeholder: 'prompt', required: true }], submit: 'create',
235
+ onSubmit: async ev => { await h0.pi.cron.create({ cron: ev.target.elements.cron.value, prompt: ev.target.elements.prompt.value }); } }) }),
236
+ Panel({ title: 'jobs', count: list.length, children: list.length === 0 ? EmptyState({ text: 'no cron jobs', glyph: '◷' }) : Table({ headers: ['id', 'cron', 'prompt', 'enabled'], rows: list.map(j => [j.id, j.cron, (j.prompt || '').slice(0, 40), j.enabled ? 'yes' : 'no']) }) }),
237
+ ];
238
+ },
239
+ async skills(h0) {
240
+ const list = [...h0.pi.skills.values()];
241
+ const byCat = list.reduce((a, s) => { (a[s.category || 'other'] = a[s.category || 'other'] || []).push(s); return a; }, {});
242
+ return [
243
+ Kpi({ items: [[list.length, 'skills'], [Object.keys(byCat).length, 'categories']] }),
244
+ list.length === 0 ? EmptyState({ text: 'no skills — add SKILL.md files to ~/.freddie/skills/', glyph: '◈' }) : null,
245
+ ...Object.entries(byCat).map(([cat, ss]) => Panel({ title: cat, count: ss.length, children: Table({ headers: ['name', 'description'], rows: ss.map(s => [skillLabel(s), (s.description || '').slice(0, 120)]) }) })),
246
+ ].filter(Boolean);
247
+ },
248
+ async config(h0) {
249
+ const cfg = typeof h0.pi.config?.load === 'function' ? await h0.pi.config.load() : {};
250
+ const commands = typeof h0.pi.cli?.values === 'function' ? [...h0.pi.cli.values()] : [];
251
+ return [
252
+ Kpi({ items: [[commands.length, 'commands'], [cfg._config_version || 0, 'config version']] }),
253
+ Panel({ title: 'set config value', children: Form({ fields: [{ name: 'key', placeholder: 'dotted.key', required: true }, { name: 'value', placeholder: 'value (json or string)', required: true }], submit: 'save',
254
+ onSubmit: async ev => { let v = ev.target.elements.value.value; try { v = JSON.parse(v); } catch {} await h0.pi.config.saveValue(ev.target.elements.key.value, v); } }) }),
255
+ Panel({ title: 'commands', count: commands.length, children: Table({ headers: ['name', 'description'], rows: commands.map(c => [c.name, c.description || '']) }) }),
256
+ Panel({ title: 'active config', children: Receipt({ rows: Object.entries(cfg).map(([k,v]) => [k, typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v ?? '')]) }) }),
257
+ ];
258
+ },
259
+ async env(h0) {
260
+ const list = typeof h0.pi.env?.list === 'function' ? h0.pi.env.list() : [];
261
+ const setCount = list.filter(k => k.set).length;
262
+ return [
263
+ Kpi({ items: [[setCount, 'set'], [list.length - setCount, 'missing'], [list.length, 'total']] }),
264
+ Panel({ title: 'environment variables', children: h('div', { class: 'fd-chips' },
265
+ ...list.map(k => h('span', { key: k.key, onclick: () => {
266
+ const v = prompt('set ' + k.key + ' (empty to unset):');
267
+ if (v == null) return;
268
+ if (typeof h0.pi.env.set === 'function') { h0.pi.env.set(k.key, v); }
269
+ }, class: 'fd-chip-wrap' }, Chip({ tone: k.set ? 'ok' : 'miss', children: k.key + (k.set ? ' ✓' : ' ·') })))
270
+ ) }),
271
+ ];
272
+ },
273
+ async tools(h0) {
274
+ const list = [...h0.pi.tools.values()];
275
+ const bySet = list.reduce((a, t) => { (a[t.toolset || 'core'] = a[t.toolset || 'core'] || []).push(t); return a; }, {});
276
+ return [
277
+ Kpi({ items: [[list.length, 'tools'], [Object.keys(bySet).length, 'toolsets']] }),
278
+ ...Object.entries(bySet).map(([ts, items]) => Panel({ title: 'toolset · ' + ts, count: items.length,
279
+ children: items.map(t => Row({ key: t.name, code: '⚒', title: t.name, sub: (t.description || (t.schema && t.schema.description) || '').slice(0, 80) })) })),
280
+ ];
281
+ },
282
+ async batch(h0) {
283
+ const out = h('div', { id: 'fd-batch-out' });
284
+ return [
285
+ Panel({ title: 'run batch', children: Form({
286
+ fields: [{ name: 'prompts', kind: 'textarea', placeholder: 'one prompt per line', rows: 6 }, { name: 'concurrency', type: 'number', value: '4' }],
287
+ submit: 'run',
288
+ onSubmit: async ev => {
289
+ const prompts = ev.target.elements.prompts.value.split('\n').map(s => s.trim()).filter(Boolean);
290
+ if (!prompts.length) return;
291
+ const root = document.getElementById('app');
292
+ const node = root.querySelector('#fd-batch-out');
293
+ if (node) node.textContent = 'running…';
294
+ try {
295
+ const r = await h0.pi.batch.run({ prompts, concurrency: Number(ev.target.elements.concurrency.value) || 4 });
296
+ if (node) {
297
+ const results = Array.isArray(r) ? r : (r && typeof r === 'object' ? Object.entries(r).map(([k,v]) => ({ prompt: k, result: v })) : []);
298
+ const tbl = document.createElement('table');
299
+ const thead = tbl.createTHead(); const hr = thead.insertRow();
300
+ ['#', 'prompt', 'result', 'status'].forEach(h => { const th = document.createElement('th'); th.textContent = h; hr.appendChild(th); });
301
+ const tbody = tbl.createTBody();
302
+ results.forEach((item, i) => { const row = tbody.insertRow(); [i+1, (item.prompt||'').slice(0,60), (item.result||item.error||'').slice(0,120), item.error ? 'error' : 'ok'].forEach(v => { const td = row.insertCell(); td.textContent = String(v); }); });
303
+ node.innerHTML = ''; node.appendChild(tbl);
304
+ }
305
+ } catch (e) { if (node) node.textContent = 'error: ' + (e.message || e); }
306
+ },
307
+ }) }),
308
+ Panel({ title: 'results', children: out }),
309
+ ];
310
+ },
311
+ async gateway(h0) {
312
+ const platforms = typeof h0.pi.gateway?.platforms === 'function' ? h0.pi.gateway.platforms() : [];
313
+ const active = platforms.filter(p => p.enabled);
314
+ return [
315
+ Kpi({ items: [[platforms.length, 'platforms'], [active.length, 'active']] }),
316
+ Panel({ title: 'platforms', count: platforms.length,
317
+ right: active.length > 0 ? Chip({ tone: 'ok', children: active.length + ' active' }) : Chip({ tone: 'miss', children: 'none active' }),
318
+ children: platforms.length === 0 ? EmptyState({ text: 'no platforms registered', glyph: '⇌' }) : platforms.map(p => Row({ key: p.name, code: p.enabled ? '●' : '○', title: p.name, sub: p.note || '', meta: p.enabled ? 'enabled' : '' })) }),
319
+ ];
320
+ },
321
+ };
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,123 @@
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
+
85
+ export function getRecentPaths() {
86
+ try { return JSON.parse(localStorage.getItem('fd_recent_cwds') || '[]'); } catch { return []; }
87
+ }
88
+ export function saveRecentPath(p) {
89
+ if (!p) return;
90
+ try {
91
+ const prev = getRecentPaths().filter(x => x !== p);
92
+ localStorage.setItem('fd_recent_cwds', JSON.stringify([p, ...prev].slice(0, 5)));
93
+ } catch {}
94
+ }
95
+ export function skillLabel(s) {
96
+ const n = s.name || '';
97
+ return n.replace(/^gm:/, '').replace(/^software-development$/, 'software dev').replace(/-/g, ' ');
98
+ }
99
+
100
+ export function renderChatMessages(container, messages) {
101
+ if (!container) return;
102
+ container.innerHTML = '';
103
+ for (const m of messages) {
104
+ if (m.role === 'tool') {
105
+ const det = document.createElement('details');
106
+ det.className = 'fd-tool-call';
107
+ const sum = document.createElement('summary');
108
+ sum.textContent = '⚒ ' + m.name + (m.argsSummary ? ' ' + m.argsSummary : '');
109
+ det.appendChild(sum);
110
+ const body = document.createElement('pre');
111
+ body.className = 'fd-tool-body';
112
+ body.textContent = m.content || '';
113
+ det.appendChild(body);
114
+ container.appendChild(det);
115
+ } else {
116
+ const el = document.createElement('div');
117
+ el.className = 'fd-msg fd-msg-' + (m.role === 'assistant' ? 'assistant' : 'user');
118
+ el.textContent = (m.role === 'assistant' ? '◈ ' : '▷ ') + (m.content || '');
119
+ container.appendChild(el);
120
+ }
121
+ }
122
+ container.scrollTop = container.scrollHeight;
123
+ }
@@ -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; } }