anentrypoint-design 0.0.65 → 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.js +251 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anentrypoint-design",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.66",
|
|
4
4
|
"description": "247420 design system SDK — webjsx + modified ripple-ui, single-file ESM bundle for reproducible use of the AnEntrypoint design.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/247420.js",
|
package/src/components/shell.js
CHANGED
|
@@ -86,9 +86,6 @@ export function Status({ left = [], right = [] } = {}) {
|
|
|
86
86
|
|
|
87
87
|
export function AppShell({ topbar, crumb, side, main, status, narrow } = {}) {
|
|
88
88
|
const hasSide = Boolean(side);
|
|
89
|
-
const sideMotionClass = hasSide
|
|
90
|
-
? ' animate__animated animate__fadeInLeft'
|
|
91
|
-
: ' animate__animated animate__fadeOutLeft';
|
|
92
89
|
const sideNode = hasSide
|
|
93
90
|
? side
|
|
94
91
|
: h('aside', { class: 'app-side', 'aria-hidden': 'true' });
|
|
@@ -97,7 +94,7 @@ export function AppShell({ topbar, crumb, side, main, status, narrow } = {}) {
|
|
|
97
94
|
topbar || null,
|
|
98
95
|
crumb || null,
|
|
99
96
|
h('div', { class: 'app-body' + (hasSide ? '' : ' no-side') },
|
|
100
|
-
h('div', { class: 'app-side-shell'
|
|
97
|
+
h('div', { class: 'app-side-shell' }, sideNode),
|
|
101
98
|
h('main', { class: 'app-main' + (narrow ? ' narrow' : '') }, ...(Array.isArray(main) ? main : [main]))
|
|
102
99
|
),
|
|
103
100
|
status || null
|
|
@@ -46,19 +46,58 @@ function form(opts) {
|
|
|
46
46
|
h('button', { type: 'submit', class: 'btn-primary' }, submit));
|
|
47
47
|
}
|
|
48
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;
|
|
92
|
+
}
|
|
93
|
+
|
|
49
94
|
export function createFreddieDashboard({ instance, bootHost, osSurfaces }) {
|
|
50
95
|
const root = document.createElement('div');
|
|
51
96
|
root.className = 'app-fd ds-247420';
|
|
52
97
|
root.style.cssText = 'height:100%;overflow:hidden;display:flex;flex-direction:column;';
|
|
53
98
|
|
|
54
|
-
const state = {
|
|
55
|
-
active: 'home',
|
|
56
|
-
ts: new Date().toLocaleTimeString(),
|
|
57
|
-
body: null,
|
|
58
|
-
error: null,
|
|
59
|
-
};
|
|
99
|
+
const state = { active: 'home', ts: new Date().toLocaleTimeString(), body: null, error: null };
|
|
60
100
|
let host = instance.host || null;
|
|
61
|
-
|
|
62
101
|
const allRoutes = osSurfaces ? [...ROUTES, ...OS_ROUTE_DEFS] : ROUTES;
|
|
63
102
|
|
|
64
103
|
async function ensureHost() {
|
|
@@ -68,10 +107,9 @@ export function createFreddieDashboard({ instance, bootHost, osSurfaces }) {
|
|
|
68
107
|
return host;
|
|
69
108
|
}
|
|
70
109
|
|
|
71
|
-
function setActive(p) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
110
|
+
function setActive(p) { state.active = p; rerender(); }
|
|
111
|
+
|
|
112
|
+
if (typeof window !== 'undefined') window.__fd_nav = setActive;
|
|
75
113
|
|
|
76
114
|
function buildSide() {
|
|
77
115
|
const sections = [{
|
|
@@ -104,10 +142,7 @@ export function createFreddieDashboard({ instance, bootHost, osSurfaces }) {
|
|
|
104
142
|
});
|
|
105
143
|
}
|
|
106
144
|
|
|
107
|
-
function rerender() {
|
|
108
|
-
webjsx.applyDiff(root, view());
|
|
109
|
-
loadActive();
|
|
110
|
-
}
|
|
145
|
+
function rerender() { webjsx.applyDiff(root, view()); loadActive(); }
|
|
111
146
|
|
|
112
147
|
async function loadActive() {
|
|
113
148
|
try {
|
|
@@ -164,31 +199,215 @@ export function createFreddieDashboard({ instance, bootHost, osSurfaces }) {
|
|
|
164
199
|
Hero({ title: 'freddie', body: 'open js agent harness — pi-mono · xstate · floosie · anentrypoint-design.', accent: h0.version || 'web' }),
|
|
165
200
|
Kpi({ items: [[sessions.length, 'sessions'], [tools, 'tools'], [skills, 'skills']] }),
|
|
166
201
|
Panel({ title: 'quick start', children: Receipt({ rows: [
|
|
167
|
-
['open chat', "click 'chat' in sidebar"],
|
|
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"],
|
|
168
205
|
['list tools', '/tools in chat → tools tab'],
|
|
169
|
-
['
|
|
170
|
-
['set api key', 'keys tab → click chip'],
|
|
206
|
+
['set api key', 'keys tab → click chip to set value'],
|
|
171
207
|
['add cron', 'cron tab → form'],
|
|
172
208
|
] }) }),
|
|
173
209
|
Panel({ title: 'host', children: Receipt({ rows: Object.entries(health).map(([k, v]) => [k, String(v)]) }) }),
|
|
174
210
|
];
|
|
175
211
|
},
|
|
176
212
|
async chat(h0) {
|
|
177
|
-
|
|
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
|
+
|
|
178
303
|
return [
|
|
179
|
-
Panel({
|
|
180
|
-
|
|
181
|
-
|
|
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
|
+
) }),
|
|
182
386
|
];
|
|
183
387
|
},
|
|
184
388
|
async sessions(h0) {
|
|
185
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
|
+
});
|
|
186
405
|
return [
|
|
187
406
|
Kpi({ items: [[list.length, 'sessions']] }),
|
|
188
407
|
Panel({ title: 'recent sessions', count: list.length, children: list.length === 0
|
|
189
|
-
? EmptyState({ text: 'no sessions yet —
|
|
190
|
-
: Table({ headers: ['id', 'title', 'platform', 'model', '
|
|
191
|
-
rows
|
|
408
|
+
? EmptyState({ text: 'no sessions yet — open chat and send a message', glyph: '✉' })
|
|
409
|
+
: Table({ headers: ['id', 'title', 'platform', 'model', 'cwd', 'skill', ''],
|
|
410
|
+
rows }) }),
|
|
192
411
|
];
|
|
193
412
|
},
|
|
194
413
|
async agents(h0) {
|
|
@@ -223,6 +442,7 @@ export function createFreddieDashboard({ instance, bootHost, osSurfaces }) {
|
|
|
223
442
|
async models(h0) {
|
|
224
443
|
const cfg = (typeof h0.pi.config?.load === 'function') ? await h0.pi.config.load() : {};
|
|
225
444
|
const agent = cfg.agent || {};
|
|
445
|
+
const providers = await fetch('/api/providers').then(r => r.json()).catch(() => []);
|
|
226
446
|
return [
|
|
227
447
|
Kpi({ items: [[agent.provider || '—', 'provider'], [agent.model || '—', 'model']] }),
|
|
228
448
|
Panel({ title: 'active model', children: Receipt({ rows: [
|
|
@@ -241,6 +461,9 @@ export function createFreddieDashboard({ instance, bootHost, osSurfaces }) {
|
|
|
241
461
|
rerender();
|
|
242
462
|
},
|
|
243
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
|
+
) }),
|
|
244
467
|
];
|
|
245
468
|
},
|
|
246
469
|
async logs(h0) {
|
|
@@ -267,10 +490,11 @@ export function createFreddieDashboard({ instance, bootHost, osSurfaces }) {
|
|
|
267
490
|
const byCat = list.reduce((a, s) => { (a[s.category || 'other'] = a[s.category || 'other'] || []).push(s); return a; }, {});
|
|
268
491
|
return [
|
|
269
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,
|
|
270
494
|
...Object.entries(byCat).map(([cat, ss]) => Panel({ title: cat, count: ss.length,
|
|
271
495
|
children: ss.length === 0 ? EmptyState({ text: 'none', glyph: '◈' })
|
|
272
|
-
: Table({ headers: ['name', 'description'], rows: ss.map(s => [s
|
|
273
|
-
];
|
|
496
|
+
: Table({ headers: ['name', 'description'], rows: ss.map(s => [skillLabel(s), (s.description || '').slice(0, 120)]) }) })),
|
|
497
|
+
].filter(Boolean);
|
|
274
498
|
},
|
|
275
499
|
async config(h0) {
|
|
276
500
|
const cfg = (typeof h0.pi.config?.load === 'function') ? await h0.pi.config.load() : {};
|