@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
@@ -0,0 +1,215 @@
1
+ /** A unit of speech handed to a {@link SpeechEngine}. */
2
+ interface SpeechRequest {
3
+ /** Plain text to speak. The widget strips Markdown before calling the engine. */
4
+ text: string;
5
+ /** Preferred voice identifier (engine-specific; browser = voice name). */
6
+ voice?: string;
7
+ /** Speech rate (engine-specific range; browser: 0.1–10, default 1). */
8
+ rate?: number;
9
+ /** Speech pitch (engine-specific range; browser: 0–2, default 1). */
10
+ pitch?: number;
11
+ }
12
+ /** Lifecycle callbacks a {@link SpeechEngine} invokes during playback. */
13
+ interface SpeechCallbacks {
14
+ /** Audio has started playing. */
15
+ onStart?: () => void;
16
+ /** Playback finished naturally (or was canceled). */
17
+ onEnd?: () => void;
18
+ /** Playback failed. */
19
+ onError?: (error: Error) => void;
20
+ }
21
+ /**
22
+ * Pluggable text-to-speech engine behind the "Read aloud" message action.
23
+ *
24
+ * The widget ships a browser Web Speech API engine by default. Provide a hosted
25
+ * engine (Runtype TTS, ElevenLabs, a server proxy, …) by implementing this
26
+ * interface and returning it from {@link TextToSpeechConfig.createEngine}; a
27
+ * server engine can stream audio into the realtime voice
28
+ * {@link VoicePlaybackEngine}.
29
+ */
30
+ interface SpeechEngine {
31
+ /** Stable identifier, e.g. `"browser"`, `"runtype"`, or a custom id. */
32
+ readonly id: string;
33
+ /**
34
+ * Whether {@link pause}/{@link resume} are supported. When `false`, the UI
35
+ * offers play/stop only (tapping a playing message stops it).
36
+ */
37
+ readonly supportsPause: boolean;
38
+ /** Begin speaking. Drives the lifecycle through `callbacks`. */
39
+ speak(request: SpeechRequest, callbacks: SpeechCallbacks): void;
40
+ /** Pause playback (no-op if unsupported). */
41
+ pause(): void;
42
+ /** Resume paused playback (no-op if unsupported). */
43
+ resume(): void;
44
+ /** Stop playback and discard any queued audio. */
45
+ stop(): void;
46
+ /** Optional teardown of engine resources. */
47
+ destroy?(): void;
48
+ }
49
+ /**
50
+ * Streaming PCM playback engine used by the realtime voice provider.
51
+ *
52
+ * The provider feeds raw PCM16 LE mono @ 24kHz (WAV header already stripped)
53
+ * via `enqueue`, signals end-of-reply with `markStreamEnd`, and learns when
54
+ * playback has fully drained via `onFinished`. The default implementation is
55
+ * `AudioPlaybackManager`; an optional jitter-buffered AudioWorklet engine ships
56
+ * from `@runtypelabs/persona/voice-worklet-player` and can be injected via the
57
+ * `runtype.createPlaybackEngine` config hook.
58
+ */
59
+ interface VoicePlaybackEngine {
60
+ /** Enqueue a raw PCM16 LE mono @ 24kHz chunk (no WAV header). */
61
+ enqueue(pcm: Uint8Array): void;
62
+ /** Signal that no more chunks will arrive for the current reply. */
63
+ markStreamEnd(): void;
64
+ /** Immediately stop playback and discard queued audio. */
65
+ flush(): void;
66
+ /** Register a callback fired once all queued audio has finished playing. */
67
+ onFinished(callback: () => void): void;
68
+ /** Release all audio resources. */
69
+ destroy(): Promise<void> | void;
70
+ }
71
+ /**
72
+ * A {@link VoicePlaybackEngine} that also supports pause/resume. Returned by
73
+ * `createPcmStreamPlayer` (`@runtypelabs/persona/voice-worklet-player`): a
74
+ * jitter-buffered AudioWorklet player for raw PCM16 / 24 kHz / mono streams.
75
+ * Reuse it inside a hosted {@link SpeechEngine} to get gapless playback with a
76
+ * configurable prebuffer and graceful underrun handling — feed each streamed
77
+ * chunk to {@link VoicePlaybackEngine.enqueue} and the worklet does the rest.
78
+ */
79
+ interface PcmStreamPlayer extends VoicePlaybackEngine {
80
+ /** Pause playback; the audio clock suspends and {@link resume} continues in place. */
81
+ pause(): void;
82
+ /** Resume playback after {@link pause}. */
83
+ resume(): void;
84
+ /**
85
+ * Register a callback fired once audible playback actually begins — i.e. the
86
+ * prebuffer waterline filled and the first sample reached the output. Use this
87
+ * (rather than "first chunk enqueued") to flip a UI from loading to playing, so
88
+ * the spinner holds through the prebuffer. Fires once per playback session
89
+ * (cleared by {@link VoicePlaybackEngine.flush}); a mid-reply underrun re-buffer
90
+ * does not re-fire it.
91
+ */
92
+ onStarted(callback: () => void): void;
93
+ }
94
+
95
+ interface PcmStreamPlayerOptions {
96
+ /**
97
+ * Audio (ms) to buffer before the first sample, and to re-buffer after an
98
+ * underrun. Higher = smoother on bursty/jittery streams, at the cost of a
99
+ * slightly later first sound (latency ↔ smoothness). Default 150 — good for
100
+ * realtime, server-paced audio; HTTP-pulled TTS often wants ~400–600.
101
+ */
102
+ prebufferMs?: number;
103
+ }
104
+ /**
105
+ * Create a jitter-buffered AudioWorklet PCM player with pause/resume.
106
+ *
107
+ * Feed it raw PCM16 / 24 kHz / mono via `enqueue()`; it handles prebuffering,
108
+ * gapless playback, and graceful underrun. Reuse it inside a hosted
109
+ * {@link SpeechEngine} so streamed TTS plays smoothly:
110
+ *
111
+ * @example
112
+ * import { createPcmStreamPlayer } from '@runtypelabs/persona/voice-worklet-player'
113
+ * const player = await createPcmStreamPlayer({ prebufferMs: 500 })
114
+ * // for each streamed chunk: player.enqueue(pcmChunk)
115
+ * player.markStreamEnd()
116
+ */
117
+ declare function createPcmStreamPlayer(options?: PcmStreamPlayerOptions): Promise<PcmStreamPlayer>;
118
+ /**
119
+ * Realtime-named alias of {@link createPcmStreamPlayer} (default prebuffer),
120
+ * typed as a plain {@link VoicePlaybackEngine}. Pass it to the realtime voice
121
+ * provider's `createPlaybackEngine`:
122
+ *
123
+ * @example
124
+ * import { createWorkletPlaybackEngine } from '@runtypelabs/persona/voice-worklet-player'
125
+ *
126
+ * initAgentWidget({ config: { voiceRecognition: { enabled: true, provider: {
127
+ * type: 'runtype',
128
+ * runtype: { agentId, createPlaybackEngine: createWorkletPlaybackEngine },
129
+ * } } } })
130
+ */
131
+ declare function createWorkletPlaybackEngine(): Promise<VoicePlaybackEngine>;
132
+
133
+ interface RuntypeSpeechEngineOptions {
134
+ /**
135
+ * Runtype API host, e.g. `https://api.runtype.com` (typically the widget's
136
+ * `apiUrl`). A trailing slash is tolerated.
137
+ */
138
+ host: string;
139
+ /** Agent whose configured voice synthesizes the text. */
140
+ agentId: string;
141
+ /** Browser-safe client token — the same one the chat widget uses. */
142
+ clientToken: string;
143
+ /**
144
+ * Default voice id, used when a `SpeechRequest` doesn't carry its own. When
145
+ * omitted the agent's configured voice is used.
146
+ */
147
+ voice?: string;
148
+ /**
149
+ * Audio (ms) the player buffers before the first sample and after an
150
+ * underrun. Runtype streams steadily, so the default (200) keeps first sound
151
+ * close to time-to-first-byte while still riding out small hiccups. Applies to
152
+ * the default {@link AudioPlaybackManager}; a custom `createPlaybackEngine` is
153
+ * responsible for its own prebuffer.
154
+ */
155
+ prebufferMs?: number;
156
+ /**
157
+ * Factory for the streaming PCM player. Defaults to the in-bundle, main-thread
158
+ * {@link AudioPlaybackManager} (with `prebufferMs`). Pass `createPcmStreamPlayer`
159
+ * from `@runtypelabs/persona/voice-worklet-player` for the jitter-buffered
160
+ * AudioWorklet player (it then ships in your bundle, not Persona's). May be
161
+ * async — it is resolved on first playback, inside the user gesture.
162
+ */
163
+ createPlaybackEngine?: () => PcmStreamPlayer | Promise<PcmStreamPlayer>;
164
+ /**
165
+ * Optional hook for surfacing fetch/stream failures (a missing endpoint, an
166
+ * expired token, an upstream 4xx) to a log or telemetry. The widget itself
167
+ * only returns the read-aloud button to idle (or, with a fallback engine,
168
+ * silently switches to the browser voice), so without this the reason is
169
+ * invisible.
170
+ */
171
+ onError?: (error: Error) => void;
172
+ }
173
+ /** Streaming `SpeechEngine` backed by Runtype's `/v1/agents/:id/speak`. */
174
+ declare class RuntypeSpeechEngine implements SpeechEngine {
175
+ private readonly opts;
176
+ readonly id = "runtype-tts";
177
+ readonly supportsPause = true;
178
+ private player;
179
+ private playerPromise;
180
+ private generation;
181
+ constructor(opts: RuntypeSpeechEngineOptions);
182
+ private ensurePlayer;
183
+ speak(request: SpeechRequest, callbacks: SpeechCallbacks): void;
184
+ private run;
185
+ pause(): void;
186
+ resume(): void;
187
+ stop(): void;
188
+ destroy(): void;
189
+ }
190
+
191
+ interface FallbackSpeechEngineOptions {
192
+ /**
193
+ * Called once when the primary engine fails before audio starts and the
194
+ * fallback takes over — so a silent downgrade is still observable in dev/
195
+ * telemetry even though the user keeps hearing speech.
196
+ */
197
+ onFallback?: (error: Error) => void;
198
+ }
199
+ /** A `SpeechEngine` that falls back from `primary` to `fallback` per utterance. */
200
+ declare class FallbackSpeechEngine implements SpeechEngine {
201
+ private readonly primary;
202
+ private readonly fallback;
203
+ private readonly options;
204
+ readonly id = "fallback";
205
+ private active;
206
+ constructor(primary: SpeechEngine, fallback: SpeechEngine, options?: FallbackSpeechEngineOptions);
207
+ get supportsPause(): boolean;
208
+ speak(request: SpeechRequest, callbacks: SpeechCallbacks): void;
209
+ pause(): void;
210
+ resume(): void;
211
+ stop(): void;
212
+ destroy(): void;
213
+ }
214
+
215
+ export { FallbackSpeechEngine, type FallbackSpeechEngineOptions, type PcmStreamPlayer, type PcmStreamPlayerOptions, RuntypeSpeechEngine, type RuntypeSpeechEngineOptions, type VoicePlaybackEngine, createPcmStreamPlayer, createWorkletPlaybackEngine };
@@ -0,0 +1,80 @@
1
+ var P=`
2
+ class PersonaPcmPlayerProcessor extends AudioWorkletProcessor {
3
+ constructor(options) {
4
+ super()
5
+ const opts = (options && options.processorOptions) || {}
6
+ this.waterline = opts.waterlineSamples > 0 ? opts.waterlineSamples : 3600
7
+ this.chunks = []
8
+ this.readOffset = 0
9
+ this.buffered = 0
10
+ this.waiting = true
11
+ // 'drained' must mean "the reply finished playing", not "momentary
12
+ // underrun": a jitter gap empties the buffer mid-reply too, and firing
13
+ // there would flap the UI status and race the audio_end handler. Only
14
+ // report drained once eos has been signalled.
15
+ this.eosSeen = false
16
+ // Fire 'started' once, when the prebuffer first releases into playback, so a
17
+ // consumer can flip UI from loading\u2192playing only when audio is truly audible.
18
+ // A mid-reply underrun re-buffers (waiting=true) but must NOT re-signal.
19
+ this.startedSignaled = false
20
+ this.port.onmessage = (e) => {
21
+ const msg = e.data
22
+ if (msg.type === 'push') {
23
+ this.eosSeen = false
24
+ this.chunks.push(msg.samples)
25
+ this.buffered += msg.samples.length
26
+ if (this.waiting && this.buffered >= this.waterline) {
27
+ this.waiting = false
28
+ this.signalStarted()
29
+ }
30
+ } else if (msg.type === 'eos') {
31
+ this.eosSeen = true
32
+ if (this.waiting && this.buffered > 0) {
33
+ this.waiting = false
34
+ this.signalStarted()
35
+ }
36
+ if (this.buffered === 0) {
37
+ this.eosSeen = false
38
+ this.port.postMessage({ type: 'drained' })
39
+ }
40
+ } else if (msg.type === 'clear') {
41
+ this.chunks = []
42
+ this.readOffset = 0
43
+ this.buffered = 0
44
+ this.waiting = true
45
+ this.eosSeen = false
46
+ this.startedSignaled = false
47
+ }
48
+ }
49
+ }
50
+ signalStarted() {
51
+ if (!this.startedSignaled) {
52
+ this.startedSignaled = true
53
+ this.port.postMessage({ type: 'started' })
54
+ }
55
+ }
56
+ process(inputs, outputs) {
57
+ const out = outputs[0][0]
58
+ if (!out || this.waiting) return true // outputs are pre-zeroed: silence
59
+ let i = 0
60
+ while (i < out.length && this.buffered > 0) {
61
+ const chunk = this.chunks[0]
62
+ out[i++] = chunk[this.readOffset++]
63
+ this.buffered--
64
+ if (this.readOffset >= chunk.length) {
65
+ this.chunks.shift()
66
+ this.readOffset = 0
67
+ }
68
+ }
69
+ if (this.buffered === 0) {
70
+ this.waiting = true // mid-reply underrun: re-buffer silently
71
+ if (this.eosSeen) {
72
+ this.eosSeen = false
73
+ this.port.postMessage({ type: 'drained' })
74
+ }
75
+ }
76
+ return true
77
+ }
78
+ }
79
+ registerProcessor('persona-pcm-player', PersonaPcmPlayerProcessor)
80
+ `;function b(o){let e=o.length>>1,t=new Float32Array(e),r=new DataView(o.buffer,o.byteOffset,o.byteLength);for(let s=0;s<e;s++)t[s]=r.getInt16(s*2,!0)/32768;return t}async function v(o={}){var f;let e=(f=o.prebufferMs)!=null?f:150,t=Math.max(1,Math.round(24e3*e/1e3)),r=window.AudioContext||window.webkitAudioContext,s=new r({sampleRate:24e3});s.state==="suspended"&&await s.resume().catch(()=>{});let a=URL.createObjectURL(new Blob([P],{type:"application/javascript"}));try{await s.audioWorklet.addModule(a)}catch(u){throw s.close().catch(()=>{}),u}finally{URL.revokeObjectURL(a)}let i=new AudioWorkletNode(s,"persona-pcm-player",{numberOfInputs:0,numberOfOutputs:1,outputChannelCount:[1],processorOptions:{waterlineSamples:t}});i.connect(s.destination);let p=[],h=[],l=null;return i.port.onmessage=u=>{var d;let n=(d=u.data)==null?void 0:d.type;if(n==="started"){let c=h.slice();h=[],c.forEach(y=>y())}else if(n==="drained"){let c=p.slice();p=[],c.forEach(y=>y())}},{enqueue(u){let n=u;if(l){let c=new Uint8Array(l.length+u.length);c.set(l),c.set(u,l.length),n=c,l=null}if(n.length%2!==0&&(l=new Uint8Array([n[n.length-1]]),n=n.subarray(0,n.length-1)),n.length===0)return;let d=b(n);d.length!==0&&i.port.postMessage({type:"push",samples:d},[d.buffer])},markStreamEnd(){i.port.postMessage({type:"eos"})},flush(){l=null,p=[],h=[],i.port.postMessage({type:"clear"})},onFinished(u){p.push(u)},onStarted(u){h.push(u)},pause(){s.state==="running"&&s.suspend()},resume(){s.state==="suspended"&&s.resume()},destroy(){i.port.onmessage=null;try{i.disconnect()}catch{}return s.close().catch(()=>{})}}}function w(){return v()}var m=class{constructor(e=24e3,t={}){this.ctx=null;this.nextStartTime=0;this.activeSources=[];this.finishedCallbacks=[];this.startedCallbacks=[];this.playing=!1;this.streamEnded=!1;this.pendingCount=0;this.started=!1;this.userPaused=!1;this.pendingBuffers=[];this.pendingSamples=0;this.remainder=null;var s;this.sampleRate=e;let r=Math.max(0,(s=t.prebufferMs)!=null?s:0);this.waterlineSamples=Math.round(e*r/1e3),this.buffering=this.waterlineSamples>0}ensureContext(){if(!this.ctx){let t=typeof window!="undefined"?window:void 0;if(!t)throw new Error("AudioPlaybackManager requires a browser environment");let r=t.AudioContext||t.webkitAudioContext;this.ctx=new r({sampleRate:this.sampleRate})}let e=this.ctx;return e.state==="suspended"&&!this.userPaused&&e.resume(),e}enqueue(e){if(e.length===0)return;let t=e;if(this.remainder){let s=new Uint8Array(this.remainder.length+e.length);s.set(this.remainder),s.set(e,this.remainder.length),t=s,this.remainder=null}if(t.length%2!==0&&(this.remainder=new Uint8Array([t[t.length-1]]),t=t.subarray(0,t.length-1)),t.length===0)return;let r=this.pcmToFloat32(t);r.length!==0&&(this.buffering?(this.pendingBuffers.push(r),this.pendingSamples+=r.length,this.pendingSamples>=this.waterlineSamples&&this.releaseBuffer()):this.scheduleSamples(r))}markStreamEnd(){this.pendingBuffers.length>0&&this.releaseBuffer(),this.streamEnded=!0,this.checkFinished()}flush(){for(let e of this.activeSources)try{e.stop(),e.disconnect()}catch{}this.activeSources=[],this.pendingCount=0,this.nextStartTime=0,this.playing=!1,this.streamEnded=!1,this.finishedCallbacks=[],this.startedCallbacks=[],this.remainder=null,this.pendingBuffers=[],this.pendingSamples=0,this.buffering=this.waterlineSamples>0,this.started=!1}isPlaying(){return this.playing}onFinished(e){this.finishedCallbacks.push(e)}onStarted(e){this.startedCallbacks.push(e)}pause(){this.userPaused=!0,this.ctx&&this.ctx.state==="running"&&this.ctx.suspend()}resume(){this.userPaused=!1,this.ctx&&this.ctx.state==="suspended"&&this.ctx.resume()}async destroy(){this.flush(),this.ctx&&(await this.ctx.close(),this.ctx=null)}releaseBuffer(){this.buffering=!1;let e=this.pendingBuffers;this.pendingBuffers=[],this.pendingSamples=0;for(let t of e)this.scheduleSamples(t)}scheduleSamples(e){if(e.length===0)return;let t=this.ensureContext(),r=t.createBuffer(1,e.length,this.sampleRate);r.getChannelData(0).set(e);let s=t.createBufferSource();s.buffer=r,s.connect(t.destination);let a=t.currentTime;if(this.nextStartTime===0?this.nextStartTime=a:this.nextStartTime<a&&(this.nextStartTime=a,this.waterlineSamples>0&&(this.buffering=!0)),s.start(this.nextStartTime),this.nextStartTime+=r.duration,this.activeSources.push(s),this.pendingCount++,this.playing=!0,!this.started){this.started=!0;let i=this.startedCallbacks.slice();this.startedCallbacks=[];for(let p of i)p()}s.onended=()=>{let i=this.activeSources.indexOf(s);i!==-1&&this.activeSources.splice(i,1),this.pendingCount--,this.checkFinished()}}checkFinished(){if(this.streamEnded&&this.pendingCount<=0&&this.pendingBuffers.length===0){this.playing=!1,this.streamEnded=!1;let e=this.finishedCallbacks.slice();this.finishedCallbacks=[];for(let t of e)t()}}pcmToFloat32(e){let t=Math.floor(e.length/2),r=new Float32Array(t),s=new DataView(e.buffer,e.byteOffset,e.byteLength);for(let a=0;a<t;a++){let i=s.getInt16(a*2,!0);r[a]=i/32768}return r}};function E(o){return o.replace(/\/+$/,"")}var g=class{constructor(e){this.opts=e;this.id="runtype-tts";this.supportsPause=!0;this.player=null;this.playerPromise=null;this.generation=0}ensurePlayer(){var e,t;return(t=this.playerPromise)!=null?t:this.playerPromise=Promise.resolve(this.opts.createPlaybackEngine?this.opts.createPlaybackEngine():new m(24e3,{prebufferMs:(e=this.opts.prebufferMs)!=null?e:200})).then(r=>this.player=r)}speak(e,t){let r=++this.generation;this.run(r,e,t)}async run(e,t,r){var s,a,i,p;try{let h=await this.ensurePlayer();if(e!==this.generation)return;h.flush(),h.resume(),h.onStarted(()=>{var n;e===this.generation&&((n=r.onStart)==null||n.call(r))}),h.onFinished(()=>{var n;e===this.generation&&((n=r.onEnd)==null||n.call(r))});let l=`${E(this.opts.host)}/v1/agents/${encodeURIComponent(this.opts.agentId)}/speak`,f=await fetch(l,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.opts.clientToken}`},body:JSON.stringify({text:t.text,voice:(s=t.voice)!=null?s:this.opts.voice,format:"pcm"})});if(e!==this.generation)return;if(!f.ok||!f.body)throw new Error(await x(f));let u=f.body.getReader();for(;;){let{done:n,value:d}=await u.read();if(e!==this.generation){await u.cancel().catch(()=>{});return}if(n)break;d&&d.byteLength>0&&h.enqueue(d)}h.markStreamEnd()}catch(h){if(e!==this.generation)return;let l=h instanceof Error?h:new Error(String(h));(i=(a=this.opts).onError)==null||i.call(a,l),(p=r.onError)==null||p.call(r,l)}}pause(){var e;(e=this.player)==null||e.pause()}resume(){var e;(e=this.player)==null||e.resume()}stop(){var e;this.generation++,(e=this.player)==null||e.flush()}destroy(){var e;this.generation++,(e=this.player)==null||e.destroy(),this.player=null,this.playerPromise=null}};async function x(o){var e,t;try{let r=await o.json();return r.detail?`${(e=r.error)!=null?e:`Runtype TTS ${o.status}`}: ${r.detail}`:(t=r.error)!=null?t:`Runtype TTS request failed (${o.status})`}catch{return`Runtype TTS request failed (${o.status})`}}var S=class{constructor(e,t,r={}){this.primary=e;this.fallback=t;this.options=r;this.id="fallback";this.active=e}get supportsPause(){return this.active.supportsPause}speak(e,t){this.active=this.primary;let r=!1;this.primary.speak(e,{onStart:()=>{var s;r=!0,(s=t.onStart)==null||s.call(t)},onEnd:()=>{var s;return(s=t.onEnd)==null?void 0:s.call(t)},onError:s=>{var a,i,p;if(r){(a=t.onError)==null||a.call(t,s);return}(p=(i=this.options).onFallback)==null||p.call(i,s),this.active=this.fallback,this.fallback.speak(e,t)}})}pause(){this.active.pause()}resume(){this.active.resume()}stop(){this.active.stop()}destroy(){var e,t,r,s;(t=(e=this.primary).destroy)==null||t.call(e),(s=(r=this.fallback).destroy)==null||s.call(r)}};export{S as FallbackSpeechEngine,g as RuntypeSpeechEngine,v as createPcmStreamPlayer,w as createWorkletPlaybackEngine};
package/dist/widget.css CHANGED
@@ -1027,7 +1027,7 @@
1027
1027
  opacity: 1;
1028
1028
  }
1029
1029
 
1030
- /* Hide tooltips on touch devices hover doesn't exist, and they cause clipping issues */
1030
+ /* Hide tooltips on touch devices: hover doesn't exist, and they cause clipping issues */
1031
1031
  @media (hover: none), (max-width: 500px) {
1032
1032
  .persona-send-button-wrapper:hover .persona-send-button-tooltip,
1033
1033
  .persona-send-button-wrapper:focus-within .persona-send-button-tooltip {
@@ -1144,7 +1144,7 @@
1144
1144
  animation: persona-voice-processing-spin 1.2s linear infinite;
1145
1145
  }
1146
1146
 
1147
- /* Voice speaking animation (gentle pulse slower/subtler than recording) */
1147
+ /* Voice speaking animation (gentle pulse: slower/subtler than recording) */
1148
1148
  @keyframes persona-voice-speaking-pulse {
1149
1149
  0%, 100% { opacity: 1; transform: scale(1); }
1150
1150
  50% { opacity: 0.85; transform: scale(1.03); }
@@ -1759,7 +1759,7 @@
1759
1759
  animation: persona-message-actions-fade-in 0.3s ease-out forwards;
1760
1760
  }
1761
1761
 
1762
- /* ask_user_question collapsed answered state.
1762
+ /* ask_user_question: collapsed answered state.
1763
1763
  * When the user picks an option, the interactive card is replaced with a
1764
1764
  * plain assistant bubble showing the question text. A short fade + slight
1765
1765
  * upward slide sells the "card resolved into history" feel.
@@ -1948,6 +1948,18 @@
1948
1948
  opacity: 0.9;
1949
1949
  }
1950
1950
 
1951
+ /* Read-aloud loading state: spin the loader icon while audio is prepared
1952
+ (near-instant for browser TTS; visible for a hosted/server engine fetch). */
1953
+ .persona-message-action-btn.persona-message-action-loading svg {
1954
+ animation: persona-voice-processing-spin 1s linear infinite;
1955
+ }
1956
+
1957
+ @media (prefers-reduced-motion: reduce) {
1958
+ .persona-message-action-btn.persona-message-action-loading svg {
1959
+ animation: none;
1960
+ }
1961
+ }
1962
+
1951
1963
  /* Vote pop animation */
1952
1964
  @keyframes persona-vote-pop {
1953
1965
  0% { transform: scale(1); }
@@ -2228,7 +2240,7 @@
2228
2240
  background-color: var(--cw-container, #f8fafc);
2229
2241
  }
2230
2242
 
2231
- /* Collapsed live preview blocks match expanded content's 1px border-top with transparent border */
2243
+ /* Collapsed live preview blocks: match expanded content's 1px border-top with transparent border */
2232
2244
  [data-persona-root] [data-persona-collapsed-preview] {
2233
2245
  border-top: 1px solid transparent;
2234
2246
  }
@@ -2380,7 +2392,7 @@
2380
2392
  }
2381
2393
 
2382
2394
  /* ============================================
2383
- Visibility (Tailwind parity prefix persona-)
2395
+ Visibility (Tailwind parity: prefix persona-)
2384
2396
  Shipped widget.css is hand-maintained; these utilities are used in TS but not generated by Tailwind at build time.
2385
2397
  ============================================ */
2386
2398
  [data-persona-root] .persona-hidden {
@@ -2402,7 +2414,7 @@
2402
2414
  gap: var(--persona-artifact-split-gap, 0.5rem);
2403
2415
  }
2404
2416
 
2405
- /* Document toolbar preset optional layout vars: --persona-artifact-doc-toolbar-icon-color, etc. */
2417
+ /* Document toolbar preset: optional layout vars: --persona-artifact-doc-toolbar-icon-color, etc. */
2406
2418
  [data-persona-root] .persona-artifact-toolbar-document .persona-artifact-doc-icon-btn {
2407
2419
  display: inline-flex;
2408
2420
  align-items: center;
@@ -2495,7 +2507,7 @@
2495
2507
 
2496
2508
  /* ── Composable button utilities ── */
2497
2509
 
2498
- /* Icon button base for all icon-only buttons created by createIconButton() */
2510
+ /* Icon button: base for all icon-only buttons created by createIconButton() */
2499
2511
  [data-persona-root] .persona-icon-btn {
2500
2512
  display: inline-flex;
2501
2513
  align-items: center;
@@ -2525,7 +2537,7 @@
2525
2537
  border-color: var(--persona-icon-btn-active-border, var(--persona-border, #e5e7eb));
2526
2538
  }
2527
2539
 
2528
- /* Label button icon + text button created by createLabelButton() */
2540
+ /* Label button: icon + text button created by createLabelButton() */
2529
2541
  [data-persona-root] .persona-label-btn {
2530
2542
  display: inline-flex;
2531
2543
  align-items: center;
@@ -2644,7 +2656,7 @@
2644
2656
  outline-offset: 2px;
2645
2657
  }
2646
2658
 
2647
- /* Toggle group mutually exclusive button set created by createToggleGroup() */
2659
+ /* Toggle group: mutually exclusive button set created by createToggleGroup() */
2648
2660
  [data-persona-root] .persona-toggle-group {
2649
2661
  display: inline-flex;
2650
2662
  gap: var(--persona-toggle-group-gap, 0);
@@ -2664,7 +2676,7 @@
2664
2676
  border-bottom-right-radius: var(--persona-toggle-group-radius, var(--persona-icon-btn-radius, var(--persona-radius-md, 0.375rem)));
2665
2677
  }
2666
2678
 
2667
- /* Combo button label + chevron with dropdown */
2679
+ /* Combo button: label + chevron with dropdown */
2668
2680
  [data-persona-root] .persona-combo-btn {
2669
2681
  font-size: var(--persona-combo-btn-font-size, 0.8125rem);
2670
2682
  font-weight: var(--persona-combo-btn-font-weight, 600);
@@ -2758,7 +2770,7 @@
2758
2770
  }
2759
2771
  }
2760
2772
 
2761
- /* paneBorderRadius works on any paneAppearance; overflow clips content to rounded shape */
2773
+ /* paneBorderRadius: works on any paneAppearance; overflow clips content to rounded shape */
2762
2774
  [data-persona-root] .persona-artifact-pane {
2763
2775
  border-radius: var(--persona-artifact-pane-radius, 0);
2764
2776
  overflow: hidden;
@@ -2769,7 +2781,7 @@
2769
2781
  );
2770
2782
  }
2771
2783
 
2772
- /* paneAppearance: 'seamless' flush with chat, no border/shadow/gap */
2784
+ /* paneAppearance: 'seamless': flush with chat, no border/shadow/gap */
2773
2785
  /* `position: relative` for resizer overlay; gap 0; positioning is applied in ui.ts */
2774
2786
  [data-persona-root].persona-artifact-appearance-seamless .persona-artifact-split-root {
2775
2787
  position: relative;
@@ -2787,7 +2799,7 @@
2787
2799
  );
2788
2800
  }
2789
2801
 
2790
- /* layout.paneBorder / paneBorderLeft theme overrides (after appearance defaults) */
2802
+ /* layout.paneBorder / paneBorderLeft: theme overrides (after appearance defaults) */
2791
2803
  [data-persona-root].persona-artifact-border-full .persona-artifact-pane {
2792
2804
  border: var(--persona-artifact-pane-border) !important;
2793
2805
  }
@@ -2869,7 +2881,7 @@
2869
2881
  }
2870
2882
 
2871
2883
  /* ============================================================
2872
- Stream animations reveal effects for assistant message text
2884
+ Stream animations: reveal effects for assistant message text
2873
2885
  while streaming. Opt-in via `features.streamAnimation.type`.
2874
2886
  Units are staggered via `--char-index` / `--word-index`
2875
2887
  (set inline on each wrapper span). Timing is configured via
@@ -2879,7 +2891,7 @@
2879
2891
 
2880
2892
  /* Per-char/per-word spans need to be inline-block so `transform` works for
2881
2893
  the rise/fade animations. Each span animates from the moment it is first
2882
- added to the DOM streaming itself provides the visible stagger, so the
2894
+ added to the DOM: streaming itself provides the visible stagger, so the
2883
2895
  CSS animation has no per-index delay. An index-based delay would compound
2884
2896
  with the stream's arrival cadence and leave later chars permanently hidden. */
2885
2897
  [data-persona-root] .persona-stream-char,
@@ -2925,7 +2937,7 @@
2925
2937
  ease-out both;
2926
2938
  }
2927
2939
 
2928
- /* The following animations live in subpath plugin modules their CSS is
2940
+ /* The following animations live in subpath plugin modules: their CSS is
2929
2941
  injected by the plugin when activated, not by the core stylesheet:
2930
2942
  - `wipe` → @runtypelabs/persona/animations/wipe
2931
2943
  - `glyph-cycle` → @runtypelabs/persona/animations/glyph-cycle */
@@ -2955,6 +2967,13 @@
2955
2967
  animation: persona-stream-blink 1s steps(1) infinite;
2956
2968
  }
2957
2969
 
2970
+ /* ---------- inline timestamp (trails the last line of message text) ---------- */
2971
+ [data-persona-root] .persona-timestamp-inline {
2972
+ display: inline-block;
2973
+ margin-left: 0.5rem;
2974
+ white-space: nowrap;
2975
+ }
2976
+
2958
2977
  /* ---------- skeleton placeholder (pre-first-token) ---------- */
2959
2978
  @keyframes persona-stream-skeleton-shimmer {
2960
2979
  0% { background-position: 200% 0; }
@@ -3002,7 +3021,7 @@
3002
3021
  }
3003
3022
 
3004
3023
  /* ========================================================================
3005
- * ask_user_question built-in answer-pill sheet
3024
+ * ask_user_question: built-in answer-pill sheet
3006
3025
  * Slides in over the composer when the assistant invokes `ask_user_question`.
3007
3026
  * Overridable via `config.features.askUserQuestion.styles`.
3008
3027
  * ======================================================================== */
@@ -3011,7 +3030,7 @@
3011
3030
  display: none !important;
3012
3031
  }
3013
3032
 
3014
- /* In-transcript stub small "Awaiting…" chip. */
3033
+ /* In-transcript stub: small "Awaiting…" chip. */
3015
3034
  [data-persona-root] .persona-ask-stub {
3016
3035
  padding: 0.35rem 0.7rem;
3017
3036
  border-radius: 999px;
@@ -3025,7 +3044,7 @@
3025
3044
  font-weight: 500;
3026
3045
  }
3027
3046
 
3028
- /* Sheet container absolute inside composerOverlay. */
3047
+ /* Sheet container: absolute inside composerOverlay. */
3029
3048
  [data-persona-root] .persona-ask-sheet {
3030
3049
  position: absolute;
3031
3050
  left: 0.75rem;
@@ -3053,7 +3072,7 @@
3053
3072
  pointer-events: none;
3054
3073
  }
3055
3074
 
3056
- /* Header row question + close button */
3075
+ /* Header row: question + close button */
3057
3076
  [data-persona-root] .persona-ask-sheet-header {
3058
3077
  margin-bottom: 0.55rem;
3059
3078
  }
@@ -3094,7 +3113,7 @@
3094
3113
  display: none;
3095
3114
  }
3096
3115
 
3097
- /* Option list stacked one per row by default ("rows" layout). The "pills"
3116
+ /* Option list: stacked one per row by default ("rows" layout). The "pills"
3098
3117
  layout opt-in switches to horizontal wrap with compact pills; see also
3099
3118
  horizontalPillsAskPlugin example for a custom variant. */
3100
3119
  [data-persona-root] .persona-ask-pills {
@@ -3111,7 +3130,7 @@
3111
3130
  gap: 0.4rem;
3112
3131
  }
3113
3132
 
3114
- /* Pills layout opt-in horizontal wrap, auto-width compact pills. */
3133
+ /* Pills layout opt-in: horizontal wrap, auto-width compact pills. */
3115
3134
  [data-persona-root] .persona-ask-sheet--pills .persona-ask-pills:not(.persona-ask-pills--rows) {
3116
3135
  flex-direction: row;
3117
3136
  flex-wrap: wrap;
@@ -3188,7 +3207,7 @@
3188
3207
  100% { background-position: -100% 50%; }
3189
3208
  }
3190
3209
 
3191
- /* Row layout full-width stacked rows with always-visible description
3210
+ /* Row layout: full-width stacked rows with always-visible description
3192
3211
  and a right-edge affordance (number badge for single-select, check for
3193
3212
  multi-select). Layered on top of .persona-ask-pill base styles. */
3194
3213
  [data-persona-root] .persona-ask-row {
@@ -3291,7 +3310,7 @@
3291
3310
  height: 3rem;
3292
3311
  }
3293
3312
 
3294
- /* Other row (rows mode) composite row that contains the free-text input
3313
+ /* Other row (rows mode): composite row that contains the free-text input
3295
3314
  * directly. The input fills the row body; the badge sits on the right. */
3296
3315
  [data-persona-root] .persona-ask-row--other {
3297
3316
  cursor: text;
@@ -3358,7 +3377,7 @@
3358
3377
  cursor: not-allowed;
3359
3378
  }
3360
3379
 
3361
- /* Nav row Back / Skip / Next-or-Submit buttons for grouped payloads. */
3380
+ /* Nav row: Back / Skip / Next-or-Submit buttons for grouped payloads. */
3362
3381
  [data-persona-root] .persona-ask-nav-back,
3363
3382
  [data-persona-root] .persona-ask-nav-skip {
3364
3383
  padding: 0.45rem 0.85rem;
@@ -3416,11 +3435,11 @@
3416
3435
  * `applyComposerBarGeometry()` in ui.ts. The pill composer
3417
3436
  * (`pill-composer-builder.ts`) ships with its own clean className
3418
3437
  * (`persona-pill-composer`, no `persona-flex-col` / `persona-rounded-2xl`
3419
- * baggage), so layout CSS does not need to fight utility classes no
3438
+ * baggage), so layout CSS does not need to fight utility classes: no
3420
3439
  * !important. The pill is the visible element in both collapsed and
3421
3440
  * expanded states; only the panel above it appears/disappears.
3422
3441
  *
3423
- * Avoid `transform: scale(...)` on the wrapper it breaks the textarea
3442
+ * Avoid `transform: scale(...)` on the wrapper: it breaks the textarea
3424
3443
  * caret/IME.
3425
3444
  * ======================================================================= */
3426
3445
  .persona-widget-wrapper[data-persona-composer-bar] {
@@ -3446,14 +3465,14 @@
3446
3465
  * so `top/left/transform` resolve to their auto/none defaults) to its
3447
3466
  * expanded position. The default `transform 220ms ease` transition would
3448
3467
  * interpolate `none → translate(...)` and visibly slide the wrapper from
3449
- * its static-default origin toward its target diagonally for modal,
3468
+ * its static-default origin toward its target: diagonally for modal,
3450
3469
  * horizontally from the right for anchored. Neither mode is morphing
3451
3470
  * something the user can see (the wrapper is invisible when collapsed
3452
3471
  * because the pill lives in pillRoot, not the wrapper), so the slide
3453
3472
  * is pure motion noise. Disable geometry transitions for both; the
3454
3473
  * container's opacity fade-in keyframe is the reveal.
3455
3474
  *
3456
- * Fullscreen intentionally keeps its geometry transition that's the
3475
+ * Fullscreen intentionally keeps its geometry transition: that's the
3457
3476
  * one mode where the wrapper genuinely morphs (empty → full viewport),
3458
3477
  * and the staggered fade-in cascade below is built to mask the
3459
3478
  * outer-edge/inner-content desync during that morph. */
@@ -3489,7 +3508,7 @@
3489
3508
  padding: 4px 0;
3490
3509
  }
3491
3510
 
3492
- /* Responsive default width for the collapsed pill applied only when
3511
+ /* Responsive default width for the collapsed pill: applied only when
3493
3512
  * `applyComposerBarGeometry()` leaves the wrapper's inline width empty
3494
3513
  * (i.e., the user did NOT set `composerBar.collapsedMaxWidth`). Inline
3495
3514
  * width from a user config still wins via specificity (inline > class). */
@@ -3543,8 +3562,7 @@
3543
3562
  * In composer-bar mode the pill (`footer`) and peek banner live in a
3544
3563
  * viewport-fixed sibling of the wrapper, NOT inside the wrapper. Reason:
3545
3564
  * modal mode applies `transform: translate(-50%, -50%)` to the wrapper,
3546
- * which establishes a containing block for `position: fixed` descendants
3547
- * a fixed pill inside a transformed wrapper would be positioned relative
3565
+ * which establishes a containing block for `position: fixed` descendants: * a fixed pill inside a transformed wrapper would be positioned relative
3548
3566
  * to the wrapper, not the viewport.
3549
3567
  *
3550
3568
  * Bottom offset and (optionally) width are written inline by ui.ts based
@@ -3574,7 +3592,7 @@
3574
3592
  }
3575
3593
  }
3576
3594
 
3577
- /* Container hidden when the chat is collapsed only the pill is visible. */
3595
+ /* Container hidden when the chat is collapsed: only the pill is visible. */
3578
3596
  [data-persona-composer-bar][data-state="collapsed"] .persona-widget-container {
3579
3597
  display: none;
3580
3598
  }
@@ -3614,7 +3632,7 @@
3614
3632
  transform 150ms ease,
3615
3633
  background-color 150ms ease;
3616
3634
  text-align: left;
3617
- /* Full-width row that matches the pill below text takes the middle
3635
+ /* Full-width row that matches the pill below: text takes the middle
3618
3636
  * (flex: 1 1 auto) and the chevron pins to the right edge. */
3619
3637
  width: 100%;
3620
3638
  align-self: stretch;
@@ -3700,7 +3718,7 @@
3700
3718
  * (including the area behind the fixed pill) so messages scroll under the
3701
3719
  * pill. The body itself drops its bottom padding so its background extends
3702
3720
  * to the viewport edge; reachability for the last bubble comes from
3703
- * padding-bottom on the messages wrapper (body's last child) that
3721
+ * padding-bottom on the messages wrapper (body's last child): that
3704
3722
  * pushes the final message above the pill area rather than hiding it
3705
3723
  * behind. */
3706
3724
  .persona-widget-wrapper[data-persona-composer-bar][data-expanded-size="fullscreen"] .persona-widget-body {
@@ -3726,7 +3744,7 @@
3726
3744
  * so the box appears to materialize as it grows rather than snap into
3727
3745
  * existence at frame 0.
3728
3746
  * - Body content (intro card, messages, composer overlay) fades in AFTER
3729
- * the geometry has settled `animation-delay: 220ms` plus
3747
+ * the geometry has settled: `animation-delay: 220ms` plus
3730
3748
  * `animation-fill-mode: both` keeps it at opacity 0 during the morph.
3731
3749
  *
3732
3750
  * Because `[data-state="collapsed"] .persona-widget-container { display: