@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "3.34.1",
3
+ "version": "3.36.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",
@@ -27,6 +27,11 @@
27
27
  "import": "./dist/theme-editor.js",
28
28
  "require": "./dist/theme-editor.cjs"
29
29
  },
30
+ "./theme-editor/preview": {
31
+ "types": "./dist/theme-editor-preview.d.ts",
32
+ "import": "./dist/theme-editor-preview.js",
33
+ "require": "./dist/theme-editor-preview.cjs"
34
+ },
30
35
  "./testing": {
31
36
  "types": "./dist/testing.d.ts",
32
37
  "import": "./dist/testing.js",
@@ -37,6 +42,11 @@
37
42
  "import": "./dist/smart-dom-reader.js",
38
43
  "require": "./dist/smart-dom-reader.cjs"
39
44
  },
45
+ "./voice-worklet-player": {
46
+ "types": "./dist/voice-worklet-player.d.ts",
47
+ "import": "./dist/voice-worklet-player.js",
48
+ "require": "./dist/voice-worklet-player.cjs"
49
+ },
40
50
  "./plugin-kit": {
41
51
  "types": "./dist/plugin-kit.d.ts",
42
52
  "import": "./dist/plugin-kit.js",
@@ -109,17 +119,21 @@
109
119
  "access": "public"
110
120
  },
111
121
  "scripts": {
112
- "build": "rimraf dist && pnpm run build:styles && pnpm run build:client && pnpm run build:installer && pnpm run build:launcher && pnpm run build:webmcp-polyfill && pnpm run build:theme-ref && pnpm run build:codegen && pnpm run build:theme-editor && pnpm run build:testing && pnpm run build:smart-dom-reader && pnpm run build:plugin-kit && pnpm run build:animations",
122
+ "build": "rimraf dist && pnpm run build:styles && pnpm run build:markdown-parsers && pnpm run build:client && pnpm run build:installer && pnpm run build:launcher && pnpm run build:webmcp-polyfill && pnpm run build:runtype-tts && pnpm run build:theme-ref && pnpm run build:codegen && pnpm run build:theme-editor && pnpm run build:theme-editor-preview && pnpm run build:testing && pnpm run build:smart-dom-reader && pnpm run build:voice-worklet-player && pnpm run build:plugin-kit && pnpm run build:animations",
123
+ "build:markdown-parsers": "tsup --config tsup.markdown-parsers.config.ts",
113
124
  "build:plugin-kit": "tsup src/plugin-kit.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
114
125
  "build:theme-editor": "tsup src/theme-editor.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
126
+ "build:theme-editor-preview": "tsup src/theme-editor-preview.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
115
127
  "build:testing": "tsup src/testing.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
116
128
  "build:smart-dom-reader": "tsup src/smart-dom-reader.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
129
+ "build:voice-worklet-player": "tsup src/voice-worklet-player.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
117
130
  "build:animations": "tsup src/animations/glyph-cycle.ts src/animations/wipe.ts --format esm,cjs --minify --dts --out-dir dist/animations --no-splitting",
118
131
  "build:theme-ref": "tsup src/theme-reference.ts --format esm,cjs --minify --dts",
119
132
  "build:codegen": "tsup src/codegen.ts --format esm,cjs --minify --dts",
120
133
  "build:styles": "node -e \"const fs=require('fs');fs.mkdirSync('dist',{recursive:true});fs.copyFileSync('src/styles/widget.css','dist/widget.css');\"",
121
134
  "build:client": "tsup src/index.ts --format esm,cjs --minify --sourcemap --splitting false --dts --loader \".css=text\" && tsup --config tsup.global.config.ts && node -e \"const fs=require('fs');for(const ext of ['.global.js','.global.js.map']){const from='dist/index-global'+ext;if(fs.existsSync(from))fs.renameSync(from,'dist/index'+ext);}\"",
122
135
  "build:webmcp-polyfill": "tsup --config tsup.webmcp-polyfill.config.ts",
136
+ "build:runtype-tts": "tsup --config tsup.runtype-tts.config.ts",
123
137
  "build:installer": "tsup src/install.ts --format iife --global-name SiteAgentInstaller --out-dir dist --minify --sourcemap --no-splitting",
124
138
  "build:launcher": "tsup src/launcher-global.ts --format iife --global-name AgentWidgetLauncher --minify --sourcemap --splitting false --out-dir dist && node -e \"const fs=require('fs');for(const ext of ['.global.js','.global.js.map']){const from='dist/launcher-global'+ext;if(fs.existsSync(from))fs.renameSync(from,'dist/launcher'+ext);}\"",
125
139
  "lint": "eslint . --ext .ts",
@@ -2,14 +2,13 @@ import type { StreamAnimationPlugin } from "../types";
2
2
  import { registerStreamAnimationPlugin } from "../utils/stream-animation";
3
3
 
4
4
  /**
5
- * Glyph-cycle animation "hacker-settling" reveal.
5
+ * Glyph-cycle animation: "hacker-settling" reveal.
6
6
  *
7
7
  * Each arriving char briefly cycles through a sequence of random glyphs
8
8
  * before locking in to its final character. Unlike a pure CSS effect, this
9
9
  * needs JS because it mutates `textContent` per tick. To keep idiomorph from
10
10
  * clobbering mid-cycle work, each cycling span carries
11
- * `data-preserve-runtime="stream-glyph-cycle"` until its cycle completes
12
- * morph honors that attribute as an absolute skip marker.
11
+ * `data-preserve-runtime="stream-glyph-cycle"` until its cycle completes: * morph honors that attribute as an absolute skip marker.
13
12
  *
14
13
  * A short buffer (`BUFFER_THRESHOLD`) holds the bubble empty (with the
15
14
  * typing indicator) until enough text has arrived to run a visible reveal.
@@ -19,7 +18,7 @@ import { registerStreamAnimationPlugin } from "../utils/stream-animation";
19
18
  * settling wave flows through the full message.
20
19
  *
21
20
  * Ships as a subpath module so consumers who don't want it pay zero cost.
22
- * Importing this module auto-registers the plugin globally just add the
21
+ * Importing this module auto-registers the plugin globally: just add the
23
22
  * import and set `features.streamAnimation.type = "glyph-cycle"`.
24
23
  *
25
24
  * ```ts
@@ -54,8 +53,7 @@ const STYLES = `
54
53
  const GLYPHS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#$%&@";
55
54
  const TICK_COUNT = 10;
56
55
  // Base interval between glyph swaps (ms). Scaled by the widget's configured
57
- // `speed` so the cycle feels coherent with the rest of the stream animation
58
- // faster speed → snappier cycling, slower speed → more deliberate flicker.
56
+ // `speed` so the cycle feels coherent with the rest of the stream animation:// faster speed → snappier cycling, slower speed → more deliberate flicker.
59
57
  const BASE_TICK_MS = 120;
60
58
  const DEFAULT_STEP_MS = 120;
61
59
  // Probability that each later still-cycling sibling gets re-randomized on
@@ -63,7 +61,7 @@ const DEFAULT_STEP_MS = 120;
63
61
  const CROSS_FLICKER_PROBABILITY = 0.4;
64
62
  // Hold the stream back until at least this many chars have arrived. Once
65
63
  // released, every char is rendered as a random glyph and settles in order
66
- // from the start so the visible animation carries through the full text.
64
+ // from the start, so the visible animation carries through the full text.
67
65
  const BUFFER_THRESHOLD = 50;
68
66
 
69
67
  const getStepMs = (container: HTMLElement | null): number => {
@@ -96,7 +94,7 @@ const flickerLaterSiblings = (span: HTMLElement): void => {
96
94
  const container = span.closest(".persona-stream-glyph-cycle") as HTMLElement | null;
97
95
  if (!container) return;
98
96
  // Any scheduled char that still has its final char stashed is "pending
99
- // or cycling" both kinds are valid flicker targets. Settled chars have
97
+ // or cycling": both kinds are valid flicker targets. Settled chars have
100
98
  // `data-glyph-cycle-final` deleted and are excluded.
101
99
  const unsettled = container.querySelectorAll<HTMLElement>(
102
100
  ".persona-stream-char[data-glyph-cycle-final]"
@@ -115,8 +113,8 @@ const flickerLaterSiblings = (span: HTMLElement): void => {
115
113
  };
116
114
 
117
115
  // Per-container timestamp tracking: when the next char's cycle should settle.
118
- // Each char starts cycling immediately on schedule no static placeholder
119
- // wait and finishes when `Date.now() >= settleAt`. This slot advances by
116
+ // Each char starts cycling immediately on schedule: no static placeholder
117
+ // wait, and finishes when `Date.now() >= settleAt`. This slot advances by
120
118
  // one stagger step per schedule, so the burst settles left-to-right while
121
119
  // every char is visibly flickering from the moment it enters the DOM.
122
120
  const containerNextSettleAt = new WeakMap<Element, number>();
@@ -139,8 +137,7 @@ const observeStagger = (container: Element | null, now: number): number => {
139
137
  let observed = containerArrivalMs.get(container) ?? DEFAULT_ARRIVAL_MS;
140
138
  if (last !== undefined) {
141
139
  const interval = now - last;
142
- // Skip near-zero intervals (burst within one MutationObserver batch)
143
- // they don't reflect the real stream cadence.
140
+ // Skip near-zero intervals (burst within one MutationObserver batch): // they don't reflect the real stream cadence.
144
141
  if (interval > 1) {
145
142
  observed = observed * (1 - EMA_ALPHA) + interval * EMA_ALPHA;
146
143
  containerArrivalMs.set(container, observed);
@@ -180,7 +177,7 @@ const scheduleCycle = (span: HTMLElement): void => {
180
177
  span.dataset.glyphCycleScheduled = "true";
181
178
  // Stash the target char and immediately paint a random glyph so the span
182
179
  // reads as "cycling" even before its staggered kickoff. Also opt out of
183
- // morph without this, streamed token re-renders would overwrite our
180
+ // morph: without this, streamed token re-renders would overwrite our
184
181
  // placeholder glyph with the final char.
185
182
  span.dataset.glyphCycleFinal = finalChar;
186
183
  span.setAttribute("data-preserve-runtime", "stream-glyph-cycle");
@@ -275,7 +272,7 @@ export const glyphCycle: StreamAnimationPlugin = {
275
272
  // `**template literal**`) would trigger a render of the partial
276
273
  // `**template`, which markdown emits as literal asterisks, gets
277
274
  // wrapped + marked `data-preserve-runtime`, and leaks through later
278
- // morphs the final structure can't reconcile.
275
+ // morphs: the final structure can't reconcile.
279
276
  let boldPairs = 0;
280
277
  let lastSafe = -1;
281
278
  let i = 0;
@@ -2,7 +2,7 @@ import type { StreamAnimationPlugin } from "../types";
2
2
  import { registerStreamAnimationPlugin } from "../utils/stream-animation";
3
3
 
4
4
  /**
5
- * Wipe animation per-word left-to-right mask reveal.
5
+ * Wipe animation: per-word left-to-right mask reveal.
6
6
  *
7
7
  * Each arriving word is revealed via a soft feathered mask that sweeps from
8
8
  * right to left. Uses `mask-image` (not `background-clip: text`) so the
@@ -137,7 +137,7 @@ describe("AgentWidgetClient - built-in ask_user_question exposure", () => {
137
137
 
138
138
  it("changes the clientTools fingerprint when toggled (diff-only resend)", () => {
139
139
  // In client-token mode the widget only resends the full clientTools[]
140
- // when the fingerprint changes toggling expose must change it so the
140
+ // when the fingerprint changes: toggling expose must change it so the
141
141
  // server actually learns about (or forgets) the built-in tool.
142
142
  const webMcpOnly = [{ name: "search", description: "s" }];
143
143
  const withBuiltIn = [ASK_USER_QUESTION_CLIENT_TOOL, ...webMcpOnly];
@@ -2,7 +2,7 @@
2
2
  * Built-in `ask_user_question` client tool.
3
3
  *
4
4
  * The widget can advertise this tool to the agent on every dispatch via
5
- * `clientTools[]` (set `features.askUserQuestion.expose: true`) the same
5
+ * `clientTools[]` (set `features.askUserQuestion.expose: true`): the same
6
6
  * wire surface WebMCP page tools ride. The server registers it as a LOCAL
7
7
  * tool under its bare name (`origin: 'sdk'` tools are not `webmcp:`-prefixed),
8
8
  * so when the model calls it the execution pauses with a `step_await`
@@ -13,7 +13,7 @@
13
13
  * This replaces the previous integrator burden of hand-declaring the tool in
14
14
  * a flow's `runtimeTools` and keeping that schema in sync with the renderer.
15
15
  * Flows that already declare `ask_user_question` server-side should leave
16
- * `expose` off the model would otherwise see the tool twice.
16
+ * `expose` off: the model would otherwise see the tool twice.
17
17
  */
18
18
 
19
19
  import {
@@ -28,8 +28,7 @@ import type { AgentWidgetConfig, ClientToolDefinition } from "./types";
28
28
 
29
29
  /**
30
30
  * JSON Schema for the tool's parameters. Mirrors {@link AskUserQuestionPayload}
31
- * the shape `parseAskUserQuestionPayload` hydrates the answer sheet from
32
- * so the schema the model is held to and the schema the renderer expects can
31
+ *, the shape `parseAskUserQuestionPayload` hydrates the answer sheet from, * so the schema the model is held to and the schema the renderer expects can
33
32
  * never drift.
34
33
  */
35
34
  export const ASK_USER_QUESTION_PARAMETERS_SCHEMA = {
@@ -57,7 +56,7 @@ export const ASK_USER_QUESTION_PARAMETERS_SCHEMA = {
57
56
  minItems: 2,
58
57
  maxItems: 4,
59
58
  description:
60
- '2-4 distinct choices. Do NOT add an "Other" option free text is automatic.',
59
+ '2-4 distinct choices. Do NOT add an "Other" option: free text is automatic.',
61
60
  items: {
62
61
  type: "object",
63
62
  properties: {
@@ -102,7 +101,7 @@ export const ASK_USER_QUESTION_CLIENT_TOOL: ClientToolDefinition = {
102
101
  name: ASK_USER_QUESTION_TOOL_NAME,
103
102
  description:
104
103
  "Ask the user multiple-choice questions and wait for their answers. Use " +
105
- "only when blocked on a decision that is the user's to make a " +
104
+ "only when blocked on a decision that is the user's to make: a " +
106
105
  "preference, a choice between valid approaches, or information you " +
107
106
  "cannot infer. Each question offers 2-4 options plus an automatic " +
108
107
  "free-text input. The result maps each question to its answer (an array " +
@@ -119,8 +118,7 @@ export const ASK_USER_QUESTION_CLIENT_TOOL: ClientToolDefinition = {
119
118
  *
120
119
  * Each tool is gated on BOTH of its feature flags: `expose` opts the tool
121
120
  * into the agent's catalog, and `enabled !== false` guarantees the widget can
122
- * actually render UI (and, for fire-and-forget tools, auto-resume) for it
123
- * exposing a tool with its feature disabled would park the execution on a
121
+ * actually render UI (and, for fire-and-forget tools, auto-resume) for it: * exposing a tool with its feature disabled would park the execution on a
124
122
  * generic tool bubble with no way to advance.
125
123
  */
126
124
  export const builtInClientToolsForDispatch = (
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import { AgentWidgetClient, preferFinalStructuredContent } from './client';
3
3
  import { AgentWidgetEvent, AgentWidgetMessage } from './types';
4
4
  import { createJsonStreamParser } from './utils/formatting';
5
+ import { VERSION } from './version';
5
6
 
6
7
  describe('AgentWidgetClient - Empty Message Filtering', () => {
7
8
  let client: AgentWidgetClient;
@@ -2348,7 +2349,7 @@ describe('AgentWidgetClient - Out-of-Order Sequence Reordering', () => {
2348
2349
  };
2349
2350
  e({ type: 'flow_start', flowId: 'f1', flowName: 'Test', totalSteps: 1 });
2350
2351
  e({ type: 'step_start', id: 's1', name: 'Prompt', stepType: 'prompt', index: 1, totalSteps: 1 });
2351
- // No seq field should append in arrival order
2352
+ // No seq field: should append in arrival order
2352
2353
  e({ type: 'step_delta', id: 's1', text: 'Hello ' });
2353
2354
  e({ type: 'step_delta', id: 's1', text: 'world' });
2354
2355
  e({ type: 'step_delta', id: 's1', text: '!' });
@@ -2386,7 +2387,7 @@ describe('AgentWidgetClient - Out-of-Order Sequence Reordering', () => {
2386
2387
  };
2387
2388
  e({ type: 'flow_start', flowId: 'f1', flowName: 'Test', totalSteps: 1 });
2388
2389
  e({ type: 'step_start', id: 's1', name: 'Prompt', stepType: 'prompt', index: 1, totalSteps: 1 });
2389
- // seq=3 arrives first (leading gap seq 1 and 2 arrive later)
2390
+ // seq=3 arrives first (leading gap: seq 1 and 2 arrive later)
2390
2391
  e({ type: 'step_delta', id: 's1', text: 'c', partId: 'text_0', seq: 3 });
2391
2392
  e({ type: 'step_delta', id: 's1', text: 'a', partId: 'text_0', seq: 1 });
2392
2393
  e({ type: 'step_delta', id: 's1', text: 'b', partId: 'text_0', seq: 2 });
@@ -2425,7 +2426,7 @@ describe('AgentWidgetClient - Out-of-Order Sequence Reordering', () => {
2425
2426
  };
2426
2427
  e({ type: 'flow_start', flowId: 'f1', flowName: 'Test', totalSteps: 1 });
2427
2428
  e({ type: 'step_start', id: 's1', name: 'Prompt', stepType: 'prompt', index: 1, totalSteps: 1 });
2428
- // reason_delta uses sequenceIndex, step_delta uses seq same counter
2429
+ // reason_delta uses sequenceIndex, step_delta uses seq: same counter
2429
2430
  e({ type: 'reason_start', reasoningId: 'r1', hidden: false, done: false });
2430
2431
  e({ type: 'reason_delta', reasoningId: 'r1', reasoningText: 'thinking', hidden: false, done: false, sequenceIndex: 1 });
2431
2432
  e({ type: 'reason_complete', reasoningId: 'r1', hidden: false, done: true });
@@ -2518,7 +2519,7 @@ describe('AgentWidgetClient - Out-of-Order Sequence Reordering', () => {
2518
2519
  };
2519
2520
  e({ type: 'flow_start', flowId: 'f1', flowName: 'Test', totalSteps: 1 });
2520
2521
  e({ type: 'step_start', id: 's1', name: 'Prompt', stepType: 'prompt', index: 1, totalSteps: 1 });
2521
- // Only a seq=3 event arrives seq=1 and seq=2 are never delivered.
2522
+ // Only a seq=3 event arrives: seq=1 and seq=2 are never delivered.
2522
2523
  // Without the end-of-stream flush, this event would be stranded in
2523
2524
  // the reorder buffer and never emitted.
2524
2525
  e({ type: 'step_delta', id: 's1', text: 'tail', partId: 'text_0', seq: 3 });
@@ -2607,7 +2608,7 @@ describe('AgentWidgetClient - Out-of-Order Sequence Reordering', () => {
2607
2608
  controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
2608
2609
  };
2609
2610
  e({ type: 'flow_start', flowId: 'f1', flowName: 'Test', totalSteps: 1 });
2610
- // Only sequenced event but seq > 1 so it would be buffered.
2611
+ // Only sequenced event, but seq > 1 so it would be buffered.
2611
2612
  e({ type: 'error', error: 'boom', seq: 2 });
2612
2613
  controller.close();
2613
2614
  },
@@ -2767,7 +2768,7 @@ describe('AgentWidgetClient - stopReason propagation', () => {
2767
2768
  });
2768
2769
 
2769
2770
  // ============================================================================
2770
- // Within-turn text/tool interleaving assistant text bubbles must seal at
2771
+ // Within-turn text/tool interleaving: assistant text bubbles must seal at
2771
2772
  // each agent_tool_start so the chronological text→tool→text→tool sequence
2772
2773
  // renders as distinct timeline entries instead of one merged bubble that
2773
2774
  // appears below all the tool cards.
@@ -3004,7 +3005,7 @@ describe('AgentWidgetClient - agent_turn text/tool interleaving', () => {
3004
3005
  // step_await (LOCAL tool pause) + resumeFlow
3005
3006
  // ============================================================================
3006
3007
 
3007
- describe('AgentWidgetClient step_await parsing', () => {
3008
+ describe('AgentWidgetClient: step_await parsing', () => {
3008
3009
  const buildStepAwaitStream = (payload: Record<string, unknown>): ReadableStream<Uint8Array> => {
3009
3010
  const encoder = new TextEncoder();
3010
3011
  const body = `event: step_await\ndata: ${JSON.stringify({ type: 'step_await', ...payload })}\n\n`;
@@ -3196,7 +3197,7 @@ describe('AgentWidgetClient.resumeFlow', () => {
3196
3197
 
3197
3198
  await client.resumeFlow('exec_xyz', { toolu_A: { ok: true } });
3198
3199
 
3199
- // Session-authed sibling of /v1/client/chat no Bearer key, sessionId in body.
3200
+ // Session-authed sibling of /v1/client/chat: no Bearer key, sessionId in body.
3200
3201
  expect(capturedUrl).toBe('https://api.runtype.com/v1/client/resume');
3201
3202
  expect(capturedBody).toEqual({
3202
3203
  executionId: 'exec_xyz',
@@ -3239,7 +3240,7 @@ describe('AgentWidgetClient.resumeFlow', () => {
3239
3240
  clientToken: 'ct_live_demo',
3240
3241
  apiUrl: 'https://api.runtype.com',
3241
3242
  });
3242
- // A stale session that has already expired a long WebMCP approval wait can
3243
+ // A stale session that has already expired: a long WebMCP approval wait can
3243
3244
  // outlive it, so resumeFlow must not trust this.clientSession directly.
3244
3245
  (client as unknown as { clientSession: { sessionId: string; expiresAt: Date } }).clientSession = {
3245
3246
  sessionId: 'cs_stale',
@@ -3647,7 +3648,7 @@ describe('AgentWidgetClient - agent_media events', () => {
3647
3648
  sseEvent('agent_iteration_start', { executionId: execId, iteration: 1, maxTurns: 1, startedAt: new Date().toISOString(), seq: 2 }),
3648
3649
  sseEvent('agent_turn_start', { executionId: execId, iteration: 1, turnIndex: 0, role: 'assistant', turnId: 'turn_1', seq: 3 }),
3649
3650
  sseEvent('agent_turn_delta', { executionId: execId, iteration: 1, delta: 'Streaming...', contentType: 'text', turnId: 'turn_1', seq: 4 }),
3650
- // Media arrives mid-stream earlier text bubble is still streaming.
3651
+ // Media arrives mid-stream: earlier text bubble is still streaming.
3651
3652
  sseEvent('agent_media', {
3652
3653
  executionId: execId, iteration: 1, toolCallId: 'tc_seal', toolName: 'shot',
3653
3654
  media: [{ type: 'media', data: 'PNG', mediaType: 'image/png' }],
@@ -3755,7 +3756,7 @@ describe('AgentWidgetClient - agent_media events', () => {
3755
3756
  sseEvent('agent_iteration_start', { executionId: execId, iteration: 1, maxTurns: 1, startedAt: new Date().toISOString(), seq: 2 }),
3756
3757
  sseEvent('agent_media', {
3757
3758
  executionId: execId, iteration: 1, toolCallId: 'tc_blob', toolName: 'opaque',
3758
- // mediaType is empty should not produce a malformed `data:;base64,...` URI.
3759
+ // mediaType is empty: should not produce a malformed `data:;base64,...` URI.
3759
3760
  media: [{ type: 'media', data: 'AAAA', mediaType: '' }],
3760
3761
  seq: 3,
3761
3762
  }),
@@ -4004,7 +4005,7 @@ describe('AgentWidgetClient - diff-only clientTools (client-token)', () => {
4004
4005
  return sse();
4005
4006
  });
4006
4007
  const client = makeClient(TOOLS);
4007
- // Pre-seed the cache so the first attempt would be fingerprint-only the
4008
+ // Pre-seed the cache so the first attempt would be fingerprint-only: the
4008
4009
  // server then forces a resend.
4009
4010
  (client as unknown as { lastSentClientToolsFingerprint: string | null; clientToolsFingerprintSessionId: string | null }).lastSentClientToolsFingerprint =
4010
4011
  'stale';
@@ -4176,3 +4177,46 @@ describe('AgentWidgetClient - Feedback request builder', () => {
4176
4177
  expect(feedbackBodies[0].rating).toBe(5);
4177
4178
  });
4178
4179
  });
4180
+
4181
+ describe('AgentWidgetClient - version header', () => {
4182
+ function captureHeaders() {
4183
+ const headers: Array<Record<string, string>> = [];
4184
+ global.fetch = vi.fn().mockImplementation(async (_url: string, options: { headers: Record<string, string> }) => {
4185
+ headers.push(options.headers);
4186
+ const encoder = new TextEncoder();
4187
+ const stream = new ReadableStream({
4188
+ start(c) {
4189
+ c.enqueue(encoder.encode('data: {"type":"flow_complete","success":true}\n\n'));
4190
+ c.close();
4191
+ },
4192
+ });
4193
+ return { ok: true, body: stream };
4194
+ });
4195
+ return headers;
4196
+ }
4197
+
4198
+ const msg = () => ({
4199
+ messages: [{ id: 'u1', role: 'user' as const, content: 'hi', createdAt: '2025-01-01T00:00:00.000Z' }],
4200
+ });
4201
+
4202
+ it('broadcasts X-Persona-Version on the dispatch request', async () => {
4203
+ const headers = captureHeaders();
4204
+ const client = new AgentWidgetClient({ apiUrl: 'http://localhost:8000' });
4205
+
4206
+ await client.dispatch(msg(), () => undefined);
4207
+
4208
+ expect(headers[0]['X-Persona-Version']).toBe(VERSION);
4209
+ });
4210
+
4211
+ it('lets an explicit config header override the version', async () => {
4212
+ const headers = captureHeaders();
4213
+ const client = new AgentWidgetClient({
4214
+ apiUrl: 'http://localhost:8000',
4215
+ headers: { 'X-Persona-Version': 'override' },
4216
+ });
4217
+
4218
+ await client.dispatch(msg(), () => undefined);
4219
+
4220
+ expect(headers[0]['X-Persona-Version']).toBe('override');
4221
+ });
4222
+ });