anentrypoint-design 0.0.186 → 0.0.190

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.186",
3
+ "version": "0.0.190",
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,10 +92,17 @@ 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
+ const showWorkingTail = busy && lastMsg && lastMsg.role === 'assistant'
105
+ && (lastMsg.content || (Array.isArray(lastMsg.parts) && lastMsg.parts.length));
99
106
  const rows = messages.map((m, i) => {
100
107
  const isAssistant = m.role === 'assistant';
101
108
  const isStreaming = busy && i === lastIdx && isAssistant;
@@ -106,7 +113,15 @@ export function AgentChat(props = {}) {
106
113
  if (!isStreaming && isAssistant && !m.content && !hasParts) return null;
107
114
  const parts = [];
108
115
  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 });
116
+ // A message's parts may be bare strings (legacy text lines) OR structured
117
+ // part objects ({ kind:'tool'|'tool_result'|'code'|..., ... }). Pass the
118
+ // structured objects straight through to ChatMessage's renderPart so the
119
+ // host can surface real tool cards / code blocks; wrap only bare strings as
120
+ // text. This is what lets an orchestration host (agentgui) render the kit's
121
+ // collapsible ToolCallNode instead of flattening tools into plain text.
122
+ if (hasParts) for (const p of m.parts) {
123
+ parts.push((p && typeof p === 'object' && p.kind) ? p : { kind: 'text', text: String(p) });
124
+ }
110
125
  return ChatMessage({
111
126
  key: m.id || String(i),
112
127
  who: isAssistant ? 'them' : 'you',
@@ -118,14 +133,36 @@ export function AgentChat(props = {}) {
118
133
  });
119
134
  });
120
135
 
136
+ // While streaming, the composer's send button becomes an inline stop button
137
+ // (busy + onCancel) so the user can halt the turn from where their hands
138
+ // already are, not only from the controls cluster up top.
121
139
  const composer = ChatComposer({
122
140
  value: draft,
123
141
  disabled: !canSend,
142
+ busy,
124
143
  placeholder: placeholder || (selectedAgent ? 'message…' : 'choose an agent first'),
125
144
  onInput: (v) => onInput && onInput(v),
126
145
  onSend: (v) => onSend && onSend(v),
146
+ onCancel: busy && onStop ? () => onStop() : undefined,
127
147
  });
128
148
 
149
+ // Empty state: a fresh thread is a void without this. Mirrors the kit's Chat
150
+ // empty surface (title, sub, optional starter prompts) so AgentChat opens to
151
+ // an invitation, not a blank panel.
152
+ const emptyState = (messages.length === 0)
153
+ ? h('div', { class: 'agentchat-empty', role: 'status' },
154
+ h('p', { class: 'agentchat-empty-title' }, selectedAgent ? 'Start a conversation with ' + name : 'Choose an agent to begin'),
155
+ h('p', { class: 'agentchat-empty-sub' },
156
+ 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.'),
157
+ (suggestions && suggestions.length)
158
+ ? h('div', { class: 'agentchat-empty-suggestions' },
159
+ ...suggestions.map((s, i) => h('button', {
160
+ key: 'sug' + i, type: 'button', class: 'agentchat-empty-suggestion',
161
+ onclick: () => { const t = typeof s === 'string' ? s : (s.prompt || s.text || ''); if (onSuggestionClick) onSuggestionClick(t); },
162
+ }, typeof s === 'string' ? s : (s.label || s.text || s.prompt))))
163
+ : null)
164
+ : null;
165
+
129
166
  return h('div', { class: 'agentchat' },
130
167
  AgentControls({ agents, selectedAgent, models, selectedModel, busy, status, modelsLoading,
131
168
  onSelectAgent, onSelectModel, onNewChat, onStop }),
@@ -137,7 +174,13 @@ export function AgentChat(props = {}) {
137
174
  h('span', { class: 'agentchat-sub', 'aria-live': 'polite' },
138
175
  busy ? 'streaming…' : (messages.length ? messages.length + (messages.length === 1 ? ' message' : ' messages') : ''))),
139
176
  h('div', { class: 'agentchat-thread', ref: threadRef(messages.length), role: 'log', 'aria-label': 'conversation' },
140
- ...rows.filter(Boolean)),
177
+ emptyState,
178
+ ...rows.filter(Boolean),
179
+ showWorkingTail
180
+ ? h('div', { key: '_working', class: 'agentchat-working', role: 'status', 'aria-live': 'polite' },
181
+ h('span', { class: 'chat-thinking-dots', 'aria-hidden': 'true' }, h('span'), h('span'), h('span')),
182
+ h('span', { class: 'agentchat-working-text' }, 'working…'))
183
+ : null),
141
184
  composer,
142
185
  );
143
186
  }
@@ -332,7 +332,8 @@ export function EventList({ items, events, emptyText = 'no events', rankPad = 3
332
332
  sub: it.sub || '',
333
333
  active: it.active,
334
334
  onClick: it.onClick,
335
- kind: it.kind
335
+ kind: it.kind,
336
+ rail: it.rail
336
337
  }))
337
338
  );
338
339
  }
@@ -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
  );