anentrypoint-design 0.0.375 → 0.0.377
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 +28 -0
- package/dist/247420.js +26 -21
- package/package.json +1 -1
- package/src/community-app.js +16 -4
- package/src/components/freddie.js +167 -73
- package/src/components/shell.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anentrypoint-design",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.377",
|
|
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/community-app.js
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
// voiceParticipants, micMuted, voiceDeafened,
|
|
14
14
|
// audioQueueItems, audioQueueCurrentId, audioQueuePaused,
|
|
15
15
|
// showAuthModal, settingsOpen, voiceSettingsOpen, replyTarget,
|
|
16
|
-
// mobileMenuOpen
|
|
16
|
+
// mobileMenuOpen, // drives the .ca-rail off-canvas drawer on narrow shells
|
|
17
|
+
// canManage // gates the rail's "+ create channel" affordance
|
|
17
18
|
// }
|
|
18
19
|
// adapter.subscribe(cb) -> unsubscribe // cb fires when any snapshot field changes
|
|
19
20
|
// adapter.actions = {
|
|
@@ -22,7 +23,8 @@
|
|
|
22
23
|
// channelContext(id, x, y), serverContext(id, x, y), switchServer(id),
|
|
23
24
|
// goHome(), openServers(), memberMenu(id, name, x, y),
|
|
24
25
|
// replaySegment(id), skipSegment(), pauseQueue(), resumeQueue(),
|
|
25
|
-
// setInput(v), startReply(msg), cancelReply(), deleteMessage(id)
|
|
26
|
+
// setInput(v), startReply(msg), cancelReply(), deleteMessage(id),
|
|
27
|
+
// createChannel() // optional; when present + canManage, rail shows a "+" next to "rooms"
|
|
26
28
|
// }
|
|
27
29
|
// adapter.helpers = { avatarColor(id), initial(name), formatTime(ts) }
|
|
28
30
|
//
|
|
@@ -65,11 +67,12 @@ export function mountCommunityApp(root, adapter = {}) {
|
|
|
65
67
|
const voice = channels.filter(c => c.type === 'voice' || c.type === 'threaded');
|
|
66
68
|
const cur = s.currentChannel || {};
|
|
67
69
|
const servers = s.servers || [];
|
|
70
|
+
if (text.length || !servers.length) {
|
|
71
|
+
out.push(groupHeader('rooms', s));
|
|
72
|
+
}
|
|
68
73
|
if (text.length) {
|
|
69
|
-
out.push(h('div', { class: 'group' }, 'rooms'));
|
|
70
74
|
for (const c of text) out.push(railPill(c, cur, false, s));
|
|
71
75
|
} else if (!servers.length) {
|
|
72
|
-
out.push(h('div', { class: 'group' }, 'rooms'));
|
|
73
76
|
out.push(h('div', { class: 'rail-empty', role: 'status' }, 'no rooms yet'));
|
|
74
77
|
}
|
|
75
78
|
if (voice.length) {
|
|
@@ -84,6 +87,15 @@ export function mountCommunityApp(root, adapter = {}) {
|
|
|
84
87
|
return h('div', {}, ...out);
|
|
85
88
|
};
|
|
86
89
|
|
|
90
|
+
const groupHeader = (label, s) => h('div', { class: 'group group-header' },
|
|
91
|
+
h('span', {}, label),
|
|
92
|
+
(s.canManage && A.createChannel)
|
|
93
|
+
? h('button', {
|
|
94
|
+
type: 'button', class: 'group-add-btn', 'aria-label': 'create channel', title: 'Create channel',
|
|
95
|
+
onclick: (e) => { e.preventDefault(); e.stopPropagation(); A.createChannel(); },
|
|
96
|
+
}, Icon('plus', { size: 13 }))
|
|
97
|
+
: null);
|
|
98
|
+
|
|
87
99
|
const railPill = (c, cur, isVoice, s) => {
|
|
88
100
|
const active = cur.id === c.id;
|
|
89
101
|
const inVoice = isVoice && s.voiceConnected && s.voiceChannelName === c.name;
|
|
@@ -17,9 +17,9 @@ import { fmtTime, fmtAgo } from './sessions.js';
|
|
|
17
17
|
import { ModelsConfig } from './models-config.js';
|
|
18
18
|
import { SkillsConfig } from './skills-config.js';
|
|
19
19
|
import { PluginsConfig } from './plugins-config.js';
|
|
20
|
-
import { createVirtualizer, measureRef } from '../virtual-scroll.js';
|
|
21
20
|
import { GitStatusPanel, GitDiffView } from './git-status.js';
|
|
22
21
|
import { WorktreeSwitcher } from './worktree-switcher.js';
|
|
22
|
+
import { AgentChat } from './agent-chat.js';
|
|
23
23
|
|
|
24
24
|
const h = webjsx.createElement;
|
|
25
25
|
|
|
@@ -46,10 +46,6 @@ const TRUNC_PROMPT = 50; // batch prompt cells
|
|
|
46
46
|
const truncSpan = (s, n) => { const t = trunc(s, n); return h('span', { title: t.title }, t.text); };
|
|
47
47
|
// Cap a raw JSON dump for an inline table cell without losing the data via tooltip.
|
|
48
48
|
const truncJson = (v, n = TRUNC_TITLE) => truncSpan(JSON.stringify(v), n);
|
|
49
|
-
// Autoscroll a thread only when the user is already near the bottom, so
|
|
50
|
-
// scrolling up to read history is not yanked back down on the next render.
|
|
51
|
-
const stickyScroll = (el) => { if (!el) return; const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80; if (nearBottom) el.scrollTop = el.scrollHeight; };
|
|
52
|
-
|
|
53
49
|
// ---- home ------------------------------------------------------------------
|
|
54
50
|
|
|
55
51
|
export const home = makePage((ctx) => {
|
|
@@ -109,98 +105,196 @@ export const home = makePage((ctx) => {
|
|
|
109
105
|
|
|
110
106
|
// ---- chat ------------------------------------------------------------------
|
|
111
107
|
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
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
|
+
}
|
|
116
147
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
+
}
|
|
126
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
|
+
|
|
127
192
|
// Offline outbox: a prompt sent while genuinely offline queues to
|
|
128
193
|
// IndexedDB and auto-flushes on the real 'online' event, rather than
|
|
129
194
|
// surfacing a hard error the user can't act on. True offline LLM
|
|
130
195
|
// response generation is impossible by definition -- a queued message
|
|
131
|
-
// only gets a reply once connectivity actually returns.
|
|
132
|
-
|
|
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) {
|
|
133
200
|
const r = await api('/api/chat', { method: 'POST', body });
|
|
134
201
|
const reply = r.result || r.content || r.message || (r.messages && r.messages.at(-1)?.content) || JSON.stringify(r);
|
|
135
|
-
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();
|
|
136
204
|
}
|
|
137
|
-
watchReconnect('chat',
|
|
205
|
+
watchReconnect('chat', sendQueuedToServer);
|
|
206
|
+
|
|
138
207
|
async function send(text) {
|
|
139
|
-
const t = (text
|
|
140
|
-
if (!t || ctx.state.
|
|
141
|
-
|
|
142
|
-
|
|
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
|
+
|
|
143
215
|
if (!isOnline()) {
|
|
144
216
|
await queueMessage('chat', { prompt: t });
|
|
145
|
-
ctx.state.messages
|
|
146
|
-
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 });
|
|
147
220
|
return;
|
|
148
221
|
}
|
|
222
|
+
|
|
223
|
+
const ctrl = new AbortController();
|
|
224
|
+
ctx.state.abort = ctrl;
|
|
225
|
+
const cur = ctx.state.messages[ctx.state.messages.length - 1];
|
|
149
226
|
try {
|
|
150
|
-
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
|
+
}
|
|
151
266
|
} catch (e) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
function threadRef(el) {
|
|
161
|
-
if (!el) { threadEl = null; return; }
|
|
162
|
-
threadEl = el;
|
|
163
|
-
stickyScroll(el);
|
|
164
|
-
if (!el.__vsScrollWired) {
|
|
165
|
-
el.__vsScrollWired = true;
|
|
166
|
-
el.addEventListener('scroll', () => { recomputeRange(); ctx.rerender(); }, { passive: true });
|
|
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 });
|
|
167
275
|
}
|
|
168
|
-
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function stop() {
|
|
279
|
+
if (ctx.state.abort) { try { ctx.state.abort.abort(); } catch { /* already settled */ } }
|
|
169
280
|
}
|
|
170
281
|
|
|
171
282
|
return () => {
|
|
172
283
|
const s = ctx.state;
|
|
173
|
-
const virtualized = s.messages.length > VIRTUALIZE_THRESHOLD;
|
|
174
|
-
let threadChildren;
|
|
175
|
-
if (!s.messages.length) {
|
|
176
|
-
threadChildren = [emptyState('send a prompt to start', Icon('forum'))];
|
|
177
|
-
} else if (!virtualized) {
|
|
178
|
-
threadChildren = s.messages.map((m, i) => ChatMessage({ ...m, key: i }));
|
|
179
|
-
} else {
|
|
180
|
-
recomputeRange();
|
|
181
|
-
const { startIndex, endIndex, topSpacerPx, bottomSpacerPx } = range;
|
|
182
|
-
threadChildren = [
|
|
183
|
-
h('div', { key: '_top_spacer', style: `height:${topSpacerPx}px` }),
|
|
184
|
-
...s.messages.slice(startIndex, endIndex).map((m, i) => {
|
|
185
|
-
const realIndex = startIndex + i;
|
|
186
|
-
return h('div', { key: 'vm' + realIndex, ref: measureRef(virtualizer, realIndex) }, ChatMessage({ ...m, key: realIndex }));
|
|
187
|
-
}),
|
|
188
|
-
h('div', { key: '_bottom_spacer', style: `height:${bottomSpacerPx}px` }),
|
|
189
|
-
];
|
|
190
|
-
}
|
|
191
284
|
return h('div', { class: 'fd-chat' },
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
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 })] : [],
|
|
202
294
|
onInput: (v) => { s.draft = v; },
|
|
203
295
|
onSend: send,
|
|
296
|
+
onStop: stop,
|
|
297
|
+
onNewChat: () => ctx.set({ messages: [], draft: '', error: null, sessionId: null }),
|
|
204
298
|
}));
|
|
205
299
|
};
|
|
206
300
|
});
|
package/src/components/shell.js
CHANGED
|
@@ -176,6 +176,7 @@ export const ICON_PATHS = {
|
|
|
176
176
|
'arrow-down': '<path d="M12 5v14M5 12l7 7 7-7"/>',
|
|
177
177
|
'arrow-right': '<path d="M5 12h14M12 5l7 7-7 7"/>',
|
|
178
178
|
x: '<path d="M18 6 6 18M6 6l12 12"/>',
|
|
179
|
+
plus: '<path d="M12 5v14M5 12h14"/>',
|
|
179
180
|
play: '<path d="M6 4v16l14-8z"/>',
|
|
180
181
|
pause: '<path d="M8 5v14M16 5v14"/>',
|
|
181
182
|
refresh: '<path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/>',
|