@runtypelabs/persona 3.34.0 → 3.35.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.
Files changed (186) hide show
  1. package/README.md +11 -11
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/{types-CthJFfNx.d.cts → types-DgYsuwXL.d.cts} +15 -14
  5. package/dist/animations/{types-CthJFfNx.d.ts → types-DgYsuwXL.d.ts} +15 -14
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/codegen.cjs +11 -11
  9. package/dist/codegen.js +10 -10
  10. package/dist/index.cjs +46 -44
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +514 -158
  13. package/dist/index.d.ts +514 -158
  14. package/dist/index.global.js +41 -89
  15. package/dist/index.global.js.map +1 -1
  16. package/dist/index.js +46 -44
  17. package/dist/index.js.map +1 -1
  18. package/dist/install.global.js +1 -1
  19. package/dist/install.global.js.map +1 -1
  20. package/dist/launcher.global.js +2 -2
  21. package/dist/launcher.global.js.map +1 -1
  22. package/dist/markdown-parsers.js +53 -0
  23. package/dist/plugin-kit.d.cts +4 -4
  24. package/dist/plugin-kit.d.ts +4 -4
  25. package/dist/runtype-tts.js +1 -0
  26. package/dist/smart-dom-reader.cjs +1 -1
  27. package/dist/smart-dom-reader.d.cts +352 -103
  28. package/dist/smart-dom-reader.d.ts +352 -103
  29. package/dist/smart-dom-reader.js +1 -1
  30. package/dist/theme-editor-preview.cjs +203 -0
  31. package/dist/theme-editor-preview.d.cts +5748 -0
  32. package/dist/theme-editor-preview.d.ts +5748 -0
  33. package/dist/theme-editor-preview.js +203 -0
  34. package/dist/theme-editor.cjs +14 -131
  35. package/dist/theme-editor.d.cts +362 -574
  36. package/dist/theme-editor.d.ts +362 -574
  37. package/dist/theme-editor.js +14 -131
  38. package/dist/theme-reference.cjs +1 -1
  39. package/dist/theme-reference.d.cts +1 -1
  40. package/dist/theme-reference.d.ts +1 -1
  41. package/dist/theme-reference.js +1 -1
  42. package/dist/voice-worklet-player.cjs +80 -0
  43. package/dist/voice-worklet-player.d.cts +215 -0
  44. package/dist/voice-worklet-player.d.ts +215 -0
  45. package/dist/voice-worklet-player.js +80 -0
  46. package/dist/widget.css +54 -36
  47. package/package.json +16 -2
  48. package/src/animations/glyph-cycle.ts +11 -14
  49. package/src/animations/wipe.ts +1 -1
  50. package/src/ask-user-question-tool.test.ts +1 -1
  51. package/src/ask-user-question-tool.ts +6 -8
  52. package/src/client.test.ts +67 -12
  53. package/src/client.ts +40 -32
  54. package/src/codegen.test.ts +1 -1
  55. package/src/codegen.ts +1 -1
  56. package/src/components/approval-bubble.test.ts +1 -1
  57. package/src/components/approval-bubble.ts +3 -3
  58. package/src/components/artifact-pane.ts +3 -3
  59. package/src/components/ask-user-question-bubble.test.ts +14 -14
  60. package/src/components/ask-user-question-bubble.ts +10 -10
  61. package/src/components/composer-builder.ts +17 -16
  62. package/src/components/composer-parts.test.ts +54 -0
  63. package/src/components/composer-parts.ts +95 -126
  64. package/src/components/demo-carousel.ts +1 -1
  65. package/src/components/event-stream-view.test.ts +7 -7
  66. package/src/components/event-stream-view.ts +5 -5
  67. package/src/components/header-builder.ts +33 -27
  68. package/src/components/header-layouts.ts +1 -1
  69. package/src/components/header-parts.test.ts +90 -0
  70. package/src/components/header-parts.ts +62 -93
  71. package/src/components/launcher.test.ts +11 -0
  72. package/src/components/launcher.ts +1 -1
  73. package/src/components/message-bubble.test.ts +74 -2
  74. package/src/components/message-bubble.ts +25 -11
  75. package/src/components/messages.ts +2 -2
  76. package/src/components/panel.test.ts +1 -1
  77. package/src/components/panel.ts +92 -97
  78. package/src/components/pill-composer-builder.ts +28 -24
  79. package/src/components/reasoning-bubble.ts +2 -2
  80. package/src/components/suggestions.ts +1 -1
  81. package/src/components/tool-bubble.ts +1 -1
  82. package/src/components/widget-view.test.ts +181 -0
  83. package/src/components/widget-view.ts +234 -0
  84. package/src/defaults.ts +54 -51
  85. package/src/generated/runtype-openapi-contract.ts +5 -0
  86. package/src/index-core.ts +30 -4
  87. package/src/index-global.ts +73 -6
  88. package/src/index.ts +7 -1
  89. package/src/install.test.ts +11 -11
  90. package/src/install.ts +28 -18
  91. package/src/launcher-global.ts +25 -14
  92. package/src/markdown-parsers-eager.ts +20 -0
  93. package/src/markdown-parsers-entry.ts +4 -0
  94. package/src/markdown-parsers-loader.ts +49 -0
  95. package/src/plugin-kit.test.ts +1 -1
  96. package/src/plugin-kit.ts +7 -7
  97. package/src/plugins/types.ts +5 -5
  98. package/src/postprocessors.ts +27 -14
  99. package/src/runtime/host-layout.test.ts +1 -1
  100. package/src/runtime/host-layout.ts +4 -5
  101. package/src/runtime/init.ts +2 -2
  102. package/src/session.test.ts +5 -6
  103. package/src/session.ts +333 -227
  104. package/src/session.voice.test.ts +146 -0
  105. package/src/session.webmcp.test.ts +24 -25
  106. package/src/smart-dom-reader.ts +3 -4
  107. package/src/styles/widget.css +54 -36
  108. package/src/suggest-replies-tool.test.ts +3 -3
  109. package/src/suggest-replies-tool.ts +5 -5
  110. package/src/testing/index.ts +1 -1
  111. package/src/theme-editor/color-utils.ts +2 -2
  112. package/src/theme-editor/index.ts +4 -13
  113. package/src/theme-editor/presets.ts +1 -1
  114. package/src/theme-editor/preview-utils.ts +22 -20
  115. package/src/theme-editor/preview.ts +4 -1
  116. package/src/theme-editor/role-mappings.ts +3 -3
  117. package/src/theme-editor/sections.ts +21 -20
  118. package/src/theme-editor/types.ts +1 -1
  119. package/src/theme-editor/webmcp/index.ts +1 -1
  120. package/src/theme-editor/webmcp/summary.ts +1 -2
  121. package/src/theme-editor/webmcp/tools.ts +30 -30
  122. package/src/theme-editor/webmcp/types.ts +2 -2
  123. package/src/theme-editor-preview.ts +10 -0
  124. package/src/theme-reference.ts +26 -26
  125. package/src/types.ts +368 -99
  126. package/src/ui.approval-plugin.test.ts +3 -3
  127. package/src/ui.ask-user-question-plugin.test.ts +9 -9
  128. package/src/ui.component-directive.test.ts +4 -4
  129. package/src/ui.composer-bar.test.ts +10 -11
  130. package/src/ui.composer-keyboard.test.ts +5 -5
  131. package/src/ui.header-icon-color.test.ts +59 -0
  132. package/src/ui.scroll.test.ts +5 -6
  133. package/src/ui.stop-button.test.ts +3 -4
  134. package/src/ui.suggest-replies.test.ts +2 -2
  135. package/src/ui.ts +292 -131
  136. package/src/utils/buttons.ts +1 -1
  137. package/src/utils/code-generators.ts +1 -1
  138. package/src/utils/composer-history.ts +3 -3
  139. package/src/utils/dom-context.test.ts +2 -2
  140. package/src/utils/dom-context.ts +7 -7
  141. package/src/utils/dom.test.ts +67 -0
  142. package/src/utils/dom.ts +75 -0
  143. package/src/utils/dropdown.ts +2 -2
  144. package/src/utils/event-stream-buffer.test.ts +2 -2
  145. package/src/utils/message-fingerprint.test.ts +9 -0
  146. package/src/utils/message-fingerprint.ts +5 -0
  147. package/src/utils/morph.ts +1 -1
  148. package/src/utils/sanitize.ts +32 -16
  149. package/src/utils/sequence-buffer.test.ts +8 -8
  150. package/src/utils/sequence-buffer.ts +4 -4
  151. package/src/utils/smart-dom-adapter.test.ts +1 -1
  152. package/src/utils/smart-dom-adapter.ts +4 -5
  153. package/src/utils/speech-text.test.ts +122 -0
  154. package/src/utils/speech-text.ts +101 -0
  155. package/src/utils/stream-animation.test.ts +3 -3
  156. package/src/utils/stream-animation.ts +6 -6
  157. package/src/utils/theme.test.ts +1 -1
  158. package/src/utils/throughput-tracker.test.ts +11 -11
  159. package/src/utils/throughput-tracker.ts +9 -9
  160. package/src/utils/tokens.ts +13 -13
  161. package/src/utils/virtual-scroller.ts +1 -1
  162. package/src/vendor/smart-dom-reader/README.md +4 -4
  163. package/src/vendor/smart-dom-reader/index.d.ts +1 -1
  164. package/src/vendor/smart-dom-reader/index.js +2 -2
  165. package/src/version.ts +1 -1
  166. package/src/voice/audio-playback-manager.test.ts +159 -0
  167. package/src/voice/audio-playback-manager.ts +145 -30
  168. package/src/voice/browser-speech-engine.ts +130 -0
  169. package/src/voice/fallback-speech-engine.ts +89 -0
  170. package/src/voice/index.ts +21 -0
  171. package/src/voice/read-aloud-controller.test.ts +143 -0
  172. package/src/voice/read-aloud-controller.ts +136 -0
  173. package/src/voice/runtype-speech-engine.test.ts +280 -0
  174. package/src/voice/runtype-speech-engine.ts +211 -0
  175. package/src/voice/runtype-tts-entry.ts +10 -0
  176. package/src/voice/runtype-tts-loader.test.ts +47 -0
  177. package/src/voice/runtype-tts-loader.ts +42 -0
  178. package/src/voice/runtype-voice-provider.ts +332 -537
  179. package/src/voice/voice-factory.ts +26 -4
  180. package/src/voice/voice.test.ts +321 -7
  181. package/src/voice/worklet-playback-engine.ts +272 -0
  182. package/src/voice-worklet-player.ts +39 -0
  183. package/src/webmcp-bridge.test.ts +6 -6
  184. package/src/webmcp-bridge.ts +24 -25
  185. package/src/webmcp-polyfill.ts +1 -1
  186. package/src/voice/voice-activity-detector.ts +0 -90
package/src/client.ts CHANGED
@@ -180,7 +180,7 @@ export class AgentWidgetClient {
180
180
  private lastSentClientToolsFingerprint: string | null = null;
181
181
  private clientToolsFingerprintSessionId: string | null = null;
182
182
 
183
- // WebMCP page-discovered tool consumption (see ./webmcp-bridge).
183
+ // WebMCP: page-discovered tool consumption (see ./webmcp-bridge).
184
184
  // Constructed lazily: null when `config.webmcp?.enabled !== true`.
185
185
  private readonly webMcpBridge: WebMcpBridge | null;
186
186
 
@@ -206,8 +206,8 @@ export class AgentWidgetClient {
206
206
  * Refresh config in place WITHOUT tearing down the live connection or the
207
207
  * WebMCP bridge. `AgentWidgetSession.updateConfig` calls this when only
208
208
  * connection-irrelevant fields changed (theme, copy, layout, suggestions, …),
209
- * so a UI update that lands mid-turn e.g. a `webmcp:*` tool restyling the
210
- * widget while the agent's turn is still streaming doesn't abandon the
209
+ * so a UI update that lands mid-turn: e.g. a `webmcp:*` tool restyling the
210
+ * widget while the agent's turn is still streaming: doesn't abandon the
211
211
  * in-flight stream/resume. Connection or request-shaping changes (apiUrl,
212
212
  * clientToken, webmcp, headers, parser, …) take the full client rebuild path
213
213
  * in the session instead, which is the only place the bridge is recreated.
@@ -215,7 +215,7 @@ export class AgentWidgetClient {
215
215
  * Only the live-read `config` is refreshed (e.g. `iterationDisplay`); the
216
216
  * constructor-derived request-shaping fields (apiUrl, headers, parser,
217
217
  * contextProviders, middleware, …) are left untouched because the session
218
- * routes any change to those down the full-rebuild path instead so they are
218
+ * routes any change to those down the full-rebuild path instead, so they are
219
219
  * guaranteed unchanged here. The `webMcpBridge` instance and its
220
220
  * installed-polyfill memo are deliberately preserved, which keeps any
221
221
  * in-flight resolve alive.
@@ -254,7 +254,7 @@ export class AgentWidgetClient {
254
254
  * WebMCP: execute a returned `webmcp:<name>` tool call against the page's
255
255
  * registry and return the normalized MCP-shaped result for `/resume`. The
256
256
  * bridge handles confirm-bubble gating, the 30s timeout, error
257
- * normalization, and `signal`-driven abort callers never see throws.
257
+ * normalization, and `signal`-driven abort: callers never see throws.
258
258
  *
259
259
  * Returns `null` when WebMCP is not enabled on this client (signal to the
260
260
  * session that it should fall back to the legacy local-tool resume path,
@@ -482,12 +482,21 @@ export class AgentWidgetClient {
482
482
  console.debug("[AgentWidgetClient] sending feedback", feedback);
483
483
  }
484
484
 
485
+ // Scope the feedback request to the caller's client token, sourced the same
486
+ // way as the chat/init requests. sendFeedback is client-token-mode only
487
+ // (guarded above), so clientToken is always present here and an API key can
488
+ // never leak into the body. Left undefined only when the embed has none.
489
+ const requestBody = {
490
+ ...feedback,
491
+ ...(this.config.clientToken && { token: this.config.clientToken }),
492
+ };
493
+
485
494
  const response = await fetch(this.getFeedbackApiUrl(), {
486
495
  method: 'POST',
487
496
  headers: {
488
497
  'Content-Type': 'application/json',
489
498
  },
490
- body: JSON.stringify(feedback),
499
+ body: JSON.stringify(requestBody),
491
500
  });
492
501
 
493
502
  if (!response.ok) {
@@ -692,7 +701,7 @@ export class AgentWidgetClient {
692
701
  this.lastSentClientToolsFingerprint = null;
693
702
  continue;
694
703
  }
695
- // Some other 409 keep the parsed body for the handler below.
704
+ // Some other 409: keep the parsed body for the handler below.
696
705
  errorData = body ?? { error: 'Chat request failed' };
697
706
  }
698
707
  break;
@@ -962,10 +971,10 @@ export class AgentWidgetClient {
962
971
  * Routes by mode:
963
972
  * - **client-token mode**: POST `${apiBase}/v1/client/resume` (the
964
973
  * session-authenticated sibling of `/v1/client/chat`; runtypelabs/core#3889),
965
- * with the active `sessionId` in the body and no Bearer key a browser
974
+ * with the active `sessionId` in the body and no Bearer key: a browser
966
975
  * client-token page holds no secret. `clientTools` are already persisted
967
976
  * server-side from the dispatch turn, so only `toolOutputs` is re-sent.
968
- * - **dispatch / proxy mode**: POST `${apiUrl}/resume` Runtype mounts
977
+ * - **dispatch / proxy mode**: POST `${apiUrl}/resume`: Runtype mounts
969
978
  * resume as a child of `/v1/dispatch`, so the URL is `${apiUrl}/resume`,
970
979
  * and proxies follow the same shape (`/api/chat/dispatch/resume`).
971
980
  *
@@ -989,8 +998,8 @@ export class AgentWidgetClient {
989
998
  // The client-token resume route authenticates the session, not a Bearer
990
999
  // key. A WebMCP approval can sit awaiting user input for a long time, so by
991
1000
  // the time we resume the original session may have expired. Re-validate (and
992
- // silently re-init if needed) via initSession() which returns the live
993
- // session when `new Date() < expiresAt`, else mints a fresh one instead of
1001
+ // silently re-init if needed) via initSession(): which returns the live
1002
+ // session when `new Date() < expiresAt`, else mints a fresh one: instead of
994
1003
  // trusting the possibly-stale `this.clientSession`. (core#3889; BugBot
995
1004
  // PR #214 r3367875360.)
996
1005
  let resumeSessionId: string | undefined;
@@ -1060,7 +1069,7 @@ export class AgentWidgetClient {
1060
1069
 
1061
1070
  // Client tools: built-in widget tools (ask_user_question, when exposed)
1062
1071
  // plus the per-turn WebMCP page-registry snapshot. Name collisions are
1063
- // impossible WebMCP entries are `webmcp:`-prefixed server-side while
1072
+ // impossible: WebMCP entries are `webmcp:`-prefixed server-side while
1064
1073
  // `sdk`-origin built-ins keep bare names. Both kinds ride the same
1065
1074
  // diff-only fingerprint path in client-token mode. Kept to a single await
1066
1075
  // so dispatch microtask timing is unchanged.
@@ -1176,7 +1185,7 @@ export class AgentWidgetClient {
1176
1185
  // dropping `clientTools` accidentally; the WebMCP wire surface
1177
1186
  // is invisible to them. The integrator can still set
1178
1187
  // `clientTools: []` or `clientTools: undefined` explicitly to
1179
- // strip them on purpose we only fall back when the field is
1188
+ // strip them on purpose: we only fall back when the field is
1180
1189
  // entirely absent from the returned object.
1181
1190
  if (
1182
1191
  payload.clientTools !== undefined &&
@@ -1249,7 +1258,7 @@ export class AgentWidgetClient {
1249
1258
  createNewAssistant(result.partId);
1250
1259
  }
1251
1260
 
1252
- // Update partId tracking (only when partId is provided backward compatible)
1261
+ // Update partId tracking (only when partId is provided: backward compatible)
1253
1262
  if (result.partId !== undefined) {
1254
1263
  partIdState.current = result.partId;
1255
1264
  }
@@ -1372,7 +1381,7 @@ export class AgentWidgetClient {
1372
1381
  // Messages produced by steps inside a nested flow executed as a tool.
1373
1382
  // Keyed by `${parentToolId}::${nestedStepId}::${partId}` so each nested
1374
1383
  // step (send-stream, prompt) gets its own assistant message, and prompts
1375
- // with inner tool calls split into one message per text segment still
1384
+ // with inner tool calls split into one message per text segment: still
1376
1385
  // attributable to the parent tool call.
1377
1386
  const nestedStepMessages = new Map<string, AgentWidgetMessage>();
1378
1387
  // Most-recent partId seen for a given `${toolId}::${stepId}` scope, used
@@ -1397,7 +1406,7 @@ export class AgentWidgetClient {
1397
1406
  ) => `${toolId}::${stepId}::${partId}`;
1398
1407
 
1399
1408
  // Prefix used to sweep every nested message belonging to a single
1400
- // (toolId, stepId) scope needed on step_complete to seal any segments
1409
+ // (toolId, stepId) scope: needed on step_complete to seal any segments
1401
1410
  // that are still streaming.
1402
1411
  const getNestedStepPrefix = (toolId: string, stepId: string) =>
1403
1412
  `${toolId}::${stepId}::`;
@@ -1719,7 +1728,7 @@ export class AgentWidgetClient {
1719
1728
  const cur = msg.content ?? "";
1720
1729
  if (mergedDisplay.trim() === "") return;
1721
1730
  // Only replace when empty, or when the stream left a strict prefix of the
1722
- // authoritative final (truncation). Do not use length alone multi-segment
1731
+ // authoritative final (truncation). Do not use length alone: multi-segment
1723
1732
  // flows can have a short last bubble whose content is not a prefix of the
1724
1733
  // full step response.
1725
1734
  if (
@@ -1801,7 +1810,7 @@ export class AgentWidgetClient {
1801
1810
  // Declared here so scheduleReadyQueueDrain can reference it; assigned
1802
1811
  // after all handler-scoped variables are initialised (before the SSE loop).
1803
1812
  let drainReadyQueue: () => void;
1804
- // Two drain paths both are intentional, do not remove either:
1813
+ // Two drain paths: both are intentional, do not remove either:
1805
1814
  // 1. Microtask drain (scheduleReadyQueueDrain): required when the
1806
1815
  // buffer's emitter fires from the gap-timeout setTimeout callback,
1807
1816
  // because there is no surrounding synchronous drain site there.
@@ -1922,7 +1931,7 @@ export class AgentWidgetClient {
1922
1931
  const toolId =
1923
1932
  resolveToolId(payload, true) ?? `tool-${nextSequence()}`;
1924
1933
  const toolName = payload.toolName ?? payload.name;
1925
- // Suppress tool UI for artifact emit tools artifacts are handled via artifact_* events
1934
+ // Suppress tool UI for artifact emit tools: artifacts are handled via artifact_* events
1926
1935
  if (isArtifactEmitToolName(toolName)) {
1927
1936
  artifactToolCallIds.add(toolId);
1928
1937
  continue;
@@ -2047,7 +2056,7 @@ export class AgentWidgetClient {
2047
2056
  // Key the message by the per-call `toolCallId` (provider `toolu_…`;
2048
2057
  // core#3878) when present. Two PARALLEL calls to the SAME tool in one
2049
2058
  // turn collapse to an identical `toolId` (`runtime_webmcp:<name>_<ms>`)
2050
- // and `index: 0` only `toolCallId` distinguishes them. Keying on it
2059
+ // and `index: 0`: only `toolCallId` distinguishes them. Keying on it
2051
2060
  // (a) keeps the two awaits as DISTINCT messages with their own args
2052
2061
  // instead of the second clobbering the first, and (b) merges each
2053
2062
  // await into the matching `tool_start` bubble (also keyed by
@@ -2096,7 +2105,7 @@ export class AgentWidgetClient {
2096
2105
  emitMessage(toolMessage);
2097
2106
  } else if (payloadType === "text_start") {
2098
2107
  // Lifecycle event: a new text segment is beginning (emitted at tool boundaries).
2099
- // When toolContext is present this fired inside a nested flow it must not
2108
+ // When toolContext is present this fired inside a nested flow: it must not
2100
2109
  // seal or rotate the outer assistant message. Nested prompt segmentation is
2101
2110
  // handled via nestedStepMessages keyed by (toolId, stepId).
2102
2111
  if ((payload as any).toolContext?.toolId) {
@@ -2118,7 +2127,7 @@ export class AgentWidgetClient {
2118
2127
  }
2119
2128
  } else if (payloadType === "text_end") {
2120
2129
  // Lifecycle event: current text segment ended (tool call about to start).
2121
- // When toolContext is present the boundary belongs to a nested flow leave
2130
+ // When toolContext is present the boundary belongs to a nested flow: leave
2122
2131
  // outer assistant state alone so the outer stream is never interrupted by
2123
2132
  // nested activity.
2124
2133
  if ((payload as any).toolContext?.toolId) {
@@ -2146,7 +2155,7 @@ export class AgentWidgetClient {
2146
2155
  // originated inside a nested flow executed as a tool. Surface it as
2147
2156
  // its own assistant message keyed by the nested step id, so authors
2148
2157
  // who add send-stream / prompt steps inside their flow see them as
2149
- // real messages in the timeline, in order rather than merging
2158
+ // real messages in the timeline, in order: rather than merging
2150
2159
  // into the outer assistant bubble or getting buried in the tool
2151
2160
  // card. Each nested step id gets its own message; the parent tool
2152
2161
  // bubble continues to represent the invocation via tool_* events.
@@ -2295,7 +2304,7 @@ export class AgentWidgetClient {
2295
2304
  const text = typeof result === 'string' ? result : result?.text ?? null;
2296
2305
 
2297
2306
  if (text !== null && text.trim() !== "") {
2298
- // Parser successfully extracted text update the chunk's assistant
2307
+ // Parser successfully extracted text: update the chunk's assistant
2299
2308
  // (not assistantMessage; text_end may have cleared that ref before microtasks run)
2300
2309
  assistant.content = text;
2301
2310
  emitMessage(assistant);
@@ -2469,14 +2478,14 @@ export class AgentWidgetClient {
2469
2478
 
2470
2479
  // Capture optional per-step stopReason emitted by the runtime
2471
2480
  // (e.g. `'max_tool_calls'`, `'length'`). This is the dispatch-mode
2472
- // fallback `agent_turn_complete` will overwrite it later in
2481
+ // fallback: `agent_turn_complete` will overwrite it later in
2473
2482
  // agent-loop streams.
2474
2483
  const stepStopReason = (payload as any).stopReason as
2475
2484
  | StopReasonKind
2476
2485
  | undefined;
2477
2486
 
2478
2487
  if (didSplitByPartId) {
2479
- // Sealed segment(s) do not create a second bubble from step_complete.
2488
+ // Sealed segment(s): do not create a second bubble from step_complete.
2480
2489
  // Merge authoritative final response into the last sealed segment (fixes async lag).
2481
2490
  if (assistantMessage !== null) {
2482
2491
  const msg: AgentWidgetMessage = assistantMessage;
@@ -2626,7 +2635,7 @@ export class AgentWidgetClient {
2626
2635
  } else if (payloadType === "flow_complete") {
2627
2636
  const finalContent = payload.result?.response;
2628
2637
  if (didSplitByPartId) {
2629
- // Content was split into multiple assistant messages the full response
2638
+ // Content was split into multiple assistant messages: the full response
2630
2639
  // in flow_complete would overwrite the last segment. Just finalize streaming.
2631
2640
  if (assistantMessage !== null) {
2632
2641
  const msg: AgentWidgetMessage = assistantMessage;
@@ -2799,8 +2808,7 @@ export class AgentWidgetClient {
2799
2808
  }
2800
2809
 
2801
2810
  // Attach the turn-level stopReason to the assistant message
2802
- // produced by this turn. Only overwrite the current message
2803
- // prior turns already sealed their own stopReason via step_complete.
2811
+ // produced by this turn. Only overwrite the current message: // prior turns already sealed their own stopReason via step_complete.
2804
2812
  // Falls back to lastAssistantInTurn when the current bubble was
2805
2813
  // sealed at a tool-call boundary mid-turn, so the notice still
2806
2814
  // attaches to the final visible text segment.
@@ -2886,7 +2894,7 @@ export class AgentWidgetClient {
2886
2894
  } else if (payloadType === "agent_media") {
2887
2895
  // A tool produced media (image / audio / video / file). Render it
2888
2896
  // as a synthetic assistant message inserted at the point the tool
2889
- // completed between the tool bubble and the next text turn.
2897
+ // completed: between the tool bubble and the next text turn.
2890
2898
  //
2891
2899
  // Wire format is the AI SDK–aligned `MediaContentPart` from
2892
2900
  // @runtypelabs/shared:
@@ -2913,7 +2921,7 @@ export class AgentWidgetClient {
2913
2921
  const data = typeof rec.data === "string" ? rec.data : undefined;
2914
2922
  if (!data) continue;
2915
2923
  // Empty/missing mediaType yields `data:;base64,...` which RFC 2397
2916
- // resolves to `text/plain` stamp a default so the data URI is
2924
+ // resolves to `text/plain`: stamp a default so the data URI is
2917
2925
  // well-formed and the part lands in the file bucket.
2918
2926
  mediaType = rawMediaType.length > 0 ? rawMediaType : "application/octet-stream";
2919
2927
  src = `data:${mediaType};base64,${data}`;
@@ -2990,7 +2998,7 @@ export class AgentWidgetClient {
2990
2998
 
2991
2999
  // Seal any in-flight assistant text bubble before splitting the
2992
3000
  // stream. Without this, an orphan bubble retains `streaming: true`
2993
- // forever `agent_complete` only finalizes the latest
3001
+ // forever: `agent_complete` only finalizes the latest
2994
3002
  // `assistantMessage`, so the typing/caret indicator would stay on
2995
3003
  // the prior bubble even though no more deltas will arrive.
2996
3004
  const prevAssistant = assistantMessage as AgentWidgetMessage | null;
@@ -5,7 +5,7 @@ import { VERSION } from "./version";
5
5
 
6
6
  // The `@runtypelabs/persona/codegen` subpath is the server/Worker-safe entry for
7
7
  // snippet generation. It must expose the exact same generator as the internal
8
- // module (and, transitively, the main barrel) no fork, no drift.
8
+ // module (and, transitively, the main barrel): no fork, no drift.
9
9
  describe("codegen subpath", () => {
10
10
  const config = {
11
11
  apiUrl: "https://api.example.com/chat",
package/src/codegen.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Pure code-snippet generation entry (`@runtypelabs/persona/codegen`).
3
3
  *
4
- * `generateCodeSnippet` is pure string-templating it builds install snippets
4
+ * `generateCodeSnippet` is pure string-templating: it builds install snippets
5
5
  * from a widget config and depends only on a type import + the `VERSION`
6
6
  * constant. The main `index.ts` barrel re-exports it too, but that barrel pulls
7
7
  * in the full widget runtime (`index-core` → idiomorph / marked / DOM), which is
@@ -280,7 +280,7 @@ describe("createApprovalBubble agent reason", () => {
280
280
  expect(reasonLine.textContent).toContain(
281
281
  "Adds the <b>large</b> blue tee to your cart as you asked."
282
282
  );
283
- // Plain text rendering markup in the agent-authored string must not
283
+ // Plain text rendering: markup in the agent-authored string must not
284
284
  // become elements (prompt-injection surface).
285
285
  expect(reasonLine.querySelector("b")).toBeNull();
286
286
  });
@@ -173,7 +173,7 @@ export const createApprovalBubble = (
173
173
  bubble.id = `bubble-${message.id}`;
174
174
  bubble.setAttribute("data-message-id", message.id);
175
175
 
176
- // Apply styling use semantic tokens with config overrides
176
+ // Apply styling: use semantic tokens with config overrides
177
177
  bubble.style.backgroundColor = approvalConfig?.backgroundColor ?? "var(--persona-approval-bg, #fefce8)";
178
178
  bubble.style.borderColor = approvalConfig?.borderColor ?? "var(--persona-approval-border, #fef08a)";
179
179
  bubble.style.boxShadow =
@@ -242,7 +242,7 @@ export const createApprovalBubble = (
242
242
 
243
243
  // User-facing summary line. The wire `description` is the tool's
244
244
  // agent-facing description (prompt prose, usage rules), so it is not shown
245
- // here it lives in the collapsible details section below. Label priority:
245
+ // here: it lives in the collapsible details section below. Label priority:
246
246
  // formatDescription → declared WebMCP `title` → humanized tool name →
247
247
  // raw description (no tool name at all).
248
248
  const isWebMcpTool =
@@ -276,7 +276,7 @@ export const createApprovalBubble = (
276
276
 
277
277
  // Agent-authored justification for this specific call. It is the agent's
278
278
  // own claim about its intent (attacker-writable under prompt injection), so
279
- // it is rendered as plain text via textContent never markdown/HTML and
279
+ // it is rendered as plain text via textContent, never markdown/HTML, and
280
280
  // explicitly attributed to the agent rather than spoken in system voice.
281
281
  if (approval.reason) {
282
282
  const reasonLine = createElement("p", "persona-text-sm persona-mt-1 persona-text-persona-muted");
@@ -36,7 +36,7 @@ export function createArtifactPane(
36
36
  config: AgentWidgetConfig,
37
37
  options: {
38
38
  onSelect: (id: string) => void;
39
- /** User closed the pane (mobile drawer or split sidebar) parent should persist “hidden until reopened”. */
39
+ /** User closed the pane (mobile drawer or split sidebar): parent should persist “hidden until reopened”. */
40
40
  onDismiss?: () => void;
41
41
  }
42
42
  ): ArtifactPaneApi {
@@ -203,7 +203,7 @@ export function createArtifactPane(
203
203
  });
204
204
 
205
205
  if (copyMenuChevronBtn && copyMenuItems?.length) {
206
- // Resolve the portal target widget root for CSS var inheritance, escaping overflow: hidden
206
+ // Resolve the portal target: widget root for CSS var inheritance, escaping overflow: hidden
207
207
  const resolvePortal = (): HTMLElement => shell.closest("[data-persona-root]") as HTMLElement ?? document.body;
208
208
 
209
209
  const initDropdown = () => {
@@ -414,7 +414,7 @@ export function createArtifactPane(
414
414
  backdrop.classList.add("persona-hidden");
415
415
  shell.classList.remove("persona-artifact-drawer-open");
416
416
  } else {
417
- // isMobile && !(has && mobileOpen): e.g. dismissed drawer keep closed chrome in sync
417
+ // isMobile && !(has && mobileOpen): e.g. dismissed drawer: keep closed chrome in sync
418
418
  backdrop.classList.add("persona-hidden");
419
419
  shell.classList.remove("persona-artifact-drawer-open");
420
420
  }
@@ -103,11 +103,11 @@ describe("ensureAskUserQuestionSheet", () => {
103
103
  expect(sheet).not.toBeNull();
104
104
  const pills = sheet!.querySelectorAll('[data-ask-user-action="pick"]');
105
105
  expect(pills.length).toBe(3);
106
- // Default rows layout label is in `.persona-ask-row-label`, alongside
106
+ // Default rows layout: label is in `.persona-ask-row-label`, alongside
107
107
  // a number-badge affordance. Read the dedicated label slot.
108
108
  const firstLabel = pills[0].querySelector(".persona-ask-row-label");
109
109
  expect(firstLabel?.textContent).toBe("Hobbyists");
110
- // Free-text affordance present by default rows mode embeds the input
110
+ // Free-text affordance present by default: rows mode embeds the input
111
111
  // inside the Other row via `focus-free-text` (digit shortcut + click chrome).
112
112
  const custom = sheet!.querySelector('[data-ask-user-action="focus-free-text"]');
113
113
  expect(custom).not.toBeNull();
@@ -144,7 +144,7 @@ describe("ensureAskUserQuestionSheet", () => {
144
144
  });
145
145
  ensureAskUserQuestionSheet(msg, {} as AgentWidgetConfig, overlay);
146
146
 
147
- // Simulate more chunks arriving options now parseable
147
+ // Simulate more chunks arriving: options now parseable
148
148
  msg.toolCall!.chunks = ['{"questions":[{"question":"X","options":[{"label":"A"},{"label":"B"}'];
149
149
  ensureAskUserQuestionSheet(msg, {} as AgentWidgetConfig, overlay);
150
150
 
@@ -155,7 +155,7 @@ describe("ensureAskUserQuestionSheet", () => {
155
155
  expect(pills[1].querySelector(".persona-ask-row-label")?.textContent).toBe("B");
156
156
  });
157
157
 
158
- it("is idempotent re-invoking does not duplicate sheets", () => {
158
+ it("is idempotent: re-invoking does not duplicate sheets", () => {
159
159
  const overlay = makeOverlay();
160
160
  ensureAskUserQuestionSheet(makeMessage(), {} as AgentWidgetConfig, overlay);
161
161
  ensureAskUserQuestionSheet(makeMessage(), {} as AgentWidgetConfig, overlay);
@@ -163,7 +163,7 @@ describe("ensureAskUserQuestionSheet", () => {
163
163
  expect(sheets.length).toBe(1);
164
164
  });
165
165
 
166
- it("respects enabled: false sheet is not mounted", () => {
166
+ it("respects enabled: false: sheet is not mounted", () => {
167
167
  const overlay = makeOverlay();
168
168
  ensureAskUserQuestionSheet(
169
169
  makeMessage(),
@@ -192,7 +192,7 @@ describe("ensureAskUserQuestionSheet", () => {
192
192
  expect(sheet.getAttribute("data-multi-select")).toBe("true");
193
193
  });
194
194
 
195
- it("does not render a dismiss button Skip in the nav row is the canonical escape", () => {
195
+ it("does not render a dismiss button: Skip in the nav row is the canonical escape", () => {
196
196
  const overlay = makeOverlay();
197
197
  ensureAskUserQuestionSheet(makeMessage(), {} as AgentWidgetConfig, overlay);
198
198
  const sheet = overlay.querySelector('[data-persona-ask-sheet-for="tool-1"]')!;
@@ -221,7 +221,7 @@ describe("removeAskUserQuestionSheet", () => {
221
221
  ensureAskUserQuestionSheet(makeMessage(), {} as AgentWidgetConfig, overlay);
222
222
  expect(overlay.querySelector('[data-persona-ask-sheet-for="tool-1"]')).not.toBeNull();
223
223
  removeAskUserQuestionSheet(overlay, "tool-1");
224
- // The implementation defers removal with setTimeout flush it.
224
+ // The implementation defers removal with setTimeout: flush it.
225
225
  await new Promise((resolve) => setTimeout(resolve, 250));
226
226
  expect(overlay.querySelector('[data-persona-ask-sheet-for="tool-1"]')).toBeNull();
227
227
  });
@@ -249,7 +249,7 @@ const makeGroupedMessage = (
249
249
  const sheetFor = (overlay: Element, toolCallId: string): HTMLElement =>
250
250
  overlay.querySelector<HTMLElement>(`[data-persona-ask-sheet-for="${toolCallId}"]`)!;
251
251
 
252
- describe("grouped questions stepper UI", () => {
252
+ describe("grouped questions: stepper UI", () => {
253
253
  it("renders 'Question 1 of N' chip and Back/Next nav row when N > 1", () => {
254
254
  const overlay = makeOverlay();
255
255
  const msg = makeGroupedMessage([
@@ -384,7 +384,7 @@ describe("grouped questions — stepper UI", () => {
384
384
  });
385
385
  });
386
386
 
387
- it("hydrates from agentMetadata restores index and prior answers on a fresh mount", () => {
387
+ it("hydrates from agentMetadata: restores index and prior answers on a fresh mount", () => {
388
388
  const overlay = makeOverlay();
389
389
  const msg = makeGroupedMessage(
390
390
  [
@@ -464,7 +464,7 @@ describe("rows layout (default)", () => {
464
464
  expect(firstRow.querySelector(".persona-ask-row-description")?.textContent).toBe(
465
465
  "Real Discord invite to link"
466
466
  );
467
- // No title-attr fallback in rows layout description is inline.
467
+ // No title-attr fallback in rows layout: description is inline.
468
468
  expect(firstRow.getAttribute("title")).toBeNull();
469
469
  });
470
470
 
@@ -507,11 +507,11 @@ describe("rows layout (default)", () => {
507
507
  expect(rows[0].querySelector(".persona-ask-row-badge")).toBeNull();
508
508
  });
509
509
 
510
- it("embeds the free-text input INSIDE the Other row (rows mode) no separate row, no Send button", () => {
510
+ it("embeds the free-text input INSIDE the Other row (rows mode): no separate row, no Send button", () => {
511
511
  const overlay = makeOverlay();
512
512
  ensureAskUserQuestionSheet(makeMessage(), {} as AgentWidgetConfig, overlay);
513
513
  const sheet = overlay.querySelector('[data-persona-ask-sheet-for="tool-1"]')!;
514
- // No standalone free-text row in rows mode input lives inside the Other row.
514
+ // No standalone free-text row in rows mode: input lives inside the Other row.
515
515
  expect(sheet.querySelector('[data-ask-free-text-row="true"]')).toBeNull();
516
516
  const otherRow = sheet.querySelector<HTMLElement>('[data-ask-other-row="true"]')!;
517
517
  expect(otherRow).not.toBeNull();
@@ -520,12 +520,12 @@ describe("rows layout (default)", () => {
520
520
  expect(otherRow.querySelector('[data-ask-user-action="submit-free-text"]')).toBeNull();
521
521
  // Other row carries the focus-free-text action (digit shortcut + click chrome).
522
522
  expect(otherRow.getAttribute("data-ask-user-action")).toBe("focus-free-text");
523
- // Number badge for the Other row (N+1 3 real options + Other = 4).
523
+ // Number badge for the Other row (N+1: 3 real options + Other = 4).
524
524
  const badge = otherRow.querySelector<HTMLElement>(".persona-ask-row-badge");
525
525
  expect(badge?.textContent).toBe("4");
526
526
  });
527
527
 
528
- it("respects layout: 'pills' opt-out no row affordances, free-text starts hidden, Send button still rendered", () => {
528
+ it("respects layout: 'pills' opt-out: no row affordances, free-text starts hidden, Send button still rendered", () => {
529
529
  const overlay = makeOverlay();
530
530
  const config = {
531
531
  features: { askUserQuestion: { layout: "pills" } },
@@ -60,7 +60,7 @@ const resolveFeature = (config?: AgentWidgetConfig): AgentWidgetAskUserQuestionF
60
60
 
61
61
  /**
62
62
  * Parse an `ask_user_question` tool-variant message into a partial payload.
63
- * Safe to call mid-stream will walk the tool call's `chunks` via
63
+ * Safe to call mid-stream: will walk the tool call's `chunks` via
64
64
  * `partial-json` and return `{ payload: null, complete: false }` when there
65
65
  * isn't enough data yet. `complete` flips to `true` once the tool call
66
66
  * reports status `"complete"`.
@@ -570,7 +570,7 @@ const syncNavState = (sheet: HTMLElement): void => {
570
570
  );
571
571
  if (next) next.disabled = !hasAnswer;
572
572
 
573
- // Multi-select submit (1-question mode) keep existing behavior.
573
+ // Multi-select submit (1-question mode): keep existing behavior.
574
574
  const multi = sheet.querySelector<HTMLButtonElement>('[data-ask-user-action="submit-multi"]');
575
575
  if (multi) {
576
576
  const labels = Array.from(
@@ -625,7 +625,7 @@ const renderCurrentPage = (
625
625
  pillList.replaceWith(fresh);
626
626
  }
627
627
 
628
- // Free-text row re-build to clear stale input value across pages.
628
+ // Free-text row: re-build to clear stale input value across pages.
629
629
  // Only present in pills (legacy) mode; in rows mode the input lives inside
630
630
  // the Other row of the pill list, which is rebuilt above.
631
631
  if (layout !== "rows") {
@@ -633,7 +633,7 @@ const renderCurrentPage = (
633
633
  if (oldFree) oldFree.replaceWith(buildFreeTextRow(feature, layout));
634
634
  }
635
635
 
636
- // Multi-select action row only relevant in 1-question mode.
636
+ // Multi-select action row: only relevant in 1-question mode.
637
637
  const oldMulti = sheet.querySelector<HTMLElement>('[data-ask-multi-actions="true"]');
638
638
  if (!grouped && multiSelect && !oldMulti) {
639
639
  sheet.appendChild(buildMultiSelectActions(feature));
@@ -698,7 +698,7 @@ const buildSheet = (
698
698
 
699
699
  // Header: question text (flex-1) + compact "N/M" stepper indicator on the
700
700
  // right (grouped only). Skip in the nav row is the canonical escape hatch
701
- // plugins that want a different escape model render their own UX.
701
+ //: plugins that want a different escape model render their own UX.
702
702
  const header = createElement(
703
703
  "div",
704
704
  "persona-ask-sheet-header persona-flex persona-items-center persona-gap-3"
@@ -721,7 +721,7 @@ const buildSheet = (
721
721
 
722
722
  sheet.appendChild(header);
723
723
 
724
- // Skeleton placeholders these get replaced wholesale by renderCurrentPage.
724
+ // Skeleton placeholders: these get replaced wholesale by renderCurrentPage.
725
725
  const skeletonClass =
726
726
  layout === "rows"
727
727
  ? "persona-ask-pills persona-ask-pills--rows persona-flex persona-flex-col persona-gap-2"
@@ -768,7 +768,7 @@ const syncSheetFromMessage = (
768
768
 
769
769
  /**
770
770
  * Create the small in-transcript stub for an `ask_user_question` tool call.
771
- * The stub is passive the interactive sheet is mounted separately into
771
+ * The stub is passive: the interactive sheet is mounted separately into
772
772
  * the composer overlay via `ensureAskUserQuestionSheet`.
773
773
  */
774
774
  export const createAskUserQuestionBubble = (
@@ -796,7 +796,7 @@ export const createAskUserQuestionBubble = (
796
796
 
797
797
  /**
798
798
  * Mount or update the interactive answer-pill sheet for a given message.
799
- * Idempotent if a sheet already exists for the tool-call id, it is hydrated
799
+ * Idempotent: if a sheet already exists for the tool-call id, it is hydrated
800
800
  * in-place instead of remounted, so streaming updates don't flicker.
801
801
  */
802
802
  export const ensureAskUserQuestionSheet = (
@@ -812,7 +812,7 @@ export const ensureAskUserQuestionSheet = (
812
812
 
813
813
  const toolCallId = message.toolCall!.id;
814
814
 
815
- // Only keep the latest sheet in the overlay clear any stale siblings.
815
+ // Only keep the latest sheet in the overlay: clear any stale siblings.
816
816
  const siblings = overlay.querySelectorAll<HTMLElement>(`[${SHEET_SENTINEL}]`);
817
817
  siblings.forEach((el) => {
818
818
  if (el.getAttribute(SHEET_SENTINEL) !== toolCallId) {
@@ -916,7 +916,7 @@ export const refreshNavState = (sheet: HTMLElement): void => {
916
916
  };
917
917
 
918
918
  /**
919
- * Test seam reset the one-shot truncation warning so each test can assert
919
+ * Test seam: reset the one-shot truncation warning so each test can assert
920
920
  * the warn fires exactly once.
921
921
  */
922
922
  export const __resetTruncateWarn = (): void => {
@@ -1,4 +1,4 @@
1
- import { createElement } from "../utils/dom";
1
+ import { createElement, createNode } from "../utils/dom";
2
2
  import { AgentWidgetConfig, ContentPart } from "../types";
3
3
  import {
4
4
  createAttachmentControls,
@@ -57,20 +57,20 @@ export interface ComposerBuildContext {
57
57
  export const buildComposer = (context: ComposerBuildContext): ComposerElements => {
58
58
  const { config } = context;
59
59
 
60
- const footer = createElement(
61
- "div",
62
- "persona-widget-footer persona-border-t-persona-divider persona-bg-persona-surface persona-px-6 persona-py-4"
63
- );
64
- footer.setAttribute("data-persona-theme-zone", "composer");
60
+ const footer = createNode("div", {
61
+ className:
62
+ "persona-widget-footer persona-border-t-persona-divider persona-bg-persona-surface persona-px-6 persona-py-4",
63
+ attrs: { "data-persona-theme-zone": "composer" },
64
+ });
65
65
 
66
66
  const suggestions = createSuggestionsRow();
67
67
 
68
- const composerForm = createElement(
69
- "form",
70
- "persona-widget-composer persona-flex persona-flex-col persona-gap-2 persona-rounded-2xl persona-border persona-border-gray-200 persona-bg-persona-input-background persona-px-4 persona-py-3"
71
- ) as HTMLFormElement;
72
- composerForm.setAttribute("data-persona-composer-form", "");
73
- composerForm.style.outline = "none";
68
+ const composerForm = createNode("form", {
69
+ className:
70
+ "persona-widget-composer persona-flex persona-flex-col persona-gap-2 persona-rounded-2xl persona-border persona-border-gray-200 persona-bg-persona-input-background persona-px-4 persona-py-3",
71
+ attrs: { "data-persona-composer-form": "" },
72
+ style: { outline: "none" },
73
+ });
74
74
 
75
75
  const { textarea, attachAutoResize } = createComposerTextarea(config);
76
76
  attachAutoResize();
@@ -93,10 +93,11 @@ export const buildComposer = (context: ComposerBuildContext): ComposerElements =
93
93
  // The bare class names (persona-widget-composer__actions / __left-actions /
94
94
  // __right-actions) are stable CSS hooks. The pill composer reuses
95
95
  // __left-actions / __right-actions as semantic markers in its grid.
96
- const actionsRow = createElement(
97
- "div",
98
- "persona-widget-composer__actions persona-flex persona-items-center persona-justify-between persona-w-full"
99
- );
96
+ const actionsRow = createNode("div", {
97
+ className:
98
+ "persona-widget-composer__actions persona-flex persona-items-center persona-justify-between persona-w-full",
99
+ attrs: { "data-persona-composer-actions": "" },
100
+ });
100
101
  const leftActions = createElement(
101
102
  "div",
102
103
  "persona-widget-composer__left-actions persona-flex persona-items-center persona-gap-2"