anentrypoint-design 0.0.188 → 0.0.191
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 +9 -5
- package/chat.css +94 -1
- package/dist/247420.css +103 -6
- package/dist/247420.js +12 -12
- package/package.json +1 -1
- package/src/components/agent-chat.js +68 -6
- package/src/components/chat.js +6 -2
- package/src/components/content.js +10 -2
- package/src/components/shell.js +3 -1
- package/src/kits/os/app-panes.css +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anentrypoint-design",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.191",
|
|
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",
|
|
@@ -92,21 +92,52 @@ export function AgentChat(props = {}) {
|
|
|
92
92
|
onSelectAgent, onSelectModel, onSend, onStop, onNewChat, onInput,
|
|
93
93
|
onCwdEdit, onCwdSave, onCwdCancel, onCwdClear, onCwdDraft,
|
|
94
94
|
canSend = true,
|
|
95
|
+
suggestions = [], onSuggestionClick,
|
|
95
96
|
} = props;
|
|
96
97
|
|
|
97
98
|
const name = agentName || (agents.find((a) => a.id === selectedAgent)?.name) || selectedAgent || 'agent';
|
|
98
99
|
const lastIdx = messages.length - 1;
|
|
100
|
+
const lastMsg = messages[lastIdx];
|
|
101
|
+
// True when streaming but the live assistant turn already shows content/parts,
|
|
102
|
+
// so its inline typing dots have stopped — a long silent tool call would
|
|
103
|
+
// otherwise read as frozen. We append a standalone "working" indicator below.
|
|
104
|
+
// A message carries content (text/parts) when it has a non-empty content
|
|
105
|
+
// string OR at least one part. Used for the empty-shell skip + working tail
|
|
106
|
+
// so an interleaved turn (parts-only, no m.content) is not treated as empty.
|
|
107
|
+
const msgHasBody = (m) => !!(m.content || (Array.isArray(m.parts) && m.parts.length));
|
|
108
|
+
const showWorkingTail = busy && lastMsg && lastMsg.role === 'assistant' && msgHasBody(lastMsg);
|
|
99
109
|
const rows = messages.map((m, i) => {
|
|
100
110
|
const isAssistant = m.role === 'assistant';
|
|
101
111
|
const isStreaming = busy && i === lastIdx && isAssistant;
|
|
102
112
|
const hasParts = Array.isArray(m.parts) && m.parts.length > 0;
|
|
103
|
-
const emptyStreaming = isStreaming && !m
|
|
113
|
+
const emptyStreaming = isStreaming && !msgHasBody(m);
|
|
104
114
|
// A finished assistant message with no content and no parts is an empty
|
|
105
115
|
// shell (e.g. an aborted turn) — render nothing rather than a blank bubble.
|
|
106
|
-
if (!isStreaming && isAssistant && !m
|
|
116
|
+
if (!isStreaming && isAssistant && !msgHasBody(m)) return null;
|
|
117
|
+
// Render order follows m.parts so text and tool cards INTERLEAVE in arrival
|
|
118
|
+
// order (text -> tool -> text -> tool). A message's parts may be bare
|
|
119
|
+
// strings (legacy) OR structured {kind,...} objects (md/tool/tool_result/
|
|
120
|
+
// code/...) passed straight through to ChatMessage.renderPart — this is what
|
|
121
|
+
// lets an orchestration host render the kit's collapsible ToolCallNode
|
|
122
|
+
// inline instead of flattening tools to the end of the turn.
|
|
107
123
|
const parts = [];
|
|
108
|
-
if (
|
|
109
|
-
|
|
124
|
+
if (hasParts) {
|
|
125
|
+
for (const p of m.parts) {
|
|
126
|
+
const part = (p && typeof p === 'object' && p.kind) ? p : { kind: 'text', text: String(p) };
|
|
127
|
+
// While a turn is still streaming, render its prose as cheap inline text
|
|
128
|
+
// rather than full markdown: MdNode re-parses + re-sanitizes the WHOLE
|
|
129
|
+
// accumulated source and swaps the entire bubble innerHTML on every frame
|
|
130
|
+
// (O(n^2) over the turn, with a visible reflow). Downgrade md -> text
|
|
131
|
+
// mid-stream; the settled turn below renders real markdown once.
|
|
132
|
+
if (isStreaming && part.kind === 'md') parts.push({ kind: 'text', text: part.text });
|
|
133
|
+
else parts.push(part);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// m.content is the legacy/simple path (user messages, hosts that don't build
|
|
137
|
+
// interleaved parts). Only prepend it when the parts array doesn't already
|
|
138
|
+
// carry prose, so a parts-driven turn isn't double-rendered.
|
|
139
|
+
const partsHaveProse = parts.some(p => p.kind === 'md' || p.kind === 'text');
|
|
140
|
+
if (m.content && !partsHaveProse) parts.unshift({ kind: isAssistant ? 'md' : 'text', text: m.content });
|
|
110
141
|
return ChatMessage({
|
|
111
142
|
key: m.id || String(i),
|
|
112
143
|
who: isAssistant ? 'them' : 'you',
|
|
@@ -118,14 +149,36 @@ export function AgentChat(props = {}) {
|
|
|
118
149
|
});
|
|
119
150
|
});
|
|
120
151
|
|
|
152
|
+
// While streaming, the composer's send button becomes an inline stop button
|
|
153
|
+
// (busy + onCancel) so the user can halt the turn from where their hands
|
|
154
|
+
// already are, not only from the controls cluster up top.
|
|
121
155
|
const composer = ChatComposer({
|
|
122
156
|
value: draft,
|
|
123
157
|
disabled: !canSend,
|
|
158
|
+
busy,
|
|
124
159
|
placeholder: placeholder || (selectedAgent ? 'message…' : 'choose an agent first'),
|
|
125
160
|
onInput: (v) => onInput && onInput(v),
|
|
126
161
|
onSend: (v) => onSend && onSend(v),
|
|
162
|
+
onCancel: busy && onStop ? () => onStop() : undefined,
|
|
127
163
|
});
|
|
128
164
|
|
|
165
|
+
// Empty state: a fresh thread is a void without this. Mirrors the kit's Chat
|
|
166
|
+
// empty surface (title, sub, optional starter prompts) so AgentChat opens to
|
|
167
|
+
// an invitation, not a blank panel.
|
|
168
|
+
const emptyState = (messages.length === 0)
|
|
169
|
+
? h('div', { class: 'agentchat-empty', role: 'status' },
|
|
170
|
+
h('p', { class: 'agentchat-empty-title' }, selectedAgent ? 'Start a conversation with ' + name : 'Choose an agent to begin'),
|
|
171
|
+
h('p', { class: 'agentchat-empty-sub' },
|
|
172
|
+
selectedAgent ? 'Type a message below. The agent can read files, run tools, and search.' : 'Pick an agent from the selector above, then send a message.'),
|
|
173
|
+
(suggestions && suggestions.length)
|
|
174
|
+
? h('div', { class: 'agentchat-empty-suggestions' },
|
|
175
|
+
...suggestions.map((s, i) => h('button', {
|
|
176
|
+
key: 'sug' + i, type: 'button', class: 'agentchat-empty-suggestion',
|
|
177
|
+
onclick: () => { const t = typeof s === 'string' ? s : (s.prompt || s.text || ''); if (onSuggestionClick) onSuggestionClick(t); },
|
|
178
|
+
}, typeof s === 'string' ? s : (s.label || s.text || s.prompt))))
|
|
179
|
+
: null)
|
|
180
|
+
: null;
|
|
181
|
+
|
|
129
182
|
return h('div', { class: 'agentchat' },
|
|
130
183
|
AgentControls({ agents, selectedAgent, models, selectedModel, busy, status, modelsLoading,
|
|
131
184
|
onSelectAgent, onSelectModel, onNewChat, onStop }),
|
|
@@ -135,9 +188,18 @@ export function AgentChat(props = {}) {
|
|
|
135
188
|
h('div', { class: 'agentchat-head', role: 'banner' },
|
|
136
189
|
h('h2', { class: 'agentchat-title' }, name + (selectedModel ? ' · ' + selectedModel : '')),
|
|
137
190
|
h('span', { class: 'agentchat-sub', 'aria-live': 'polite' },
|
|
138
|
-
|
|
191
|
+
// Derive the busy label from the same status prop the controls use, so a
|
|
192
|
+
// reconnecting-while-streaming state reads one word everywhere instead of
|
|
193
|
+
// the head saying "streaming…" while the controls say "reconnecting…".
|
|
194
|
+
busy ? (status || 'streaming…') : (messages.length ? messages.length + (messages.length === 1 ? ' message' : ' messages') : ''))),
|
|
139
195
|
h('div', { class: 'agentchat-thread', ref: threadRef(messages.length), role: 'log', 'aria-label': 'conversation' },
|
|
140
|
-
|
|
196
|
+
emptyState,
|
|
197
|
+
...rows.filter(Boolean),
|
|
198
|
+
showWorkingTail
|
|
199
|
+
? h('div', { key: '_working', class: 'agentchat-working', role: 'status', 'aria-live': 'polite' },
|
|
200
|
+
h('span', { class: 'chat-thinking-dots', 'aria-hidden': 'true' }, h('span'), h('span'), h('span')),
|
|
201
|
+
h('span', { class: 'agentchat-working-text' }, 'working…'))
|
|
202
|
+
: null),
|
|
141
203
|
composer,
|
|
142
204
|
);
|
|
143
205
|
}
|
package/src/components/chat.js
CHANGED
|
@@ -92,8 +92,12 @@ function MdNode(p) {
|
|
|
92
92
|
function CodeNode(p) {
|
|
93
93
|
const refSink = (el) => {
|
|
94
94
|
if (!el) return;
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
// Key on the full code, not its length: two different blocks of the same
|
|
96
|
+
// length (e.g. an edit that swaps a line) would otherwise share a key and
|
|
97
|
+
// skip re-highlighting, leaving stale syntax coloring.
|
|
98
|
+
const codeKey = (p.lang || '') + '|' + (p.code || '');
|
|
99
|
+
if (el.dataset.codeKey === codeKey) return;
|
|
100
|
+
el.dataset.codeKey = codeKey;
|
|
97
101
|
highlightCodeBlockCached(el);
|
|
98
102
|
};
|
|
99
103
|
return h('div', { class: 'chat-bubble chat-code', ref: refSink },
|
|
@@ -20,7 +20,7 @@ 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
|
-
export function Row({ code, rank, title, sub, meta, active, state = 'default', onClick, key, style, href, kind, cols, leading, trailing, target, selected, rail }) {
|
|
23
|
+
export function Row({ code, rank, title, sub, meta, active, state = 'default', onClick, key, style, href, kind, cols, leading, trailing, target, selected, rail, expanded }) {
|
|
24
24
|
// `rank` is an alias for `code` (the leading monospace index); callers use
|
|
25
25
|
// either name. `rail` renders a thin colour bar at the row's leading edge as
|
|
26
26
|
// a status indicator (tone: green | purple | flame | <any token>).
|
|
@@ -45,6 +45,10 @@ export function Row({ code, rank, title, sub, meta, active, state = 'default', o
|
|
|
45
45
|
props.onkeydown = (e) => {
|
|
46
46
|
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(e); }
|
|
47
47
|
};
|
|
48
|
+
// When the row is a disclosure toggle (host passes a boolean `expanded`),
|
|
49
|
+
// announce its open/closed state so AT users hear "expanded/collapsed".
|
|
50
|
+
// Omitted entirely for plain action buttons (expanded === undefined).
|
|
51
|
+
if (expanded === true || expanded === false) props['aria-expanded'] = expanded ? 'true' : 'false';
|
|
48
52
|
}
|
|
49
53
|
if (isDisabled) props['aria-disabled'] = 'true';
|
|
50
54
|
if (isActive && (isLink || isButton)) props['aria-current'] = isActive ? 'page' : null;
|
|
@@ -332,7 +336,11 @@ export function EventList({ items, events, emptyText = 'no events', rankPad = 3
|
|
|
332
336
|
sub: it.sub || '',
|
|
333
337
|
active: it.active,
|
|
334
338
|
onClick: it.onClick,
|
|
335
|
-
kind: it.kind
|
|
339
|
+
kind: it.kind,
|
|
340
|
+
rail: it.rail,
|
|
341
|
+
// Forward a disclosure state when the host marks the row as a toggle,
|
|
342
|
+
// so a clickable event row announces aria-expanded.
|
|
343
|
+
expanded: it.expanded
|
|
336
344
|
}))
|
|
337
345
|
);
|
|
338
346
|
}
|
package/src/components/shell.js
CHANGED
|
@@ -249,7 +249,9 @@ export function AppShell({ topbar, crumb, side, main, status, narrow } = {}) {
|
|
|
249
249
|
h('div', { class: 'app-body' + (hasSide ? '' : ' no-side') },
|
|
250
250
|
h('div', { class: 'app-side-scrim', 'aria-hidden': 'true', onclick: () => toggleSide(false) }),
|
|
251
251
|
h('div', { class: 'app-side-shell', onclick: (e) => { if (e.target.closest('a')) toggleSide(false); } }, sideNode),
|
|
252
|
-
|
|
252
|
+
// tabindex=-1 so the skip-link (href="#app-main") actually moves
|
|
253
|
+
// keyboard focus into the main region, not just scroll to it.
|
|
254
|
+
h('main', { class: 'app-main' + (narrow ? ' narrow' : ''), id: 'app-main', tabindex: '-1' }, ...(Array.isArray(main) ? main : [main]))
|
|
253
255
|
),
|
|
254
256
|
status || null
|
|
255
257
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* App-pane chrome for kit components rendered inside .wm-win bodies.
|
|
2
2
|
Bible tokens only — no hardcoded color, font, or radius. Focus = inset rail; no shadows, no gradients. */
|
|
3
3
|
|
|
4
|
-
/*
|
|
4
|
+
/* --- terminal-app --- */
|
|
5
5
|
.terminal-app {
|
|
6
6
|
display: flex;
|
|
7
7
|
flex-direction: column;
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
}
|
|
49
49
|
.terminal-app-slot > * { width: 100%; height: 100%; }
|
|
50
50
|
|
|
51
|
-
/*
|
|
51
|
+
/* --- browser-app --- */
|
|
52
52
|
.browser-app {
|
|
53
53
|
display: flex;
|
|
54
54
|
flex-direction: column;
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
min-height: 18px;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
/*
|
|
114
|
+
/* --- validator-app --- */
|
|
115
115
|
.validator-app {
|
|
116
116
|
display: flex;
|
|
117
117
|
flex-direction: column;
|