anentrypoint-design 0.0.63 → 0.0.66
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 +2 -2
- package/dist/247420.css +3 -1
- package/dist/247420.js +19 -19
- package/package.json +1 -1
- package/src/components/shell.js +1 -4
- package/src/desktop/freddie-dashboard.css +16 -32
- package/src/desktop/freddie-dashboard.js +526 -277
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import * as webjsx from '../../vendor/webjsx/index.js';
|
|
2
|
+
import * as components from '../components.js';
|
|
3
|
+
|
|
4
|
+
const h = webjsx.createElement;
|
|
5
|
+
const {
|
|
6
|
+
AppShell, Topbar, Side, Crumb, Status, Brand, Glyph,
|
|
7
|
+
Panel, Row, RowLink, Hero, Receipt, Kpi, Table, Section,
|
|
8
|
+
EmptyState, Chip,
|
|
9
|
+
} = components;
|
|
10
|
+
|
|
1
11
|
const ROUTES = [
|
|
2
12
|
{ path: 'projects', label: 'projects', glyph: '◆' },
|
|
3
13
|
{ path: 'home', label: 'home', glyph: '⌂' },
|
|
@@ -16,106 +26,79 @@ const ROUTES = [
|
|
|
16
26
|
{ path: 'gateway', label: 'gateway', glyph: '⇌' },
|
|
17
27
|
];
|
|
18
28
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
return e;
|
|
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
|
+
];
|
|
35
|
+
|
|
36
|
+
function pre(obj) {
|
|
37
|
+
return h('pre', { class: 'fd-pre' }, typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2));
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
function
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
return c;
|
|
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));
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
function
|
|
43
|
-
|
|
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;
|
|
49
|
+
function getRecentPaths() {
|
|
50
|
+
try { return JSON.parse(localStorage.getItem('fd_recent_cwds') || '[]'); } catch { return []; }
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return r;
|
|
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
59
|
}
|
|
60
60
|
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
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;
|
|
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, ' ');
|
|
74
65
|
}
|
|
75
66
|
|
|
76
|
-
function
|
|
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;
|
|
92
|
+
}
|
|
77
93
|
|
|
78
94
|
export function createFreddieDashboard({ instance, bootHost, osSurfaces }) {
|
|
79
|
-
const root =
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const main = el('div', 'fdash-main');
|
|
83
|
-
side.appendChild(nav);
|
|
84
|
-
root.appendChild(side); root.appendChild(main);
|
|
95
|
+
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;';
|
|
85
98
|
|
|
86
|
-
|
|
99
|
+
const state = { active: 'home', ts: new Date().toLocaleTimeString(), body: null, error: null };
|
|
87
100
|
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
|
-
}
|
|
101
|
+
const allRoutes = osSurfaces ? [...ROUTES, ...OS_ROUTE_DEFS] : ROUTES;
|
|
119
102
|
|
|
120
103
|
async function ensureHost() {
|
|
121
104
|
if (host) return host;
|
|
@@ -124,281 +107,547 @@ export function createFreddieDashboard({ instance, bootHost, osSurfaces }) {
|
|
|
124
107
|
return host;
|
|
125
108
|
}
|
|
126
109
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
110
|
+
function setActive(p) { state.active = p; rerender(); }
|
|
111
|
+
|
|
112
|
+
if (typeof window !== 'undefined') window.__fd_nav = setActive;
|
|
113
|
+
|
|
114
|
+
function buildSide() {
|
|
115
|
+
const sections = [{
|
|
116
|
+
group: 'FREDDIE',
|
|
117
|
+
items: ROUTES.map(r => ({
|
|
118
|
+
glyph: r.glyph, label: r.label, href: '#fd-' + r.path,
|
|
119
|
+
active: state.active === r.path,
|
|
120
|
+
onClick: (ev) => { ev.preventDefault(); setActive(r.path); },
|
|
121
|
+
})),
|
|
122
|
+
}];
|
|
123
|
+
if (osSurfaces) sections.push({
|
|
124
|
+
group: 'OS',
|
|
125
|
+
items: OS_ROUTE_DEFS.map(r => ({
|
|
126
|
+
glyph: r.glyph, label: r.label, href: '#fd-' + r.path,
|
|
127
|
+
active: state.active === r.path,
|
|
128
|
+
onClick: (ev) => { ev.preventDefault(); setActive(r.path); },
|
|
129
|
+
})),
|
|
130
|
+
});
|
|
131
|
+
return Side({ sections });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function view() {
|
|
135
|
+
const route = allRoutes.find(r => r.path === state.active) || ROUTES[1];
|
|
136
|
+
return AppShell({
|
|
137
|
+
topbar: Topbar({ brand: 'freddie', leaf: 'dashboard', items: [], active: '' }),
|
|
138
|
+
crumb: Crumb({ trail: ['freddie', instance.id], leaf: route.path, right: state.error ? Chip({ tone: 'miss', children: 'error' }) : Chip({ tone: 'ok', children: 'live' }) }),
|
|
139
|
+
side: buildSide(),
|
|
140
|
+
main: state.body || EmptyState({ text: 'loading…', glyph: '◌' }),
|
|
141
|
+
status: Status({ left: ['ds-247420 · webjsx · ' + allRoutes.length + ' routes', 'instance=' + instance.id], right: [state.ts] }),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function rerender() { webjsx.applyDiff(root, view()); loadActive(); }
|
|
146
|
+
|
|
147
|
+
async function loadActive() {
|
|
132
148
|
try {
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
149
|
+
const h0 = await ensureHost();
|
|
150
|
+
const page = PAGES[state.active] || PAGES.home;
|
|
151
|
+
state.body = await page(h0, instance);
|
|
152
|
+
state.error = null;
|
|
136
153
|
} catch (e) {
|
|
137
|
-
|
|
154
|
+
state.error = String(e && e.stack || e);
|
|
155
|
+
state.body = Panel({ title: 'error', children: pre(state.error) });
|
|
138
156
|
}
|
|
157
|
+
state.ts = new Date().toLocaleTimeString();
|
|
158
|
+
webjsx.applyDiff(root, view());
|
|
139
159
|
}
|
|
140
160
|
|
|
141
161
|
const PAGES = {
|
|
142
|
-
async projects(
|
|
143
|
-
const list =
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
});
|
|
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
|
+
}));
|
|
168
172
|
return [
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
] }) }),
|
|
172
191
|
];
|
|
173
192
|
},
|
|
174
|
-
async home(
|
|
175
|
-
const sessions = await
|
|
176
|
-
const tools =
|
|
177
|
-
const skills =
|
|
178
|
-
const health =
|
|
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 };
|
|
179
198
|
return [
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
['
|
|
184
|
-
['
|
|
185
|
-
['
|
|
186
|
-
|
|
187
|
-
|
|
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)]) }) }),
|
|
188
210
|
];
|
|
189
211
|
},
|
|
190
|
-
async chat(
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
+
];
|
|
196
387
|
},
|
|
197
|
-
async sessions(
|
|
198
|
-
const list = await
|
|
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
|
+
});
|
|
199
405
|
return [
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
?
|
|
203
|
-
:
|
|
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 }) }),
|
|
204
411
|
];
|
|
205
412
|
},
|
|
206
|
-
async agents(
|
|
207
|
-
const a = await
|
|
413
|
+
async agents(h0) {
|
|
414
|
+
const a = (typeof h0.pi.agents === 'function') ? await h0.pi.agents() : { count: 0, turns: 0, active: null };
|
|
208
415
|
return [
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
['total turns', String(a.turns)],
|
|
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)],
|
|
212
419
|
['active session', a.active || '(none)'],
|
|
213
420
|
['last activity', a.last_activity ? new Date(a.last_activity).toLocaleString() : '—'],
|
|
214
|
-
])),
|
|
421
|
+
] }) }),
|
|
215
422
|
];
|
|
216
423
|
},
|
|
217
|
-
async analytics(
|
|
218
|
-
const list = await
|
|
219
|
-
const tools = [...
|
|
424
|
+
async analytics(h0) {
|
|
425
|
+
const list = await h0.pi.sessions.list();
|
|
426
|
+
const tools = [...h0.pi.tools.values()];
|
|
220
427
|
const byPlatform = list.reduce((a, s) => { const k = s.platform || '?'; a[k] = (a[k] || 0) + 1; return a; }, {});
|
|
221
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; }, {});
|
|
222
430
|
return [
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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 ? '…' : '')]) }) }),
|
|
227
440
|
];
|
|
228
441
|
},
|
|
229
|
-
async models(
|
|
230
|
-
const cfg =
|
|
442
|
+
async models(h0) {
|
|
443
|
+
const cfg = (typeof h0.pi.config?.load === 'function') ? await h0.pi.config.load() : {};
|
|
231
444
|
const agent = cfg.agent || {};
|
|
232
|
-
const
|
|
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' }));
|
|
445
|
+
const providers = await fetch('/api/providers').then(r => r.json()).catch(() => []);
|
|
241
446
|
return [
|
|
242
|
-
|
|
243
|
-
|
|
447
|
+
Kpi({ items: [[agent.provider || '—', 'provider'], [agent.model || '—', 'model']] }),
|
|
448
|
+
Panel({ title: 'active model', children: Receipt({ rows: [
|
|
244
449
|
['provider', agent.provider || '(unset)'],
|
|
245
450
|
['model', agent.model || '(unset)'],
|
|
246
451
|
['max_iterations', String(agent.max_iterations || '—')],
|
|
247
|
-
|
|
248
|
-
|
|
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
|
+
) }),
|
|
249
467
|
];
|
|
250
468
|
},
|
|
251
|
-
async logs(
|
|
252
|
-
const dbg =
|
|
253
|
-
return [
|
|
254
|
-
panel('host debug snapshot', pre(dbg)),
|
|
255
|
-
];
|
|
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) })];
|
|
256
472
|
},
|
|
257
|
-
async cron(
|
|
258
|
-
const list = await
|
|
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']));
|
|
473
|
+
async cron(h0) {
|
|
474
|
+
const list = await h0.pi.cron.list();
|
|
270
475
|
return [
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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']) }) }),
|
|
274
486
|
];
|
|
275
487
|
},
|
|
276
|
-
async skills(
|
|
277
|
-
const list = [...
|
|
488
|
+
async skills(h0) {
|
|
489
|
+
const list = [...h0.pi.skills.values()];
|
|
278
490
|
const byCat = list.reduce((a, s) => { (a[s.category || 'other'] = a[s.category || 'other'] || []).push(s); return a; }, {});
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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);
|
|
284
498
|
},
|
|
285
|
-
async config(
|
|
286
|
-
const cfg =
|
|
287
|
-
const profiles =
|
|
288
|
-
const commands =
|
|
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' }));
|
|
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() : [];
|
|
299
503
|
return [
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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) }),
|
|
304
518
|
];
|
|
305
519
|
},
|
|
306
|
-
async env(
|
|
307
|
-
const list =
|
|
520
|
+
async env(h0) {
|
|
521
|
+
const list = (typeof h0.pi.env?.list === 'function') ? h0.pi.env.list() : [];
|
|
308
522
|
const setCount = list.filter(k => k.set).length;
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
));
|
|
320
536
|
return [
|
|
321
|
-
|
|
322
|
-
|
|
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
|
+
}),
|
|
323
543
|
];
|
|
324
544
|
},
|
|
325
|
-
async tools(
|
|
326
|
-
const list = [...
|
|
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; }, {});
|
|
327
548
|
return [
|
|
328
|
-
|
|
329
|
-
|
|
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) })) })),
|
|
330
552
|
];
|
|
331
553
|
},
|
|
332
|
-
async batch(
|
|
333
|
-
const out =
|
|
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' }));
|
|
554
|
+
async batch(h0) {
|
|
555
|
+
const out = h('div', { id: 'fd-batch-out' });
|
|
347
556
|
return [
|
|
348
|
-
|
|
349
|
-
|
|
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
|
+
] }),
|
|
350
579
|
];
|
|
351
580
|
},
|
|
352
|
-
async gateway(
|
|
353
|
-
const platforms =
|
|
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);
|
|
354
584
|
return [
|
|
355
|
-
|
|
356
|
-
|
|
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
|
+
] }) }),
|
|
357
595
|
];
|
|
358
596
|
},
|
|
359
597
|
async ['os-instances']() {
|
|
360
598
|
const list = (osSurfaces && osSurfaces.instances && osSurfaces.instances()) || [];
|
|
361
599
|
const activeId = osSurfaces && osSurfaces.activeInstanceId && osSurfaces.activeInstanceId();
|
|
362
600
|
return [
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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)]) }) }),
|
|
366
606
|
];
|
|
367
607
|
},
|
|
368
608
|
async ['os-windows']() {
|
|
369
609
|
const wins = (osSurfaces && osSurfaces.wm && osSurfaces.wm.list && osSurfaces.wm.list()) || [];
|
|
370
610
|
const focused = osSurfaces && osSurfaces.wm && osSurfaces.wm.focused;
|
|
371
611
|
return [
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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}` : '')]) }) }),
|
|
376
618
|
];
|
|
377
619
|
},
|
|
378
620
|
async ['os-x']() {
|
|
379
621
|
const x = osSurfaces && osSurfaces.xServer && osSurfaces.xServer();
|
|
380
|
-
if (!x) return [
|
|
622
|
+
if (!x) return [Panel({ title: 'x-server', children: EmptyState({ text: 'x-server not running in this instance', glyph: '✕' }) })];
|
|
381
623
|
return [
|
|
382
|
-
|
|
383
|
-
|
|
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) }),
|
|
384
626
|
];
|
|
385
627
|
},
|
|
386
628
|
async ['os-fs']() {
|
|
387
629
|
const list = await instance.fs.list('/');
|
|
388
630
|
return [
|
|
389
|
-
|
|
390
|
-
|
|
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')) }),
|
|
391
635
|
];
|
|
392
636
|
},
|
|
393
637
|
};
|
|
394
638
|
|
|
395
|
-
|
|
639
|
+
rerender();
|
|
396
640
|
|
|
397
641
|
if (typeof window !== 'undefined') {
|
|
398
642
|
window.__debug = window.__debug || {};
|
|
399
643
|
window.__debug.instances = window.__debug.instances || {};
|
|
400
644
|
window.__debug.instances[instance.id] = window.__debug.instances[instance.id] || {};
|
|
401
|
-
window.__debug.instances[instance.id].dashboard = {
|
|
645
|
+
window.__debug.instances[instance.id].dashboard = {
|
|
646
|
+
root,
|
|
647
|
+
routes: allRoutes.map(r => r.path),
|
|
648
|
+
setActive,
|
|
649
|
+
get active() { return state.active; },
|
|
650
|
+
};
|
|
402
651
|
}
|
|
403
652
|
|
|
404
653
|
return { node: root, dispose() {} };
|