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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anentrypoint-design",
3
- "version": "0.0.188",
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.content && !hasParts;
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.content && !hasParts) return null;
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 (m.content) parts.push({ kind: isAssistant ? 'md' : 'text', text: m.content });
109
- if (hasParts) for (const p of m.parts) parts.push({ kind: 'text', text: p });
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
- busy ? 'streaming…' : (messages.length ? messages.length + (messages.length === 1 ? ' message' : ' messages') : ''))),
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
- ...rows.filter(Boolean)),
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
  }
@@ -92,8 +92,12 @@ function MdNode(p) {
92
92
  function CodeNode(p) {
93
93
  const refSink = (el) => {
94
94
  if (!el) return;
95
- if (el.dataset.codeKey === (p.lang || '') + '|' + (p.code || '').length) return;
96
- el.dataset.codeKey = (p.lang || '') + '|' + (p.code || '').length;
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
  }
@@ -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
- h('main', { class: 'app-main' + (narrow ? ' narrow' : ''), id: 'app-main' }, ...(Array.isArray(main) ? main : [main]))
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
- /* ── terminal-app ───────────────────────────────────────────────────── */
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
- /* ── browser-app ────────────────────────────────────────────────────── */
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
- /* ── validator-app ──────────────────────────────────────────────────── */
114
+ /* --- validator-app --- */
115
115
  .validator-app {
116
116
  display: flex;
117
117
  flex-direction: column;