anentrypoint-design 0.0.99 → 0.0.101
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/dist/247420.app.js +4 -4
- package/dist/247420.js +6 -6
- package/package.json +1 -1
- package/src/components/content.js +11 -16
- package/src/desktop/freddie/helpers.js +59 -0
- package/src/desktop/freddie/pages-chat.js +143 -0
- package/src/desktop/freddie/pages-core.js +101 -0
- package/src/desktop/freddie/pages-os.js +51 -0
- package/src/desktop/freddie/pages-tools.js +183 -0
- package/src/desktop/freddie/routes.js +24 -0
- package/src/desktop/freddie-dashboard.css +38 -3
- package/src/desktop/freddie-dashboard.js +19 -572
|
@@ -1,100 +1,20 @@
|
|
|
1
1
|
import * as webjsx from '../../vendor/webjsx/index.js';
|
|
2
2
|
import * as components from '../components.js';
|
|
3
|
+
import { ROUTES, OS_ROUTE_DEFS } from './freddie/routes.js';
|
|
4
|
+
import { makeCorePages } from './freddie/pages-core.js';
|
|
5
|
+
import { makeChatPage } from './freddie/pages-chat.js';
|
|
6
|
+
import { makeToolsPages } from './freddie/pages-tools.js';
|
|
7
|
+
import { makeOsPages } from './freddie/pages-os.js';
|
|
3
8
|
|
|
4
|
-
const
|
|
5
|
-
const {
|
|
6
|
-
AppShell, Topbar, Side, Crumb, Status, Brand, Glyph,
|
|
7
|
-
Panel, Row, RowLink, Hero, Receipt, Kpi, Table, Section,
|
|
8
|
-
EmptyState, Chip,
|
|
9
|
-
} = components;
|
|
10
|
-
|
|
11
|
-
const ROUTES = [
|
|
12
|
-
{ path: 'projects', label: 'projects', glyph: '◆' },
|
|
13
|
-
{ path: 'home', label: 'home', glyph: '⌂' },
|
|
14
|
-
{ path: 'chat', label: 'chat', glyph: '⌨' },
|
|
15
|
-
{ path: 'sessions', label: 'sessions', glyph: '✉' },
|
|
16
|
-
{ path: 'agents', label: 'agents', glyph: '◈' },
|
|
17
|
-
{ path: 'analytics', label: 'analytics', glyph: '◉' },
|
|
18
|
-
{ path: 'models', label: 'models', glyph: '◎' },
|
|
19
|
-
{ path: 'logs', label: 'logs', glyph: '☰' },
|
|
20
|
-
{ path: 'cron', label: 'cron', glyph: '◷' },
|
|
21
|
-
{ path: 'skills', label: 'skills', glyph: '◈' },
|
|
22
|
-
{ path: 'config', label: 'config', glyph: '⚙' },
|
|
23
|
-
{ path: 'env', label: 'keys', glyph: '⚿' },
|
|
24
|
-
{ path: 'tools', label: 'tools', glyph: '⚒' },
|
|
25
|
-
{ path: 'batch', label: 'batch', glyph: '⊞' },
|
|
26
|
-
{ path: 'gateway', label: 'gateway', glyph: '⇌' },
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
const OS_ROUTE_DEFS = [
|
|
30
|
-
{ path: 'os-instances', label: 'instances', glyph: '◫' },
|
|
31
|
-
{ path: 'os-windows', label: 'windows', glyph: '▭' },
|
|
32
|
-
{ path: 'os-x', label: 'x-server', glyph: '✕' },
|
|
33
|
-
{ path: 'os-fs', label: 'fs', glyph: '📁' },
|
|
34
|
-
];
|
|
9
|
+
const { AppShell, Topbar, Side, Crumb, Status, Panel, Chip, EmptyState } = components;
|
|
35
10
|
|
|
36
11
|
function pre(obj) {
|
|
37
|
-
return
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function form(opts) {
|
|
41
|
-
const { fields = [], submit = 'submit', onSubmit } = opts;
|
|
42
|
-
return h('form', { class: 'row-form', onsubmit: (ev) => { ev.preventDefault(); onSubmit && onSubmit(ev); } },
|
|
43
|
-
...fields.map(f => f.kind === 'textarea'
|
|
44
|
-
? h('textarea', { name: f.name, placeholder: f.placeholder || '', rows: f.rows || 4 })
|
|
45
|
-
: h('input', { name: f.name, type: f.type || 'text', placeholder: f.placeholder || '', value: f.value || '', required: f.required ? 'true' : null })),
|
|
46
|
-
h('button', { type: 'submit', class: 'btn-primary' }, submit));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function getRecentPaths() {
|
|
50
|
-
try { return JSON.parse(localStorage.getItem('fd_recent_cwds') || '[]'); } catch { return []; }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function saveRecentPath(p) {
|
|
54
|
-
if (!p) return;
|
|
55
|
-
try {
|
|
56
|
-
const prev = getRecentPaths().filter(x => x !== p);
|
|
57
|
-
localStorage.setItem('fd_recent_cwds', JSON.stringify([p, ...prev].slice(0, 5)));
|
|
58
|
-
} catch {}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function skillLabel(s) {
|
|
62
|
-
if (s.shortName) return s.shortName;
|
|
63
|
-
const n = s.name || '';
|
|
64
|
-
return n.replace(/^gm:/, '').replace(/^software-development$/, 'software dev').replace(/-/g, ' ');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function renderChatMessages(container, messages) {
|
|
68
|
-
if (!container) return;
|
|
69
|
-
container.innerHTML = '';
|
|
70
|
-
for (const m of messages) {
|
|
71
|
-
if (m.role === 'tool') {
|
|
72
|
-
const det = document.createElement('details');
|
|
73
|
-
det.style.cssText = 'margin:4px 0;padding:4px 8px;background:rgba(0,0,0,0.18);border-radius:4px;font-family:monospace;font-size:0.85em;';
|
|
74
|
-
const sum = document.createElement('summary');
|
|
75
|
-
sum.style.cssText = 'cursor:pointer;color:var(--color-warn,#fc9);padding:2px 0;';
|
|
76
|
-
sum.textContent = '⚒ ' + m.name + (m.argsSummary ? ' ' + m.argsSummary : '');
|
|
77
|
-
det.appendChild(sum);
|
|
78
|
-
const body = document.createElement('pre');
|
|
79
|
-
body.style.cssText = 'margin:4px 0 0;white-space:pre-wrap;word-break:break-all;max-height:200px;overflow-y:auto;';
|
|
80
|
-
body.textContent = m.content || '';
|
|
81
|
-
det.appendChild(body);
|
|
82
|
-
container.appendChild(det);
|
|
83
|
-
} else {
|
|
84
|
-
const el = document.createElement('div');
|
|
85
|
-
el.style.cssText = 'padding:6px 10px;border-bottom:1px solid rgba(128,128,128,0.15);white-space:pre-wrap;word-break:break-word;';
|
|
86
|
-
el.style.color = m.role === 'assistant' ? 'var(--color-accent,#7c9)' : 'inherit';
|
|
87
|
-
el.textContent = (m.role === 'assistant' ? '◈ ' : '▷ ') + (m.content || '');
|
|
88
|
-
container.appendChild(el);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
container.scrollTop = container.scrollHeight;
|
|
12
|
+
return webjsx.createElement('pre', { class: 'fd-pre' }, typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2));
|
|
92
13
|
}
|
|
93
14
|
|
|
94
15
|
export function createFreddieDashboard({ instance, bootHost, osSurfaces }) {
|
|
95
16
|
const root = document.createElement('div');
|
|
96
|
-
root.className = 'app-fd ds-247420';
|
|
97
|
-
root.style.cssText = 'height:100%;overflow:hidden;display:flex;flex-direction:column;';
|
|
17
|
+
root.className = 'app-fd ds-247420 fd-root';
|
|
98
18
|
|
|
99
19
|
const state = { active: 'home', ts: new Date().toLocaleTimeString(), body: null, error: null };
|
|
100
20
|
let host = instance.host || null;
|
|
@@ -108,9 +28,18 @@ export function createFreddieDashboard({ instance, bootHost, osSurfaces }) {
|
|
|
108
28
|
}
|
|
109
29
|
|
|
110
30
|
function setActive(p) { state.active = p; rerender(); }
|
|
111
|
-
|
|
112
31
|
if (typeof window !== 'undefined') window.__fd_nav = setActive;
|
|
113
32
|
|
|
33
|
+
function rerender() { webjsx.applyDiff(root, view()); loadActive(); }
|
|
34
|
+
|
|
35
|
+
const ctx = { instance, osSurfaces, root, state, rerender, get host() { return host; } };
|
|
36
|
+
const PAGES = {
|
|
37
|
+
...makeCorePages(ctx),
|
|
38
|
+
chat: makeChatPage(ctx),
|
|
39
|
+
...makeToolsPages(ctx),
|
|
40
|
+
...(osSurfaces ? makeOsPages(ctx) : {}),
|
|
41
|
+
};
|
|
42
|
+
|
|
114
43
|
function buildSide() {
|
|
115
44
|
const sections = [{
|
|
116
45
|
group: 'FREDDIE',
|
|
@@ -142,8 +71,6 @@ export function createFreddieDashboard({ instance, bootHost, osSurfaces }) {
|
|
|
142
71
|
});
|
|
143
72
|
}
|
|
144
73
|
|
|
145
|
-
function rerender() { webjsx.applyDiff(root, view()); loadActive(); }
|
|
146
|
-
|
|
147
74
|
async function loadActive() {
|
|
148
75
|
try {
|
|
149
76
|
const h0 = await ensureHost();
|
|
@@ -158,484 +85,6 @@ export function createFreddieDashboard({ instance, bootHost, osSurfaces }) {
|
|
|
158
85
|
webjsx.applyDiff(root, view());
|
|
159
86
|
}
|
|
160
87
|
|
|
161
|
-
const PAGES = {
|
|
162
|
-
async projects(h0) {
|
|
163
|
-
const list = h0.pi.projects.list();
|
|
164
|
-
const activeProj = (typeof h0.pi.projects.active === 'function') ? h0.pi.projects.active() : null;
|
|
165
|
-
const rows = list.map(p => Row({
|
|
166
|
-
key: p.name,
|
|
167
|
-
code: p.name === activeProj?.name ? '●' : '○',
|
|
168
|
-
title: p.name + (p.name === activeProj?.name ? ' (active)' : ''),
|
|
169
|
-
meta: p.path,
|
|
170
|
-
onClick: () => { if (p.name !== activeProj?.name) try { h0.pi.projects.setActive(p.name); rerender(); } catch (e) { alert(e.message); } },
|
|
171
|
-
}));
|
|
172
|
-
return [
|
|
173
|
-
Hero({ title: 'projects', body: 'each project is its own ~/.freddie home: separate sessions, agents, skills, config, env, cron, batches.', accent: activeProj ? 'active · ' + activeProj.name : 'no active project' }),
|
|
174
|
-
Kpi({ items: [[list.length, 'projects'], [activeProj?.name || '—', 'active'], [activeProj?.path?.length > 30 ? '…' + activeProj.path.slice(-28) : (activeProj?.path || '—'), 'path']] }),
|
|
175
|
-
Panel({ title: 'add a project', children: form({
|
|
176
|
-
fields: [{ name: 'name', placeholder: 'project name', required: true }, { name: 'path', placeholder: '/abs/path' }],
|
|
177
|
-
submit: 'add',
|
|
178
|
-
onSubmit: (ev) => { try { h0.pi.projects.create({ name: ev.target.elements.name.value, path: ev.target.elements.path.value }); rerender(); } catch (e) { alert(e.message); } },
|
|
179
|
-
}) }),
|
|
180
|
-
Panel({ title: 'all projects', count: list.length, children: rows.length ? rows : EmptyState({ text: 'no projects', glyph: '◆' }) }),
|
|
181
|
-
Panel({ title: 'how encapsulation works', children: Receipt({ rows: [
|
|
182
|
-
['sessions db', '<project>/sessions.db'],
|
|
183
|
-
['config', '<project>/config.json'],
|
|
184
|
-
['skills', '<project>/skills/'],
|
|
185
|
-
['plugins', '<project>/plugins/'],
|
|
186
|
-
['cron', '<project>/cron.db'],
|
|
187
|
-
['batches', '<project>/batches/'],
|
|
188
|
-
['logs', '<project>/logs/'],
|
|
189
|
-
['auth', '<project>/auth.json'],
|
|
190
|
-
] }) }),
|
|
191
|
-
];
|
|
192
|
-
},
|
|
193
|
-
async home(h0) {
|
|
194
|
-
const sessions = await h0.pi.sessions.list();
|
|
195
|
-
const tools = h0.pi.tools.size;
|
|
196
|
-
const skills = h0.pi.skills.size;
|
|
197
|
-
const health = (typeof h0.pi.health === 'function') ? h0.pi.health() : { ok: true };
|
|
198
|
-
return [
|
|
199
|
-
Hero({ title: 'freddie', body: 'open js agent harness — pi-mono · xstate · floosie · anentrypoint-design.', accent: h0.version || 'web' }),
|
|
200
|
-
Kpi({ items: [[sessions.length, 'sessions'], [tools, 'tools'], [skills, 'skills']] }),
|
|
201
|
-
Panel({ title: 'quick start', children: Receipt({ rows: [
|
|
202
|
-
['open chat', "click 'chat' in sidebar — set a working directory and pick a skill"],
|
|
203
|
-
['pick skill', "software dev, research, planning — shown with descriptions"],
|
|
204
|
-
['pick model', "select a configured provider + model in the chat bar"],
|
|
205
|
-
['list tools', '/tools in chat → tools tab'],
|
|
206
|
-
['set api key', 'keys tab → click chip to set value'],
|
|
207
|
-
['add cron', 'cron tab → form'],
|
|
208
|
-
] }) }),
|
|
209
|
-
Panel({ title: 'host', children: Receipt({ rows: Object.entries(health).map(([k, v]) => [k, String(v)]) }) }),
|
|
210
|
-
];
|
|
211
|
-
},
|
|
212
|
-
async chat(h0) {
|
|
213
|
-
const skills = [...h0.pi.skills.values()];
|
|
214
|
-
const providers = await fetch('/api/providers').then(r => r.json()).catch(() => []);
|
|
215
|
-
const configuredProviders = providers.filter(p => p.configured);
|
|
216
|
-
|
|
217
|
-
const chatState = window.__fd_chatState = window.__fd_chatState || {
|
|
218
|
-
cwd: '', skill: '', provider: '', model: '', messages: [], busy: false, sessionId: null,
|
|
219
|
-
};
|
|
220
|
-
if (!chatState.cwd) chatState.cwd = (getRecentPaths()[0] || '');
|
|
221
|
-
|
|
222
|
-
function getMsgsContainer() { return root.querySelector('#fd-chat-msgs'); }
|
|
223
|
-
|
|
224
|
-
function newSession() {
|
|
225
|
-
if (chatState.busy) return;
|
|
226
|
-
chatState.messages = [];
|
|
227
|
-
chatState.sessionId = null;
|
|
228
|
-
renderChatMessages(getMsgsContainer(), chatState.messages);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const parseSseEvents = (text) => {
|
|
232
|
-
const events = [];
|
|
233
|
-
let curEvent = null, curData = '';
|
|
234
|
-
for (const line of text.split('\n')) {
|
|
235
|
-
if (line.startsWith('event: ')) { curEvent = line.slice(7).trim(); }
|
|
236
|
-
else if (line.startsWith('data: ')) { curData = line.slice(6).trim(); }
|
|
237
|
-
else if (line === '' && curEvent) {
|
|
238
|
-
try { events.push({ event: curEvent, data: JSON.parse(curData) }); } catch {}
|
|
239
|
-
curEvent = null; curData = '';
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return events;
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
const sendChat = async (ev) => {
|
|
246
|
-
ev.preventDefault();
|
|
247
|
-
if (chatState.busy) return;
|
|
248
|
-
const promptEl = ev.target.elements.prompt;
|
|
249
|
-
const prompt = promptEl.value.trim();
|
|
250
|
-
if (!prompt) return;
|
|
251
|
-
chatState.messages.push({ role: 'user', content: prompt });
|
|
252
|
-
promptEl.value = '';
|
|
253
|
-
promptEl.style.height = 'auto';
|
|
254
|
-
chatState.busy = true;
|
|
255
|
-
saveRecentPath(chatState.cwd);
|
|
256
|
-
renderChatMessages(getMsgsContainer(), chatState.messages);
|
|
257
|
-
try {
|
|
258
|
-
const body = { prompt, cwd: chatState.cwd || undefined, skill: chatState.skill || undefined, provider: chatState.provider || undefined, model: chatState.model || undefined, sessionId: chatState.sessionId || undefined };
|
|
259
|
-
const resp = await fetch('/api/chat', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
|
|
260
|
-
const text = await resp.text();
|
|
261
|
-
const events = parseSseEvents(text);
|
|
262
|
-
let assistantContent = '';
|
|
263
|
-
for (const { event, data } of events) {
|
|
264
|
-
if (event === 'start' && data.sessionId) chatState.sessionId = data.sessionId;
|
|
265
|
-
if (event === 'done' && data.sessionId) chatState.sessionId = data.sessionId;
|
|
266
|
-
if (event === 'message') {
|
|
267
|
-
const role = data.role;
|
|
268
|
-
if (role === 'assistant') {
|
|
269
|
-
const content = Array.isArray(data.content) ? data.content : [{ type: 'text', text: String(data.content || '') }];
|
|
270
|
-
for (const block of content) {
|
|
271
|
-
if (block.type === 'text') assistantContent += block.text;
|
|
272
|
-
if (block.type === 'tool_use') {
|
|
273
|
-
if (assistantContent) { chatState.messages.push({ role: 'assistant', content: assistantContent }); assistantContent = ''; }
|
|
274
|
-
const argsSummary = JSON.stringify(block.input || {}).slice(0, 60);
|
|
275
|
-
chatState.messages.push({ role: 'tool', name: block.name, argsSummary, content: JSON.stringify(block.input || {}, null, 2) });
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
} else if (role === 'tool') {
|
|
279
|
-
const tc = Array.isArray(data.content) ? data.content[0] : data;
|
|
280
|
-
chatState.messages.push({ role: 'tool', name: 'result', argsSummary: '', content: String(tc?.content || tc?.text || JSON.stringify(tc)) });
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
if (event === 'done' && data.result) {
|
|
284
|
-
if (!assistantContent) assistantContent = data.result;
|
|
285
|
-
}
|
|
286
|
-
if (event === 'error') assistantContent = 'error: ' + (data.error || 'unknown');
|
|
287
|
-
}
|
|
288
|
-
if (assistantContent) chatState.messages.push({ role: 'assistant', content: assistantContent });
|
|
289
|
-
if (!events.length) chatState.messages.push({ role: 'assistant', content: '(no response)' });
|
|
290
|
-
} catch (e) {
|
|
291
|
-
chatState.messages.push({ role: 'assistant', content: 'error: ' + e.message });
|
|
292
|
-
}
|
|
293
|
-
chatState.busy = false;
|
|
294
|
-
renderChatMessages(getMsgsContainer(), chatState.messages);
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
const recentPaths = getRecentPaths();
|
|
298
|
-
const datalistId = 'fd-cwd-list';
|
|
299
|
-
const byCat = skills.reduce((a, s) => { const c = s.category || 'other'; (a[c] = a[c] || []).push(s); return a; }, {});
|
|
300
|
-
|
|
301
|
-
setTimeout(() => renderChatMessages(getMsgsContainer(), chatState.messages), 50);
|
|
302
|
-
|
|
303
|
-
return [
|
|
304
|
-
Panel({
|
|
305
|
-
title: 'chat',
|
|
306
|
-
right: h('button', {
|
|
307
|
-
class: 'btn-primary', style: 'padding:2px 10px;font-size:0.8em;',
|
|
308
|
-
onclick: (ev) => { ev.preventDefault(); newSession(); },
|
|
309
|
-
disabled: chatState.busy ? 'true' : null,
|
|
310
|
-
}, '+ new session'),
|
|
311
|
-
children: [
|
|
312
|
-
h('datalist', { id: datalistId }, ...recentPaths.map(p => h('option', { value: p }))),
|
|
313
|
-
h('form', { class: 'row-form', style: 'display:flex;flex-direction:column;gap:8px;', onsubmit: sendChat },
|
|
314
|
-
h('div', { style: 'display:flex;flex-direction:column;gap:4px;' },
|
|
315
|
-
h('label', { style: 'font-size:0.75em;opacity:0.7;letter-spacing:0.05em;' }, 'WORKING DIRECTORY'),
|
|
316
|
-
h('input', {
|
|
317
|
-
name: 'cwd', type: 'text', placeholder: 'e.g. C:/dev/myproject or /home/user/project',
|
|
318
|
-
value: chatState.cwd, list: datalistId,
|
|
319
|
-
style: 'width:100%;box-sizing:border-box;',
|
|
320
|
-
oninput: (ev) => { chatState.cwd = ev.target.value; },
|
|
321
|
-
})
|
|
322
|
-
),
|
|
323
|
-
h('div', { style: 'display:flex;gap:8px;flex-wrap:wrap;' },
|
|
324
|
-
h('div', { style: 'display:flex;flex-direction:column;gap:4px;flex:2;min-width:160px;' },
|
|
325
|
-
h('label', { style: 'font-size:0.75em;opacity:0.7;letter-spacing:0.05em;' }, 'SKILL'),
|
|
326
|
-
h('select', { name: 'skill', onchange: (ev) => { chatState.skill = ev.target.value; } },
|
|
327
|
-
h('option', { value: '' }, '— no skill —'),
|
|
328
|
-
...Object.entries(byCat).map(([cat, ss]) =>
|
|
329
|
-
h('optgroup', { label: cat },
|
|
330
|
-
...ss.map(s => h('option', {
|
|
331
|
-
value: s.name,
|
|
332
|
-
selected: chatState.skill === s.name ? 'true' : null,
|
|
333
|
-
title: s.description || s.name,
|
|
334
|
-
}, skillLabel(s)))
|
|
335
|
-
)
|
|
336
|
-
)
|
|
337
|
-
)
|
|
338
|
-
),
|
|
339
|
-
h('div', { style: 'display:flex;flex-direction:column;gap:4px;flex:2;min-width:140px;' },
|
|
340
|
-
h('label', { style: 'font-size:0.75em;opacity:0.7;letter-spacing:0.05em;' }, 'PROVIDER'),
|
|
341
|
-
h('select', { name: 'provider', onchange: (ev) => { chatState.provider = ev.target.value; } },
|
|
342
|
-
h('option', { value: '' }, configuredProviders.length ? '— auto —' : '— no providers configured —'),
|
|
343
|
-
...configuredProviders.map(p => h('option', {
|
|
344
|
-
value: p.name,
|
|
345
|
-
selected: chatState.provider === p.name ? 'true' : null,
|
|
346
|
-
}, (p.available ? '● ' : '○ ') + p.name))
|
|
347
|
-
)
|
|
348
|
-
),
|
|
349
|
-
h('div', { style: 'display:flex;flex-direction:column;gap:4px;flex:2;min-width:120px;' },
|
|
350
|
-
h('label', { style: 'font-size:0.75em;opacity:0.7;letter-spacing:0.05em;' }, 'MODEL (optional)'),
|
|
351
|
-
h('input', {
|
|
352
|
-
name: 'model', type: 'text',
|
|
353
|
-
placeholder: configuredProviders.find(p => p.name === chatState.provider)?.defaultModel || 'default',
|
|
354
|
-
value: chatState.model,
|
|
355
|
-
oninput: (ev) => { chatState.model = ev.target.value; },
|
|
356
|
-
})
|
|
357
|
-
)
|
|
358
|
-
),
|
|
359
|
-
h('div', { style: 'display:flex;gap:8px;align-items:flex-end;' },
|
|
360
|
-
h('textarea', {
|
|
361
|
-
name: 'prompt', placeholder: 'describe what you want to do in the working directory…',
|
|
362
|
-
rows: 4, style: 'flex:1;resize:none;min-height:80px;',
|
|
363
|
-
oninput: (ev) => {
|
|
364
|
-
ev.target.style.height = 'auto';
|
|
365
|
-
ev.target.style.height = Math.min(ev.target.scrollHeight, 240) + 'px';
|
|
366
|
-
},
|
|
367
|
-
}),
|
|
368
|
-
h('button', {
|
|
369
|
-
type: 'submit', class: 'btn-primary', style: 'align-self:flex-end;',
|
|
370
|
-
disabled: chatState.busy ? 'true' : null,
|
|
371
|
-
}, chatState.busy ? '…' : 'send')
|
|
372
|
-
)
|
|
373
|
-
),
|
|
374
|
-
h('div', { id: 'fd-chat-msgs', style: 'max-height:420px;overflow-y:auto;background:rgba(0,0,0,0.12);border-radius:4px;padding:4px;margin-top:8px;' }),
|
|
375
|
-
],
|
|
376
|
-
}),
|
|
377
|
-
configuredProviders.length === 0
|
|
378
|
-
? Panel({ title: 'no providers configured', children: Receipt({ rows: [
|
|
379
|
-
['set API key', 'go to keys tab, click a provider chip to set its key'],
|
|
380
|
-
['then reload', 'refresh this page to see providers here'],
|
|
381
|
-
['or use acptoapi', 'run acptoapi server on localhost:4800 for local LLMs'],
|
|
382
|
-
] }) })
|
|
383
|
-
: Panel({ title: 'configured providers', children: h('div', { style: 'display:flex;flex-wrap:wrap;gap:6px;padding:8px 4px;' },
|
|
384
|
-
...providers.map(p => Chip({ tone: p.configured ? (p.available ? 'ok' : 'warn') : 'miss', children: p.name + (p.configured ? (p.available ? ' ●' : ' ○') : '') }))
|
|
385
|
-
) }),
|
|
386
|
-
];
|
|
387
|
-
},
|
|
388
|
-
async sessions(h0) {
|
|
389
|
-
const list = await h0.pi.sessions.list();
|
|
390
|
-
const rows = list.map(s => {
|
|
391
|
-
const cont = h('button', {
|
|
392
|
-
class: 'btn-primary', style: 'padding:2px 8px;font-size:0.8em;',
|
|
393
|
-
onclick: async () => {
|
|
394
|
-
const msgs = await h0.pi.sessions.getMessages(s.id);
|
|
395
|
-
const cs = window.__fd_chatState = window.__fd_chatState || { messages: [], busy: false, sessionId: null, cwd: '', skill: '', provider: '', model: '' };
|
|
396
|
-
cs.sessionId = s.id;
|
|
397
|
-
cs.messages = msgs.map(m => ({ role: m.role, content: String(m.content || '') }));
|
|
398
|
-
if (s.cwd) cs.cwd = s.cwd;
|
|
399
|
-
if (s.skill) cs.skill = s.skill;
|
|
400
|
-
if (typeof window.__fd_nav === 'function') window.__fd_nav('chat');
|
|
401
|
-
},
|
|
402
|
-
}, 'continue');
|
|
403
|
-
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];
|
|
404
|
-
});
|
|
405
|
-
return [
|
|
406
|
-
Kpi({ items: [[list.length, 'sessions']] }),
|
|
407
|
-
Panel({ title: 'recent sessions', count: list.length, children: list.length === 0
|
|
408
|
-
? EmptyState({ text: 'no sessions yet — open chat and send a message', glyph: '✉' })
|
|
409
|
-
: Table({ headers: ['id', 'title', 'platform', 'model', 'cwd', 'skill', ''],
|
|
410
|
-
rows }) }),
|
|
411
|
-
];
|
|
412
|
-
},
|
|
413
|
-
async agents(h0) {
|
|
414
|
-
const a = (typeof h0.pi.agents === 'function') ? await h0.pi.agents() : { count: 0, turns: 0, active: null };
|
|
415
|
-
return [
|
|
416
|
-
Kpi({ items: [[a.count || 0, 'active'], [a.turns || 0, 'turns']] }),
|
|
417
|
-
Panel({ title: 'agent overview', children: Receipt({ rows: [
|
|
418
|
-
['total turns', String(a.turns || 0)],
|
|
419
|
-
['active session', a.active || '(none)'],
|
|
420
|
-
['last activity', a.last_activity ? new Date(a.last_activity).toLocaleString() : '—'],
|
|
421
|
-
] }) }),
|
|
422
|
-
];
|
|
423
|
-
},
|
|
424
|
-
async analytics(h0) {
|
|
425
|
-
const list = await h0.pi.sessions.list();
|
|
426
|
-
const tools = [...h0.pi.tools.values()];
|
|
427
|
-
const byPlatform = list.reduce((a, s) => { const k = s.platform || '?'; a[k] = (a[k] || 0) + 1; return a; }, {});
|
|
428
|
-
const byModel = list.reduce((a, s) => { const k = s.model || '?'; a[k] = (a[k] || 0) + 1; return a; }, {});
|
|
429
|
-
const byToolset = tools.reduce((a, t) => { (a[t.toolset || 'core'] = a[t.toolset || 'core'] || []).push(t.name); return a; }, {});
|
|
430
|
-
return [
|
|
431
|
-
Kpi({ items: [[list.length, 'sessions'], [tools.length, 'tools']] }),
|
|
432
|
-
Panel({ title: 'sessions by platform', children: Object.keys(byPlatform).length === 0
|
|
433
|
-
? EmptyState({ text: 'no data', glyph: '◉' })
|
|
434
|
-
: Table({ headers: ['platform', 'count'], rows: Object.entries(byPlatform).sort((a, b) => b[1] - a[1]) }) }),
|
|
435
|
-
Panel({ title: 'sessions by model', children: Object.keys(byModel).length === 0
|
|
436
|
-
? EmptyState({ text: 'no data', glyph: '◎' })
|
|
437
|
-
: Table({ headers: ['model', 'count'], rows: Object.entries(byModel).sort((a, b) => b[1] - a[1]) }) }),
|
|
438
|
-
Panel({ title: 'tool distribution', children: Table({ headers: ['toolset', 'count', 'tools'],
|
|
439
|
-
rows: Object.entries(byToolset).map(([k, v]) => [k, v.length, v.slice(0, 4).join(', ') + (v.length > 4 ? '…' : '')]) }) }),
|
|
440
|
-
];
|
|
441
|
-
},
|
|
442
|
-
async models(h0) {
|
|
443
|
-
const cfg = (typeof h0.pi.config?.load === 'function') ? await h0.pi.config.load() : {};
|
|
444
|
-
const agent = cfg.agent || {};
|
|
445
|
-
const providers = await fetch('/api/providers').then(r => r.json()).catch(() => []);
|
|
446
|
-
return [
|
|
447
|
-
Kpi({ items: [[agent.provider || '—', 'provider'], [agent.model || '—', 'model']] }),
|
|
448
|
-
Panel({ title: 'active model', children: Receipt({ rows: [
|
|
449
|
-
['provider', agent.provider || '(unset)'],
|
|
450
|
-
['model', agent.model || '(unset)'],
|
|
451
|
-
['max_iterations', String(agent.max_iterations || '—')],
|
|
452
|
-
['max_tokens', String(agent.max_tokens || '—')],
|
|
453
|
-
['temperature', String(agent.temperature ?? '—')],
|
|
454
|
-
] }) }),
|
|
455
|
-
Panel({ title: 'change model', children: form({
|
|
456
|
-
fields: [{ name: 'provider', placeholder: 'provider', value: agent.provider || '' }, { name: 'model', placeholder: 'model id', value: agent.model || '' }],
|
|
457
|
-
submit: 'update',
|
|
458
|
-
onSubmit: async (ev) => {
|
|
459
|
-
await h0.pi.config.saveValue('agent.provider', ev.target.elements.provider.value);
|
|
460
|
-
await h0.pi.config.saveValue('agent.model', ev.target.elements.model.value);
|
|
461
|
-
rerender();
|
|
462
|
-
},
|
|
463
|
-
}) }),
|
|
464
|
-
Panel({ title: 'provider availability', children: h('div', { style: 'display:flex;flex-wrap:wrap;gap:6px;padding:8px 4px;' },
|
|
465
|
-
...providers.map(p => Chip({ tone: p.configured ? (p.available ? 'ok' : 'warn') : 'miss', children: p.name + (p.configured ? (p.available ? ' ●' : ' ○') : ' ·') }))
|
|
466
|
-
) }),
|
|
467
|
-
];
|
|
468
|
-
},
|
|
469
|
-
async logs(h0) {
|
|
470
|
-
const dbg = (typeof h0.pi.debug === 'function') ? h0.pi.debug() : { note: 'no debug surface' };
|
|
471
|
-
return [Panel({ title: 'host debug snapshot', children: pre(dbg) })];
|
|
472
|
-
},
|
|
473
|
-
async cron(h0) {
|
|
474
|
-
const list = await h0.pi.cron.list();
|
|
475
|
-
return [
|
|
476
|
-
Kpi({ items: [[list.length, 'cron jobs']] }),
|
|
477
|
-
Panel({ title: 'add job', children: form({
|
|
478
|
-
fields: [{ name: 'cron', placeholder: '* * * * *', required: true }, { name: 'prompt', placeholder: 'prompt', required: true }],
|
|
479
|
-
submit: 'create',
|
|
480
|
-
onSubmit: async (ev) => { try { await h0.pi.cron.create({ cron: ev.target.elements.cron.value, prompt: ev.target.elements.prompt.value }); rerender(); } catch (e) { alert(e.message); } },
|
|
481
|
-
}) }),
|
|
482
|
-
Panel({ title: 'scheduled jobs', count: list.length, children: list.length === 0
|
|
483
|
-
? EmptyState({ text: 'no cron jobs — add one above', glyph: '◷' })
|
|
484
|
-
: Table({ headers: ['id', 'cron', 'prompt', 'enabled'],
|
|
485
|
-
rows: list.map(j => [j.id, j.cron, (j.prompt || '').slice(0, 40), j.enabled ? 'yes' : 'no']) }) }),
|
|
486
|
-
];
|
|
487
|
-
},
|
|
488
|
-
async skills(h0) {
|
|
489
|
-
const list = [...h0.pi.skills.values()];
|
|
490
|
-
const byCat = list.reduce((a, s) => { (a[s.category || 'other'] = a[s.category || 'other'] || []).push(s); return a; }, {});
|
|
491
|
-
return [
|
|
492
|
-
Kpi({ items: [[list.length, 'skills'], [Object.keys(byCat).length, 'categories']] }),
|
|
493
|
-
list.length === 0 ? EmptyState({ text: 'no skills loaded — add SKILL.md files to ~/.freddie/skills/', glyph: '◈' }) : null,
|
|
494
|
-
...Object.entries(byCat).map(([cat, ss]) => Panel({ title: cat, count: ss.length,
|
|
495
|
-
children: ss.length === 0 ? EmptyState({ text: 'none', glyph: '◈' })
|
|
496
|
-
: Table({ headers: ['name', 'description'], rows: ss.map(s => [skillLabel(s), (s.description || '').slice(0, 120)]) }) })),
|
|
497
|
-
].filter(Boolean);
|
|
498
|
-
},
|
|
499
|
-
async config(h0) {
|
|
500
|
-
const cfg = (typeof h0.pi.config?.load === 'function') ? await h0.pi.config.load() : {};
|
|
501
|
-
const profiles = (typeof h0.pi.profiles?.list === 'function') ? h0.pi.profiles.list() : [];
|
|
502
|
-
const commands = (typeof h0.pi.commands?.list === 'function') ? h0.pi.commands.list() : [];
|
|
503
|
-
return [
|
|
504
|
-
Kpi({ items: [[profiles.length, 'profiles'], [commands.length, 'commands'], [cfg._config_version || 0, 'config version']] }),
|
|
505
|
-
Panel({ title: 'set config value', children: form({
|
|
506
|
-
fields: [{ name: 'key', placeholder: 'dotted.key (e.g. agent.model)', required: true }, { name: 'value', placeholder: 'value (json or string)', required: true }],
|
|
507
|
-
submit: 'save',
|
|
508
|
-
onSubmit: async (ev) => {
|
|
509
|
-
let v = ev.target.elements.value.value;
|
|
510
|
-
try { v = JSON.parse(v); } catch {}
|
|
511
|
-
await h0.pi.config.saveValue(ev.target.elements.key.value, v);
|
|
512
|
-
rerender();
|
|
513
|
-
},
|
|
514
|
-
}) }),
|
|
515
|
-
Panel({ title: 'commands', count: commands.length,
|
|
516
|
-
children: Table({ headers: ['name', 'category', 'description'], rows: commands.map(c => [c.name, c.category || '', c.description || '']) }) }),
|
|
517
|
-
Panel({ title: 'active config', children: pre(cfg) }),
|
|
518
|
-
];
|
|
519
|
-
},
|
|
520
|
-
async env(h0) {
|
|
521
|
-
const list = (typeof h0.pi.env?.list === 'function') ? h0.pi.env.list() : [];
|
|
522
|
-
const setCount = list.filter(k => k.set).length;
|
|
523
|
-
const chipNodes = list.map(k => h(
|
|
524
|
-
'span',
|
|
525
|
-
{
|
|
526
|
-
key: k.key,
|
|
527
|
-
onclick: () => {
|
|
528
|
-
const v = prompt('set ' + k.key + ' (empty to unset):');
|
|
529
|
-
if (v == null) return;
|
|
530
|
-
if (typeof h0.pi.env.set === 'function') { h0.pi.env.set(k.key, v); rerender(); }
|
|
531
|
-
},
|
|
532
|
-
style: 'cursor:pointer',
|
|
533
|
-
},
|
|
534
|
-
Chip({ tone: k.set ? 'ok' : 'miss', children: k.key + (k.set ? ' ✓' : ' ·') })
|
|
535
|
-
));
|
|
536
|
-
return [
|
|
537
|
-
Kpi({ items: [[setCount, 'set'], [list.length - setCount, 'missing'], [list.length, 'total known']] }),
|
|
538
|
-
Panel({
|
|
539
|
-
title: 'environment variables',
|
|
540
|
-
right: h('span', {}, Chip({ tone: 'ok', children: setCount + ' set' }), ' ', Chip({ tone: 'miss', children: (list.length - setCount) + ' missing' })),
|
|
541
|
-
children: h('div', { style: 'padding:8px 4px;display:flex;flex-wrap:wrap;gap:6px' }, ...chipNodes),
|
|
542
|
-
}),
|
|
543
|
-
];
|
|
544
|
-
},
|
|
545
|
-
async tools(h0) {
|
|
546
|
-
const list = [...h0.pi.tools.values()];
|
|
547
|
-
const byToolset = list.reduce((a, t) => { (a[t.toolset || 'core'] = a[t.toolset || 'core'] || []).push(t); return a; }, {});
|
|
548
|
-
return [
|
|
549
|
-
Kpi({ items: [[list.length, 'tools'], [Object.keys(byToolset).length, 'toolsets']] }),
|
|
550
|
-
...Object.entries(byToolset).map(([ts, items]) => Panel({ title: 'toolset · ' + ts, count: items.length,
|
|
551
|
-
children: items.map(t => Row({ key: t.name, code: '⚒', title: t.name, sub: (t.description || (t.schema && t.schema.description) || '').slice(0, 80) })) })),
|
|
552
|
-
];
|
|
553
|
-
},
|
|
554
|
-
async batch(h0) {
|
|
555
|
-
const out = h('div', { id: 'fd-batch-out' });
|
|
556
|
-
return [
|
|
557
|
-
Section({ title: '// batch runner', children: [
|
|
558
|
-
Panel({ title: 'run prompts', children: form({
|
|
559
|
-
fields: [{ name: 'prompts', kind: 'textarea', placeholder: 'one prompt per line' }, { name: 'concurrency', type: 'number', value: '4' }],
|
|
560
|
-
submit: 'run',
|
|
561
|
-
onSubmit: async (ev) => {
|
|
562
|
-
const prompts = ev.target.elements.prompts.value.split('\n').map(s => s.trim()).filter(Boolean);
|
|
563
|
-
if (!prompts.length) return;
|
|
564
|
-
const node = root.querySelector('#fd-batch-out');
|
|
565
|
-
if (node) node.textContent = 'running…';
|
|
566
|
-
try {
|
|
567
|
-
const r = await h0.pi.batch.run({ prompts, concurrency: Number(ev.target.elements.concurrency.value) || 4 });
|
|
568
|
-
if (node) { node.innerHTML = ''; node.appendChild(document.createTextNode(JSON.stringify(r, null, 2))); }
|
|
569
|
-
} catch (e) { if (node) node.textContent = 'error: ' + (e.message || e); }
|
|
570
|
-
},
|
|
571
|
-
}) }),
|
|
572
|
-
Panel({ title: 'results', children: out }),
|
|
573
|
-
Panel({ title: 'cli usage', children: Receipt({ rows: [
|
|
574
|
-
['run batch file', 'freddie batch prompts.txt'],
|
|
575
|
-
['set concurrency', 'freddie batch prompts.txt --concurrency 8'],
|
|
576
|
-
['jsonl output', 'freddie batch prompts.txt > out.jsonl'],
|
|
577
|
-
] }) }),
|
|
578
|
-
] }),
|
|
579
|
-
];
|
|
580
|
-
},
|
|
581
|
-
async gateway(h0) {
|
|
582
|
-
const platforms = (typeof h0.pi.gateway?.platforms === 'function') ? h0.pi.gateway.platforms() : [];
|
|
583
|
-
const active = platforms.filter(p => p.enabled);
|
|
584
|
-
return [
|
|
585
|
-
Kpi({ items: [[platforms.length, 'platforms'], [active.length, 'active']] }),
|
|
586
|
-
Panel({ title: 'platforms', count: platforms.length,
|
|
587
|
-
right: active.length > 0 ? Chip({ tone: 'ok', children: active.length + ' active' }) : Chip({ tone: 'miss', children: 'none active' }),
|
|
588
|
-
children: platforms.length === 0 ? EmptyState({ text: 'no platforms registered', glyph: '⇌' })
|
|
589
|
-
: platforms.map(p => Row({ key: p.name, code: p.enabled ? '●' : '○', title: p.name, sub: p.note || '', meta: p.enabled ? 'enabled' : '' })) }),
|
|
590
|
-
Panel({ title: 'start gateway', children: Receipt({ rows: [
|
|
591
|
-
['webhook + api_server', 'freddie gateway --port 3000'],
|
|
592
|
-
['specific platform', 'TELEGRAM_BOT_TOKEN=… freddie gateway'],
|
|
593
|
-
['all platforms', 'set env vars per platform, then freddie gateway'],
|
|
594
|
-
] }) }),
|
|
595
|
-
];
|
|
596
|
-
},
|
|
597
|
-
async ['os-instances']() {
|
|
598
|
-
const list = (osSurfaces && osSurfaces.instances && osSurfaces.instances()) || [];
|
|
599
|
-
const activeId = osSurfaces && osSurfaces.activeInstanceId && osSurfaces.activeInstanceId();
|
|
600
|
-
return [
|
|
601
|
-
Kpi({ items: [[list.length, 'instances'], [activeId || '—', 'active']] }),
|
|
602
|
-
Panel({ title: 'instances', count: list.length, children: list.length === 0
|
|
603
|
-
? EmptyState({ text: 'no instances', glyph: '◫' })
|
|
604
|
-
: Table({ headers: ['id', 'active', 'shells', 'windows'],
|
|
605
|
-
rows: list.map(i => [i.id, i.id === activeId ? '●' : '', String((i.shells || []).length), String((i.windows || []).length)]) }) }),
|
|
606
|
-
];
|
|
607
|
-
},
|
|
608
|
-
async ['os-windows']() {
|
|
609
|
-
const wins = (osSurfaces && osSurfaces.wm && osSurfaces.wm.list && osSurfaces.wm.list()) || [];
|
|
610
|
-
const focused = osSurfaces && osSurfaces.wm && osSurfaces.wm.focused;
|
|
611
|
-
return [
|
|
612
|
-
Kpi({ items: [[wins.length, 'windows'], [focused ? (focused.id || focused.title || '?') : '—', 'focused']] }),
|
|
613
|
-
Panel({ title: 'windows', count: wins.length, children: wins.length === 0
|
|
614
|
-
? EmptyState({ text: 'no windows open', glyph: '▭' })
|
|
615
|
-
: Table({ headers: ['id', 'title', 'min', 'max', 'pos'],
|
|
616
|
-
rows: wins.map(w => [w.id || '?', w.title || '', w.min ? '●' : '', w.max ? '●' : '',
|
|
617
|
-
(w.el ? `${w.el.offsetLeft},${w.el.offsetTop} ${w.el.offsetWidth}×${w.el.offsetHeight}` : '')]) }) }),
|
|
618
|
-
];
|
|
619
|
-
},
|
|
620
|
-
async ['os-x']() {
|
|
621
|
-
const x = osSurfaces && osSurfaces.xServer && osSurfaces.xServer();
|
|
622
|
-
if (!x) return [Panel({ title: 'x-server', children: EmptyState({ text: 'x-server not running in this instance', glyph: '✕' }) })];
|
|
623
|
-
return [
|
|
624
|
-
Kpi({ items: [[x.windows, 'windows'], [x.pixmaps, 'pixmaps'], [x.gcs, 'gcs'], [x.atoms, 'atoms'], [x.cursors, 'cursors']] }),
|
|
625
|
-
Panel({ title: 'display', children: pre(x) }),
|
|
626
|
-
];
|
|
627
|
-
},
|
|
628
|
-
async ['os-fs']() {
|
|
629
|
-
const list = await instance.fs.list('/');
|
|
630
|
-
return [
|
|
631
|
-
Kpi({ items: [[list.length, 'paths'], [instance.id, 'instance']] }),
|
|
632
|
-
Panel({ title: 'paths', count: list.length, children: list.length === 0
|
|
633
|
-
? EmptyState({ text: 'empty fs', glyph: '📁' })
|
|
634
|
-
: pre(list.join('\n')) }),
|
|
635
|
-
];
|
|
636
|
-
},
|
|
637
|
-
};
|
|
638
|
-
|
|
639
88
|
rerender();
|
|
640
89
|
|
|
641
90
|
if (typeof window !== 'undefined') {
|
|
@@ -643,9 +92,7 @@ export function createFreddieDashboard({ instance, bootHost, osSurfaces }) {
|
|
|
643
92
|
window.__debug.instances = window.__debug.instances || {};
|
|
644
93
|
window.__debug.instances[instance.id] = window.__debug.instances[instance.id] || {};
|
|
645
94
|
window.__debug.instances[instance.id].dashboard = {
|
|
646
|
-
root,
|
|
647
|
-
routes: allRoutes.map(r => r.path),
|
|
648
|
-
setActive,
|
|
95
|
+
root, routes: allRoutes.map(r => r.path), setActive,
|
|
649
96
|
get active() { return state.active; },
|
|
650
97
|
};
|
|
651
98
|
}
|