@runtypelabs/persona 3.34.1 → 3.36.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 +56 -12
  53. package/src/client.ts +35 -31
  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
@@ -33,6 +33,7 @@ import {
33
33
  createXmlParser
34
34
  } from "./utils/formatting";
35
35
  import { SequenceReorderBuffer } from "./utils/sequence-buffer";
36
+ import { VERSION } from "./version";
36
37
  // artifactsSidebarEnabled is used in ui.ts to gate the sidebar pane rendering;
37
38
  // artifact events are always processed here regardless of config.
38
39
 
@@ -180,7 +181,7 @@ export class AgentWidgetClient {
180
181
  private lastSentClientToolsFingerprint: string | null = null;
181
182
  private clientToolsFingerprintSessionId: string | null = null;
182
183
 
183
- // WebMCP page-discovered tool consumption (see ./webmcp-bridge).
184
+ // WebMCP: page-discovered tool consumption (see ./webmcp-bridge).
184
185
  // Constructed lazily: null when `config.webmcp?.enabled !== true`.
185
186
  private readonly webMcpBridge: WebMcpBridge | null;
186
187
 
@@ -188,6 +189,7 @@ export class AgentWidgetClient {
188
189
  this.apiUrl = config.apiUrl ?? DEFAULT_ENDPOINT;
189
190
  this.headers = {
190
191
  "Content-Type": "application/json",
192
+ "X-Persona-Version": VERSION,
191
193
  ...config.headers
192
194
  };
193
195
  this.debug = Boolean(config.debug);
@@ -206,8 +208,8 @@ export class AgentWidgetClient {
206
208
  * Refresh config in place WITHOUT tearing down the live connection or the
207
209
  * WebMCP bridge. `AgentWidgetSession.updateConfig` calls this when only
208
210
  * 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
211
+ * so a UI update that lands mid-turn: e.g. a `webmcp:*` tool restyling the
212
+ * widget while the agent's turn is still streaming: doesn't abandon the
211
213
  * in-flight stream/resume. Connection or request-shaping changes (apiUrl,
212
214
  * clientToken, webmcp, headers, parser, …) take the full client rebuild path
213
215
  * in the session instead, which is the only place the bridge is recreated.
@@ -215,7 +217,7 @@ export class AgentWidgetClient {
215
217
  * Only the live-read `config` is refreshed (e.g. `iterationDisplay`); the
216
218
  * constructor-derived request-shaping fields (apiUrl, headers, parser,
217
219
  * contextProviders, middleware, …) are left untouched because the session
218
- * routes any change to those down the full-rebuild path instead so they are
220
+ * routes any change to those down the full-rebuild path instead, so they are
219
221
  * guaranteed unchanged here. The `webMcpBridge` instance and its
220
222
  * installed-polyfill memo are deliberately preserved, which keeps any
221
223
  * in-flight resolve alive.
@@ -254,7 +256,7 @@ export class AgentWidgetClient {
254
256
  * WebMCP: execute a returned `webmcp:<name>` tool call against the page's
255
257
  * registry and return the normalized MCP-shaped result for `/resume`. The
256
258
  * bridge handles confirm-bubble gating, the 30s timeout, error
257
- * normalization, and `signal`-driven abort callers never see throws.
259
+ * normalization, and `signal`-driven abort: callers never see throws.
258
260
  *
259
261
  * Returns `null` when WebMCP is not enabled on this client (signal to the
260
262
  * session that it should fall back to the legacy local-tool resume path,
@@ -355,6 +357,7 @@ export class AgentWidgetClient {
355
357
  method: 'POST',
356
358
  headers: {
357
359
  'Content-Type': 'application/json',
360
+ 'X-Persona-Version': VERSION,
358
361
  },
359
362
  body: JSON.stringify(requestBody),
360
363
  });
@@ -495,6 +498,7 @@ export class AgentWidgetClient {
495
498
  method: 'POST',
496
499
  headers: {
497
500
  'Content-Type': 'application/json',
501
+ 'X-Persona-Version': VERSION,
498
502
  },
499
503
  body: JSON.stringify(requestBody),
500
504
  });
@@ -682,6 +686,7 @@ export class AgentWidgetClient {
682
686
  method: 'POST',
683
687
  headers: {
684
688
  'Content-Type': 'application/json',
689
+ 'X-Persona-Version': VERSION,
685
690
  },
686
691
  body: JSON.stringify(chatRequest),
687
692
  signal: controller.signal,
@@ -701,7 +706,7 @@ export class AgentWidgetClient {
701
706
  this.lastSentClientToolsFingerprint = null;
702
707
  continue;
703
708
  }
704
- // Some other 409 keep the parsed body for the handler below.
709
+ // Some other 409: keep the parsed body for the handler below.
705
710
  errorData = body ?? { error: 'Chat request failed' };
706
711
  }
707
712
  break;
@@ -971,10 +976,10 @@ export class AgentWidgetClient {
971
976
  * Routes by mode:
972
977
  * - **client-token mode**: POST `${apiBase}/v1/client/resume` (the
973
978
  * session-authenticated sibling of `/v1/client/chat`; runtypelabs/core#3889),
974
- * with the active `sessionId` in the body and no Bearer key a browser
979
+ * with the active `sessionId` in the body and no Bearer key: a browser
975
980
  * client-token page holds no secret. `clientTools` are already persisted
976
981
  * server-side from the dispatch turn, so only `toolOutputs` is re-sent.
977
- * - **dispatch / proxy mode**: POST `${apiUrl}/resume` Runtype mounts
982
+ * - **dispatch / proxy mode**: POST `${apiUrl}/resume`: Runtype mounts
978
983
  * resume as a child of `/v1/dispatch`, so the URL is `${apiUrl}/resume`,
979
984
  * and proxies follow the same shape (`/api/chat/dispatch/resume`).
980
985
  *
@@ -998,8 +1003,8 @@ export class AgentWidgetClient {
998
1003
  // The client-token resume route authenticates the session, not a Bearer
999
1004
  // key. A WebMCP approval can sit awaiting user input for a long time, so by
1000
1005
  // the time we resume the original session may have expired. Re-validate (and
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
1006
+ // silently re-init if needed) via initSession(): which returns the live
1007
+ // session when `new Date() < expiresAt`, else mints a fresh one: instead of
1003
1008
  // trusting the possibly-stale `this.clientSession`. (core#3889; BugBot
1004
1009
  // PR #214 r3367875360.)
1005
1010
  let resumeSessionId: string | undefined;
@@ -1069,7 +1074,7 @@ export class AgentWidgetClient {
1069
1074
 
1070
1075
  // Client tools: built-in widget tools (ask_user_question, when exposed)
1071
1076
  // plus the per-turn WebMCP page-registry snapshot. Name collisions are
1072
- // impossible WebMCP entries are `webmcp:`-prefixed server-side while
1077
+ // impossible: WebMCP entries are `webmcp:`-prefixed server-side while
1073
1078
  // `sdk`-origin built-ins keep bare names. Both kinds ride the same
1074
1079
  // diff-only fingerprint path in client-token mode. Kept to a single await
1075
1080
  // so dispatch microtask timing is unchanged.
@@ -1185,7 +1190,7 @@ export class AgentWidgetClient {
1185
1190
  // dropping `clientTools` accidentally; the WebMCP wire surface
1186
1191
  // is invisible to them. The integrator can still set
1187
1192
  // `clientTools: []` or `clientTools: undefined` explicitly to
1188
- // strip them on purpose we only fall back when the field is
1193
+ // strip them on purpose: we only fall back when the field is
1189
1194
  // entirely absent from the returned object.
1190
1195
  if (
1191
1196
  payload.clientTools !== undefined &&
@@ -1258,7 +1263,7 @@ export class AgentWidgetClient {
1258
1263
  createNewAssistant(result.partId);
1259
1264
  }
1260
1265
 
1261
- // Update partId tracking (only when partId is provided backward compatible)
1266
+ // Update partId tracking (only when partId is provided: backward compatible)
1262
1267
  if (result.partId !== undefined) {
1263
1268
  partIdState.current = result.partId;
1264
1269
  }
@@ -1381,7 +1386,7 @@ export class AgentWidgetClient {
1381
1386
  // Messages produced by steps inside a nested flow executed as a tool.
1382
1387
  // Keyed by `${parentToolId}::${nestedStepId}::${partId}` so each nested
1383
1388
  // step (send-stream, prompt) gets its own assistant message, and prompts
1384
- // with inner tool calls split into one message per text segment still
1389
+ // with inner tool calls split into one message per text segment: still
1385
1390
  // attributable to the parent tool call.
1386
1391
  const nestedStepMessages = new Map<string, AgentWidgetMessage>();
1387
1392
  // Most-recent partId seen for a given `${toolId}::${stepId}` scope, used
@@ -1406,7 +1411,7 @@ export class AgentWidgetClient {
1406
1411
  ) => `${toolId}::${stepId}::${partId}`;
1407
1412
 
1408
1413
  // Prefix used to sweep every nested message belonging to a single
1409
- // (toolId, stepId) scope needed on step_complete to seal any segments
1414
+ // (toolId, stepId) scope: needed on step_complete to seal any segments
1410
1415
  // that are still streaming.
1411
1416
  const getNestedStepPrefix = (toolId: string, stepId: string) =>
1412
1417
  `${toolId}::${stepId}::`;
@@ -1728,7 +1733,7 @@ export class AgentWidgetClient {
1728
1733
  const cur = msg.content ?? "";
1729
1734
  if (mergedDisplay.trim() === "") return;
1730
1735
  // Only replace when empty, or when the stream left a strict prefix of the
1731
- // authoritative final (truncation). Do not use length alone multi-segment
1736
+ // authoritative final (truncation). Do not use length alone: multi-segment
1732
1737
  // flows can have a short last bubble whose content is not a prefix of the
1733
1738
  // full step response.
1734
1739
  if (
@@ -1810,7 +1815,7 @@ export class AgentWidgetClient {
1810
1815
  // Declared here so scheduleReadyQueueDrain can reference it; assigned
1811
1816
  // after all handler-scoped variables are initialised (before the SSE loop).
1812
1817
  let drainReadyQueue: () => void;
1813
- // Two drain paths both are intentional, do not remove either:
1818
+ // Two drain paths: both are intentional, do not remove either:
1814
1819
  // 1. Microtask drain (scheduleReadyQueueDrain): required when the
1815
1820
  // buffer's emitter fires from the gap-timeout setTimeout callback,
1816
1821
  // because there is no surrounding synchronous drain site there.
@@ -1931,7 +1936,7 @@ export class AgentWidgetClient {
1931
1936
  const toolId =
1932
1937
  resolveToolId(payload, true) ?? `tool-${nextSequence()}`;
1933
1938
  const toolName = payload.toolName ?? payload.name;
1934
- // Suppress tool UI for artifact emit tools artifacts are handled via artifact_* events
1939
+ // Suppress tool UI for artifact emit tools: artifacts are handled via artifact_* events
1935
1940
  if (isArtifactEmitToolName(toolName)) {
1936
1941
  artifactToolCallIds.add(toolId);
1937
1942
  continue;
@@ -2056,7 +2061,7 @@ export class AgentWidgetClient {
2056
2061
  // Key the message by the per-call `toolCallId` (provider `toolu_…`;
2057
2062
  // core#3878) when present. Two PARALLEL calls to the SAME tool in one
2058
2063
  // turn collapse to an identical `toolId` (`runtime_webmcp:<name>_<ms>`)
2059
- // and `index: 0` only `toolCallId` distinguishes them. Keying on it
2064
+ // and `index: 0`: only `toolCallId` distinguishes them. Keying on it
2060
2065
  // (a) keeps the two awaits as DISTINCT messages with their own args
2061
2066
  // instead of the second clobbering the first, and (b) merges each
2062
2067
  // await into the matching `tool_start` bubble (also keyed by
@@ -2105,7 +2110,7 @@ export class AgentWidgetClient {
2105
2110
  emitMessage(toolMessage);
2106
2111
  } else if (payloadType === "text_start") {
2107
2112
  // Lifecycle event: a new text segment is beginning (emitted at tool boundaries).
2108
- // When toolContext is present this fired inside a nested flow it must not
2113
+ // When toolContext is present this fired inside a nested flow: it must not
2109
2114
  // seal or rotate the outer assistant message. Nested prompt segmentation is
2110
2115
  // handled via nestedStepMessages keyed by (toolId, stepId).
2111
2116
  if ((payload as any).toolContext?.toolId) {
@@ -2127,7 +2132,7 @@ export class AgentWidgetClient {
2127
2132
  }
2128
2133
  } else if (payloadType === "text_end") {
2129
2134
  // Lifecycle event: current text segment ended (tool call about to start).
2130
- // When toolContext is present the boundary belongs to a nested flow leave
2135
+ // When toolContext is present the boundary belongs to a nested flow: leave
2131
2136
  // outer assistant state alone so the outer stream is never interrupted by
2132
2137
  // nested activity.
2133
2138
  if ((payload as any).toolContext?.toolId) {
@@ -2155,7 +2160,7 @@ export class AgentWidgetClient {
2155
2160
  // originated inside a nested flow executed as a tool. Surface it as
2156
2161
  // its own assistant message keyed by the nested step id, so authors
2157
2162
  // who add send-stream / prompt steps inside their flow see them as
2158
- // real messages in the timeline, in order rather than merging
2163
+ // real messages in the timeline, in order: rather than merging
2159
2164
  // into the outer assistant bubble or getting buried in the tool
2160
2165
  // card. Each nested step id gets its own message; the parent tool
2161
2166
  // bubble continues to represent the invocation via tool_* events.
@@ -2304,7 +2309,7 @@ export class AgentWidgetClient {
2304
2309
  const text = typeof result === 'string' ? result : result?.text ?? null;
2305
2310
 
2306
2311
  if (text !== null && text.trim() !== "") {
2307
- // Parser successfully extracted text update the chunk's assistant
2312
+ // Parser successfully extracted text: update the chunk's assistant
2308
2313
  // (not assistantMessage; text_end may have cleared that ref before microtasks run)
2309
2314
  assistant.content = text;
2310
2315
  emitMessage(assistant);
@@ -2478,14 +2483,14 @@ export class AgentWidgetClient {
2478
2483
 
2479
2484
  // Capture optional per-step stopReason emitted by the runtime
2480
2485
  // (e.g. `'max_tool_calls'`, `'length'`). This is the dispatch-mode
2481
- // fallback `agent_turn_complete` will overwrite it later in
2486
+ // fallback: `agent_turn_complete` will overwrite it later in
2482
2487
  // agent-loop streams.
2483
2488
  const stepStopReason = (payload as any).stopReason as
2484
2489
  | StopReasonKind
2485
2490
  | undefined;
2486
2491
 
2487
2492
  if (didSplitByPartId) {
2488
- // Sealed segment(s) do not create a second bubble from step_complete.
2493
+ // Sealed segment(s): do not create a second bubble from step_complete.
2489
2494
  // Merge authoritative final response into the last sealed segment (fixes async lag).
2490
2495
  if (assistantMessage !== null) {
2491
2496
  const msg: AgentWidgetMessage = assistantMessage;
@@ -2635,7 +2640,7 @@ export class AgentWidgetClient {
2635
2640
  } else if (payloadType === "flow_complete") {
2636
2641
  const finalContent = payload.result?.response;
2637
2642
  if (didSplitByPartId) {
2638
- // Content was split into multiple assistant messages the full response
2643
+ // Content was split into multiple assistant messages: the full response
2639
2644
  // in flow_complete would overwrite the last segment. Just finalize streaming.
2640
2645
  if (assistantMessage !== null) {
2641
2646
  const msg: AgentWidgetMessage = assistantMessage;
@@ -2808,8 +2813,7 @@ export class AgentWidgetClient {
2808
2813
  }
2809
2814
 
2810
2815
  // Attach the turn-level stopReason to the assistant message
2811
- // produced by this turn. Only overwrite the current message
2812
- // prior turns already sealed their own stopReason via step_complete.
2816
+ // produced by this turn. Only overwrite the current message: // prior turns already sealed their own stopReason via step_complete.
2813
2817
  // Falls back to lastAssistantInTurn when the current bubble was
2814
2818
  // sealed at a tool-call boundary mid-turn, so the notice still
2815
2819
  // attaches to the final visible text segment.
@@ -2895,7 +2899,7 @@ export class AgentWidgetClient {
2895
2899
  } else if (payloadType === "agent_media") {
2896
2900
  // A tool produced media (image / audio / video / file). Render it
2897
2901
  // as a synthetic assistant message inserted at the point the tool
2898
- // completed between the tool bubble and the next text turn.
2902
+ // completed: between the tool bubble and the next text turn.
2899
2903
  //
2900
2904
  // Wire format is the AI SDK–aligned `MediaContentPart` from
2901
2905
  // @runtypelabs/shared:
@@ -2922,7 +2926,7 @@ export class AgentWidgetClient {
2922
2926
  const data = typeof rec.data === "string" ? rec.data : undefined;
2923
2927
  if (!data) continue;
2924
2928
  // Empty/missing mediaType yields `data:;base64,...` which RFC 2397
2925
- // resolves to `text/plain` stamp a default so the data URI is
2929
+ // resolves to `text/plain`: stamp a default so the data URI is
2926
2930
  // well-formed and the part lands in the file bucket.
2927
2931
  mediaType = rawMediaType.length > 0 ? rawMediaType : "application/octet-stream";
2928
2932
  src = `data:${mediaType};base64,${data}`;
@@ -2999,7 +3003,7 @@ export class AgentWidgetClient {
2999
3003
 
3000
3004
  // Seal any in-flight assistant text bubble before splitting the
3001
3005
  // stream. Without this, an orphan bubble retains `streaming: true`
3002
- // forever `agent_complete` only finalizes the latest
3006
+ // forever: `agent_complete` only finalizes the latest
3003
3007
  // `assistantMessage`, so the typing/caret indicator would stay on
3004
3008
  // the prior bubble even though no more deltas will arrive.
3005
3009
  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"