anentrypoint-design 0.0.374 → 0.0.376
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/community.css +5 -0
- package/dist/247420.css +5 -0
- package/dist/247420.js +27 -22
- package/package.json +1 -1
- package/src/components/agent-chat.js +30 -2
- package/src/components/freddie.js +240 -116
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anentrypoint-design",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.376",
|
|
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",
|
|
@@ -17,6 +17,7 @@ import { Btn, Icon } from './shell.js';
|
|
|
17
17
|
import { BreadcrumbPath } from './files.js';
|
|
18
18
|
import { SplitPanel } from './editor-primitives.js';
|
|
19
19
|
import { initializeCachesEagerly } from '../markdown-cache.js';
|
|
20
|
+
import { ChatMinimap } from './chat-minimap.js';
|
|
20
21
|
|
|
21
22
|
const h = webjsx.createElement;
|
|
22
23
|
|
|
@@ -224,6 +225,17 @@ export function AgentChat(props = {}) {
|
|
|
224
225
|
onPasteFiles, onDropFiles, onEmoji,
|
|
225
226
|
shownMessages, onShowEarlier,
|
|
226
227
|
streamingSince, detectAttachment,
|
|
228
|
+
// @-mention file autocomplete in the composer — a flat list of file paths
|
|
229
|
+
// the host already has (e.g. its Files tab data source). Purely forwarded
|
|
230
|
+
// to ChatComposer; omitting it keeps every existing caller unchanged (no
|
|
231
|
+
// mention affordance appears without it).
|
|
232
|
+
mentionFiles,
|
|
233
|
+
// Optional scroll-position minimap alongside the thread (ChatMinimap).
|
|
234
|
+
// false/omitted keeps every existing caller byte-identical (no minimap
|
|
235
|
+
// column at all). true renders the strip using this component's own
|
|
236
|
+
// thread ref via a shared getter — no extra DOM wiring needed from the
|
|
237
|
+
// host beyond passing showMinimap.
|
|
238
|
+
showMinimap = false,
|
|
227
239
|
// Optional inline content viewer beside the thread (a docstudio-cue
|
|
228
240
|
// addition: its chat view keeps a live document/PDF preview open next to
|
|
229
241
|
// the conversation instead of forcing a separate tab/window). The host
|
|
@@ -414,6 +426,7 @@ export function AgentChat(props = {}) {
|
|
|
414
426
|
onEmoji,
|
|
415
427
|
streamingSince,
|
|
416
428
|
detectAttachment,
|
|
429
|
+
mentionFiles,
|
|
417
430
|
});
|
|
418
431
|
|
|
419
432
|
// Contextual follow-up chips below the last SETTLED assistant turn (claude.ai/
|
|
@@ -472,8 +485,18 @@ export function AgentChat(props = {}) {
|
|
|
472
485
|
: null)
|
|
473
486
|
: null;
|
|
474
487
|
|
|
488
|
+
// ChatMinimap needs a getter that resolves the live thread element lazily
|
|
489
|
+
// (it may mount before the thread's own ref fires). A holder object keeps
|
|
490
|
+
// the element across re-renders without introducing component state.
|
|
491
|
+
const threadElHolder = { el: null };
|
|
492
|
+
const combinedThreadRef = (el) => {
|
|
493
|
+
threadElHolder.el = el;
|
|
494
|
+
const dispose = threadRef(messages.length)(el);
|
|
495
|
+
return dispose;
|
|
496
|
+
};
|
|
497
|
+
|
|
475
498
|
const threadBody = h('div', { class: 'agentchat-thread-wrap' },
|
|
476
|
-
h('div', { class: 'agentchat-thread', ref:
|
|
499
|
+
h('div', { class: 'agentchat-thread', ref: combinedThreadRef, role: 'log', 'aria-label': 'conversation', 'aria-live': 'polite', 'aria-relevant': 'additions' },
|
|
477
500
|
emptyState,
|
|
478
501
|
earlierRow,
|
|
479
502
|
...rows.filter(Boolean),
|
|
@@ -488,7 +511,12 @@ export function AgentChat(props = {}) {
|
|
|
488
511
|
// stateless chrome, so the host needn't thread scroll state through state.
|
|
489
512
|
h('button', { class: 'agentchat-jump', type: 'button', 'aria-label': 'jump to latest', title: 'jump to latest',
|
|
490
513
|
onclick: (e) => scrollThreadToBottom(e.currentTarget) },
|
|
491
|
-
Icon('arrow-down', { size: 16 }), h('span', { class: 'agentchat-jump-label' }, 'latest'))
|
|
514
|
+
Icon('arrow-down', { size: 16 }), h('span', { class: 'agentchat-jump-label' }, 'latest')),
|
|
515
|
+
// Optional scroll-position overview strip, sharing the same live thread
|
|
516
|
+
// element the auto-scroll/jump logic already resolves via threadElHolder.
|
|
517
|
+
showMinimap
|
|
518
|
+
? ChatMinimap({ messages, getThreadEl: () => threadElHolder.el })
|
|
519
|
+
: null);
|
|
492
520
|
|
|
493
521
|
const mainColumn = h('div', { class: 'agentchat-main-col' },
|
|
494
522
|
h('div', { class: 'agentchat-head' },
|
|
@@ -14,9 +14,12 @@ import { register as registerDebug, unregister as unregisterDebug } from '../deb
|
|
|
14
14
|
import { queueMessage, watchReconnect, isOnline } from '../idb-outbox.js';
|
|
15
15
|
import { ChatMessage, ChatComposer } from './chat.js';
|
|
16
16
|
import { fmtTime, fmtAgo } from './sessions.js';
|
|
17
|
-
import {
|
|
17
|
+
import { ModelsConfig } from './models-config.js';
|
|
18
|
+
import { SkillsConfig } from './skills-config.js';
|
|
19
|
+
import { PluginsConfig } from './plugins-config.js';
|
|
18
20
|
import { GitStatusPanel, GitDiffView } from './git-status.js';
|
|
19
21
|
import { WorktreeSwitcher } from './worktree-switcher.js';
|
|
22
|
+
import { AgentChat } from './agent-chat.js';
|
|
20
23
|
|
|
21
24
|
const h = webjsx.createElement;
|
|
22
25
|
|
|
@@ -43,10 +46,6 @@ const TRUNC_PROMPT = 50; // batch prompt cells
|
|
|
43
46
|
const truncSpan = (s, n) => { const t = trunc(s, n); return h('span', { title: t.title }, t.text); };
|
|
44
47
|
// Cap a raw JSON dump for an inline table cell without losing the data via tooltip.
|
|
45
48
|
const truncJson = (v, n = TRUNC_TITLE) => truncSpan(JSON.stringify(v), n);
|
|
46
|
-
// Autoscroll a thread only when the user is already near the bottom, so
|
|
47
|
-
// scrolling up to read history is not yanked back down on the next render.
|
|
48
|
-
const stickyScroll = (el) => { if (!el) return; const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80; if (nearBottom) el.scrollTop = el.scrollHeight; };
|
|
49
|
-
|
|
50
49
|
// ---- home ------------------------------------------------------------------
|
|
51
50
|
|
|
52
51
|
export const home = makePage((ctx) => {
|
|
@@ -106,98 +105,196 @@ export const home = makePage((ctx) => {
|
|
|
106
105
|
|
|
107
106
|
// ---- chat ------------------------------------------------------------------
|
|
108
107
|
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
108
|
+
// Parse a fetch Response body as a Server-Sent-Events frame stream. There is
|
|
109
|
+
// no EventSource-over-POST in browsers (EventSource only does GET, no custom
|
|
110
|
+
// headers/body), so a POST-based SSE consumer has to manually decode the
|
|
111
|
+
// ReadableStream and split on blank-line-terminated `event: X\ndata: Y\n\n`
|
|
112
|
+
// frames. No existing SSE-parsing utility exists elsewhere in this SDK
|
|
113
|
+
// (checked idb-outbox.js and grepped src/ for `text/event-stream`) -- this is
|
|
114
|
+
// the first, generic enough (event name + JSON.parse'd data) to reuse for any
|
|
115
|
+
// future SSE endpoint, not freddie-chat-specific in shape.
|
|
116
|
+
async function* parseSseStream(response) {
|
|
117
|
+
const reader = response.body.getReader();
|
|
118
|
+
const decoder = new TextDecoder();
|
|
119
|
+
let buf = '';
|
|
120
|
+
try {
|
|
121
|
+
for (;;) {
|
|
122
|
+
const { done, value } = await reader.read();
|
|
123
|
+
if (done) break;
|
|
124
|
+
buf += decoder.decode(value, { stream: true });
|
|
125
|
+
// Frames are separated by a blank line; a frame may itself contain
|
|
126
|
+
// multiple `field: value` lines (event/data/id/retry) but this
|
|
127
|
+
// server only ever emits one `event:` + one `data:` line per frame.
|
|
128
|
+
let sep;
|
|
129
|
+
while ((sep = buf.indexOf('\n\n')) !== -1) {
|
|
130
|
+
const frame = buf.slice(0, sep);
|
|
131
|
+
buf = buf.slice(sep + 2);
|
|
132
|
+
let event = 'message', dataLines = [];
|
|
133
|
+
for (const line of frame.split('\n')) {
|
|
134
|
+
if (line.startsWith('event:')) event = line.slice(6).trim();
|
|
135
|
+
else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
|
|
136
|
+
}
|
|
137
|
+
if (!dataLines.length) continue;
|
|
138
|
+
let data;
|
|
139
|
+
try { data = JSON.parse(dataLines.join('\n')); } catch { data = dataLines.join('\n'); }
|
|
140
|
+
yield { event, data };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} finally {
|
|
144
|
+
try { reader.releaseLock(); } catch { /* stream already closed/errored */ }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
113
147
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
148
|
+
// Map a freddie tool_progress SSE payload ({name, args, partial}) to an
|
|
149
|
+
// AgentChat tool part. There is no matching id to correlate a later
|
|
150
|
+
// "done" state (freddie's stream has no discrete tool-start/tool-end pair --
|
|
151
|
+
// tool_progress fires zero-or-more times per call while it runs, and the
|
|
152
|
+
// authoritative role:'tool' result only arrives batched in the final
|
|
153
|
+
// `message`/`done` events) so every progress part renders as 'running'; the
|
|
154
|
+
// turn-settle pass below promotes matching parts to 'done'/'error' once the
|
|
155
|
+
// real tool_call_id/content pairs are known.
|
|
156
|
+
function toolProgressPart(payload) {
|
|
157
|
+
return { kind: 'tool', name: payload.name || 'tool', args: payload.args || {}, status: 'running' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// After `done`, freddie's persisted message list is the source of truth:
|
|
161
|
+
// walk it and rebuild the assistant turn's parts as interleaved
|
|
162
|
+
// text/tool/tool_result, replacing the provisional tool_progress-only parts
|
|
163
|
+
// accumulated during streaming. assistant messages with tool_calls become
|
|
164
|
+
// running tool parts (by call id); role:'tool' messages settle the matching
|
|
165
|
+
// part to done/error by tool_call_id.
|
|
166
|
+
function partsFromMessages(assistantAndToolMessages) {
|
|
167
|
+
const parts = [];
|
|
168
|
+
const byId = new Map();
|
|
169
|
+
for (const m of assistantAndToolMessages) {
|
|
170
|
+
if (m.role === 'assistant') {
|
|
171
|
+
if (m.content) parts.push({ kind: 'md', text: m.content });
|
|
172
|
+
for (const tc of (m.tool_calls || [])) {
|
|
173
|
+
const part = { kind: 'tool', _id: tc.id, name: tc.name || tc.function?.name || 'tool', args: tc.arguments || tc.function?.arguments || {}, status: 'running' };
|
|
174
|
+
parts.push(part);
|
|
175
|
+
if (tc.id) byId.set(tc.id, part);
|
|
176
|
+
}
|
|
177
|
+
} else if (m.role === 'tool') {
|
|
178
|
+
const target = m.tool_call_id ? byId.get(m.tool_call_id) : null;
|
|
179
|
+
let content = m.content;
|
|
180
|
+
let isError = false;
|
|
181
|
+
try { const parsed = JSON.parse(content); if (parsed && parsed.error) { isError = true; } } catch { /* not JSON, leave as-is */ }
|
|
182
|
+
if (target) { target.result = content; target.status = isError ? 'error' : 'done'; target.error = isError || undefined; }
|
|
183
|
+
else parts.push({ kind: 'tool_result', name: 'result', result: content, error: isError || undefined, status: isError ? 'error' : 'done' });
|
|
184
|
+
}
|
|
123
185
|
}
|
|
186
|
+
return parts;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const chat = makePage((ctx) => {
|
|
190
|
+
Object.assign(ctx.state, { loading: false, messages: [], draft: '', busy: false, error: null, abort: null });
|
|
191
|
+
|
|
124
192
|
// Offline outbox: a prompt sent while genuinely offline queues to
|
|
125
193
|
// IndexedDB and auto-flushes on the real 'online' event, rather than
|
|
126
194
|
// surfacing a hard error the user can't act on. True offline LLM
|
|
127
195
|
// response generation is impossible by definition -- a queued message
|
|
128
|
-
// only gets a reply once connectivity actually returns.
|
|
129
|
-
|
|
196
|
+
// only gets a reply once connectivity actually returns. Reconnect-flush
|
|
197
|
+
// still goes through the single-shot JSON path (no live UI to stream
|
|
198
|
+
// into for a message sent while this page may not even be mounted).
|
|
199
|
+
async function sendQueuedToServer(body) {
|
|
130
200
|
const r = await api('/api/chat', { method: 'POST', body });
|
|
131
201
|
const reply = r.result || r.content || r.message || (r.messages && r.messages.at(-1)?.content) || JSON.stringify(r);
|
|
132
|
-
ctx.state.messages.push({ role: 'assistant',
|
|
202
|
+
ctx.state.messages.push({ id: 'a' + Date.now(), role: 'assistant', content: String(reply), time: formatTime(Date.now()) });
|
|
203
|
+
ctx.rerender();
|
|
133
204
|
}
|
|
134
|
-
watchReconnect('chat',
|
|
205
|
+
watchReconnect('chat', sendQueuedToServer);
|
|
206
|
+
|
|
135
207
|
async function send(text) {
|
|
136
|
-
const t = (text
|
|
137
|
-
if (!t || ctx.state.
|
|
138
|
-
|
|
139
|
-
|
|
208
|
+
const t = (typeof text === 'string' ? text : ctx.state.draft || '').trim();
|
|
209
|
+
if (!t || ctx.state.busy) return;
|
|
210
|
+
const userMsg = { id: 'u' + Date.now(), role: 'user', content: t, time: formatTime(Date.now()) };
|
|
211
|
+
const curMsg = { id: 'a' + (Date.now() + 1), role: 'assistant', content: '', time: formatTime(Date.now()), parts: [] };
|
|
212
|
+
ctx.state.messages = [...ctx.state.messages, userMsg, curMsg];
|
|
213
|
+
ctx.set({ draft: '', busy: true, error: null });
|
|
214
|
+
|
|
140
215
|
if (!isOnline()) {
|
|
141
216
|
await queueMessage('chat', { prompt: t });
|
|
142
|
-
ctx.state.messages
|
|
143
|
-
ctx.
|
|
217
|
+
ctx.state.messages = ctx.state.messages.slice(0, -1);
|
|
218
|
+
ctx.state.messages.push({ id: curMsg.id, role: 'assistant', content: '(offline -- queued, will send when connection returns)', time: formatTime(Date.now()) });
|
|
219
|
+
ctx.set({ busy: false });
|
|
144
220
|
return;
|
|
145
221
|
}
|
|
222
|
+
|
|
223
|
+
const ctrl = new AbortController();
|
|
224
|
+
ctx.state.abort = ctrl;
|
|
225
|
+
const cur = ctx.state.messages[ctx.state.messages.length - 1];
|
|
146
226
|
try {
|
|
147
|
-
await
|
|
227
|
+
const res = await fetch('/api/chat', {
|
|
228
|
+
method: 'POST',
|
|
229
|
+
headers: { 'content-type': 'application/json', accept: 'text/event-stream' },
|
|
230
|
+
body: JSON.stringify({ prompt: t, sessionId: ctx.state.sessionId || undefined }),
|
|
231
|
+
signal: ctrl.signal,
|
|
232
|
+
});
|
|
233
|
+
if (!res.ok || !res.body) {
|
|
234
|
+
const txt = await res.text().catch(() => '');
|
|
235
|
+
throw new Error(txt || ('HTTP ' + res.status));
|
|
236
|
+
}
|
|
237
|
+
let finalMessages = null;
|
|
238
|
+
for await (const { event, data } of parseSseStream(res)) {
|
|
239
|
+
if (ctrl.signal.aborted) break;
|
|
240
|
+
if (event === 'start') {
|
|
241
|
+
if (data && data.sessionId) ctx.state.sessionId = data.sessionId;
|
|
242
|
+
} else if (event === 'tool_progress') {
|
|
243
|
+
cur.parts.push(toolProgressPart(data || {}));
|
|
244
|
+
ctx.rerender();
|
|
245
|
+
} else if (event === 'message') {
|
|
246
|
+
// Buffered per-message events land right before `done` -- accumulate
|
|
247
|
+
// rather than rerender per-message; the final rebuild below is O(1)
|
|
248
|
+
// extra work and avoids a flurry of rerenders in the same tick.
|
|
249
|
+
(finalMessages || (finalMessages = [])).push(data);
|
|
250
|
+
} else if (event === 'error') {
|
|
251
|
+
cur.error = String((data && data.error) || 'stream error');
|
|
252
|
+
ctx.rerender();
|
|
253
|
+
} else if (event === 'done') {
|
|
254
|
+
if (finalMessages && finalMessages.length) {
|
|
255
|
+
cur.parts = partsFromMessages(finalMessages);
|
|
256
|
+
// Prefer the settled assistant text as plain content when the
|
|
257
|
+
// rebuilt parts carry exactly one md part (the common no-tool-call
|
|
258
|
+
// case) -- keeps AgentChat's md-vs-content dedup path simple.
|
|
259
|
+
cur.content = '';
|
|
260
|
+
} else if (data && data.result) {
|
|
261
|
+
cur.content = String(data.result);
|
|
262
|
+
}
|
|
263
|
+
ctx.rerender();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
148
266
|
} catch (e) {
|
|
149
|
-
|
|
267
|
+
if (e && e.name === 'AbortError') {
|
|
268
|
+
cur.stopped = true;
|
|
269
|
+
} else {
|
|
270
|
+
cur.error = String(e && e.message || e);
|
|
271
|
+
}
|
|
272
|
+
} finally {
|
|
273
|
+
ctx.state.abort = null;
|
|
274
|
+
ctx.set({ busy: false });
|
|
150
275
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// SAME element -- one ref callback wiring both keeps a single listener
|
|
156
|
-
// registration instead of two refs fighting over the same node.
|
|
157
|
-
function threadRef(el) {
|
|
158
|
-
if (!el) { threadEl = null; return; }
|
|
159
|
-
threadEl = el;
|
|
160
|
-
stickyScroll(el);
|
|
161
|
-
if (!el.__vsScrollWired) {
|
|
162
|
-
el.__vsScrollWired = true;
|
|
163
|
-
el.addEventListener('scroll', () => { recomputeRange(); ctx.rerender(); }, { passive: true });
|
|
164
|
-
}
|
|
165
|
-
recomputeRange();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function stop() {
|
|
279
|
+
if (ctx.state.abort) { try { ctx.state.abort.abort(); } catch { /* already settled */ } }
|
|
166
280
|
}
|
|
167
281
|
|
|
168
282
|
return () => {
|
|
169
283
|
const s = ctx.state;
|
|
170
|
-
const virtualized = s.messages.length > VIRTUALIZE_THRESHOLD;
|
|
171
|
-
let threadChildren;
|
|
172
|
-
if (!s.messages.length) {
|
|
173
|
-
threadChildren = [emptyState('send a prompt to start', Icon('forum'))];
|
|
174
|
-
} else if (!virtualized) {
|
|
175
|
-
threadChildren = s.messages.map((m, i) => ChatMessage({ ...m, key: i }));
|
|
176
|
-
} else {
|
|
177
|
-
recomputeRange();
|
|
178
|
-
const { startIndex, endIndex, topSpacerPx, bottomSpacerPx } = range;
|
|
179
|
-
threadChildren = [
|
|
180
|
-
h('div', { key: '_top_spacer', style: `height:${topSpacerPx}px` }),
|
|
181
|
-
...s.messages.slice(startIndex, endIndex).map((m, i) => {
|
|
182
|
-
const realIndex = startIndex + i;
|
|
183
|
-
return h('div', { key: 'vm' + realIndex, ref: measureRef(virtualizer, realIndex) }, ChatMessage({ ...m, key: realIndex }));
|
|
184
|
-
}),
|
|
185
|
-
h('div', { key: '_bottom_spacer', style: `height:${bottomSpacerPx}px` }),
|
|
186
|
-
];
|
|
187
|
-
}
|
|
188
284
|
return h('div', { class: 'fd-chat' },
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
disabled: s.sending,
|
|
285
|
+
AgentChat({
|
|
286
|
+
messages: s.messages,
|
|
287
|
+
busy: s.busy,
|
|
288
|
+
draft: s.draft,
|
|
289
|
+
status: s.busy ? 'streaming…' : 'ready',
|
|
290
|
+
agentName: 'freddie',
|
|
291
|
+
placeholder: s.busy ? 'waiting for reply…' : 'message…',
|
|
292
|
+
showMinimap: true,
|
|
293
|
+
banners: s.error ? [noteAlert({ kind: 'error', msg: s.error })] : [],
|
|
199
294
|
onInput: (v) => { s.draft = v; },
|
|
200
295
|
onSend: send,
|
|
296
|
+
onStop: stop,
|
|
297
|
+
onNewChat: () => ctx.set({ messages: [], draft: '', error: null, sessionId: null }),
|
|
201
298
|
}));
|
|
202
299
|
};
|
|
203
300
|
});
|
|
@@ -364,44 +461,34 @@ export const analytics = makePage((ctx) => {
|
|
|
364
461
|
// ---- models ----------------------------------------------------------------
|
|
365
462
|
|
|
366
463
|
export const models = makePage((ctx) => {
|
|
367
|
-
Object.assign(ctx.state, {
|
|
464
|
+
Object.assign(ctx.state, { rebuilding: false, selectedProviderId: null, selectedModel: null });
|
|
465
|
+
// GET /api/models/availability — the real per-(provider x model x mode)
|
|
466
|
+
// availability matrix (plugins/gui-models-discover), per freddie's AGENTS.md
|
|
467
|
+
// "Model availability matrix" section. 404 with {error,hint} when the
|
|
468
|
+
// matrix file hasn't been built yet — ModelsConfig itself renders that
|
|
469
|
+
// as an empty state with a "build availability matrix" action.
|
|
368
470
|
async function load() {
|
|
369
|
-
try {
|
|
370
|
-
|
|
371
|
-
api('/api/models/providers').catch(() => []),
|
|
372
|
-
api('/api/models/cached').catch(() => ({})),
|
|
373
|
-
api('/api/models/sampler').catch(() => ({})),
|
|
374
|
-
]);
|
|
375
|
-
ctx.set({ loading: false, providers, cached, sampler, error: null });
|
|
376
|
-
} catch (e) { ctx.set({ loading: false, error: e }); }
|
|
471
|
+
try { ctx.set({ loading: false, data: await api('/api/models/availability'), error: null }); }
|
|
472
|
+
catch (e) { ctx.set({ loading: false, data: null, error: (e && e.body) || e }); }
|
|
377
473
|
}
|
|
378
|
-
async function
|
|
379
|
-
if (ctx.state.
|
|
380
|
-
ctx.set({
|
|
381
|
-
try { await api('/api/models/
|
|
382
|
-
catch (e) { ctx.set({
|
|
383
|
-
ctx.set({
|
|
474
|
+
async function rebuild() {
|
|
475
|
+
if (ctx.state.rebuilding) return;
|
|
476
|
+
ctx.set({ rebuilding: true, rebuildError: null });
|
|
477
|
+
try { await api('/api/models/availability/rebuild', { method: 'POST', body: {} }); await load(); }
|
|
478
|
+
catch (e) { ctx.set({ rebuildError: e }); }
|
|
479
|
+
ctx.set({ rebuilding: false });
|
|
384
480
|
}
|
|
385
481
|
load();
|
|
386
482
|
return () => {
|
|
387
483
|
const s = ctx.state;
|
|
388
|
-
if (s.loading) return loadingState('loading models…');
|
|
389
|
-
if (s.error && !s.providers) return errorState(s.error, load);
|
|
390
|
-
const providers = Array.isArray(s.providers) ? s.providers : [];
|
|
391
|
-
const cached = s.cached || {};
|
|
392
|
-
const status = s.sampler?.status || {};
|
|
393
484
|
return [
|
|
394
|
-
PageHeader({ eyebrow: 'freddie', title: 'models', lede:
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const cm = (cached[name] || []);
|
|
402
|
-
return [name, st ? (st.available === false ? Chip({ tone: 'miss', children: 'down' }) : Chip({ tone: 'ok', children: 'up' })) : '—', Array.isArray(cm) ? cm.length : '—'];
|
|
403
|
-
}),
|
|
404
|
-
}) : emptyState('no providers; set provider API keys')),
|
|
485
|
+
PageHeader({ eyebrow: 'freddie', title: 'models', lede: s.data ? (s.data.summary?.total_models ?? 0) + ' models across ' + (s.data.summary?.total_providers ?? 0) + ' providers' : 'model availability matrix' }),
|
|
486
|
+
ModelsConfig({
|
|
487
|
+
data: s.data, loading: s.loading, error: s.error,
|
|
488
|
+
selectedProviderId: s.selectedProviderId, onSelectProvider: (id) => ctx.set({ selectedProviderId: id, selectedModel: null }),
|
|
489
|
+
selectedModel: s.selectedModel, onSelectModel: (m) => ctx.set({ selectedModel: m }),
|
|
490
|
+
onRefresh: load, onRebuild: rebuild, rebuilding: s.rebuilding, rebuildError: s.rebuildError,
|
|
491
|
+
}),
|
|
405
492
|
];
|
|
406
493
|
};
|
|
407
494
|
});
|
|
@@ -444,22 +531,59 @@ export const cron = makePage((ctx) => {
|
|
|
444
531
|
// ---- skills ----------------------------------------------------------------
|
|
445
532
|
|
|
446
533
|
export const skills = makePage((ctx) => {
|
|
447
|
-
Object.assign(ctx.state, {
|
|
534
|
+
Object.assign(ctx.state, { selected: null, query: '', busyName: null });
|
|
448
535
|
async function load() { try { ctx.set({ loading: false, list: await api('/api/skills'), error: null }); } catch (e) { ctx.set({ loading: false, error: e }); } }
|
|
449
536
|
load();
|
|
450
537
|
return () => {
|
|
451
538
|
const s = ctx.state;
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
539
|
+
// GET /api/skills returns {home:[...], bundled:[...], skillState} —
|
|
540
|
+
// two source lists (user ~/.freddie/skills vs bundled skills/ dirs)
|
|
541
|
+
// plus a per-skill enabled/disabled state map, not a flat array.
|
|
542
|
+
// Concat both sources (home overrides bundled on name collision,
|
|
543
|
+
// matching src/skills/index.js's own findSkill() precedence) and
|
|
544
|
+
// resolve enabled state from skillState (default true when absent).
|
|
545
|
+
const raw = s.list && typeof s.list === 'object' ? s.list : {};
|
|
546
|
+
const rawList = Array.isArray(raw) ? raw : [...(raw.bundled || []), ...(raw.home || [])];
|
|
547
|
+
const skillState = raw.skillState || {};
|
|
548
|
+
const mapped = rawList.map((sk) => ({
|
|
549
|
+
file: sk.file || sk.path || sk.name,
|
|
550
|
+
name: sk.name,
|
|
551
|
+
description: sk.description || (sk.frontmatter && sk.frontmatter.description) || '',
|
|
552
|
+
platforms: sk.platforms || (sk.frontmatter && sk.frontmatter.platforms),
|
|
553
|
+
enabled: skillState[sk.name] !== false,
|
|
554
|
+
}));
|
|
455
555
|
return [
|
|
456
|
-
PageHeader({ eyebrow: 'freddie', title: 'skills', lede:
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
)
|
|
462
|
-
]
|
|
556
|
+
PageHeader({ eyebrow: 'freddie', title: 'skills', lede: mapped.length + ' skills' }),
|
|
557
|
+
SkillsConfig({
|
|
558
|
+
skills: mapped, selected: s.selected, loading: s.loading, error: s.error,
|
|
559
|
+
busyName: s.busyName, query: s.query, onQuery: (q) => ctx.set({ query: q }),
|
|
560
|
+
onSelect: (name) => ctx.set({ selected: s.selected === name ? null : name }),
|
|
561
|
+
}),
|
|
562
|
+
];
|
|
563
|
+
};
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// ---- plugins -----------------------------------------------------------------
|
|
567
|
+
|
|
568
|
+
export const plugins = makePage((ctx) => {
|
|
569
|
+
Object.assign(ctx.state, { selected: null });
|
|
570
|
+
// GET /api/plugins — flat {name,version,surfaces,requires,source,enabled}
|
|
571
|
+
// list, per plugins/gui-plugins-list/plugin.js (distinct from
|
|
572
|
+
// /api/plugin-graph's D3 {nodes,edges} shape built for the dependency
|
|
573
|
+
// visualization, not a flat list UI).
|
|
574
|
+
async function load() { try { ctx.set({ loading: false, list: await api('/api/plugins'), error: null }); } catch (e) { ctx.set({ loading: false, error: e }); } }
|
|
575
|
+
load();
|
|
576
|
+
return () => {
|
|
577
|
+
const s = ctx.state;
|
|
578
|
+
const list = Array.isArray(s.list) ? s.list : (s.list?.plugins || []);
|
|
579
|
+
return [
|
|
580
|
+
PageHeader({ eyebrow: 'freddie', title: 'plugins', lede: list.length + ' plugins loaded' }),
|
|
581
|
+
PluginsConfig({
|
|
582
|
+
plugins: list, selected: s.selected, loading: s.loading, error: s.error,
|
|
583
|
+
onSelect: (name) => ctx.set({ selected: s.selected === name ? null : name }),
|
|
584
|
+
onReload: load,
|
|
585
|
+
}),
|
|
586
|
+
];
|
|
463
587
|
};
|
|
464
588
|
});
|
|
465
589
|
|
|
@@ -950,7 +1074,7 @@ export const git = makePage((ctx) => {
|
|
|
950
1074
|
|
|
951
1075
|
export const FREDDIE_PAGES = {
|
|
952
1076
|
home, chat, voice, sessions, projects, agents, analytics,
|
|
953
|
-
models, cron, skills, config, env, tools, batch, gateway, chains,
|
|
1077
|
+
models, cron, skills, plugins, config, env, tools, batch, gateway, chains,
|
|
954
1078
|
machines, health, debug, logs, git,
|
|
955
1079
|
};
|
|
956
1080
|
|