anentrypoint-design 0.0.200 → 0.0.201
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 +42 -0
- package/chat.css +68 -0
- package/dist/247420.css +110 -0
- package/dist/247420.js +12 -12
- package/package.json +1 -1
- package/src/components/agent-chat.js +46 -3
- package/src/components/content.js +69 -8
- package/src/components/context-pane.js +11 -2
- package/src/components/files.js +35 -7
- package/src/components/sessions.js +44 -4
- package/src/components.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anentrypoint-design",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.201",
|
|
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",
|
|
@@ -55,7 +55,7 @@ function scrollThreadToBottom(btn) {
|
|
|
55
55
|
// The agent picker: agent-then-model, not a flat model list. Unavailable agents
|
|
56
56
|
// are disabled (unless installable via npx). Ordering is the host's concern.
|
|
57
57
|
function AgentControls({ agents, selectedAgent, models, selectedModel, busy, status, modelsLoading,
|
|
58
|
-
onSelectAgent, onSelectModel, onNewChat, onStop }) {
|
|
58
|
+
onSelectAgent, onSelectModel, onNewChat, onStop, exportActions }) {
|
|
59
59
|
const agentOptions = (agents || []).map((a) => ({
|
|
60
60
|
value: a.id,
|
|
61
61
|
label: a.name + (a.available === false ? (a.npxInstallable ? ' (via npx)' : ' (not installed)') : ''),
|
|
@@ -85,6 +85,16 @@ function AgentControls({ agents, selectedAgent, models, selectedModel, busy, sta
|
|
|
85
85
|
h('span', { key: 'st', class: 'agentchat-status', role: 'status', 'aria-live': 'polite' },
|
|
86
86
|
h('span', { class: 'status-dot-disc ' + (busy ? 'status-dot-live' : ''), 'aria-hidden': 'true' }),
|
|
87
87
|
h('span', {}, status || (busy ? 'streaming…' : 'ready'))),
|
|
88
|
+
// Host-supplied transcript actions (copy-all / export-md / export-json):
|
|
89
|
+
// small text-labeled buttons riding the same controls row. All siblings in
|
|
90
|
+
// this h(...) call are keyed VElements or null — never bare strings.
|
|
91
|
+
...(exportActions && exportActions.length
|
|
92
|
+
? exportActions.map((a, i) => h('button', {
|
|
93
|
+
key: 'exp' + i, type: 'button', class: 'agentchat-export-act',
|
|
94
|
+
title: a.title || a.label,
|
|
95
|
+
onclick: () => a.onClick && a.onClick(),
|
|
96
|
+
}, a.label))
|
|
97
|
+
: []),
|
|
88
98
|
);
|
|
89
99
|
}
|
|
90
100
|
|
|
@@ -124,8 +134,10 @@ export function AgentChat(props = {}) {
|
|
|
124
134
|
canSend = true,
|
|
125
135
|
suggestions = [], onSuggestionClick,
|
|
126
136
|
onCopyMessage, onRetryMessage, onEditMessage,
|
|
137
|
+
confirmEdit = false, onArmEdit,
|
|
127
138
|
avatar, composerContext,
|
|
128
139
|
followups = [], onFollowupClick,
|
|
140
|
+
installHint, exportActions = [],
|
|
129
141
|
} = props;
|
|
130
142
|
|
|
131
143
|
const name = agentName || (agents.find((a) => a.id === selectedAgent)?.name) || selectedAgent || 'agent';
|
|
@@ -194,7 +206,10 @@ export function AgentChat(props = {}) {
|
|
|
194
206
|
const built = [];
|
|
195
207
|
if (onCopyMessage) built.push({ label: 'copy', icon: 'copy', title: 'copy message', onClick: () => onCopyMessage(m) });
|
|
196
208
|
if (isAssistant && onRetryMessage && i === lastIdx) built.push({ label: 'retry', icon: 'refresh', title: 'retry this turn', onClick: () => onRetryMessage(m) });
|
|
197
|
-
|
|
209
|
+
// With confirmEdit the host arms its own confirm affordance (onArmEdit)
|
|
210
|
+
// instead of resending immediately; the kit stays stateless either way.
|
|
211
|
+
if (!isAssistant && onEditMessage) built.push({ label: 'edit', icon: 'pencil', title: 'edit and resend',
|
|
212
|
+
onClick: () => (confirmEdit && onArmEdit) ? onArmEdit(m) : onEditMessage(m) });
|
|
198
213
|
if (built.length) actions = built;
|
|
199
214
|
}
|
|
200
215
|
return ChatMessage({
|
|
@@ -253,12 +268,40 @@ export function AgentChat(props = {}) {
|
|
|
253
268
|
key: 'sug' + i, type: 'button', class: 'agentchat-empty-suggestion',
|
|
254
269
|
onclick: () => { const t = typeof s === 'string' ? s : (s.prompt || s.text || ''); if (onSuggestionClick) onSuggestionClick(t); },
|
|
255
270
|
}, typeof s === 'string' ? s : (s.label || s.text || s.prompt))))
|
|
271
|
+
: null,
|
|
272
|
+
// Guided install path for a brand-new user with zero installed agents:
|
|
273
|
+
// a plain copy line, a monospaced command per row (each with its own
|
|
274
|
+
// copy button, pure-DOM label flip like the code-block copy), and a
|
|
275
|
+
// recheck button so the user needn't reload after installing.
|
|
276
|
+
installHint
|
|
277
|
+
? h('div', { class: 'agentchat-install', role: 'group', 'aria-label': 'install an agent' },
|
|
278
|
+
installHint.text ? h('p', { class: 'agentchat-install-text' }, installHint.text) : null,
|
|
279
|
+
(installHint.commands && installHint.commands.length)
|
|
280
|
+
? h('ul', { class: 'agentchat-install-list' },
|
|
281
|
+
...installHint.commands.map((c, i) => h('li', { key: 'inst' + i, class: 'agentchat-install-row' },
|
|
282
|
+
h('span', { class: 'agentchat-install-agent' }, c.agent),
|
|
283
|
+
h('code', { class: 'agentchat-install-cmd' }, c.command),
|
|
284
|
+
h('button', {
|
|
285
|
+
type: 'button', class: 'agentchat-install-copy',
|
|
286
|
+
'aria-label': 'copy install command for ' + c.agent, title: 'copy command',
|
|
287
|
+
onclick: (e) => {
|
|
288
|
+
const btn = e.currentTarget;
|
|
289
|
+
navigator.clipboard && navigator.clipboard.writeText(c.command);
|
|
290
|
+
btn.textContent = 'copied';
|
|
291
|
+
setTimeout(() => { btn.textContent = 'copy'; }, 1200);
|
|
292
|
+
},
|
|
293
|
+
}, 'copy'))))
|
|
294
|
+
: null,
|
|
295
|
+
installHint.onRecheck
|
|
296
|
+
? h('div', { class: 'agentchat-install-actions' },
|
|
297
|
+
Btn({ onClick: () => installHint.onRecheck(), children: 'recheck agents', title: 'Re-check installed agents' }))
|
|
298
|
+
: null)
|
|
256
299
|
: null)
|
|
257
300
|
: null;
|
|
258
301
|
|
|
259
302
|
return h('div', { class: 'agentchat' },
|
|
260
303
|
AgentControls({ agents, selectedAgent, models, selectedModel, busy, status, modelsLoading,
|
|
261
|
-
onSelectAgent, onSelectModel, onNewChat, onStop }),
|
|
304
|
+
onSelectAgent, onSelectModel, onNewChat, onStop, exportActions }),
|
|
262
305
|
CwdBar({ cwd, editing: cwdEditing, draft: cwdDraft,
|
|
263
306
|
onEdit: onCwdEdit, onSave: onCwdSave, onCancel: onCwdCancel, onClear: onCwdClear, onDraft: onCwdDraft }),
|
|
264
307
|
...(banners || []).filter(Boolean),
|
|
@@ -6,9 +6,9 @@ import * as webjsx from '../../vendor/webjsx/index.js';
|
|
|
6
6
|
import { Btn, Heading, Lede, Dot, Icon } from './shell.js';
|
|
7
7
|
const h = webjsx.createElement;
|
|
8
8
|
|
|
9
|
-
export function Panel({ title, count, right, style = '', children, kind }) {
|
|
9
|
+
export function Panel({ title, count, right, style = '', children, kind, id }) {
|
|
10
10
|
const cls = 'panel' + (kind ? ' panel-' + kind : '');
|
|
11
|
-
return h('div', { class: cls, style },
|
|
11
|
+
return h('div', { class: cls, style, id: id || null },
|
|
12
12
|
title != null ? h('div', { class: 'panel-head' },
|
|
13
13
|
h('span', {}, title),
|
|
14
14
|
right != null ? right : (count != null ? h('span', {}, String(count)) : null)
|
|
@@ -20,7 +20,30 @@ export function Panel({ title, count, right, style = '', children, kind }) {
|
|
|
20
20
|
// Card — semantic alias of Panel; behaves identically.
|
|
21
21
|
export const Card = Panel;
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// Split a title string around case-insensitive matches of `highlight`, wrapping
|
|
24
|
+
// hits in <mark class="ds-hl">. Every segment is a keyed span so the children
|
|
25
|
+
// array never mixes keyed VElements with bare strings (webjsx applyDiff crashes
|
|
26
|
+
// on mixed keying).
|
|
27
|
+
function highlightTitle(title, highlight) {
|
|
28
|
+
const text = String(title);
|
|
29
|
+
const needle = String(highlight).toLowerCase();
|
|
30
|
+
if (!needle) return text;
|
|
31
|
+
const lower = text.toLowerCase();
|
|
32
|
+
const segs = [];
|
|
33
|
+
let pos = 0, n = 0;
|
|
34
|
+
while (pos <= text.length) {
|
|
35
|
+
const hit = lower.indexOf(needle, pos);
|
|
36
|
+
if (hit === -1) break;
|
|
37
|
+
if (hit > pos) segs.push(h('span', { key: 'hs' + n++ }, text.slice(pos, hit)));
|
|
38
|
+
segs.push(h('mark', { key: 'hs' + n++, class: 'ds-hl' }, text.slice(hit, hit + needle.length)));
|
|
39
|
+
pos = hit + needle.length;
|
|
40
|
+
}
|
|
41
|
+
if (!segs.length) return text;
|
|
42
|
+
if (pos < text.length) segs.push(h('span', { key: 'hs' + n++ }, text.slice(pos)));
|
|
43
|
+
return segs;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function Row({ code, rank, title, sub, meta, active, state = 'default', onClick, key, style, href, kind, cols, leading, trailing, target, selected, rail, expanded, highlight, actions }) {
|
|
24
47
|
// `rank` is an alias for `code` (the leading monospace index); callers use
|
|
25
48
|
// either name. `rail` renders a thin colour bar at the row's leading edge as
|
|
26
49
|
// a status indicator (tone: green | purple | flame | <any token>).
|
|
@@ -52,10 +75,32 @@ export function Row({ code, rank, title, sub, meta, active, state = 'default', o
|
|
|
52
75
|
}
|
|
53
76
|
if (isDisabled) props['aria-disabled'] = 'true';
|
|
54
77
|
if (isActive && (isLink || isButton)) props['aria-current'] = isActive ? 'page' : null;
|
|
78
|
+
// `highlight` wraps case-insensitive matches in the title in <mark class="ds-hl">.
|
|
79
|
+
// The segments live inside a single wrapper span so the title's child list
|
|
80
|
+
// never mixes keyed and unkeyed siblings.
|
|
81
|
+
const titleNode = (highlight && typeof title === 'string')
|
|
82
|
+
? h('span', {}, ...[].concat(highlightTitle(title, highlight)))
|
|
83
|
+
: title;
|
|
84
|
+
// `actions` render ONLY when the row is expanded, as a sibling action strip
|
|
85
|
+
// inside the row container; each button stops propagation so it never fires
|
|
86
|
+
// the row onClick.
|
|
87
|
+
const actionRow = (expanded === true && Array.isArray(actions) && actions.length)
|
|
88
|
+
? h('span', { class: 'row-actions', role: 'group', 'aria-label': 'row actions' },
|
|
89
|
+
...actions.map((a, i) => h('button', {
|
|
90
|
+
key: 'ract' + i,
|
|
91
|
+
type: 'button',
|
|
92
|
+
class: 'row-act',
|
|
93
|
+
title: a.title || a.label,
|
|
94
|
+
'aria-label': a.title || a.label,
|
|
95
|
+
onclick: (e) => { e.stopPropagation(); a.onClick && a.onClick(e); },
|
|
96
|
+
onkeydown: (e) => { e.stopPropagation(); },
|
|
97
|
+
}, a.label)))
|
|
98
|
+
: null;
|
|
55
99
|
return h(isLink ? 'a' : 'div', props,
|
|
56
100
|
leading != null ? leading : (codeVal != null ? h('span', { class: 'code' }, codeVal) : null),
|
|
57
|
-
h('span', { class: 'title' },
|
|
58
|
-
trailing != null ? trailing : (meta != null ? h('span', { class: 'meta' }, meta) : null)
|
|
101
|
+
h('span', { class: 'title' }, titleNode, sub ? h('span', { class: 'sub' }, sub) : null),
|
|
102
|
+
trailing != null ? trailing : (meta != null ? h('span', { class: 'meta' }, meta) : null),
|
|
103
|
+
actionRow);
|
|
59
104
|
}
|
|
60
105
|
|
|
61
106
|
export function RowLink({ code, title, sub, meta, href = '#', key, target }) {
|
|
@@ -253,11 +298,12 @@ export function ProjectView({ project = {}, copied, onCopy } = {}) {
|
|
|
253
298
|
].filter(Boolean).flat();
|
|
254
299
|
}
|
|
255
300
|
|
|
256
|
-
export function PageHeader({ title, lede, eyebrow, right, compact }) {
|
|
301
|
+
export function PageHeader({ title, lede, eyebrow, right, compact, id }) {
|
|
257
302
|
// `compact` drops the large leading/trailing section margins so a PageHeader
|
|
258
303
|
// used as a page's first element top-aligns cleanly without the consumer
|
|
259
|
-
// having to !important-override the .ds-section margin.
|
|
260
|
-
|
|
304
|
+
// having to !important-override the .ds-section margin. `id` lands on the
|
|
305
|
+
// outermost section so the header can serve as a deep-link anchor.
|
|
306
|
+
return h('section', { class: 'ds-section' + (compact ? ' ds-section-compact' : ''), id: id || null },
|
|
261
307
|
eyebrow ? h('span', { class: 'eyebrow' }, eyebrow) : null,
|
|
262
308
|
title != null ? h('h1', {}, title) : null,
|
|
263
309
|
lede != null ? h('p', { class: 'lede' }, lede) : null,
|
|
@@ -409,6 +455,21 @@ export function Skeleton({ height = '1em', width = '100%', count = 1, label = 'l
|
|
|
409
455
|
);
|
|
410
456
|
}
|
|
411
457
|
|
|
458
|
+
// FilterPills — a role=group of pill toggle buttons for quick category filters.
|
|
459
|
+
// `options` is [{ id, label }]; `selected` the active id; clicking a pill calls
|
|
460
|
+
// onSelect(id). Pressed state is announced via aria-pressed.
|
|
461
|
+
export function FilterPills({ options = [], selected, onSelect, label = 'filters' } = {}) {
|
|
462
|
+
if (!options.length) return null;
|
|
463
|
+
return h('div', { class: 'ds-filter-pills', role: 'group', 'aria-label': label },
|
|
464
|
+
...options.map((o) => h('button', {
|
|
465
|
+
key: 'fp-' + o.id,
|
|
466
|
+
type: 'button',
|
|
467
|
+
class: 'ds-filter-pill' + (o.id === selected ? ' active' : ''),
|
|
468
|
+
'aria-pressed': o.id === selected ? 'true' : 'false',
|
|
469
|
+
onclick: () => onSelect && onSelect(o.id),
|
|
470
|
+
}, o.label != null ? o.label : o.id)));
|
|
471
|
+
}
|
|
472
|
+
|
|
412
473
|
export function Alert({ kind = 'info', children, onDismiss, title, key } = {}) {
|
|
413
474
|
const icons = { info: 'info', success: 'check', warn: 'warn', error: 'x' };
|
|
414
475
|
const cls = 'ds-alert ds-alert-' + kind;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// cwd : the chat working directory (string) or falsy for server default
|
|
14
14
|
// toolCount : number of tool calls running in the current live turn (>=0)
|
|
15
15
|
// usage : OPTIONAL last-turn usage { inputTokens, outputTokens, costUsd, turns, durationMs }
|
|
16
|
-
// session : OPTIONAL
|
|
16
|
+
// session : OPTIONAL whole-conversation totals { turns, cost } shown as a block
|
|
17
17
|
// onSetCwd : optional callback for the "set working directory" affordance
|
|
18
18
|
//
|
|
19
19
|
// No decorative glyphs — words + the kit's Icon SVGs only.
|
|
@@ -34,10 +34,11 @@ function fmtTok(n) {
|
|
|
34
34
|
export function ContextPane({ agent, model, cwd, toolCount = 0, usage, session, onSetCwd } = {}) {
|
|
35
35
|
const running = Number(toolCount) > 0;
|
|
36
36
|
const hasUsage = usage && (usage.inputTokens != null || usage.outputTokens != null || usage.costUsd != null);
|
|
37
|
+
const hasSession = session && (session.turns != null || session.cost != null);
|
|
37
38
|
// Empty state: before an agent is picked AND with no usage/session, four
|
|
38
39
|
// placeholder rows (agent: none / model: dash / ...) read as a dead panel.
|
|
39
40
|
// Show one honest line instead.
|
|
40
|
-
if (!agent && !hasUsage && !
|
|
41
|
+
if (!agent && !hasUsage && !hasSession && !cwd) {
|
|
41
42
|
return h('div', { class: 'ds-context' },
|
|
42
43
|
h('div', { class: 'ds-context-empty', role: 'status' },
|
|
43
44
|
'No active conversation — start a chat to see context here'),
|
|
@@ -66,6 +67,14 @@ export function ContextPane({ agent, model, cwd, toolCount = 0, usage, session,
|
|
|
66
67
|
],
|
|
67
68
|
}),
|
|
68
69
|
];
|
|
70
|
+
// Conversation block: whole-session totals (turn count + accumulated cost)
|
|
71
|
+
// between the context panel and the per-turn usage panel.
|
|
72
|
+
if (hasSession) {
|
|
73
|
+
const sesRows = [];
|
|
74
|
+
if (session.turns != null) sesRows.push(Row({ title: 'turns', meta: String(session.turns) }));
|
|
75
|
+
if (session.cost != null) sesRows.push(Row({ title: 'total cost', meta: '$' + Number(session.cost).toFixed(4) }));
|
|
76
|
+
panels.push(Panel({ title: 'conversation', children: sesRows }));
|
|
77
|
+
}
|
|
69
78
|
// Usage block: surface the last turn's token/cost/turn/duration so the
|
|
70
79
|
// result event is no longer silently dropped.
|
|
71
80
|
if (hasUsage) {
|
package/src/components/files.js
CHANGED
|
@@ -43,7 +43,12 @@ export function FileIcon({ type = 'other' } = {}) {
|
|
|
43
43
|
return h('span', { class: 'ds-file-icon', 'data-file-type': type, 'aria-label': TYPE_LABELS[type] || 'file', role: 'img' }, Icon(fileGlyph(type)));
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
// Default action set for FileRow. A host without mutation endpoints passes a
|
|
47
|
+
// narrower `actions` list (e.g. ['download']) so the row renders no dead controls.
|
|
48
|
+
const FILE_ROW_ACTIONS = ['download', 'rename', 'delete'];
|
|
49
|
+
|
|
50
|
+
export function FileRow({ name, type = 'other', size, modified, code, onOpen, onAction, active, key, permissions, locked,
|
|
51
|
+
actions = FILE_ROW_ACTIONS, busy = false } = {}) {
|
|
47
52
|
// permissions: ['read','write'] | ['read'] | 'EACCES'. A no-access entry can
|
|
48
53
|
// be listed (the dir stat saw it) but not opened — show an ASCII tag and
|
|
49
54
|
// disable the open button so the row reads honestly instead of silently
|
|
@@ -54,7 +59,29 @@ export function FileRow({ name, type = 'other', size, modified, code, onOpen, on
|
|
|
54
59
|
const meta = [type === 'dir' ? null : fmtFileSize(size), modified || null, permTag].filter(Boolean).join(' · ');
|
|
55
60
|
const typeLabel = TYPE_LABELS[type] || 'file';
|
|
56
61
|
const accessibleLabel = `${typeLabel}: ${name}${meta ? ` (${meta})` : ''}`;
|
|
57
|
-
const canOpen = onOpen && !noAccess;
|
|
62
|
+
const canOpen = onOpen && !noAccess && !busy;
|
|
63
|
+
// Mutation actions on a read-only/no-access row render disabled (with a
|
|
64
|
+
// 'read-only' title) instead of vanishing, so the affordance reads honestly.
|
|
65
|
+
// `busy` (in-flight mutation) disables every control on the row.
|
|
66
|
+
const mutateDisabled = busy || readOnly || noAccess;
|
|
67
|
+
const actBtn = (act, title, ariaLabel, icon, warn) => h('button', {
|
|
68
|
+
key: 'act-' + act,
|
|
69
|
+
type: 'button',
|
|
70
|
+
class: 'ds-file-act' + (warn ? ' ds-file-act-warn' : ''),
|
|
71
|
+
title: mutateDisabled && act !== 'download' ? 'read-only' : title,
|
|
72
|
+
'aria-label': ariaLabel,
|
|
73
|
+
disabled: (act === 'download' ? busy : mutateDisabled) ? true : null,
|
|
74
|
+
'aria-disabled': (act === 'download' ? busy : mutateDisabled) ? 'true' : null,
|
|
75
|
+
onclick: () => onAction(act),
|
|
76
|
+
}, Icon(icon));
|
|
77
|
+
const actionBtns = onAction ? [
|
|
78
|
+
actions.indexOf('download') !== -1 && type !== 'dir'
|
|
79
|
+
? actBtn('download', 'download', `download ${name}`, 'arrow-down', false) : null,
|
|
80
|
+
actions.indexOf('rename') !== -1
|
|
81
|
+
? actBtn('rename', 'rename', `rename ${name}`, 'pencil', false) : null,
|
|
82
|
+
actions.indexOf('delete') !== -1
|
|
83
|
+
? actBtn('delete', 'delete', `delete ${name}`, 'x', true) : null,
|
|
84
|
+
].filter(Boolean) : [];
|
|
58
85
|
// A role=button row containing real <button> action controls is invalid
|
|
59
86
|
// HTML (interactive nesting). Instead the row is a plain container and the
|
|
60
87
|
// primary "open" affordance is itself a real <button> (native keyboard +
|
|
@@ -63,6 +90,7 @@ export function FileRow({ name, type = 'other', size, modified, code, onOpen, on
|
|
|
63
90
|
key,
|
|
64
91
|
class: 'ds-file-row row' + (active ? ' active' : '') + (noAccess ? ' is-locked' : ''),
|
|
65
92
|
'data-file-type': type,
|
|
93
|
+
'aria-busy': busy ? 'true' : null,
|
|
66
94
|
},
|
|
67
95
|
h('button', {
|
|
68
96
|
type: 'button',
|
|
@@ -77,10 +105,8 @@ export function FileRow({ name, type = 'other', size, modified, code, onOpen, on
|
|
|
77
105
|
h('span', { class: 'title' }, name),
|
|
78
106
|
h('span', { class: 'ds-file-meta meta', 'aria-label': meta ? `metadata: ${meta}` : null }, meta || '—')
|
|
79
107
|
),
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
h('button', { class: 'ds-file-act', title: 'rename', 'aria-label': `rename ${name}`, onclick: () => onAction('rename') }, Icon('pencil')),
|
|
83
|
-
h('button', { class: 'ds-file-act ds-file-act-warn', title: 'delete', 'aria-label': `delete ${name}`, onclick: () => onAction('delete') }, Icon('x'))
|
|
108
|
+
actionBtns.length ? h('span', { class: 'ds-file-actions', role: 'group', 'aria-label': `actions for ${name}` },
|
|
109
|
+
...actionBtns
|
|
84
110
|
) : null
|
|
85
111
|
);
|
|
86
112
|
}
|
|
@@ -133,7 +159,7 @@ const FILE_GRID_CAP = 200;
|
|
|
133
159
|
|
|
134
160
|
export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No files here yet',
|
|
135
161
|
columns = 'auto', sort, filter, loading = false,
|
|
136
|
-
shown, onShowMore } = {}) {
|
|
162
|
+
shown, onShowMore, actions, busy } = {}) {
|
|
137
163
|
if (loading) return FileSkeleton({});
|
|
138
164
|
if (!files.length) return EmptyState({ text: emptyText });
|
|
139
165
|
// Cap the rendered rows. `shown` (host-controlled) overrides the default cap
|
|
@@ -179,6 +205,8 @@ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No f
|
|
|
179
205
|
key: f.path || f.name + i,
|
|
180
206
|
name: f.name, type: f.type, size: f.size, modified: f.modified, code: f.code, active: f.active,
|
|
181
207
|
permissions: f.permissions, locked: f.locked,
|
|
208
|
+
actions: actions != null ? actions : undefined,
|
|
209
|
+
busy: busy != null ? !!busy : !!f.busy,
|
|
182
210
|
onOpen: onOpen ? () => onOpen(f) : null,
|
|
183
211
|
onAction: onAction ? (act) => onAction(act, f) : null
|
|
184
212
|
}))
|
|
@@ -21,7 +21,8 @@ const h = webjsx.createElement;
|
|
|
21
21
|
// emptyText, loading, error : explicit states
|
|
22
22
|
export function ConversationList({ sessions = [], selected, groups, search, caption,
|
|
23
23
|
onSelect, onNew, newLabel = 'New chat',
|
|
24
|
-
emptyText = 'No conversations yet', loading = false, error = null
|
|
24
|
+
emptyText = 'No conversations yet', loading = false, error = null,
|
|
25
|
+
loadingText = 'Loading conversations…' } = {}) {
|
|
25
26
|
const rowFor = (s, i) => h('button', {
|
|
26
27
|
// Stable key: prefer sid, else position - a missing/duplicate sid would make
|
|
27
28
|
// key undefined and crash webjsx applyDiff ("reading 'key'" of undefined).
|
|
@@ -53,7 +54,7 @@ export function ConversationList({ sessions = [], selected, groups, search, capt
|
|
|
53
54
|
// are uniformly keyed; non-row states render a single unkeyed status line.
|
|
54
55
|
let inner;
|
|
55
56
|
if (loading) {
|
|
56
|
-
inner = [h('div', { key: 'st', class: 'ds-session-state', role: 'status', 'aria-live': 'polite' },
|
|
57
|
+
inner = [h('div', { key: 'st', class: 'ds-session-state', role: 'status', 'aria-live': 'polite' }, loadingText)];
|
|
57
58
|
} else if (error) {
|
|
58
59
|
inner = [h('div', { key: 'st', class: 'ds-session-state ds-session-state-error', role: 'status' }, String(error))];
|
|
59
60
|
} else if (!sessions.length) {
|
|
@@ -85,6 +86,30 @@ export function ConversationList({ sessions = [], selected, groups, search, capt
|
|
|
85
86
|
body);
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
// SessionMeta — a middot-separated metadata strip for a session detail surface.
|
|
90
|
+
// items : [{ label, value, title, onCopy }]
|
|
91
|
+
// Each item is a span (label dimmed, value mono) with an optional per-item copy
|
|
92
|
+
// button; the strip flex-wraps at narrow widths. Class is .ds-session-meta-strip
|
|
93
|
+
// (the bare .ds-session-meta is already taken by ConversationList row meta).
|
|
94
|
+
export function SessionMeta({ items = [] } = {}) {
|
|
95
|
+
if (!items.length) return null;
|
|
96
|
+
return h('div', { class: 'ds-session-meta-strip', role: 'group', 'aria-label': 'session metadata' },
|
|
97
|
+
...items.map((it, i) => h('span', {
|
|
98
|
+
key: 'sm-' + (it.label != null ? it.label : i),
|
|
99
|
+
class: 'ds-session-meta-item',
|
|
100
|
+
title: it.title || null,
|
|
101
|
+
},
|
|
102
|
+
[
|
|
103
|
+
it.label != null ? h('span', { key: 'l', class: 'ds-session-meta-label' }, it.label) : null,
|
|
104
|
+
h('span', { key: 'v', class: 'ds-session-meta-value' }, it.value != null ? String(it.value) : ''),
|
|
105
|
+
it.onCopy ? h('button', {
|
|
106
|
+
key: 'c', type: 'button', class: 'ds-session-meta-copy',
|
|
107
|
+
'aria-label': 'copy ' + (it.title || it.label || 'value'),
|
|
108
|
+
onclick: () => it.onCopy(it.value),
|
|
109
|
+
}, 'copy') : null,
|
|
110
|
+
].filter(Boolean))));
|
|
111
|
+
}
|
|
112
|
+
|
|
88
113
|
// SessionCard — one running session in the live dashboard. Status dot, agent /
|
|
89
114
|
// model / cwd, elapsed, live counter, last activity, and per-session controls
|
|
90
115
|
// that each act on this session's id independently.
|
|
@@ -154,7 +179,14 @@ export function SessionCard({ session = {}, onStop, onOpen, onView, active = fal
|
|
|
154
179
|
// still tells the user the dashboard is listening (vs a dropped stream).
|
|
155
180
|
const STREAM_WORD = { connected: 'listening for activity', connecting: 'connecting to live stream…', lost: 'live stream lost — retrying…' };
|
|
156
181
|
|
|
182
|
+
// The stop-all / stop-selected danger buttons are two-step (host-driven, the kit
|
|
183
|
+
// is stateless): the first click fires onArmStop* so the host flips confirming*
|
|
184
|
+
// true and re-renders; the armed button reads 'stop N sessions - press again'
|
|
185
|
+
// and only THAT click fires the real onStopAll/onStopSelected. Hosts that wire
|
|
186
|
+
// no onArmStop* keep the old single-click behavior.
|
|
157
187
|
export function SessionDashboard({ sessions = [], onStop, onOpen, onView, onStopAll, onStopSelected,
|
|
188
|
+
confirmingStopAll = false, confirmingStopSelected = false,
|
|
189
|
+
onArmStopAll, onArmStopSelected,
|
|
158
190
|
sort, filter, errorsOnly = false, onErrorsOnly,
|
|
159
191
|
selectable = false, selected, onToggleSelect,
|
|
160
192
|
activeSid, streamState,
|
|
@@ -194,8 +226,16 @@ export function SessionDashboard({ sessions = [], onStop, onOpen, onView, onStop
|
|
|
194
226
|
streamLine,
|
|
195
227
|
h('span', { class: 'spread' }),
|
|
196
228
|
selectable && selCount && onStopSelected
|
|
197
|
-
?
|
|
198
|
-
|
|
229
|
+
? (onArmStopSelected && !confirmingStopSelected
|
|
230
|
+
? Btn({ key: 'stopsel', danger: true, onClick: () => onArmStopSelected([...selSet]), children: 'stop selected' })
|
|
231
|
+
: Btn({ key: 'stopsel', danger: true, onClick: () => onStopSelected([...selSet]),
|
|
232
|
+
children: confirmingStopSelected ? 'stop ' + selCount + ' sessions - press again' : 'stop selected' }))
|
|
233
|
+
: (onStopAll
|
|
234
|
+
? (onArmStopAll && !confirmingStopAll
|
|
235
|
+
? Btn({ key: 'stopall', danger: true, onClick: () => onArmStopAll(sessions), children: 'stop all' })
|
|
236
|
+
: Btn({ key: 'stopall', danger: true, onClick: () => onStopAll(sessions),
|
|
237
|
+
children: confirmingStopAll ? 'stop ' + sessions.length + ' sessions - press again' : 'stop all' }))
|
|
238
|
+
: null),
|
|
199
239
|
toolbar);
|
|
200
240
|
const grid = h('div', { class: 'ds-dash-grid', role: 'list', 'aria-label': 'live sessions' },
|
|
201
241
|
...sessions.map((s) => h('div', { key: s.sid, role: 'listitem' },
|
package/src/components.js
CHANGED
|
@@ -16,7 +16,7 @@ export {
|
|
|
16
16
|
WorksList, WritingList, Manifesto, Section, PageHeader,
|
|
17
17
|
Kpi, Table, SearchInput, TextField, Select, EventList,
|
|
18
18
|
HomeView, ProjectView, Form,
|
|
19
|
-
Spinner, Skeleton, Alert
|
|
19
|
+
Spinner, Skeleton, Alert, FilterPills
|
|
20
20
|
} from './components/content.js';
|
|
21
21
|
|
|
22
22
|
export {
|
|
@@ -28,7 +28,7 @@ export {
|
|
|
28
28
|
export { AgentChat } from './components/agent-chat.js';
|
|
29
29
|
|
|
30
30
|
export {
|
|
31
|
-
ConversationList, SessionCard, SessionDashboard
|
|
31
|
+
ConversationList, SessionCard, SessionDashboard, SessionMeta
|
|
32
32
|
} from './components/sessions.js';
|
|
33
33
|
|
|
34
34
|
export { ContextPane } from './components/context-pane.js';
|