@runtypelabs/persona 4.25.0 → 4.26.1

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 (236) hide show
  1. package/dist/animations/glyph-cycle.d.cts +1 -1
  2. package/dist/animations/glyph-cycle.d.ts +1 -1
  3. package/dist/animations/{types-Db61aI9J.d.cts → types-BwKTZhg9.d.cts} +14 -0
  4. package/dist/animations/{types-Db61aI9J.d.ts → types-BwKTZhg9.d.ts} +14 -0
  5. package/dist/animations/wipe.d.cts +1 -1
  6. package/dist/animations/wipe.d.ts +1 -1
  7. package/dist/animations-extra.d.cts +14 -0
  8. package/dist/animations-extra.d.ts +14 -0
  9. package/dist/approval-ui-QAKWNFNM.js +1 -0
  10. package/dist/approval-ui.cjs +2 -2
  11. package/dist/approval-ui.d.cts +186 -8
  12. package/dist/approval-ui.d.ts +186 -8
  13. package/dist/approval-ui.js +2 -2
  14. package/dist/{artifacts-ui-IGB5YKLO.js → artifacts-ui-NJGMM27I.js} +1 -1
  15. package/dist/artifacts-ui.cjs +4 -4
  16. package/dist/artifacts-ui.d.cts +186 -8
  17. package/dist/artifacts-ui.d.ts +186 -8
  18. package/dist/artifacts-ui.js +5 -5
  19. package/dist/chunk-2ANTRKTV.js +1 -0
  20. package/dist/chunk-4HPTPUC6.js +1 -0
  21. package/dist/chunk-6V3MNEKB.js +1 -0
  22. package/dist/chunk-GKPCIQPB.js +1 -0
  23. package/dist/chunk-HGUCGM3K.js +9 -0
  24. package/dist/chunk-IO5VVUKP.js +3 -0
  25. package/dist/chunk-J4J3Y2P2.js +1 -0
  26. package/dist/chunk-K6HKIP5K.js +1 -0
  27. package/dist/chunk-K7NA7G7J.js +1 -0
  28. package/dist/chunk-MXUTS3LB.js +1 -0
  29. package/dist/chunk-PDPVORUF.js +1 -0
  30. package/dist/chunk-QRKGQPU7.js +1 -0
  31. package/dist/chunk-S6PBWE75.js +1 -0
  32. package/dist/chunk-T7QM7YMF.js +1 -0
  33. package/dist/chunk-TR2H475E.js +1 -0
  34. package/dist/chunk-UPO4GUFC.js +1 -0
  35. package/dist/chunk-ZC66NAIR.js +1 -0
  36. package/dist/client-history-GOZWBDA5.js +1 -0
  37. package/dist/client-history.js +1 -0
  38. package/dist/client-stream-42FOI3EC.js +4 -0
  39. package/dist/client-stream.js +5 -0
  40. package/dist/codegen.cjs +1 -1
  41. package/dist/codegen.js +1 -1
  42. package/dist/context-mentions-E7HJYWO6.js +161 -0
  43. package/dist/{context-mentions-inline-I2RDEBAK.js → context-mentions-inline-QVESLW55.js} +1 -1
  44. package/dist/context-mentions.cjs +7 -7
  45. package/dist/context-mentions.d.cts +186 -8
  46. package/dist/context-mentions.d.ts +186 -8
  47. package/dist/context-mentions.js +8 -8
  48. package/dist/event-stream-view-FOIJHFQO.js +1 -0
  49. package/dist/event-stream-view.cjs +1 -1
  50. package/dist/event-stream-view.d.cts +189 -31
  51. package/dist/event-stream-view.d.ts +189 -31
  52. package/dist/event-stream-view.js +1 -1
  53. package/dist/forms-ui.d.cts +227 -667
  54. package/dist/forms-ui.d.ts +227 -667
  55. package/dist/history-shell-QDQ44PSE.js +3 -0
  56. package/dist/history-shell.js +3 -0
  57. package/dist/{history-view-BD6OXLNA.js → history-view-NCFBI5GR.js} +2 -2
  58. package/dist/history-view.cjs +2 -2
  59. package/dist/history-view.d.cts +35 -1
  60. package/dist/history-view.d.ts +35 -1
  61. package/dist/history-view.js +2 -2
  62. package/dist/index.cjs +80 -74
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +233 -738
  65. package/dist/index.d.ts +233 -738
  66. package/dist/index.global.js +3 -215
  67. package/dist/index.global.js.map +1 -1
  68. package/dist/index.js +81 -75
  69. package/dist/index.js.map +1 -1
  70. package/dist/install.global.js +1 -1
  71. package/dist/install.global.js.map +1 -1
  72. package/dist/launcher.global.js +3 -67
  73. package/dist/launcher.global.js.map +1 -1
  74. package/dist/markdown-parsers.js +22 -22
  75. package/dist/runtype-tts-entry-NFBVWR7N.js +1 -0
  76. package/dist/runtype-tts.js +1 -1
  77. package/dist/session-actions-ISSKSI66.js +1 -0
  78. package/dist/session-actions.js +1 -0
  79. package/dist/session-reconnect.d.cts +186 -8
  80. package/dist/session-reconnect.d.ts +186 -8
  81. package/dist/smart-dom-reader.cjs +13 -13
  82. package/dist/smart-dom-reader.d.cts +186 -8
  83. package/dist/smart-dom-reader.d.ts +186 -8
  84. package/dist/smart-dom-reader.js +11 -11
  85. package/dist/theme-editor-preview.cjs +116 -98
  86. package/dist/theme-editor-preview.d.cts +188 -8
  87. package/dist/theme-editor-preview.d.ts +188 -8
  88. package/dist/theme-editor-preview.js +49 -54
  89. package/dist/theme-editor.cjs +2 -2
  90. package/dist/theme-editor.d.cts +187 -23
  91. package/dist/theme-editor.d.ts +187 -23
  92. package/dist/theme-editor.js +2 -2
  93. package/dist/theme-reference.d.cts +14 -0
  94. package/dist/theme-reference.d.ts +14 -0
  95. package/dist/ui-extras-entry-WAYFKVNW.js +3 -0
  96. package/dist/ui-extras.js +72 -0
  97. package/dist/voice-runtime-XLU6YK2N.js +17 -0
  98. package/dist/voice-runtime.cjs +17 -1
  99. package/dist/voice-runtime.d.cts +11656 -232
  100. package/dist/voice-runtime.d.ts +11656 -232
  101. package/dist/voice-runtime.js +17 -1
  102. package/dist/voice-worklet-player.cjs +19 -4
  103. package/dist/voice-worklet-player.d.cts +9 -10
  104. package/dist/voice-worklet-player.d.ts +9 -10
  105. package/dist/voice-worklet-player.js +19 -4
  106. package/dist/webmcp-polyfill.js +1 -1
  107. package/dist/webmcp-runtime-V6ZWW3PI.js +6 -0
  108. package/dist/webmcp-runtime.cjs +2 -2
  109. package/dist/webmcp-runtime.d.cts +1 -33
  110. package/dist/webmcp-runtime.d.ts +1 -33
  111. package/dist/webmcp-runtime.js +2 -2
  112. package/dist/widget.css +1 -1
  113. package/package.json +11 -5
  114. package/src/approval-paused-tool-settle.test.ts +325 -0
  115. package/src/client-approve.test.ts +311 -0
  116. package/src/client-history-eager.ts +9 -0
  117. package/src/client-history-loader.ts +23 -0
  118. package/src/client-history.ts +579 -0
  119. package/src/client-resume.ts +177 -0
  120. package/src/client-stream-eager.ts +12 -0
  121. package/src/client-stream-loader.ts +25 -0
  122. package/src/client-stream.ts +1880 -0
  123. package/src/client.test.ts +31 -1
  124. package/src/client.ts +293 -2607
  125. package/src/components/approval-actions.test.ts +27 -0
  126. package/src/components/approval-actions.ts +27 -3
  127. package/src/components/message-bubble.test.ts +19 -0
  128. package/src/components/message-bubble.ts +2 -0
  129. package/src/components/registry.ts +6 -6
  130. package/src/components/tool-bubble.test.ts +9 -0
  131. package/src/components/tool-bubble.ts +1 -0
  132. package/src/history-shell-bundle.test.ts +54 -0
  133. package/src/history-shell-eager.ts +12 -0
  134. package/src/history-shell-loader.ts +27 -0
  135. package/src/history-shell.ts +2362 -0
  136. package/src/history-view-loader.ts +2 -0
  137. package/src/history-view.ts +3 -0
  138. package/src/index-core.ts +6 -0
  139. package/src/index-global.ts +90 -248
  140. package/src/index.ts +13 -0
  141. package/src/install.ts +6 -4
  142. package/src/internal/runtype-history-provider.test.ts +1 -0
  143. package/src/internal/runtype-history-provider.ts +28 -13
  144. package/src/plugins/registry.ts +8 -8
  145. package/src/session-actions-chunk.test.ts +55 -0
  146. package/src/session-actions-eager.ts +11 -0
  147. package/src/session-actions-loader.ts +37 -0
  148. package/src/session-actions.ts +909 -0
  149. package/src/session-history.test.ts +3 -1
  150. package/src/session.ts +895 -1538
  151. package/src/session.voice.test.ts +1171 -5
  152. package/src/styles/widget.css +58 -0
  153. package/src/theme-editor/state.ts +91 -91
  154. package/src/types.ts +204 -5
  155. package/src/ui-ask-user-sheet.ts +382 -0
  156. package/src/ui-extras-eager.ts +10 -0
  157. package/src/ui-extras-entry.ts +9 -0
  158. package/src/ui-extras-loader.ts +26 -0
  159. package/src/ui.artifact-rail-resize.test.ts +2 -1
  160. package/src/ui.ask-user-chunk.test.ts +93 -0
  161. package/src/ui.history-chunk.test.ts +2 -1
  162. package/src/ui.history-config-provider.test.ts +2 -1
  163. package/src/ui.history-confirm-fallback.test.ts +142 -0
  164. package/src/ui.history-controller.test.ts +2 -1
  165. package/src/ui.history-lifecycle.test.ts +2 -1
  166. package/src/ui.history-render-hooks.test.ts +2 -1
  167. package/src/ui.history-shell.test.ts +2 -1
  168. package/src/ui.rail-overlay.test.ts +2 -1
  169. package/src/ui.rail-resize.test.ts +2 -1
  170. package/src/ui.rail-trigger-pending.test.ts +2 -1
  171. package/src/ui.ts +340 -2770
  172. package/src/utils/attachment-manager.ts +67 -67
  173. package/src/utils/constants.ts +3 -0
  174. package/src/utils/context-mention-controller.ts +323 -323
  175. package/src/utils/context-mention-manager.ts +40 -40
  176. package/src/utils/context-mention-orchestrator.ts +28 -0
  177. package/src/utils/event-stream-buffer.ts +50 -50
  178. package/src/utils/event-stream-store.ts +43 -43
  179. package/src/utils/throughput-tracker.ts +34 -34
  180. package/src/utils/virtual-scroller.ts +67 -67
  181. package/src/utils/visitor-store.test.ts +50 -0
  182. package/src/utils/visitor-store.ts +23 -5
  183. package/src/voice/__fixtures__/voice-wire/README.md +1 -0
  184. package/src/voice/__fixtures__/voice-wire/client/cancel.json +3 -0
  185. package/src/voice/__fixtures__/voice-wire/client/context.json +4 -0
  186. package/src/voice/__fixtures__/voice-wire/client/delegation_result.json +6 -0
  187. package/src/voice/__fixtures__/voice-wire/client/delegation_update.json +6 -0
  188. package/src/voice/__fixtures__/voice-wire/client/ping.json +3 -0
  189. package/src/voice/__fixtures__/voice-wire/client/playback_progress.json +5 -0
  190. package/src/voice/__fixtures__/voice-wire/client/start.json +3 -0
  191. package/src/voice/__fixtures__/voice-wire/server/artifact.json +11 -0
  192. package/src/voice/__fixtures__/voice-wire/server/attached.json +4 -0
  193. package/src/voice/__fixtures__/voice-wire/server/audio_clear.json +4 -0
  194. package/src/voice/__fixtures__/voice-wire/server/audio_end.json +3 -0
  195. package/src/voice/__fixtures__/voice-wire/server/delegation_cancelled.json +5 -0
  196. package/src/voice/__fixtures__/voice-wire/server/delegation_completed.json +8 -0
  197. package/src/voice/__fixtures__/voice-wire/server/delegation_started.json +15 -0
  198. package/src/voice/__fixtures__/voice-wire/server/error.json +6 -0
  199. package/src/voice/__fixtures__/voice-wire/server/metrics.json +10 -0
  200. package/src/voice/__fixtures__/voice-wire/server/pong.json +3 -0
  201. package/src/voice/__fixtures__/voice-wire/server/session_config.json +16 -0
  202. package/src/voice/__fixtures__/voice-wire/server/transcript_final.json +7 -0
  203. package/src/voice/__fixtures__/voice-wire/server/transcript_interim.json +4 -0
  204. package/src/voice/__fixtures__/voice-wire/server/transcript_partial.json +6 -0
  205. package/src/voice/__fixtures__/voice-wire/server/transcript_update.json +10 -0
  206. package/src/voice/__fixtures__/voice-wire/server/warning.json +5 -0
  207. package/src/voice/audio-playback-manager.test.ts +57 -0
  208. package/src/voice/audio-playback-manager.ts +120 -98
  209. package/src/voice/browser-voice-provider.ts +37 -37
  210. package/src/voice/fallback-speech-engine.ts +22 -16
  211. package/src/voice/keyed-voice-transcript.ts +314 -0
  212. package/src/voice/mic-state-styles.ts +182 -0
  213. package/src/voice/read-aloud-controller.ts +46 -44
  214. package/src/voice/runtype-speech-engine.ts +35 -33
  215. package/src/voice/runtype-voice-provider.ts +680 -189
  216. package/src/voice/session-voice-wiring.ts +266 -0
  217. package/src/voice/voice-delegation.test.ts +71 -0
  218. package/src/voice/voice-delegation.ts +364 -0
  219. package/src/voice/voice-turn-tracker.ts +6 -6
  220. package/src/voice/voice.test.ts +1060 -8
  221. package/src/voice/worklet-playback-engine.test.ts +81 -0
  222. package/src/voice/worklet-playback-engine.ts +21 -3
  223. package/src/voice-runtime-loader.ts +4 -1
  224. package/src/voice-runtime.ts +7 -0
  225. package/src/webmcp-runtime-entry.ts +41 -39
  226. package/dist/approval-ui-3NMYNXBD.js +0 -1
  227. package/dist/chunk-5EIIHQLQ.js +0 -1
  228. package/dist/chunk-7CO4T5KB.js +0 -1
  229. package/dist/chunk-EFJBUH5F.js +0 -1
  230. package/dist/chunk-R2TMIMDI.js +0 -3
  231. package/dist/chunk-UTVLOTWB.js +0 -9
  232. package/dist/context-mentions-45MODWOY.js +0 -161
  233. package/dist/event-stream-view-GL3ADRY2.js +0 -1
  234. package/dist/runtype-tts-entry-UJAEF7NZ.js +0 -1
  235. package/dist/voice-runtime-7DA7MJH4.js +0 -1
  236. package/dist/webmcp-runtime-JI7T4QYF.js +0 -6
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Session-side voice wiring: attaches a freshly constructed provider's
3
+ * callbacks (transcripts, status, level, errors) to an `AgentWidgetSession`.
4
+ *
5
+ * Lives in the lazy voice-runtime chunk (exported from `voice-runtime.ts`),
6
+ * not in `session.ts`: it only runs after that chunk resolves in
7
+ * `setupVoice`, so pages without voice never download it. The session hands
8
+ * over its private voice state through a `VoiceWiringHost`.
9
+ */
10
+ import type { AgentWidgetSession } from "../session";
11
+ import type { AgentWidgetMessage, VoiceProvider, VoiceStatus } from "../types";
12
+ import type { KeyedVoiceTranscript } from "./keyed-voice-transcript";
13
+ import type { VoiceDelegationCapture, createVoiceSessionBridge } from "./voice-delegation";
14
+ import type { VoiceTurnTracker } from "./voice-turn-tracker";
15
+
16
+ /** The session members the wiring touches (some are TS-private on the class). */
17
+ export type VoiceWiringSession = Pick<
18
+ AgentWidgetSession,
19
+ "injectMessage" | "sendMessage" | "resolveWebMcpApproval" | "resolveApproval"
20
+ > & {
21
+ messages: AgentWidgetMessage[];
22
+ callbacks: AgentWidgetSession["callbacks"];
23
+ config: AgentWidgetSession["config"];
24
+ streaming: boolean;
25
+ ttsSpokenMessageIds: Set<string>;
26
+ webMcpApprovalResolvers: Map<string, unknown>;
27
+ upsertMessage: (message: AgentWidgetMessage) => void;
28
+ speakLatestAssistantMessage: (only?: string[]) => void;
29
+ };
30
+
31
+ /** Accessors for the session's `#private` voice state. */
32
+ export type VoiceWiringHost = {
33
+ session: VoiceWiringSession;
34
+ provider: () => VoiceProvider | null;
35
+ generation: () => number;
36
+ keyed: KeyedVoiceTranscript | null;
37
+ pendingUser: string | null;
38
+ pendingAssistant: string | null;
39
+ turns: () => VoiceTurnTracker;
40
+ busy: () => boolean;
41
+ setStreaming: (streaming: boolean) => void;
42
+ setDelegation: (capture: VoiceDelegationCapture | null) => void;
43
+ setLevel: (level: number) => void;
44
+ setStatus: (status: VoiceStatus) => void;
45
+ settlePending: (final: boolean) => void;
46
+ };
47
+
48
+ /** Wire callbacks onto a freshly constructed provider and connect it. */
49
+ export const wireSessionVoice = (
50
+ p: VoiceWiringHost,
51
+ provider: VoiceProvider,
52
+ Keyed?: typeof KeyedVoiceTranscript,
53
+ createBridge?: typeof createVoiceSessionBridge
54
+ ): void => {
55
+ const s = p.session;
56
+ try {
57
+ p.keyed = Keyed
58
+ ? new Keyed({
59
+ find: (id) => s.messages.find((m) => m.id === id),
60
+ inject: (options) => s.injectMessage(options),
61
+ upsert: (message) => s.upsertMessage(message),
62
+ settle: (ids) => {
63
+ s.messages = s.messages.map((m) =>
64
+ ids.has(m.id) ? { ...m, streaming: false, voiceProcessing: false } : m
65
+ );
66
+ s.callbacks.onMessagesChanged([...s.messages]);
67
+ },
68
+ // A chat turn (e.g. a delegated one) owns the flag while it runs.
69
+ setStreaming: (streaming) => {
70
+ if (streaming || !p.busy()) p.setStreaming(streaming);
71
+ },
72
+ markSpoken: (id) => {
73
+ s.ttsSpokenMessageIds.add(id);
74
+ }
75
+ })
76
+ : null;
77
+ if (createBridge && provider.setSessionBridge) {
78
+ provider.setSessionBridge(
79
+ createBridge({
80
+ messages: () => s.messages,
81
+ busy: () => p.busy(),
82
+ parked: () => s.webMcpApprovalResolvers.size > 0,
83
+ claim: (text, userUtteranceIds) => p.keyed?.claimUserTurn(text, userUtteranceIds) ?? null,
84
+ send: (text, userMessageId) =>
85
+ s.sendMessage(text, { viaVoice: true, voiceTurn: { userMessageId } }),
86
+ track: (capture) => {
87
+ p.setDelegation(capture);
88
+ },
89
+ unspoken: (ids) => {
90
+ for (const id of ids) s.ttsSpokenMessageIds.delete(id);
91
+ // Its stream already ended (browser TTS skipped it then): read it
92
+ // now. A stream still running reads it when it ends, once.
93
+ if (!s.streaming) s.speakLatestAssistantMessage(ids);
94
+ },
95
+ decide: (id) => {
96
+ if (s.webMcpApprovalResolvers.has(id)) return s.resolveWebMcpApproval(id, 'denied');
97
+ const approval = s.messages.find((m) => m.id === id)?.approval;
98
+ if (approval?.status === 'pending') void s.resolveApproval(approval, 'denied');
99
+ }
100
+ })
101
+ );
102
+ }
103
+ const generation = p.generation();
104
+ const isCurrent = () => p.provider() === provider && generation === p.generation();
105
+
106
+ // Read configurable text from widget config
107
+ const voiceRecognitionConfig = s.config.voiceRecognition ?? {};
108
+ const processingErrorText = voiceRecognitionConfig.processingErrorText ?? 'Voice processing failed. Please try again.';
109
+
110
+ // STT-style providers (browser + bring-your-own `custom`) deliver a final
111
+ // transcript that we send as a normal user message: the agent then runs
112
+ // via the standard SSE chat path. Only the realtime `runtype` provider is
113
+ // excluded here: it owns the whole turn and drives onTranscript below.
114
+ provider.onResult((result) => {
115
+ if (!isCurrent()) return;
116
+ if (result.provider !== 'runtype') {
117
+ if (result.text && result.text.trim()) {
118
+ s.sendMessage(result.text, { viaVoice: true });
119
+ }
120
+ }
121
+ });
122
+
123
+ // Realtime (runtype) voice: drive the chat thread from streaming
124
+ // transcript frames. Live interim user text grows in place; the user
125
+ // message finalizes immediately on transcript_final{user}; the assistant
126
+ // reply lands (a single block, synced with audio) on its final frame.
127
+ // In-flight bubbles carry voiceProcessing=true so consumers can style
128
+ // them via messageTransform; it clears once the text is final.
129
+ if (provider.onTranscript) {
130
+ provider.onTranscript((role, text, isFinal, metadata) => {
131
+ if (!isCurrent()) return;
132
+ if (metadata?.turnId) {
133
+ p.keyed?.apply(role, text, isFinal, metadata.turnId, metadata.startMs, metadata.caption);
134
+ return;
135
+ }
136
+ if (role === 'user') {
137
+ if (!p.pendingUser) {
138
+ const msg = s.injectMessage({
139
+ role: 'user',
140
+ content: text,
141
+ streaming: false,
142
+ voiceProcessing: !isFinal
143
+ });
144
+ p.pendingUser = msg.id;
145
+ } else {
146
+ s.upsertMessage({
147
+ id: p.pendingUser,
148
+ role: 'user',
149
+ content: text,
150
+ createdAt: new Date().toISOString(),
151
+ streaming: false,
152
+ voiceProcessing: !isFinal
153
+ });
154
+ }
155
+
156
+ if (isFinal) {
157
+ p.turns().start(metadata?.turnId);
158
+ // User finished: the agent is now thinking. Release the user
159
+ // bubble (a new interim starts a fresh turn) and show a typing
160
+ // indicator in a fresh assistant placeholder.
161
+ p.pendingUser = null;
162
+ const assistantMsg = s.injectMessage({
163
+ role: 'assistant',
164
+ content: '',
165
+ streaming: true,
166
+ voiceProcessing: true
167
+ });
168
+ p.pendingAssistant = assistantMsg.id;
169
+ p.setStreaming(true);
170
+ }
171
+ } else {
172
+ if (!p.turns().accepts(metadata?.turnId)) return;
173
+ // assistant: runtype sends a single final; the isFinal=false path
174
+ // is reserved for delta-streaming providers (future BYO).
175
+ if (p.pendingAssistant) {
176
+ s.upsertMessage({
177
+ id: p.pendingAssistant,
178
+ role: 'assistant',
179
+ content: text,
180
+ createdAt: new Date().toISOString(),
181
+ streaming: !isFinal,
182
+ voiceProcessing: !isFinal
183
+ });
184
+ } else {
185
+ const msg = s.injectMessage({
186
+ role: 'assistant',
187
+ content: text,
188
+ streaming: !isFinal,
189
+ voiceProcessing: !isFinal
190
+ });
191
+ p.pendingAssistant = msg.id;
192
+ }
193
+
194
+ if (isFinal) {
195
+ // The provider plays this reply's audio: mark it spoken so
196
+ // browser TTS doesn't double-speak when streaming ends. Must run
197
+ // BEFORE setStreaming(false), which triggers the TTS check.
198
+ if (p.pendingAssistant) {
199
+ s.ttsSpokenMessageIds.add(p.pendingAssistant);
200
+ }
201
+ p.setStreaming(false);
202
+ p.pendingAssistant = null;
203
+ }
204
+ }
205
+ });
206
+ }
207
+
208
+ // Live capture amplitude, when the provider owns an audio graph. Stored,
209
+ // not forwarded per callback: the UI samples it on its own frame loop.
210
+ if (provider.onLevel) {
211
+ provider.onLevel((level) => {
212
+ if (!isCurrent()) return;
213
+ p.setLevel(
214
+ Number.isFinite(level) ? Math.max(0, Math.min(1, level)) : 0
215
+ );
216
+ });
217
+ }
218
+
219
+ // Surface per-turn latency metrics to the optional config hook.
220
+ if (provider.onMetrics) {
221
+ provider.onMetrics((metrics) => {
222
+ if (!isCurrent()) return;
223
+ s.config.voiceRecognition?.onMetrics?.(metrics);
224
+ });
225
+ }
226
+
227
+ provider.onError((error) => {
228
+ if (!isCurrent()) return;
229
+ console.error('Voice error:', error);
230
+
231
+ // If error occurs while placeholders are pending, update assistant with error text
232
+ if (p.pendingAssistant) {
233
+ s.upsertMessage({
234
+ id: p.pendingAssistant,
235
+ role: 'assistant',
236
+ content: processingErrorText,
237
+ createdAt: new Date().toISOString(),
238
+ streaming: false,
239
+ voiceProcessing: false
240
+ });
241
+ p.setStreaming(false);
242
+ p.pendingUser = null;
243
+ p.pendingAssistant = null;
244
+ }
245
+ p.keyed?.fail(processingErrorText);
246
+ });
247
+
248
+ provider.onStatusChange((status) => {
249
+ if (!isCurrent()) return;
250
+ p.setStatus(status);
251
+ if (status === 'listening' || status === 'idle' || status === 'disconnected') {
252
+ p.settlePending(status !== 'listening');
253
+ }
254
+ // Keyed turns overlap listening (full duplex), so only a call end settles them.
255
+ if (status === 'idle' || status === 'disconnected') {
256
+ p.keyed?.settle();
257
+ }
258
+ s.callbacks.onVoiceStatusChanged?.(status);
259
+ });
260
+
261
+ provider.connect();
262
+
263
+ } catch (error) {
264
+ console.error('Failed to setup voice:', error);
265
+ }
266
+ };
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { AgentWidgetApproval } from "../types";
3
+ import { buildApprovalScript, isVoiceDecline } from "./voice-delegation";
4
+
5
+ const approval = (extra: Partial<AgentWidgetApproval>): AgentWidgetApproval => ({
6
+ id: "ap",
7
+ status: "pending",
8
+ agentId: "a1",
9
+ executionId: "e1",
10
+ toolName: "tool",
11
+ description: "",
12
+ ...extra,
13
+ });
14
+
15
+ describe("buildApprovalScript", () => {
16
+ it("lists every pending action, humanized, with its arguments", () => {
17
+ const script = buildApprovalScript([
18
+ approval({ toolName: "webmcp:addToCart", parameters: { sku: "AB-1", quantity: 3 } }),
19
+ approval({ toolName: "send-receipt", parameters: { to: { email: "n@example.com" }, copies: [] } }),
20
+ ]);
21
+ expect(script).toBe(
22
+ "These actions need the user's approval in the chat before they happen:\n" +
23
+ "- add to cart with sku: AB-1; quantity: 3\n" +
24
+ "- send receipt with to: email n@example.com\n\n" +
25
+ "Briefly tell the user what you're about to do and ask them to approve or decline it in the chat. Don't claim it's done.",
26
+ );
27
+ });
28
+
29
+ it("stays under about 1,000 characters however much the tools carry", () => {
30
+ const big = { notes: "x".repeat(5_000), items: Array.from({ length: 200 }, (_, i) => ({ name: `item ${i}`, qty: i })) };
31
+ const script = buildApprovalScript(
32
+ Array.from({ length: 5 }, (_, i) =>
33
+ approval({ toolName: `tool_${i}`, description: "d".repeat(500), reason: "r".repeat(500), parameters: big }),
34
+ ),
35
+ );
36
+ expect(script.length).toBeLessThanOrEqual(1_000);
37
+ expect(script).toContain("- tool 4");
38
+ expect(script.endsWith("Don't claim it's done.")).toBe(true);
39
+ });
40
+ });
41
+
42
+ describe("isVoiceDecline", () => {
43
+ it.each([
44
+ "No.",
45
+ "no thanks",
46
+ "Cancel that.",
47
+ "cancel it",
48
+ "Cancel the order",
49
+ "cancel my pickup order",
50
+ "No, cancel that.",
51
+ "Don't place it.",
52
+ "don't do that",
53
+ "Don't place the pickup order",
54
+ "Never mind.",
55
+ "nevermind",
56
+ "Decline it",
57
+ " Nope ",
58
+ ])("declines on %j", (text) => expect(isVoiceDecline(text)).toBe(true));
59
+
60
+ it.each([
61
+ "yes",
62
+ "go ahead",
63
+ "no wait, make it three",
64
+ "cancel the cake and add bread",
65
+ "don't forget the coffee",
66
+ "I said no sugar",
67
+ "can you cancel it later",
68
+ "not now",
69
+ "",
70
+ ])("runs %j as a normal turn", (text) => expect(isVoiceDecline(text)).toBe(false));
71
+ });