@runtypelabs/persona 3.24.0 → 3.26.0

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": "@runtypelabs/persona",
3
- "version": "3.24.0",
3
+ "version": "3.26.0",
4
4
  "description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -1633,6 +1633,55 @@ describe('AgentWidgetClient - partId Text/Tool Interleaving', () => {
1633
1633
  expect(seqTool).toBeLessThan(seq1);
1634
1634
  });
1635
1635
 
1636
+ it('should not emit a whitespace-only assistant bubble before a leading tool call', async () => {
1637
+ const events: AgentWidgetEvent[] = [];
1638
+
1639
+ const encoder = new TextEncoder();
1640
+ global.fetch = vi.fn().mockImplementation(async () => {
1641
+ const stream = new ReadableStream({
1642
+ start(controller) {
1643
+ const e = (eventType: string, data: Record<string, unknown>) =>
1644
+ controller.enqueue(encoder.encode(`event: ${eventType}\ndata: ${JSON.stringify({ type: eventType, ...data })}\n\n`));
1645
+
1646
+ e('flow_start', { flowId: 'f1', flowName: 'Test', totalSteps: 1 });
1647
+ e('step_start', { id: 's1', name: 'Prompt', stepType: 'prompt', index: 1, totalSteps: 1 });
1648
+ // Tool UI is the first meaningful output. Some providers still emit
1649
+ // newline-only text lifecycle events around the tool boundary; those
1650
+ // must not become an empty assistant message bubble.
1651
+ e('tool_start', { toolId: 'tc_1', name: 'add_to_cart', toolType: 'local', sequenceIndex: 4 });
1652
+ e('text_start', { partId: 'text_0', messageId: 'msg_s1', seq: 1 });
1653
+ e('step_delta', { id: 's1', text: '\n', partId: 'text_0', messageId: 'msg_s1', seq: 2 });
1654
+ e('text_end', { partId: 'text_0', messageId: 'msg_s1', seq: 3 });
1655
+ e('tool_complete', { toolId: 'tc_1', name: 'add_to_cart', success: true, completedAt: new Date().toISOString(), executionTime: 20, sequenceIndex: 5 });
1656
+ e('flow_complete', { success: true });
1657
+ controller.close();
1658
+ }
1659
+ });
1660
+ return { ok: true, body: stream };
1661
+ });
1662
+
1663
+ const client = new AgentWidgetClient({ apiUrl: 'http://localhost:8000' });
1664
+ await client.dispatch(
1665
+ { messages: [{ id: 'usr_1', role: 'user', content: 'Add to cart', createdAt: new Date().toISOString() }] },
1666
+ (event) => events.push(event)
1667
+ );
1668
+
1669
+ const messageEvents = events.filter(e => e.type === 'message');
1670
+ const messagesById = new Map<string, AgentWidgetMessage>();
1671
+ for (const event of messageEvents) {
1672
+ if (event.type === 'message') messagesById.set(event.message.id, event.message);
1673
+ }
1674
+
1675
+ const allMessages = Array.from(messagesById.values());
1676
+ const assistantTexts = allMessages.filter(m => m.role === 'assistant' && !m.variant);
1677
+ const toolMsgs = allMessages.filter(m => m.variant === 'tool');
1678
+
1679
+ expect(assistantTexts).toHaveLength(0);
1680
+ expect(toolMsgs).toHaveLength(1);
1681
+ expect(toolMsgs[0].toolCall?.name).toBe('add_to_cart');
1682
+ expect(toolMsgs[0].toolCall?.status).toBe('complete');
1683
+ });
1684
+
1636
1685
  it('should not split when partId is absent (backward compatible)', async () => {
1637
1686
  const events: AgentWidgetEvent[] = [];
1638
1687
 
package/src/client.ts CHANGED
@@ -1297,7 +1297,25 @@ export class AgentWidgetClient {
1297
1297
  };
1298
1298
  };
1299
1299
 
1300
+ const shouldEmitMessage = (msg: AgentWidgetMessage): boolean => {
1301
+ if (msg.role !== "assistant" || msg.variant) return true;
1302
+
1303
+ const hasContentParts =
1304
+ Array.isArray(msg.contentParts) && msg.contentParts.length > 0;
1305
+ const hasRawContent =
1306
+ typeof msg.rawContent === "string" && msg.rawContent.trim() !== "";
1307
+ const hasVisibleText =
1308
+ typeof msg.content === "string" && msg.content.trim() !== "";
1309
+
1310
+ // Do not surface assistant text bubbles that only contain whitespace.
1311
+ // Some providers emit newline-only text parts around a leading tool call;
1312
+ // rendering those as normal messages creates an empty bubble above the
1313
+ // tool card. Keep media/component/stop-reason messages renderable.
1314
+ return hasVisibleText || hasContentParts || hasRawContent || Boolean(msg.stopReason);
1315
+ };
1316
+
1300
1317
  const emitMessage = (msg: AgentWidgetMessage) => {
1318
+ if (!shouldEmitMessage(msg)) return;
1301
1319
  onEvent({
1302
1320
  type: "message",
1303
1321
  message: cloneMessage(msg)
@@ -0,0 +1,406 @@
1
+ import {
2
+ initAgentWidget as initAgentWidgetFn,
3
+ type AgentWidgetInitHandle
4
+ } from "./runtime/init";
5
+
6
+ export type {
7
+ AgentWidgetConfig,
8
+ AgentWidgetFeatureFlags,
9
+ AgentWidgetArtifactsFeature,
10
+ AgentWidgetArtifactsLayoutConfig,
11
+ PersonaArtifactKind,
12
+ PersonaArtifactRecord,
13
+ PersonaArtifactManualUpsert,
14
+ ArtifactConfigPayload,
15
+ AgentWidgetInitOptions,
16
+ AgentWidgetMessage,
17
+ AgentWidgetLauncherConfig,
18
+ AgentWidgetDockConfig,
19
+ AgentWidgetEvent,
20
+ AgentWidgetStreamParser,
21
+ AgentWidgetStreamParserResult,
22
+ AgentWidgetRequestPayload,
23
+ // Context provider types (e.g. for config.contextProviders)
24
+ AgentWidgetContextProvider,
25
+ AgentWidgetContextProviderContext,
26
+ AgentWidgetCustomFetch,
27
+ AgentWidgetSSEEventParser,
28
+ AgentWidgetSSEEventResult,
29
+ AgentWidgetHeadersFunction,
30
+ // Multi-modal content types
31
+ TextContentPart,
32
+ ImageContentPart,
33
+ ContentPart,
34
+ MessageContent,
35
+ // Attachment config type
36
+ AgentWidgetAttachmentsConfig,
37
+ AgentWidgetComposerConfig,
38
+ // Layout types
39
+ AgentWidgetLayoutConfig,
40
+ AgentWidgetHeaderLayoutConfig,
41
+ AgentWidgetMessageLayoutConfig,
42
+ AgentWidgetAvatarConfig,
43
+ AgentWidgetTimestampConfig,
44
+ WidgetLayoutSlot,
45
+ SlotRenderer,
46
+ SlotRenderContext,
47
+ HeaderRenderContext,
48
+ MessageRenderContext,
49
+ // Markdown types
50
+ AgentWidgetMarkdownConfig,
51
+ AgentWidgetMarkdownOptions,
52
+ AgentWidgetMarkdownRendererOverrides,
53
+ // Message actions types
54
+ AgentWidgetMessageActionsConfig,
55
+ AgentWidgetMessageFeedback,
56
+ // Client token types
57
+ ClientSession,
58
+ ClientInitResponse,
59
+ ClientChatRequest,
60
+ ClientFeedbackRequest,
61
+ ClientFeedbackType,
62
+ // Message injection types
63
+ InjectMessageOptions,
64
+ InjectAssistantMessageOptions,
65
+ InjectUserMessageOptions,
66
+ InjectSystemMessageOptions,
67
+ InjectComponentDirectiveOptions,
68
+ // Loading indicator types
69
+ LoadingIndicatorRenderContext,
70
+ AgentWidgetLoadingIndicatorConfig,
71
+ // Idle indicator types
72
+ IdleIndicatorRenderContext,
73
+ // Agent execution types
74
+ AgentConfig,
75
+ AgentLoopConfig,
76
+ AgentToolsConfig,
77
+ AgentRequestOptions,
78
+ AgentExecutionState,
79
+ AgentMessageMetadata,
80
+ AgentWidgetAgentRequestPayload,
81
+ // Approval types
82
+ AgentWidgetApproval,
83
+ AgentWidgetApprovalConfig,
84
+ // WebMCP — page-discovered tool consumption
85
+ AgentWidgetWebMcpConfig,
86
+ ClientToolDefinition,
87
+ WebMcpConfirmHandler,
88
+ WebMcpConfirmInfo,
89
+ WebMcpToolResult,
90
+ // Event stream types
91
+ SSEEventRecord,
92
+ EventStreamConfig,
93
+ EventStreamBadgeColor,
94
+ EventStreamViewRenderContext,
95
+ EventStreamRowRenderContext,
96
+ EventStreamToolbarRenderContext,
97
+ EventStreamPayloadRenderContext,
98
+ // Controller event map
99
+ AgentWidgetControllerEventMap,
100
+ // Ask-user-question (built-in answer-pill sheet) types
101
+ AskUserQuestionPayload,
102
+ AskUserQuestionPrompt,
103
+ AskUserQuestionOption,
104
+ AgentWidgetAskUserQuestionFeature,
105
+ AgentWidgetAskUserQuestionStyles
106
+ } from "./types";
107
+
108
+ export {
109
+ ASK_USER_QUESTION_TOOL_NAME,
110
+ createAskUserQuestionBubble,
111
+ ensureAskUserQuestionSheet,
112
+ removeAskUserQuestionSheet,
113
+ isAskUserQuestionMessage,
114
+ parseAskUserQuestionPayload
115
+ } from "./components/ask-user-question-bubble";
116
+
117
+ export { initAgentWidgetFn as initAgentWidget };
118
+ export {
119
+ createWidgetHostLayout,
120
+ type WidgetHostLayout,
121
+ type WidgetHostLayoutMode
122
+ } from "./runtime/host-layout";
123
+ export {
124
+ createAgentExperience,
125
+ type AgentWidgetController
126
+ } from "./ui";
127
+ export {
128
+ AgentWidgetSession,
129
+ type AgentWidgetSessionStatus
130
+ } from "./session";
131
+ export { AgentWidgetClient } from "./client";
132
+ export type { SSEEventCallback } from "./client";
133
+ export {
134
+ WebMcpBridge,
135
+ WEBMCP_TOOL_PREFIX,
136
+ isWebMcpToolName,
137
+ stripWebMcpPrefix
138
+ } from "./webmcp-bridge";
139
+ export { createLocalStorageAdapter } from "./utils/storage";
140
+ export {
141
+ createActionManager,
142
+ defaultActionHandlers,
143
+ defaultJsonActionParser
144
+ } from "./utils/actions";
145
+ export {
146
+ markdownPostprocessor,
147
+ escapeHtml,
148
+ directivePostprocessor,
149
+ createMarkdownProcessor,
150
+ createMarkdownProcessorFromConfig,
151
+ createDirectivePostprocessor
152
+ } from "./postprocessors";
153
+ export type { MarkdownProcessorOptions } from "./postprocessors";
154
+ export {
155
+ createDefaultSanitizer,
156
+ resolveSanitizer
157
+ } from "./utils/sanitize";
158
+ export type { SanitizeFunction } from "./utils/sanitize";
159
+ export {
160
+ createPlainTextParser,
161
+ createJsonStreamParser,
162
+ createFlexibleJsonStreamParser,
163
+ createRegexJsonParser,
164
+ createXmlParser
165
+ } from "./utils/formatting";
166
+ export {
167
+ // Multi-modal content utilities
168
+ normalizeContent,
169
+ getDisplayText,
170
+ hasImages,
171
+ getImageParts,
172
+ createTextPart,
173
+ createImagePart,
174
+ fileToImagePart,
175
+ validateImageFile
176
+ } from "./utils/content";
177
+ export {
178
+ collectEnrichedPageContext,
179
+ formatEnrichedContext,
180
+ generateStableSelector,
181
+ defaultParseRules
182
+ } from "./utils/dom-context";
183
+ export type {
184
+ EnrichedPageElement,
185
+ DomContextOptions,
186
+ DomContextMode,
187
+ ParseOptionsConfig,
188
+ ParseRule,
189
+ RuleScoringContext,
190
+ FormatEnrichedContextOptions
191
+ } from "./utils/dom-context";
192
+ export {
193
+ AttachmentManager,
194
+ type PendingAttachment,
195
+ type AttachmentManagerConfig
196
+ } from "./utils/attachment-manager";
197
+ export {
198
+ generateMessageId,
199
+ generateUserMessageId,
200
+ generateAssistantMessageId
201
+ } from "./utils/message-id";
202
+ export { isDockedMountMode, resolveDockConfig } from "./utils/dock";
203
+ // NOTE: `generateCodeSnippet` (dev/config-tool helper) is intentionally NOT
204
+ // re-exported here so it stays out of the IIFE/CDN bundle (index-global.ts
205
+ // re-exports from this module). It is re-added to the npm barrel in `index.ts`.
206
+ export { VERSION } from "./version";
207
+ export type { AgentWidgetInitHandle };
208
+
209
+ // Plugin system exports
210
+ export type { AgentWidgetPlugin } from "./plugins/types";
211
+ export { pluginRegistry } from "./plugins/registry";
212
+
213
+ // Stream animation plugin API — lets consumers register custom animations
214
+ // that match the built-in surface (typewriter, pop-bubble) and subpath
215
+ // modules (letter-rise, word-fade, wipe, glyph-cycle).
216
+ export {
217
+ registerStreamAnimationPlugin,
218
+ unregisterStreamAnimationPlugin,
219
+ listRegisteredStreamAnimations,
220
+ } from "./utils/stream-animation";
221
+ export type {
222
+ StreamAnimationPlugin,
223
+ StreamAnimationContext,
224
+ AgentWidgetStreamAnimationBuffer,
225
+ AgentWidgetStreamAnimationBuiltinType,
226
+ AgentWidgetStreamAnimationType,
227
+ AgentWidgetStreamAnimationFeature,
228
+ AgentWidgetStreamAnimationPlaceholder,
229
+ } from "./types";
230
+
231
+ // Action system types — needed to type the `actionHandlers` / `actionParsers`
232
+ // config options and to author custom handlers/parsers.
233
+ export type {
234
+ AgentWidgetActionHandler,
235
+ AgentWidgetActionHandlerResult,
236
+ AgentWidgetActionParser,
237
+ AgentWidgetParsedAction,
238
+ AgentWidgetActionContext,
239
+ AgentWidgetActionEventPayload,
240
+ } from "./types";
241
+
242
+ // Dropdown utility exports
243
+ export { createDropdownMenu } from "./utils/dropdown";
244
+ export type { DropdownMenuItem, CreateDropdownOptions, DropdownMenuHandle } from "./utils/dropdown";
245
+
246
+ // Icon utility exports
247
+ export { renderLucideIcon } from "./utils/icons";
248
+ export type { IconName } from "./utils/icons";
249
+
250
+ // Button utility exports
251
+ export { createIconButton, createLabelButton, createToggleGroup, createComboButton } from "./utils/buttons";
252
+ export type {
253
+ CreateIconButtonOptions,
254
+ CreateLabelButtonOptions,
255
+ CreateToggleGroupOptions,
256
+ ToggleGroupItem,
257
+ ToggleGroupHandle,
258
+ CreateComboButtonOptions,
259
+ ComboButtonHandle
260
+ } from "./utils/buttons";
261
+
262
+ // NOTE: `createDemoCarousel` (demo-only component) is intentionally NOT
263
+ // re-exported here so it stays out of the IIFE/CDN bundle. It is re-added to
264
+ // the npm barrel in `index.ts`.
265
+
266
+ // Theme system exports
267
+ export {
268
+ createTheme,
269
+ resolveTokens,
270
+ themeToCssVariables,
271
+ applyThemeVariables,
272
+ getActiveTheme,
273
+ getColorScheme,
274
+ detectColorScheme,
275
+ createThemeObserver
276
+ } from "./utils/theme";
277
+ export {
278
+ DEFAULT_PALETTE,
279
+ DEFAULT_SEMANTIC,
280
+ DEFAULT_COMPONENTS,
281
+ validateTheme,
282
+ THEME_ZONES
283
+ } from "./utils/tokens";
284
+ export type { ThemeZone } from "./utils/tokens";
285
+ export {
286
+ accessibilityPlugin,
287
+ animationsPlugin,
288
+ brandPlugin,
289
+ reducedMotionPlugin,
290
+ highContrastPlugin,
291
+ createPlugin
292
+ } from "./utils/plugins";
293
+ export type {
294
+ DeepPartial,
295
+ PersonaTheme,
296
+ PersonaThemePlugin,
297
+ CreateThemeOptions,
298
+ TokenReference,
299
+ ColorShade,
300
+ ColorPalette,
301
+ SpacingScale,
302
+ TypographyScale,
303
+ ShadowScale,
304
+ BorderScale,
305
+ RadiusScale,
306
+ SemanticColors,
307
+ SemanticSpacing,
308
+ SemanticTypography,
309
+ ComponentTokens,
310
+ ArtifactToolbarTokens,
311
+ ArtifactTabTokens,
312
+ ArtifactPaneTokens,
313
+ IconButtonTokens,
314
+ LabelButtonTokens,
315
+ ToggleGroupTokens,
316
+ ThemeValidationResult,
317
+ ThemeValidationError
318
+ } from "./types/theme";
319
+
320
+ // Component system exports
321
+ export { componentRegistry } from "./components/registry";
322
+ export type { ComponentRenderer, ComponentContext } from "./components/registry";
323
+ export {
324
+ createComponentStreamParser,
325
+ isComponentDirectiveType
326
+ } from "./utils/component-parser";
327
+ export type { ComponentDirective } from "./utils/component-parser";
328
+ export {
329
+ renderComponentDirective,
330
+ createComponentMiddleware,
331
+ hasComponentDirective,
332
+ extractComponentDirectiveFromMessage
333
+ } from "./utils/component-middleware";
334
+
335
+ // Default configuration exports
336
+ export {
337
+ DEFAULT_WIDGET_CONFIG,
338
+ DEFAULT_FLOATING_LAUNCHER_MAX_WIDTH,
339
+ DEFAULT_FLOATING_LAUNCHER_WIDTH,
340
+ mergeWithDefaults
341
+ } from "./defaults";
342
+ export {
343
+ PRESETS,
344
+ getPreset,
345
+ PRESET_SHOP,
346
+ PRESET_MINIMAL,
347
+ PRESET_FULLSCREEN
348
+ } from "./presets";
349
+ export type { WidgetPreset } from "./presets";
350
+
351
+ // Layout system exports
352
+ export {
353
+ buildHeader,
354
+ buildComposer,
355
+ attachHeaderToContainer
356
+ } from "./components/panel";
357
+ export type {
358
+ HeaderElements,
359
+ HeaderBuildContext,
360
+ ComposerElements,
361
+ ComposerBuildContext
362
+ } from "./components/panel";
363
+ export {
364
+ headerLayouts,
365
+ getHeaderLayout,
366
+ buildHeaderWithLayout,
367
+ buildDefaultHeader,
368
+ buildMinimalHeader
369
+ } from "./components/header-layouts";
370
+ export type {
371
+ HeaderLayoutContext,
372
+ HeaderLayoutRenderer
373
+ } from "./components/header-layouts";
374
+ export {
375
+ createStandardBubble,
376
+ createBubbleWithLayout,
377
+ createTypingIndicator,
378
+ createMessageActions,
379
+ renderLoadingIndicatorWithFallback
380
+ } from "./components/message-bubble";
381
+ export type {
382
+ MessageTransform,
383
+ MessageActionCallbacks,
384
+ LoadingIndicatorRenderer,
385
+ CreateStandardBubbleOptions
386
+ } from "./components/message-bubble";
387
+ export {
388
+ createCSATFeedback,
389
+ createNPSFeedback
390
+ } from "./components/feedback";
391
+ export type { CSATFeedbackOptions, NPSFeedbackOptions } from "./components/feedback";
392
+
393
+ // Voice module exports
394
+ export {
395
+ createVoiceProvider,
396
+ createBestAvailableVoiceProvider,
397
+ isVoiceSupported
398
+ } from "./voice";
399
+ export type {
400
+ VoiceProvider,
401
+ VoiceResult,
402
+ VoiceStatus,
403
+ VoiceConfig
404
+ } from "./types";
405
+
406
+ export default initAgentWidgetFn;
@@ -12,8 +12,10 @@
12
12
  * modules so bundlers can tree-shake them.
13
13
  */
14
14
 
15
- // Re-export the full public API.
16
- export * from "./index";
15
+ // Re-export the full public API — from `index-core` (NOT `index`) so the
16
+ // dev-only helpers (`generateCodeSnippet`, `createDemoCarousel`) stay out of the
17
+ // CDN/IIFE bundle. npm consumers still get them via the `index.ts` barrel.
18
+ export * from "./index-core";
17
19
 
18
20
  // Side-import the remaining subpath animations so they're available to
19
21
  // script-tag consumers without an explicit import. (`letter-rise` and