anentrypoint-design 0.0.208 → 0.0.210
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/app-shell.css +268 -93
- package/chat.css +111 -29
- package/colors_and_type.css +34 -1
- package/dist/247420.css +412 -123
- package/dist/247420.js +13 -13
- package/package.json +4 -2
- package/src/components/agent-chat.js +8 -2
- package/src/components/chat.js +26 -9
- package/src/components/content.js +28 -3
- package/src/components/context-pane.js +9 -9
- package/src/components/files-modals.js +3 -3
- package/src/components/files.js +21 -5
- package/src/components/freddie.js +25 -25
- package/src/components/sessions.js +37 -23
- package/src/components/shell.js +67 -5
- package/src/components/theme-toggle.js +12 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anentrypoint-design",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.210",
|
|
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",
|
|
@@ -67,7 +67,9 @@
|
|
|
67
67
|
],
|
|
68
68
|
"scripts": {
|
|
69
69
|
"lint:tokens": "node scripts/lint-tokens.mjs",
|
|
70
|
-
"build": "node scripts/build.mjs"
|
|
70
|
+
"build": "node scripts/build.mjs",
|
|
71
|
+
"lint:null-children": "node scripts/lint-null-children.mjs",
|
|
72
|
+
"lint:classes": "node scripts/lint-classes.mjs"
|
|
71
73
|
},
|
|
72
74
|
"repository": {
|
|
73
75
|
"type": "git",
|
|
@@ -14,6 +14,7 @@ import * as webjsx from '../../vendor/webjsx/index.js';
|
|
|
14
14
|
import { ChatComposer, ChatMessage, makeThreadAutoScroll } from './chat.js';
|
|
15
15
|
import { Select } from './content.js';
|
|
16
16
|
import { Btn, Icon } from './shell.js';
|
|
17
|
+
import { initializeCachesEagerly } from '../markdown-cache.js';
|
|
17
18
|
|
|
18
19
|
const h = webjsx.createElement;
|
|
19
20
|
|
|
@@ -119,7 +120,7 @@ function CwdBar({ cwd, editing, draft, onEdit, onSave, onCancel, onClear, onDraf
|
|
|
119
120
|
'aria-describedby': hint ? 'agentchat-cwd-hint' : null,
|
|
120
121
|
'aria-invalid': error ? 'true' : null,
|
|
121
122
|
oninput: (e) => onDraft && onDraft(e.target.value) }),
|
|
122
|
-
Btn({ key: 'save',
|
|
123
|
+
Btn({ key: 'save', variant: 'primary', disabled: !!(error || checking), onClick: () => onSave && onSave(), children: 'save' }),
|
|
123
124
|
Btn({ key: 'cancel', onClick: () => onCancel && onCancel(), children: 'cancel' }),
|
|
124
125
|
hint ? h('span', { key: 'hint', id: 'agentchat-cwd-hint', role: 'status', 'aria-live': 'polite',
|
|
125
126
|
class: 'agentchat-cwd-hint' + (error ? ' is-error' : ' is-checking') }, hint) : null);
|
|
@@ -158,6 +159,11 @@ export function AgentChat(props = {}) {
|
|
|
158
159
|
shownMessages, onShowEarlier,
|
|
159
160
|
} = props;
|
|
160
161
|
|
|
162
|
+
// Warm the markdown/Prism stack the moment the surface mounts so the CDN
|
|
163
|
+
// round-trip never starts mid-first-response. Self-idempotent (internal
|
|
164
|
+
// _initPromise), so the per-render call is free after the first.
|
|
165
|
+
initializeCachesEagerly().catch((err) => console.warn('[247420] cache init error:', err));
|
|
166
|
+
|
|
161
167
|
const name = agentName || (agents.find((a) => a.id === selectedAgent)?.name) || selectedAgent || 'agent';
|
|
162
168
|
const lastIdx = messages.length - 1;
|
|
163
169
|
const lastMsg = messages[lastIdx];
|
|
@@ -251,7 +257,7 @@ export function AgentChat(props = {}) {
|
|
|
251
257
|
}
|
|
252
258
|
return ChatMessage({
|
|
253
259
|
key: m.id || String(i),
|
|
254
|
-
|
|
260
|
+
role: isAssistant ? 'assistant' : 'user',
|
|
255
261
|
// Claude-Code-web layout: flat full-width turns (no avatar disc, no colored
|
|
256
262
|
// bubble), distinguished by a role label + a faint assistant background.
|
|
257
263
|
// aicat is left OFF so the mascot tint never reaches the agent surface.
|
package/src/components/chat.js
CHANGED
|
@@ -164,6 +164,11 @@ function MdNode(p) {
|
|
|
164
164
|
const srcKey = (isMarkdownDegraded() ? '~degraded~' : '') + (p.text || '');
|
|
165
165
|
if (el.dataset.mdSrc === srcKey) return;
|
|
166
166
|
el.dataset.mdSrc = srcKey;
|
|
167
|
+
// Markdown stack still loading (or down): paint the raw text
|
|
168
|
+
// synchronously so streamed tokens are visible the same frame they
|
|
169
|
+
// arrive (an empty bubble until the CDN import resolves reads as a
|
|
170
|
+
// hang); the resolved render swaps in sanitized markdown in place.
|
|
171
|
+
if (isMarkdownDegraded()) el.textContent = p.text || '';
|
|
167
172
|
renderMarkdownCached(p.text || '').then((html) => { el.innerHTML = html; injectCodeCopy(el); });
|
|
168
173
|
};
|
|
169
174
|
return h('div', { class: 'chat-bubble chat-md', ref: refSink });
|
|
@@ -446,25 +451,37 @@ export function ChatComposer({ value, onInput, onSend, onAttach, onEmoji, onMenu
|
|
|
446
451
|
// segment routes to the cwd editor WITHOUT making the whole line one giant
|
|
447
452
|
// click target. Legacy whole-line context.onClick is honored only when no
|
|
448
453
|
// bit carries its own handler. All children are keyed VElements.
|
|
449
|
-
|
|
450
|
-
|
|
454
|
+
// Normalize FIRST (a bit may carry `text` or `label`), then drop empties -
|
|
455
|
+
// separators must only ever sit between bits that actually render. An
|
|
456
|
+
// object bit whose text resolved empty used to leave a dangling trailing
|
|
457
|
+
// middot AND an invisible zero-width button.
|
|
458
|
+
const ctxBits = ((context && context.bits) ? context.bits : [])
|
|
459
|
+
.map((b) => {
|
|
460
|
+
if (b == null) return null;
|
|
461
|
+
if (typeof b === 'object') {
|
|
462
|
+
const text = b.text || b.label || '';
|
|
463
|
+
return text ? { text, onClick: b.onClick, title: b.title } : null;
|
|
464
|
+
}
|
|
465
|
+
const text = String(b);
|
|
466
|
+
return text ? { text } : null;
|
|
467
|
+
})
|
|
468
|
+
.filter(Boolean);
|
|
469
|
+
const hasBitClicks = ctxBits.some((b) => b.onClick);
|
|
451
470
|
let contextLine = null;
|
|
452
471
|
if (ctxBits.length && hasBitClicks) {
|
|
453
472
|
const kids = [];
|
|
454
473
|
ctxBits.forEach((b, i) => {
|
|
455
474
|
if (i) kids.push(h('span', { key: 'csep' + i, class: 'chat-composer-context-sep', 'aria-hidden': 'true' }, ' · '));
|
|
456
|
-
|
|
457
|
-
const text = isObj ? (b.text || '') : String(b);
|
|
458
|
-
if (isObj && b.onClick) kids.push(h('button', {
|
|
475
|
+
if (b.onClick) kids.push(h('button', {
|
|
459
476
|
key: 'cbit' + i, type: 'button', class: 'chat-composer-context-bit',
|
|
460
|
-
title: b.title || null, 'aria-label': b.title || text,
|
|
477
|
+
title: b.title || null, 'aria-label': b.title || b.text,
|
|
461
478
|
onclick: (e) => { e.preventDefault(); b.onClick(e); },
|
|
462
|
-
}, text));
|
|
463
|
-
else kids.push(h('span', { key: 'cbit' + i, class: 'chat-composer-context-text' }, text));
|
|
479
|
+
}, b.text));
|
|
480
|
+
else kids.push(h('span', { key: 'cbit' + i, class: 'chat-composer-context-text' }, b.text));
|
|
464
481
|
});
|
|
465
482
|
contextLine = h('div', { class: 'chat-composer-context' }, ...kids);
|
|
466
483
|
} else if (ctxBits.length) {
|
|
467
|
-
const joined = ctxBits.map((b) =>
|
|
484
|
+
const joined = ctxBits.map((b) => b.text).join(' · ');
|
|
468
485
|
contextLine = h(context.onClick ? 'button' : 'div', {
|
|
469
486
|
class: 'chat-composer-context', type: context.onClick ? 'button' : null,
|
|
470
487
|
'aria-label': context.onClick ? ('change target: ' + joined) : null,
|
|
@@ -183,7 +183,7 @@ export function WorksList({ works = [], openedIndex = -1, onToggle }) {
|
|
|
183
183
|
h('p', { class: 'ds-work-body' }, w.body)
|
|
184
184
|
),
|
|
185
185
|
h('div', { class: 'ds-work-actions' },
|
|
186
|
-
Btn({
|
|
186
|
+
Btn({ variant: 'primary', href: w.href || '#', children: 'open ->' }),
|
|
187
187
|
Btn({ href: w.source || '#', children: 'source' })
|
|
188
188
|
)
|
|
189
189
|
) : null
|
|
@@ -302,11 +302,24 @@ export function ProjectView({ project = {}, copied, onCopy } = {}) {
|
|
|
302
302
|
].filter(Boolean).flat();
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
export function PageHeader({ title, lede, eyebrow, right, compact, id }) {
|
|
305
|
+
export function PageHeader({ title, lede, eyebrow, right, compact, dense, id }) {
|
|
306
306
|
// `compact` drops the large leading/trailing section margins so a PageHeader
|
|
307
307
|
// used as a page's first element top-aligns cleanly without the consumer
|
|
308
308
|
// having to !important-override the .ds-section margin. `id` lands on the
|
|
309
309
|
// outermost section so the header can serve as a deep-link anchor.
|
|
310
|
+
// `dense` is the content-first working-surface form: one row - a small
|
|
311
|
+
// heading with the lede beside it, clamped to a single muted line - instead
|
|
312
|
+
// of a display H1 over a paragraph. App surfaces (files, dashboards,
|
|
313
|
+
// settings) should not spend 150px of fold on an intro.
|
|
314
|
+
if (dense) {
|
|
315
|
+
return h('section', { class: 'ds-section ds-section-compact ds-page-header-dense', id: id || null },
|
|
316
|
+
h('div', { class: 'ds-page-header-dense-row' },
|
|
317
|
+
...[
|
|
318
|
+
title != null ? h('h1', { key: 'dh' }, title) : null,
|
|
319
|
+
lede != null ? h('span', { key: 'dl', class: 'ds-page-header-dense-lede', title: typeof lede === 'string' ? lede : null }, lede) : null,
|
|
320
|
+
right != null ? h('div', { key: 'dr', class: 'ds-page-header-right' }, ...(Array.isArray(right) ? right : [right])) : null,
|
|
321
|
+
].filter(Boolean)));
|
|
322
|
+
}
|
|
310
323
|
return h('section', { class: 'ds-section' + (compact ? ' ds-section-compact' : ''), id: id || null },
|
|
311
324
|
eyebrow ? h('span', { class: 'eyebrow' }, eyebrow) : null,
|
|
312
325
|
title != null ? h('h1', {}, title) : null,
|
|
@@ -379,8 +392,20 @@ export function Select({ label, value = '', options = [], onChange, name, key, p
|
|
|
379
392
|
);
|
|
380
393
|
}
|
|
381
394
|
|
|
382
|
-
export function EventList({ items, events, emptyText = 'no events', rankPad = 3 }) {
|
|
395
|
+
export function EventList({ items, events, emptyText = 'no events', rankPad = 3, loading = false, loadingText = 'loading events…' }) {
|
|
383
396
|
const list = items || events || [];
|
|
397
|
+
// Shape-matched skeleton rows for the slow first events fetch (the ccsniff
|
|
398
|
+
// cold walk can take 30-90s) - a lone spinner collapses the whole pane.
|
|
399
|
+
// Keying discipline mirrors ConversationList: a single keyed wrapper with
|
|
400
|
+
// all-keyed siblings (webjsx applyDiff crashes on mixed keyed/unkeyed).
|
|
401
|
+
if (loading && !list.length) {
|
|
402
|
+
return h('section', { class: 'ds-section ds-event-list' },
|
|
403
|
+
h('div', { key: 'st', role: 'status', 'aria-live': 'polite', class: 'ds-event-state lede' }, loadingText),
|
|
404
|
+
...Array.from({ length: 7 }, (_, i) => h('div', { key: 'sk' + i, class: 'ds-event-row-skeleton', 'aria-hidden': 'true' },
|
|
405
|
+
h('span', { key: 'r', class: 'ds-skel ds-skel-rank' }),
|
|
406
|
+
h('span', { key: 't', class: 'ds-skel ds-skel-title' }),
|
|
407
|
+
h('span', { key: 'm', class: 'ds-skel ds-skel-meta' }))));
|
|
408
|
+
}
|
|
384
409
|
if (!list.length) return h('p', { class: 'lede' }, emptyText);
|
|
385
410
|
return h('section', { class: 'ds-section ds-event-list' },
|
|
386
411
|
...list.map((it, i) => Row({
|
|
@@ -59,6 +59,10 @@ export function ContextPane({ agent, model, cwd, toolCount = 0, usage, session,
|
|
|
59
59
|
// Use the rail tone consistently with the GUI-wide semantics:
|
|
60
60
|
// green = active/ok. A default cwd carries no rail (neutral).
|
|
61
61
|
rail: cwd ? 'green' : null,
|
|
62
|
+
// The change-cwd affordance belongs ON the working-dir fact,
|
|
63
|
+
// not as a button floating under the panels.
|
|
64
|
+
onClick: onSetCwd || undefined,
|
|
65
|
+
meta: onSetCwd ? 'change' : undefined,
|
|
62
66
|
}),
|
|
63
67
|
Row({
|
|
64
68
|
title: 'running tools',
|
|
@@ -69,8 +73,9 @@ export function ContextPane({ agent, model, cwd, toolCount = 0, usage, session,
|
|
|
69
73
|
}),
|
|
70
74
|
];
|
|
71
75
|
// Conversation block: whole-session totals (turn count + accumulated cost)
|
|
72
|
-
// between the context panel and the per-turn usage panel.
|
|
73
|
-
|
|
76
|
+
// between the context panel and the per-turn usage panel. All-zero totals
|
|
77
|
+
// are noise, not context - hide the block until there is a conversation.
|
|
78
|
+
if (hasSession && (Number(session.turns) > 0 || Number(session.cost) > 0)) {
|
|
74
79
|
const sesRows = [];
|
|
75
80
|
if (session.turns != null) sesRows.push(Row({ title: 'turns', meta: String(session.turns) }));
|
|
76
81
|
if (session.cost != null) sesRows.push(Row({ title: 'total cost', meta: '$' + Number(session.cost).toFixed(4) }));
|
|
@@ -88,11 +93,6 @@ export function ContextPane({ agent, model, cwd, toolCount = 0, usage, session,
|
|
|
88
93
|
if (usage.durationMs != null) tokRows.push(Row({ title: 'duration', meta: fmtDuration(usage.durationMs) }));
|
|
89
94
|
panels.push(Panel({ title: 'last turn', children: tokRows }));
|
|
90
95
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
onSetCwd
|
|
94
|
-
? h('div', { class: 'ds-context-actions' },
|
|
95
|
-
Btn({ onClick: onSetCwd, children: 'set working dir' }))
|
|
96
|
-
: null,
|
|
97
|
-
);
|
|
96
|
+
// The cwd action lives on the working-dir row above; no floating footer button.
|
|
97
|
+
return h('div', { class: 'ds-context' }, ...panels);
|
|
98
98
|
}
|
|
@@ -151,7 +151,7 @@ function modalError(error) {
|
|
|
151
151
|
// `error` renders inside .ds-modal-body (role=alert, error tone). `busy`
|
|
152
152
|
// disables both action buttons AND the Escape/backdrop close paths; the confirm
|
|
153
153
|
// label flips to `busyLabel` (default 'working…') so the in-flight state reads.
|
|
154
|
-
export function ConfirmDialog({ title = '
|
|
154
|
+
export function ConfirmDialog({ title = 'Are you sure?', message, confirmLabel = 'confirm', cancelLabel = 'cancel', destructive, onConfirm, onCancel, error, busy = false, busyLabel = 'working…' } = {}) {
|
|
155
155
|
return Modal({
|
|
156
156
|
onClose: onCancel,
|
|
157
157
|
kind: 'small',
|
|
@@ -160,12 +160,12 @@ export function ConfirmDialog({ title = 'confirm', message, confirmLabel = 'conf
|
|
|
160
160
|
body: [message || '', modalError(error)].filter(Boolean),
|
|
161
161
|
actions: [
|
|
162
162
|
Btn({ onClick: onCancel, disabled: busy, children: cancelLabel }),
|
|
163
|
-
Btn({
|
|
163
|
+
Btn({ variant: 'primary', disabled: busy, onClick: onConfirm, children: busy ? busyLabel : confirmLabel })
|
|
164
164
|
]
|
|
165
165
|
});
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
export function PromptDialog({ title = 'name', value = '', placeholder = '', confirmLabel = 'ok', cancelLabel = 'cancel', onConfirm, onCancel, onInput, error, busy = false, busyLabel = 'working…' } = {}) {
|
|
168
|
+
export function PromptDialog({ title = 'Enter a name', value = '', placeholder = '', confirmLabel = 'ok', cancelLabel = 'cancel', onConfirm, onCancel, onInput, error, busy = false, busyLabel = 'working…' } = {}) {
|
|
169
169
|
return Modal({
|
|
170
170
|
onClose: onCancel,
|
|
171
171
|
kind: 'small',
|
package/src/components/files.js
CHANGED
|
@@ -180,10 +180,20 @@ const FILE_GRID_CAP = 200;
|
|
|
180
180
|
export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No files here yet',
|
|
181
181
|
columns = 'auto', sort, filter, loading = false,
|
|
182
182
|
shown, onShowMore, actions, busy,
|
|
183
|
-
|
|
183
|
+
// Canonical multi-select contract (shared with
|
|
184
|
+
// SessionDashboard): selected/onToggleSelect.
|
|
185
|
+
// marked/onMark are accepted FileGrid aliases.
|
|
186
|
+
selectable = false, selected, onToggleSelect,
|
|
187
|
+
marked = selected, onMark = onToggleSelect,
|
|
188
|
+
onSelectAll, onClearSelection,
|
|
184
189
|
density = 'list', onDensity, thumbUrl } = {}) {
|
|
185
|
-
|
|
190
|
+
// Skeleton ONLY for a cold load. A refresh of a populated grid (rename /
|
|
191
|
+
// delete / upload round-trip) keeps the rows on screen and dims them -
|
|
192
|
+
// flashing the whole directory to shimmer rows on every mutation reads as
|
|
193
|
+
// data loss.
|
|
194
|
+
if (loading && !files.length) return FileSkeleton({});
|
|
186
195
|
if (!files.length) return EmptyState({ text: emptyText });
|
|
196
|
+
const refreshing = loading && files.length > 0;
|
|
187
197
|
// Cap the rendered rows. `shown` (host-controlled) overrides the default cap
|
|
188
198
|
// so "show more" can grow it; otherwise default to FILE_GRID_CAP.
|
|
189
199
|
const limit = shown != null ? shown : FILE_GRID_CAP;
|
|
@@ -264,8 +274,9 @@ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No f
|
|
|
264
274
|
// listbox/option semantics are invalid (an option can't host interactive
|
|
265
275
|
// children). Keyboard nav still works via roving focus over the open buttons.
|
|
266
276
|
const grid = h('div', {
|
|
267
|
-
class: 'ds-file-grid' + (isThumb ? ' ds-file-grid-thumb' : ''),
|
|
277
|
+
class: 'ds-file-grid' + (isThumb ? ' ds-file-grid-thumb' : '') + (refreshing ? ' is-refreshing' : ''),
|
|
268
278
|
role: 'group', 'aria-label': 'files', tabindex: '0',
|
|
279
|
+
'aria-busy': refreshing ? 'true' : 'false',
|
|
269
280
|
// Always concrete (webjsx's attribute diff can leave a null-valued
|
|
270
281
|
// attribute unset when toggling away from the default).
|
|
271
282
|
'data-density': density || 'list',
|
|
@@ -401,8 +412,13 @@ export function RootsPicker({ roots = [], selected, onSelect, label = 'roots' }
|
|
|
401
412
|
}
|
|
402
413
|
|
|
403
414
|
export function DropZone({ children, dragover, onDrop, onDragOver, onDragLeave, label = 'drop files here', onPick } = {}) {
|
|
415
|
+
// With children the zone is a passive WRAPPER: content renders normally and
|
|
416
|
+
// the dashed affordance appears only while a drag is over it (real file
|
|
417
|
+
// managers never burn a permanent band on a maybe-drop). Without children
|
|
418
|
+
// it keeps the explicit picker-block look.
|
|
419
|
+
const kids = Array.isArray(children) ? children : children ? [children] : [];
|
|
404
420
|
return h('div', {
|
|
405
|
-
class: 'ds-dropzone' + (dragover ? ' dragover' : ''),
|
|
421
|
+
class: 'ds-dropzone' + (kids.length ? ' ds-dropzone--wrap' : '') + (dragover ? ' dragover' : ''),
|
|
406
422
|
ondragover: (e) => { e.preventDefault(); onDragOver && onDragOver(e); },
|
|
407
423
|
ondragleave: (e) => { onDragLeave && onDragLeave(e); },
|
|
408
424
|
ondrop: (e) => { e.preventDefault(); onDrop && onDrop(e.dataTransfer.files); }
|
|
@@ -412,7 +428,7 @@ export function DropZone({ children, dragover, onDrop, onDragOver, onDragLeave,
|
|
|
412
428
|
h('span', { class: 'ds-dropzone-label' }, label),
|
|
413
429
|
onPick ? Btn({ onClick: onPick, children: 'pick files' }) : null
|
|
414
430
|
),
|
|
415
|
-
...
|
|
431
|
+
...kids
|
|
416
432
|
);
|
|
417
433
|
}
|
|
418
434
|
|
|
@@ -211,7 +211,7 @@ export const sessions = makePage((ctx) => {
|
|
|
211
211
|
s.msgLoading ? loadingState('loading messages…')
|
|
212
212
|
: (s.messages || []).length ? (s.messages).map((m, i) => ChatMessage({ role: m.role, text: m.content || m.text || '', time: m.ts ? fmtTime(m.ts) : '', key: i }))
|
|
213
213
|
: emptyState('no messages')) : null,
|
|
214
|
-
];
|
|
214
|
+
].filter(Boolean);
|
|
215
215
|
};
|
|
216
216
|
});
|
|
217
217
|
|
|
@@ -249,13 +249,13 @@ export const projects = makePage((ctx) => {
|
|
|
249
249
|
active: p.name === activeName,
|
|
250
250
|
trailing: h('span', { class: 'fd-row-actions' },
|
|
251
251
|
p.name !== activeName ? Btn({ children: 'activate', onClick: () => activate(p.name) }) : Chip({ tone: 'ok', children: 'active' }),
|
|
252
|
-
p.name !== 'default' ? Btn({
|
|
252
|
+
p.name !== 'default' ? Btn({ variant: 'danger', children: 'delete', onClick: () => del(p.name) }) : null),
|
|
253
253
|
})) : emptyState('no projects')),
|
|
254
254
|
section('new project',
|
|
255
255
|
TextField({ label: 'name', value: s.newName, onInput: (v) => { s.newName = v; }, placeholder: 'my-project' }),
|
|
256
256
|
TextField({ label: 'path (optional)', value: s.newPath, onInput: (v) => { s.newPath = v; }, placeholder: 'C:/path/to/dir' }),
|
|
257
|
-
Btn({
|
|
258
|
-
];
|
|
257
|
+
Btn({ variant: 'primary', disabled: s.busy, children: s.busy ? 'working…' : 'create', onClick: create })),
|
|
258
|
+
].filter(Boolean);
|
|
259
259
|
};
|
|
260
260
|
});
|
|
261
261
|
|
|
@@ -274,7 +274,7 @@ export const agents = makePage((ctx) => {
|
|
|
274
274
|
s.error && s.data ? refreshError(s.error) : null,
|
|
275
275
|
Kpi({ items: [[d.count ?? 0, 'active'], [d.turns ?? 0, 'total turns'], [d.last_activity ? fmtAgo(d.last_activity) : '—', 'last activity']] }),
|
|
276
276
|
section('detail', Table({ headers: ['field', 'value'], rows: Object.entries(d).map(([k, v]) => [k, String(v)]) })),
|
|
277
|
-
];
|
|
277
|
+
].filter(Boolean);
|
|
278
278
|
};
|
|
279
279
|
});
|
|
280
280
|
|
|
@@ -303,7 +303,7 @@ export const analytics = makePage((ctx) => {
|
|
|
303
303
|
s.error && (s.sampler || s.avail) ? refreshError(s.error) : null,
|
|
304
304
|
Kpi({ items: [[ok + '/' + samp.length, 'providers up'], [sum.total_models ?? '—', 'models'], [sum.usable_in_any_mode ?? '—', 'usable']] }),
|
|
305
305
|
section('sampler', samp.length ? Table({ headers: ['provider', 'available', 'fails'], rows: Object.entries(s.sampler.status).map(([k, v]) => [k, v.available === false ? 'no' : 'yes', String(v.failCount ?? 0)]) }) : emptyState('no sampler data')),
|
|
306
|
-
];
|
|
306
|
+
].filter(Boolean);
|
|
307
307
|
};
|
|
308
308
|
});
|
|
309
309
|
|
|
@@ -337,7 +337,7 @@ export const models = makePage((ctx) => {
|
|
|
337
337
|
const cached = s.cached || {};
|
|
338
338
|
const status = s.sampler?.status || {};
|
|
339
339
|
return [
|
|
340
|
-
PageHeader({ eyebrow: 'freddie', title: 'models', lede: providers.length + ' providers', right: Btn({
|
|
340
|
+
PageHeader({ eyebrow: 'freddie', title: 'models', lede: providers.length + ' providers', right: Btn({ variant: 'primary', disabled: s.discovering, children: s.discovering ? 'discovering…' : 'discover', onClick: discover }) }),
|
|
341
341
|
liveRegion(s.discovering ? 'discovering models' : ''),
|
|
342
342
|
section('providers', providers.length ? Table({
|
|
343
343
|
headers: ['provider', 'sampler', 'cached models'],
|
|
@@ -377,12 +377,12 @@ export const cron = makePage((ctx) => {
|
|
|
377
377
|
noteAlert(s.note),
|
|
378
378
|
section('jobs', list.length ? list.map((j, i) => Row({
|
|
379
379
|
key: i, code: j.enabled ? Icon('play') : Icon('pause'), title: j.cron, sub: trunc(j.prompt, TRUNC_SUB).text,
|
|
380
|
-
trailing: Btn({
|
|
380
|
+
trailing: Btn({ variant: 'danger', children: 'delete', onClick: () => del(j.id) }),
|
|
381
381
|
})) : emptyState('no cron jobs')),
|
|
382
382
|
section('new job',
|
|
383
383
|
TextField({ label: 'cron expression', value: s.expr, onInput: (v) => { s.expr = v; }, placeholder: '0 9 * * *' }),
|
|
384
384
|
TextField({ label: 'prompt', value: s.prompt, multiline: true, onInput: (v) => { s.prompt = v; }, placeholder: 'what to run…' }),
|
|
385
|
-
Btn({
|
|
385
|
+
Btn({ variant: 'primary', disabled: s.busy, children: s.busy ? 'working…' : 'add job', onClick: add })),
|
|
386
386
|
];
|
|
387
387
|
};
|
|
388
388
|
});
|
|
@@ -405,7 +405,7 @@ export const skills = makePage((ctx) => {
|
|
|
405
405
|
onClick: () => ctx.set({ open: s.open === i ? null : i }), active: s.open === i }),
|
|
406
406
|
s.open === i ? h('pre', { class: 'fd-pre fd-skill-body' }, sk.body || sk.content || '(no body)') : null,
|
|
407
407
|
)) : emptyState('no skills')),
|
|
408
|
-
];
|
|
408
|
+
].filter(Boolean);
|
|
409
409
|
};
|
|
410
410
|
});
|
|
411
411
|
|
|
@@ -456,8 +456,8 @@ export const config = makePage((ctx) => {
|
|
|
456
456
|
) : emptyState('no scalar config keys')),
|
|
457
457
|
section('raw', h('pre', { class: 'fd-pre' }, JSON.stringify(cfg, null, 2))),
|
|
458
458
|
section('actions',
|
|
459
|
-
Btn({
|
|
460
|
-
];
|
|
459
|
+
Btn({ variant: 'primary', disabled: s.busy || !Object.keys(s.edited).length, children: s.busy ? 'saving…' : 'save changes', onClick: save })),
|
|
460
|
+
].filter(Boolean);
|
|
461
461
|
};
|
|
462
462
|
});
|
|
463
463
|
|
|
@@ -506,11 +506,11 @@ export const env = makePage((ctx) => {
|
|
|
506
506
|
trailing: h('span', { class: 'fd-row-actions' },
|
|
507
507
|
a.set ? Chip({ tone: 'ok', children: 'set' }) : Chip({ tone: 'neutral', children: 'unset' }),
|
|
508
508
|
TextField({ type: 'password', value: s.draft[a.provider] || '', onInput: (v) => { s.draft[a.provider] = v; }, placeholder: 'paste key', 'aria-label': 'key for ' + a.provider }),
|
|
509
|
-
Btn({
|
|
510
|
-
(a.set && a.source === 'stored') ? Btn({
|
|
509
|
+
Btn({ variant: 'primary', disabled: s.busy === a.provider, children: s.busy === a.provider ? '…' : 'save', onClick: () => setKey(a.provider) }),
|
|
510
|
+
(a.set && a.source === 'stored') ? Btn({ variant: 'danger', disabled: s.busy === a.provider, children: 'remove', onClick: () => removeKey(a.provider) }) : null),
|
|
511
511
|
})) : emptyState('no providers')),
|
|
512
512
|
otherRows.length ? section('other environment', Table({ headers: ['key', 'status'], rows: otherRows })) : null,
|
|
513
|
-
];
|
|
513
|
+
].filter(Boolean);
|
|
514
514
|
};
|
|
515
515
|
});
|
|
516
516
|
|
|
@@ -536,7 +536,7 @@ export const tools = makePage((ctx) => {
|
|
|
536
536
|
ctx.state.open === t.name ? h('pre', { class: 'fd-pre' }, JSON.stringify(t.schema || t, null, 2)) : null,
|
|
537
537
|
)))),
|
|
538
538
|
list.length ? null : emptyState('no tools match'),
|
|
539
|
-
];
|
|
539
|
+
].filter(Boolean);
|
|
540
540
|
};
|
|
541
541
|
});
|
|
542
542
|
|
|
@@ -560,7 +560,7 @@ export const batch = makePage((ctx) => {
|
|
|
560
560
|
section('prompts',
|
|
561
561
|
TextField({ label: 'prompts (one per line)', value: s.prompts, multiline: true, rows: 6, onInput: (v) => { s.prompts = v; } }),
|
|
562
562
|
TextField({ label: 'concurrency', type: 'number', min: 1, 'aria-label': 'batch concurrency', value: String(s.concurrency), onInput: (v) => { s.concurrency = v; } }),
|
|
563
|
-
Btn({
|
|
563
|
+
Btn({ variant: 'primary', disabled: s.busy, children: s.busy ? 'running…' : 'run batch', onClick: run })),
|
|
564
564
|
s.result ? section('result', (() => {
|
|
565
565
|
const r = s.result;
|
|
566
566
|
const items = Array.isArray(r.results) ? r.results : (Array.isArray(r) ? r : null);
|
|
@@ -572,7 +572,7 @@ export const batch = makePage((ctx) => {
|
|
|
572
572
|
}) }),
|
|
573
573
|
];
|
|
574
574
|
})()) : null,
|
|
575
|
-
];
|
|
575
|
+
].filter(Boolean);
|
|
576
576
|
};
|
|
577
577
|
});
|
|
578
578
|
|
|
@@ -592,7 +592,7 @@ export const gateway = makePage((ctx) => {
|
|
|
592
592
|
PageHeader({ eyebrow: 'freddie', title: 'gateway', lede: 'messaging platform status' }),
|
|
593
593
|
s.error && s.data ? refreshError(s.error) : null,
|
|
594
594
|
section('platforms', rows.length ? Table({ headers: ['platform', 'status'], rows }) : emptyState('no platforms configured')),
|
|
595
|
-
];
|
|
595
|
+
].filter(Boolean);
|
|
596
596
|
};
|
|
597
597
|
});
|
|
598
598
|
|
|
@@ -632,14 +632,14 @@ export const chains = makePage((ctx) => {
|
|
|
632
632
|
noteAlert(s.note),
|
|
633
633
|
section('chains', Array.isArray(chainsList) && chainsList.length ? chainsList.map((c, i) => Row({
|
|
634
634
|
key: i, title: c.name || c, sub: Array.isArray(c.links) ? c.links.join(' -> ') : '',
|
|
635
|
-
trailing: Btn({
|
|
635
|
+
trailing: Btn({ variant: 'danger', children: 'delete', onClick: () => del(c.name || c) }),
|
|
636
636
|
})) : emptyState('no chains defined')),
|
|
637
637
|
section('new chain',
|
|
638
638
|
TextField({ label: 'name', value: s.name, onInput: (v) => { s.name = v; } }),
|
|
639
639
|
TextField({ label: 'links (comma-separated models)', value: s.links, onInput: (v) => { s.links = v; }, placeholder: 'mistral/large, openrouter/auto' }),
|
|
640
|
-
Btn({
|
|
640
|
+
Btn({ variant: 'primary', disabled: s.busy, children: s.busy ? 'working…' : 'create chain', onClick: create })),
|
|
641
641
|
s.cfg ? section('config', h('pre', { class: 'fd-pre' }, JSON.stringify(s.cfg, null, 2))) : null,
|
|
642
|
-
];
|
|
642
|
+
].filter(Boolean);
|
|
643
643
|
};
|
|
644
644
|
});
|
|
645
645
|
|
|
@@ -661,7 +661,7 @@ export const machines = makePage((ctx) => {
|
|
|
661
661
|
headers: ['kind', 'key', 'state'],
|
|
662
662
|
rows: list.map(m => [m.kind || '—', m.key || m.machine_id || '—', m.state || m.value || truncJson(m)]),
|
|
663
663
|
}) : emptyState('no live machines')),
|
|
664
|
-
];
|
|
664
|
+
].filter(Boolean);
|
|
665
665
|
};
|
|
666
666
|
});
|
|
667
667
|
|
|
@@ -689,7 +689,7 @@ export const health = makePage((ctx) => {
|
|
|
689
689
|
s.error && (s.health || s.providers) ? refreshError(s.error) : null,
|
|
690
690
|
section('checks', Object.keys(hd).length ? Table({ headers: ['check', 'status'], rows: Object.entries(hd).map(([k, v]) => [k, typeof v === 'object' ? truncJson(v) : (v === true ? Chip({ tone: 'ok', children: 'ok' }) : v === false ? Chip({ tone: 'miss', children: 'no' }) : String(v))]) }) : emptyState('no health data')),
|
|
691
691
|
provs.length ? section('providers', Table({ headers: ['provider', 'status'], rows: provs.map(p => { const n = typeof p === 'string' ? p : p.name || p.id; const ok = typeof p === 'object' ? (p.ok ?? p.available) : null; return [n, ok == null ? '—' : (ok ? Chip({ tone: 'ok', children: 'up' }) : Chip({ tone: 'miss', children: 'down' }))]; }) })) : null,
|
|
692
|
-
];
|
|
692
|
+
].filter(Boolean);
|
|
693
693
|
};
|
|
694
694
|
});
|
|
695
695
|
|
|
@@ -716,7 +716,7 @@ export const debug = makePage((ctx) => {
|
|
|
716
716
|
key: i, title: name, onClick: () => loadLogs(name), active: s.sub === name,
|
|
717
717
|
})) : emptyState('no debug subsystems')),
|
|
718
718
|
s.sub ? section('logs · ' + s.sub, h('pre', { class: 'fd-pre' }, JSON.stringify(s.logs, null, 2))) : null,
|
|
719
|
-
];
|
|
719
|
+
].filter(Boolean);
|
|
720
720
|
};
|
|
721
721
|
});
|
|
722
722
|
|
|
@@ -199,11 +199,11 @@ export function SessionCard({ session = {}, onStop, onOpen, onView, active = fal
|
|
|
199
199
|
activityBits.length ? h('span', { class: 'ds-dash-activity' }, activityBits.join(' · ')) : null,
|
|
200
200
|
].filter(Boolean));
|
|
201
201
|
const actions = h('div', { class: 'ds-dash-actions', role: 'group', 'aria-label': 'session actions' }, ...[
|
|
202
|
-
onOpen ? Btn({ key: 'open',
|
|
202
|
+
onOpen ? Btn({ key: 'open', variant: 'primary', 'aria-label': 'open session', onClick: () => onOpen(s),
|
|
203
203
|
children: [Icon('external-link', { size: 14 }), h('span', {}, 'open')] }) : null,
|
|
204
204
|
onView ? Btn({ key: 'view', 'aria-label': s.external ? 'open in history' : 'view events', onClick: () => onView(s),
|
|
205
205
|
children: [Icon('file-text', { size: 14 }), h('span', {}, s.external ? 'history' : 'events')] }) : null,
|
|
206
|
-
(onStop && !s.external) ? Btn({ key: 'stop',
|
|
206
|
+
(onStop && !s.external) ? Btn({ key: 'stop', variant: 'danger', disabled: !!s.stopping, 'aria-label': 'stop session',
|
|
207
207
|
onClick: () => !s.stopping && onStop(s),
|
|
208
208
|
children: [Icon('square', { size: 14 }), h('span', {}, s.stopping ? 'stopping…' : 'stop')] }) : null,
|
|
209
209
|
].filter(Boolean));
|
|
@@ -244,7 +244,7 @@ export function SessionDashboard({ sessions = [], onStop, onOpen, onView, onStop
|
|
|
244
244
|
sort, filter, errorsOnly = false, onErrorsOnly,
|
|
245
245
|
selectable = false, selected, onToggleSelect, onSelectAll, onClearSelection,
|
|
246
246
|
activeSid, streamState,
|
|
247
|
-
emptyText = 'No live sessions', offline = false } = {}) {
|
|
247
|
+
emptyText = 'No live sessions', emptyAction, offline = false } = {}) {
|
|
248
248
|
if (offline) {
|
|
249
249
|
return h('div', { class: 'ds-dash-state ds-dash-state-error', role: 'status' }, 'Backend offline — live sessions unavailable');
|
|
250
250
|
}
|
|
@@ -297,12 +297,11 @@ export function SessionDashboard({ sessions = [], onStop, onOpen, onView, onStop
|
|
|
297
297
|
onErrorsOnly ? h('button', { key: 'eo', type: 'button', class: 'ds-dash-errors-toggle' + (errorsOnly ? ' active' : ''),
|
|
298
298
|
'aria-pressed': errorsOnly ? 'true' : 'false', onclick: () => onErrorsOnly(!errorsOnly) }, 'errors only') : null)
|
|
299
299
|
: null;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
300
|
+
// NOTE: no separate empty-branch return. The empty state renders as a KEYED
|
|
301
|
+
// child of the same stable body wrapper the populated states use - swapping
|
|
302
|
+
// an unkeyed .ds-dash-state for keyed group children used to crash webjsx
|
|
303
|
+
// applyDiff (reading 'key') the moment the first session appeared, leaving a
|
|
304
|
+
// half-applied DOM ('1 running' header over 'No live sessions' body).
|
|
306
305
|
// Tri-state select-all over the selectable (non-external) sessions.
|
|
307
306
|
const selectableSids = sessions.filter((s) => !s.external).map((s) => s.sid);
|
|
308
307
|
const selOfVisible = selectableSids.filter((sid) => selSet.has(sid)).length;
|
|
@@ -318,16 +317,16 @@ export function SessionDashboard({ sessions = [], onStop, onOpen, onView, onStop
|
|
|
318
317
|
? h('button', { key: 'selclr', type: 'button', class: 'ds-dash-clear', onclick: () => onClearSelection() }, 'clear')
|
|
319
318
|
: null;
|
|
320
319
|
const stopBtn = stoppingCount > 0 && (onStopSelected || onStopAll)
|
|
321
|
-
? Btn({ key: 'stopbusy',
|
|
320
|
+
? Btn({ key: 'stopbusy', variant: 'danger', disabled: true, children: 'stopping ' + stoppingCount + '…' })
|
|
322
321
|
: (selectable && selCount && onStopSelected
|
|
323
322
|
? (onArmStopSelected && !confirmingStopSelected
|
|
324
|
-
? Btn({ key: 'stopsel',
|
|
325
|
-
: Btn({ key: 'stopsel',
|
|
323
|
+
? Btn({ key: 'stopsel', variant: 'danger', onClick: () => onArmStopSelected([...selSet]), children: 'stop selected' })
|
|
324
|
+
: Btn({ key: 'stopsel', variant: 'danger', className: confirmingStopSelected ? 'is-armed' : null, onClick: () => onStopSelected([...selSet]),
|
|
326
325
|
children: confirmingStopSelected ? 'stop ' + selCount + ' sessions - press again' : 'stop selected' }))
|
|
327
326
|
: (onStopAll
|
|
328
327
|
? (onArmStopAll && !confirmingStopAll
|
|
329
|
-
? Btn({ key: 'stopall',
|
|
330
|
-
: Btn({ key: 'stopall',
|
|
328
|
+
? Btn({ key: 'stopall', variant: 'danger', onClick: () => onArmStopAll(sessions), children: 'stop all' })
|
|
329
|
+
: Btn({ key: 'stopall', variant: 'danger', className: confirmingStopAll ? 'is-armed' : null, onClick: () => onStopAll(sessions),
|
|
331
330
|
children: confirmingStopAll ? 'stop ' + sessions.length + ' sessions - press again' : 'stop all' }))
|
|
332
331
|
: null));
|
|
333
332
|
// Build header children as a filtered array: webjsx applyDiff crashes
|
|
@@ -336,10 +335,14 @@ export function SessionDashboard({ sessions = [], onStop, onOpen, onView, onStop
|
|
|
336
335
|
const headerKids = [
|
|
337
336
|
selectable && selCount
|
|
338
337
|
? h('span', { key: 'cnt', class: 'ds-dash-count', role: 'status', 'aria-live': 'polite' }, selCount + ' selected')
|
|
339
|
-
: (breakdown || h('span', { key: 'cnt', class: 'ds-dash-count', role: 'status', 'aria-live': 'polite' },
|
|
338
|
+
: (breakdown || h('span', { key: 'cnt', class: 'ds-dash-count', role: 'status', 'aria-live': 'polite' },
|
|
339
|
+
sessions.length ? sessions.length + ' running' : '0 running')),
|
|
340
340
|
selectAllCtl, clearCtl, streamLine,
|
|
341
341
|
h('span', { key: 'spread', class: 'spread' }),
|
|
342
|
-
|
|
342
|
+
// No stop control without a session to stop; the empty dashboard keeps
|
|
343
|
+
// only the count, heartbeat, and (when wired) filter/sort chrome.
|
|
344
|
+
sessions.length ? stopBtn : null,
|
|
345
|
+
toolbar,
|
|
343
346
|
].filter(Boolean);
|
|
344
347
|
const header = h('div', { class: 'ds-dash-header', role: 'group', 'aria-label': 'live session controls' }, ...headerKids);
|
|
345
348
|
// Status-bucketed command center: when sorting by status (the default), the
|
|
@@ -350,20 +353,31 @@ export function SessionDashboard({ sessions = [], onStop, onOpen, onView, onStop
|
|
|
350
353
|
const cardOf = (s) => h('div', { key: s.sid, role: 'listitem' },
|
|
351
354
|
SessionCard({ session: s, onStop, onOpen, onView, active: s.sid === activeSid,
|
|
352
355
|
selectable, selected: selSet.has(s.sid), onToggleSelect }));
|
|
353
|
-
|
|
354
|
-
|
|
356
|
+
// ONE stable body wrapper across every state (empty / grouped / flat), with
|
|
357
|
+
// KEYED children - the ConversationList stable-keyed-body rule. Diffing
|
|
358
|
+
// happens on the children, never by swapping the container's shape.
|
|
359
|
+
let bodyKids;
|
|
360
|
+
if (!sessions.length) {
|
|
361
|
+
bodyKids = [h('div', { key: 'empty', class: 'ds-dash-state', role: 'status' },
|
|
362
|
+
...[
|
|
363
|
+
h('span', { key: 'et' }, emptyText),
|
|
364
|
+
(emptyAction && emptyAction.onClick)
|
|
365
|
+
? Btn({ key: 'ea', onClick: emptyAction.onClick, children: emptyAction.label || 'start a chat' })
|
|
366
|
+
: null,
|
|
367
|
+
].filter(Boolean))];
|
|
368
|
+
} else if (grouped) {
|
|
355
369
|
const buckets = [
|
|
356
370
|
{ key: 'error', label: 'Errored', rows: sessions.filter((s) => !s.external && s.status === 'error') },
|
|
357
371
|
{ key: 'running', label: 'Running', rows: sessions.filter((s) => !s.external && s.status !== 'error' && s.status !== 'stale') },
|
|
358
372
|
{ key: 'idle', label: 'Idle', rows: sessions.filter((s) => !s.external && s.status === 'stale') },
|
|
359
373
|
{ key: 'external', label: 'External', rows: sessions.filter((s) => s.external) },
|
|
360
374
|
].filter((b) => b.rows.length);
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
h('div', { class: 'ds-dash-grid', role: 'list', 'aria-label': b.label + ' sessions' }, ...b.rows.map(cardOf)))));
|
|
375
|
+
bodyKids = buckets.map((b) => h('div', { key: 'grp' + b.key, class: 'ds-dash-group', role: 'group', 'aria-label': b.label + ' sessions' },
|
|
376
|
+
h('div', { key: 'gl', class: 'ds-dash-group-label' }, b.label + ' · ' + b.rows.length),
|
|
377
|
+
h('div', { key: 'gg', class: 'ds-dash-grid', role: 'list', 'aria-label': b.label + ' sessions' }, ...b.rows.map(cardOf))));
|
|
365
378
|
} else {
|
|
366
|
-
|
|
379
|
+
bodyKids = [h('div', { key: 'flat', class: 'ds-dash-grid', role: 'list', 'aria-label': 'live sessions' }, ...sessions.map(cardOf))];
|
|
367
380
|
}
|
|
381
|
+
const body = h('div', { key: 'body', class: 'ds-dash-groups' }, ...bodyKids);
|
|
368
382
|
return h('div', { class: 'ds-dash' }, header, body);
|
|
369
383
|
}
|