@agent-native/toolkit 0.5.1 → 0.7.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 (349) hide show
  1. package/README.md +17 -1
  2. package/dist/chat-history/ChatHistoryList.d.ts +103 -0
  3. package/dist/chat-history/ChatHistoryList.d.ts.map +1 -0
  4. package/dist/chat-history/ChatHistoryList.js +109 -0
  5. package/dist/chat-history/ChatHistoryList.js.map +1 -0
  6. package/dist/chat-history/ChatHistoryList.spec.d.ts +2 -0
  7. package/dist/chat-history/ChatHistoryList.spec.d.ts.map +1 -0
  8. package/dist/chat-history/ChatHistoryList.spec.js +283 -0
  9. package/dist/chat-history/ChatHistoryList.spec.js.map +1 -0
  10. package/dist/chat-history/index.d.ts +2 -0
  11. package/dist/chat-history/index.d.ts.map +1 -0
  12. package/dist/chat-history/index.js +2 -0
  13. package/dist/chat-history/index.js.map +1 -0
  14. package/dist/chat-history.css +314 -0
  15. package/dist/composer/AgentComposerFrame.d.ts +19 -0
  16. package/dist/composer/AgentComposerFrame.d.ts.map +1 -0
  17. package/dist/composer/AgentComposerFrame.js +14 -0
  18. package/dist/composer/AgentComposerFrame.js.map +1 -0
  19. package/dist/composer/ComposerPlusMenu.d.ts +17 -0
  20. package/dist/composer/ComposerPlusMenu.d.ts.map +1 -0
  21. package/dist/composer/ComposerPlusMenu.js +501 -0
  22. package/dist/composer/ComposerPlusMenu.js.map +1 -0
  23. package/dist/composer/MentionPopover.d.ts +29 -0
  24. package/dist/composer/MentionPopover.d.ts.map +1 -0
  25. package/dist/composer/MentionPopover.js +180 -0
  26. package/dist/composer/MentionPopover.js.map +1 -0
  27. package/dist/composer/PastedTextChip.d.ts +9 -0
  28. package/dist/composer/PastedTextChip.d.ts.map +1 -0
  29. package/dist/composer/PastedTextChip.js +47 -0
  30. package/dist/composer/PastedTextChip.js.map +1 -0
  31. package/dist/composer/PromptComposer.d.ts +114 -0
  32. package/dist/composer/PromptComposer.d.ts.map +1 -0
  33. package/dist/composer/PromptComposer.js +302 -0
  34. package/dist/composer/PromptComposer.js.map +1 -0
  35. package/dist/composer/PromptComposer.spec.d.ts +2 -0
  36. package/dist/composer/PromptComposer.spec.d.ts.map +1 -0
  37. package/dist/composer/PromptComposer.spec.js +64 -0
  38. package/dist/composer/PromptComposer.spec.js.map +1 -0
  39. package/dist/composer/RealtimeVoiceMode.d.ts +104 -0
  40. package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -0
  41. package/dist/composer/RealtimeVoiceMode.js +245 -0
  42. package/dist/composer/RealtimeVoiceMode.js.map +1 -0
  43. package/dist/composer/RealtimeVoiceMode.spec.d.ts +2 -0
  44. package/dist/composer/RealtimeVoiceMode.spec.d.ts.map +1 -0
  45. package/dist/composer/RealtimeVoiceMode.spec.js +439 -0
  46. package/dist/composer/RealtimeVoiceMode.spec.js.map +1 -0
  47. package/dist/composer/TiptapComposer.d.ts +221 -0
  48. package/dist/composer/TiptapComposer.d.ts.map +1 -0
  49. package/dist/composer/TiptapComposer.js +1838 -0
  50. package/dist/composer/TiptapComposer.js.map +1 -0
  51. package/dist/composer/TiptapComposer.spec.d.ts +2 -0
  52. package/dist/composer/TiptapComposer.spec.d.ts.map +1 -0
  53. package/dist/composer/TiptapComposer.spec.js +277 -0
  54. package/dist/composer/TiptapComposer.spec.js.map +1 -0
  55. package/dist/composer/VoiceButton.d.ts +25 -0
  56. package/dist/composer/VoiceButton.d.ts.map +1 -0
  57. package/dist/composer/VoiceButton.js +100 -0
  58. package/dist/composer/VoiceButton.js.map +1 -0
  59. package/dist/composer/VoiceButton.spec.d.ts +2 -0
  60. package/dist/composer/VoiceButton.spec.d.ts.map +1 -0
  61. package/dist/composer/VoiceButton.spec.js +19 -0
  62. package/dist/composer/VoiceButton.spec.js.map +1 -0
  63. package/dist/composer/asset-picker-url.d.ts +16 -0
  64. package/dist/composer/asset-picker-url.d.ts.map +1 -0
  65. package/dist/composer/asset-picker-url.js +64 -0
  66. package/dist/composer/asset-picker-url.js.map +1 -0
  67. package/dist/composer/asset-picker-url.spec.d.ts +2 -0
  68. package/dist/composer/asset-picker-url.spec.d.ts.map +1 -0
  69. package/dist/composer/asset-picker-url.spec.js +23 -0
  70. package/dist/composer/asset-picker-url.spec.js.map +1 -0
  71. package/dist/composer/attachment-accept.d.ts +9 -0
  72. package/dist/composer/attachment-accept.d.ts.map +1 -0
  73. package/dist/composer/attachment-accept.js +65 -0
  74. package/dist/composer/attachment-accept.js.map +1 -0
  75. package/dist/composer/attachment-accept.spec.d.ts +2 -0
  76. package/dist/composer/attachment-accept.spec.d.ts.map +1 -0
  77. package/dist/composer/attachment-accept.spec.js +15 -0
  78. package/dist/composer/attachment-accept.spec.js.map +1 -0
  79. package/dist/composer/draft-key.d.ts +2 -0
  80. package/dist/composer/draft-key.d.ts.map +1 -0
  81. package/dist/composer/draft-key.js +8 -0
  82. package/dist/composer/draft-key.js.map +1 -0
  83. package/dist/composer/draft-key.spec.d.ts +2 -0
  84. package/dist/composer/draft-key.spec.d.ts.map +1 -0
  85. package/dist/composer/draft-key.spec.js +13 -0
  86. package/dist/composer/draft-key.spec.js.map +1 -0
  87. package/dist/composer/extensions/FileReference.d.ts +3 -0
  88. package/dist/composer/extensions/FileReference.d.ts.map +1 -0
  89. package/dist/composer/extensions/FileReference.js +36 -0
  90. package/dist/composer/extensions/FileReference.js.map +1 -0
  91. package/dist/composer/extensions/MentionReference.d.ts +3 -0
  92. package/dist/composer/extensions/MentionReference.d.ts.map +1 -0
  93. package/dist/composer/extensions/MentionReference.js +66 -0
  94. package/dist/composer/extensions/MentionReference.js.map +1 -0
  95. package/dist/composer/extensions/SkillReference.d.ts +3 -0
  96. package/dist/composer/extensions/SkillReference.d.ts.map +1 -0
  97. package/dist/composer/extensions/SkillReference.js +40 -0
  98. package/dist/composer/extensions/SkillReference.js.map +1 -0
  99. package/dist/composer/index.d.ts +14 -0
  100. package/dist/composer/index.d.ts.map +1 -0
  101. package/dist/composer/index.js +13 -0
  102. package/dist/composer/index.js.map +1 -0
  103. package/dist/composer/pasted-text.d.ts +31 -0
  104. package/dist/composer/pasted-text.d.ts.map +1 -0
  105. package/dist/composer/pasted-text.js +130 -0
  106. package/dist/composer/pasted-text.js.map +1 -0
  107. package/dist/composer/pasted-text.spec.d.ts +2 -0
  108. package/dist/composer/pasted-text.spec.d.ts.map +1 -0
  109. package/dist/composer/pasted-text.spec.js +107 -0
  110. package/dist/composer/pasted-text.spec.js.map +1 -0
  111. package/dist/composer/prompt-attachments.d.ts +18 -0
  112. package/dist/composer/prompt-attachments.d.ts.map +1 -0
  113. package/dist/composer/prompt-attachments.js +65 -0
  114. package/dist/composer/prompt-attachments.js.map +1 -0
  115. package/dist/composer/prompt-attachments.spec.d.ts +2 -0
  116. package/dist/composer/prompt-attachments.spec.d.ts.map +1 -0
  117. package/dist/composer/prompt-attachments.spec.js +63 -0
  118. package/dist/composer/prompt-attachments.spec.js.map +1 -0
  119. package/dist/composer/realtime-voice-audio-level.d.ts +14 -0
  120. package/dist/composer/realtime-voice-audio-level.d.ts.map +1 -0
  121. package/dist/composer/realtime-voice-audio-level.js +47 -0
  122. package/dist/composer/realtime-voice-audio-level.js.map +1 -0
  123. package/dist/composer/realtime-voice-audio-level.spec.d.ts +2 -0
  124. package/dist/composer/realtime-voice-audio-level.spec.d.ts.map +1 -0
  125. package/dist/composer/realtime-voice-audio-level.spec.js +25 -0
  126. package/dist/composer/realtime-voice-audio-level.spec.js.map +1 -0
  127. package/dist/composer/realtime-voice-transcript.d.ts +26 -0
  128. package/dist/composer/realtime-voice-transcript.d.ts.map +1 -0
  129. package/dist/composer/realtime-voice-transcript.js +103 -0
  130. package/dist/composer/realtime-voice-transcript.js.map +1 -0
  131. package/dist/composer/realtime-voice-transcript.spec.d.ts +2 -0
  132. package/dist/composer/realtime-voice-transcript.spec.d.ts.map +1 -0
  133. package/dist/composer/realtime-voice-transcript.spec.js +76 -0
  134. package/dist/composer/realtime-voice-transcript.spec.js.map +1 -0
  135. package/dist/composer/runtime-adapters.d.ts +163 -0
  136. package/dist/composer/runtime-adapters.d.ts.map +1 -0
  137. package/dist/composer/runtime-adapters.js +130 -0
  138. package/dist/composer/runtime-adapters.js.map +1 -0
  139. package/dist/composer/types.d.ts +60 -0
  140. package/dist/composer/types.d.ts.map +1 -0
  141. package/dist/composer/types.js +2 -0
  142. package/dist/composer/types.js.map +1 -0
  143. package/dist/composer/use-file-search.d.ts +6 -0
  144. package/dist/composer/use-file-search.d.ts.map +1 -0
  145. package/dist/composer/use-file-search.js +53 -0
  146. package/dist/composer/use-file-search.js.map +1 -0
  147. package/dist/composer/use-mention-search.d.ts +6 -0
  148. package/dist/composer/use-mention-search.d.ts.map +1 -0
  149. package/dist/composer/use-mention-search.js +75 -0
  150. package/dist/composer/use-mention-search.js.map +1 -0
  151. package/dist/composer/use-skills.d.ts +7 -0
  152. package/dist/composer/use-skills.d.ts.map +1 -0
  153. package/dist/composer/use-skills.js +40 -0
  154. package/dist/composer/use-skills.js.map +1 -0
  155. package/dist/composer/useRealtimeVoiceMode.d.ts +179 -0
  156. package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -0
  157. package/dist/composer/useRealtimeVoiceMode.js +1657 -0
  158. package/dist/composer/useRealtimeVoiceMode.js.map +1 -0
  159. package/dist/composer/useRealtimeVoiceMode.spec.d.ts +2 -0
  160. package/dist/composer/useRealtimeVoiceMode.spec.d.ts.map +1 -0
  161. package/dist/composer/useRealtimeVoiceMode.spec.js +618 -0
  162. package/dist/composer/useRealtimeVoiceMode.spec.js.map +1 -0
  163. package/dist/composer/useVoiceDictation.d.ts +57 -0
  164. package/dist/composer/useVoiceDictation.d.ts.map +1 -0
  165. package/dist/composer/useVoiceDictation.js +1004 -0
  166. package/dist/composer/useVoiceDictation.js.map +1 -0
  167. package/dist/composer/useVoiceDictation.spec.d.ts +2 -0
  168. package/dist/composer/useVoiceDictation.spec.d.ts.map +1 -0
  169. package/dist/composer/useVoiceDictation.spec.js +32 -0
  170. package/dist/composer/useVoiceDictation.spec.js.map +1 -0
  171. package/dist/editor/RegistryBlockContext.d.ts +68 -0
  172. package/dist/editor/RegistryBlockContext.d.ts.map +1 -0
  173. package/dist/editor/RegistryBlockContext.js +10 -0
  174. package/dist/editor/RegistryBlockContext.js.map +1 -0
  175. package/dist/editor/RegistryBlockNode.d.ts +66 -0
  176. package/dist/editor/RegistryBlockNode.d.ts.map +1 -0
  177. package/dist/editor/RegistryBlockNode.js +406 -0
  178. package/dist/editor/RegistryBlockNode.js.map +1 -0
  179. package/dist/editor/RegistryBlockNode.spec.d.ts +2 -0
  180. package/dist/editor/RegistryBlockNode.spec.d.ts.map +1 -0
  181. package/dist/editor/RegistryBlockNode.spec.js +215 -0
  182. package/dist/editor/RegistryBlockNode.spec.js.map +1 -0
  183. package/dist/editor/index.d.ts +2 -0
  184. package/dist/editor/index.d.ts.map +1 -1
  185. package/dist/editor/index.js +2 -0
  186. package/dist/editor/index.js.map +1 -1
  187. package/dist/editor/registrySlashCommands.d.ts +68 -0
  188. package/dist/editor/registrySlashCommands.d.ts.map +1 -0
  189. package/dist/editor/registrySlashCommands.js +51 -0
  190. package/dist/editor/registrySlashCommands.js.map +1 -0
  191. package/package.json +32 -1
  192. package/src/app-shell/header-actions.tsx +84 -0
  193. package/src/app-shell/index.ts +1 -0
  194. package/src/chat-history/ChatHistoryList.spec.tsx +453 -0
  195. package/src/chat-history/ChatHistoryList.tsx +469 -0
  196. package/src/chat-history/index.ts +7 -0
  197. package/src/chat-history.css +314 -0
  198. package/src/collab-ui/AgentPresenceChip.tsx +69 -0
  199. package/src/collab-ui/LiveCursorOverlay.tsx +241 -0
  200. package/src/collab-ui/PresenceBar.tsx +346 -0
  201. package/src/collab-ui/RecentEditHighlights.tsx +225 -0
  202. package/src/collab-ui/RemoteSelectionRings.tsx +227 -0
  203. package/src/collab-ui/agent-identity.ts +16 -0
  204. package/src/collab-ui/index.ts +39 -0
  205. package/src/collab-ui/lead-client.ts +32 -0
  206. package/src/collab-ui/types.spec.ts +18 -0
  207. package/src/collab-ui/types.ts +96 -0
  208. package/src/composer/AgentComposerFrame.tsx +59 -0
  209. package/src/composer/ComposerPlusMenu.tsx +983 -0
  210. package/src/composer/MentionPopover.tsx +412 -0
  211. package/src/composer/PastedTextChip.tsx +150 -0
  212. package/src/composer/PromptComposer.spec.ts +73 -0
  213. package/src/composer/PromptComposer.tsx +718 -0
  214. package/src/composer/RealtimeVoiceMode.spec.tsx +771 -0
  215. package/src/composer/RealtimeVoiceMode.tsx +856 -0
  216. package/src/composer/TiptapComposer.spec.ts +390 -0
  217. package/src/composer/TiptapComposer.tsx +2936 -0
  218. package/src/composer/VoiceButton.spec.ts +33 -0
  219. package/src/composer/VoiceButton.tsx +255 -0
  220. package/src/composer/asset-picker-url.spec.ts +56 -0
  221. package/src/composer/asset-picker-url.ts +80 -0
  222. package/src/composer/attachment-accept.spec.ts +22 -0
  223. package/src/composer/attachment-accept.ts +71 -0
  224. package/src/composer/draft-key.spec.ts +19 -0
  225. package/src/composer/draft-key.ts +7 -0
  226. package/src/composer/extensions/FileReference.tsx +55 -0
  227. package/src/composer/extensions/MentionReference.tsx +93 -0
  228. package/src/composer/extensions/SkillReference.tsx +56 -0
  229. package/src/composer/index.ts +79 -0
  230. package/src/composer/pasted-text.spec.ts +162 -0
  231. package/src/composer/pasted-text.ts +160 -0
  232. package/src/composer/prompt-attachments.spec.ts +80 -0
  233. package/src/composer/prompt-attachments.ts +93 -0
  234. package/src/composer/realtime-voice-audio-level.spec.ts +32 -0
  235. package/src/composer/realtime-voice-audio-level.ts +64 -0
  236. package/src/composer/realtime-voice-transcript.spec.ts +95 -0
  237. package/src/composer/realtime-voice-transcript.ts +134 -0
  238. package/src/composer/runtime-adapters.tsx +343 -0
  239. package/src/composer/types.ts +66 -0
  240. package/src/composer/use-file-search.ts +64 -0
  241. package/src/composer/use-mention-search.ts +90 -0
  242. package/src/composer/use-skills.ts +45 -0
  243. package/src/composer/useRealtimeVoiceMode.spec.ts +818 -0
  244. package/src/composer/useRealtimeVoiceMode.tsx +2117 -0
  245. package/src/composer/useVoiceDictation.spec.ts +45 -0
  246. package/src/composer/useVoiceDictation.ts +1191 -0
  247. package/src/context-ui/ContextMeter.tsx +101 -0
  248. package/src/context-ui/ContextSegmentRow.tsx +184 -0
  249. package/src/context-ui/ContextTreemap.tsx +125 -0
  250. package/src/context-ui/ContextXRayPanel.tsx +377 -0
  251. package/src/context-ui/format.ts +42 -0
  252. package/src/context-ui/index.ts +15 -0
  253. package/src/context-ui/types.ts +43 -0
  254. package/src/design-tweaks/index.ts +1 -0
  255. package/src/design-tweaks/visual-style-controls.tsx +1084 -0
  256. package/src/editor/BubbleToolbar.tsx +253 -0
  257. package/src/editor/CodeBlockNode.tsx +254 -0
  258. package/src/editor/DragHandle.spec.ts +588 -0
  259. package/src/editor/DragHandle.ts +1437 -0
  260. package/src/editor/ImageExtension.roundtrip.spec.ts +117 -0
  261. package/src/editor/ImageExtension.ts +306 -0
  262. package/src/editor/RegistryBlockContext.tsx +107 -0
  263. package/src/editor/RegistryBlockNode.spec.ts +279 -0
  264. package/src/editor/RegistryBlockNode.tsx +652 -0
  265. package/src/editor/RichMarkdownEditor.collab.spec.ts +79 -0
  266. package/src/editor/RichMarkdownEditor.roundtrip.spec.ts +226 -0
  267. package/src/editor/RichMarkdownEditor.tsx +81 -0
  268. package/src/editor/RunId.ts +61 -0
  269. package/src/editor/SharedRichEditor.tsx +321 -0
  270. package/src/editor/SlashCommandMenu.tsx +310 -0
  271. package/src/editor/disableHistory.spec.ts +85 -0
  272. package/src/editor/extensions.ts +402 -0
  273. package/src/editor/gfmDoc.spec.ts +230 -0
  274. package/src/editor/gfmDoc.ts +92 -0
  275. package/src/editor/index.ts +93 -0
  276. package/src/editor/registrySlashCommands.ts +131 -0
  277. package/src/editor/structuralReorderUndo.spec.ts +144 -0
  278. package/src/editor/surgical-apply.spec.ts +279 -0
  279. package/src/editor/surgical-apply.ts +192 -0
  280. package/src/editor/useCollabReconcile.concurrent.spec.ts +422 -0
  281. package/src/editor/useCollabReconcile.idempotent.spec.ts +324 -0
  282. package/src/editor/useCollabReconcile.ts +589 -0
  283. package/src/editor.css +510 -0
  284. package/src/hooks/index.ts +2 -0
  285. package/src/hooks/use-mobile.tsx +21 -0
  286. package/src/hooks/use-toast.ts +188 -0
  287. package/src/index.ts +8 -0
  288. package/src/onboarding/index.ts +1 -0
  289. package/src/provider.tsx +47 -0
  290. package/src/sharing/VisibilityBadge.spec.ts +96 -0
  291. package/src/sharing/VisibilityBadge.tsx +37 -0
  292. package/src/sharing/index.ts +4 -0
  293. package/src/styles.css +21 -0
  294. package/src/ui/accordion.tsx +56 -0
  295. package/src/ui/alert-dialog.tsx +141 -0
  296. package/src/ui/alert.tsx +59 -0
  297. package/src/ui/aspect-ratio.tsx +5 -0
  298. package/src/ui/avatar.tsx +48 -0
  299. package/src/ui/badge.tsx +37 -0
  300. package/src/ui/breadcrumb.tsx +115 -0
  301. package/src/ui/button.tsx +78 -0
  302. package/src/ui/calendar.tsx +214 -0
  303. package/src/ui/card.tsx +86 -0
  304. package/src/ui/carousel.tsx +260 -0
  305. package/src/ui/chart.tsx +375 -0
  306. package/src/ui/checkbox.tsx +28 -0
  307. package/src/ui/collapsible.tsx +9 -0
  308. package/src/ui/command.spec.tsx +51 -0
  309. package/src/ui/command.tsx +174 -0
  310. package/src/ui/context-menu.tsx +202 -0
  311. package/src/ui/date-picker.tsx +64 -0
  312. package/src/ui/dialog.spec.tsx +43 -0
  313. package/src/ui/dialog.tsx +146 -0
  314. package/src/ui/drawer.tsx +126 -0
  315. package/src/ui/dropdown-menu.tsx +218 -0
  316. package/src/ui/form.tsx +177 -0
  317. package/src/ui/hover-card.tsx +27 -0
  318. package/src/ui/index.ts +52 -0
  319. package/src/ui/input-otp.tsx +69 -0
  320. package/src/ui/input.tsx +22 -0
  321. package/src/ui/label.tsx +24 -0
  322. package/src/ui/logo.tsx +36 -0
  323. package/src/ui/menubar.tsx +235 -0
  324. package/src/ui/navigation-menu.tsx +128 -0
  325. package/src/ui/pagination.tsx +121 -0
  326. package/src/ui/popover.tsx +44 -0
  327. package/src/ui/progress.tsx +26 -0
  328. package/src/ui/radio-group.tsx +42 -0
  329. package/src/ui/resizable.tsx +43 -0
  330. package/src/ui/scroll-area.tsx +46 -0
  331. package/src/ui/select.tsx +158 -0
  332. package/src/ui/separator.tsx +29 -0
  333. package/src/ui/sheet.tsx +160 -0
  334. package/src/ui/sidebar.tsx +777 -0
  335. package/src/ui/skeleton.tsx +18 -0
  336. package/src/ui/slider.tsx +26 -0
  337. package/src/ui/sonner.tsx +58 -0
  338. package/src/ui/spinner.tsx +17 -0
  339. package/src/ui/switch.tsx +27 -0
  340. package/src/ui/table.tsx +117 -0
  341. package/src/ui/tabs.tsx +53 -0
  342. package/src/ui/textarea.tsx +23 -0
  343. package/src/ui/toast.tsx +127 -0
  344. package/src/ui/toaster.tsx +33 -0
  345. package/src/ui/toggle-group.tsx +59 -0
  346. package/src/ui/toggle.tsx +43 -0
  347. package/src/ui/tooltip.tsx +53 -0
  348. package/src/ui/use-toast.ts +3 -0
  349. package/src/utils.ts +8 -0
@@ -0,0 +1,1657 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import { createRealtimeVoiceAudioLevelStore, normalizeRealtimeVoiceRms, smoothRealtimeVoiceLevel, } from "./realtime-voice-audio-level.js";
5
+ import { realtimeVoiceTranscriptRegistry } from "./realtime-voice-transcript.js";
6
+ import { RealtimeVoiceModeDock, } from "./RealtimeVoiceMode.js";
7
+ import { useComposerRuntimeAdapters } from "./runtime-adapters.js";
8
+ const REALTIME_VOICE_STATE_KEY = "realtime-voice-session";
9
+ const REALTIME_VOICE_PREFERENCES_KEY = "realtime-voice-prefs";
10
+ const REALTIME_VOICE_REQUEST_SOURCE = "realtime-voice";
11
+ const REALTIME_VOICE_SESSION_PATH = "/_agent-native/realtime-voice/session";
12
+ const REALTIME_VOICE_TOOL_PATH = "/_agent-native/realtime-voice/tool";
13
+ const REALTIME_VOICE_CAPABILITY_HEADER = "X-Agent-Native-Realtime-Capability";
14
+ const REALTIME_VOICE_CONNECTION_TIMEOUT_MS = 15_000;
15
+ const REALTIME_VOICE_MAX_TOOLS = 32;
16
+ const REALTIME_VOICE_MAX_TOOL_SCHEMA_BYTES = 32_000;
17
+ const REALTIME_VOICE_MAX_SESSION_BYTES = 64_000;
18
+ const REALTIME_VOICE_TOOL_UPDATE_TIMEOUT_MS = 5_000;
19
+ const REALTIME_VOICE_PRIORITY_TOOL_NAMES = [
20
+ "navigate",
21
+ "set-url-path",
22
+ "set-search-params",
23
+ "view-screen",
24
+ "tool-search",
25
+ ];
26
+ const REALTIME_VOICE_TOOL_NAME = /^[A-Za-z0-9_-]{1,64}$/;
27
+ const REALTIME_VOICE_MICROPHONE_STORAGE_KEY = "agent-native:realtime-voice-microphone";
28
+ export const REALTIME_VOICE_LANGUAGES = [
29
+ "auto",
30
+ "en",
31
+ "es",
32
+ "fr",
33
+ "de",
34
+ "it",
35
+ "pt",
36
+ "ja",
37
+ "ko",
38
+ "zh",
39
+ ];
40
+ export const REALTIME_VOICE_INTELLIGENCE_LEVELS = [
41
+ "instant",
42
+ "balanced",
43
+ "deep",
44
+ ];
45
+ export const REALTIME_VOICES = [
46
+ "marin",
47
+ "cedar",
48
+ "coral",
49
+ "sage",
50
+ "verse",
51
+ "alloy",
52
+ "ash",
53
+ "ballad",
54
+ "echo",
55
+ "shimmer",
56
+ ];
57
+ export const DEFAULT_REALTIME_VOICE_PREFERENCES = {
58
+ language: "auto",
59
+ intelligence: "instant",
60
+ voice: "marin",
61
+ };
62
+ export const REALTIME_VOICE_AUDIO_CONSTRAINTS = {
63
+ // Prefer the browser/OS-selected input instead of whichever physical device
64
+ // happens to be enumerated first. `ideal` keeps browsers without the
65
+ // synthetic `default` device from failing with OverconstrainedError.
66
+ deviceId: { ideal: "default" },
67
+ echoCancellation: true,
68
+ noiseSuppression: true,
69
+ autoGainControl: true,
70
+ };
71
+ export function createRealtimeVoiceAudioConstraints(deviceId = "default", exact = false) {
72
+ return {
73
+ ...REALTIME_VOICE_AUDIO_CONSTRAINTS,
74
+ deviceId: deviceId === "default"
75
+ ? { ideal: "default" }
76
+ : exact
77
+ ? { exact: deviceId }
78
+ : { ideal: deviceId },
79
+ };
80
+ }
81
+ export async function replaceRealtimeVoiceMicrophone(options) {
82
+ const replacementStream = await options.mediaDevices.getUserMedia({
83
+ audio: createRealtimeVoiceAudioConstraints(options.deviceId, true),
84
+ });
85
+ try {
86
+ const replacementTrack = replacementStream.getAudioTracks()[0];
87
+ const sender = options.peer
88
+ .getSenders()
89
+ .find((candidate) => candidate.track?.kind === "audio");
90
+ if (!replacementTrack || !sender) {
91
+ throw new Error("No active microphone track is available.");
92
+ }
93
+ await sender.replaceTrack(replacementTrack);
94
+ for (const track of options.currentStream?.getTracks() ?? []) {
95
+ track.onended = null;
96
+ track.stop();
97
+ }
98
+ return replacementStream;
99
+ }
100
+ catch (error) {
101
+ for (const track of replacementStream.getTracks())
102
+ track.stop();
103
+ throw error;
104
+ }
105
+ }
106
+ function readRealtimeVoiceMicrophoneId() {
107
+ if (typeof window === "undefined")
108
+ return "default";
109
+ try {
110
+ return (window.localStorage.getItem(REALTIME_VOICE_MICROPHONE_STORAGE_KEY) ||
111
+ "default");
112
+ }
113
+ catch {
114
+ return "default";
115
+ }
116
+ }
117
+ function writeRealtimeVoiceMicrophoneId(deviceId) {
118
+ try {
119
+ window.localStorage.setItem(REALTIME_VOICE_MICROPHONE_STORAGE_KEY, deviceId);
120
+ }
121
+ catch {
122
+ // A remembered local device is optional and must never block voice mode.
123
+ }
124
+ }
125
+ function isOneOf(values, value) {
126
+ return typeof value === "string" && values.includes(value);
127
+ }
128
+ export function normalizeRealtimeVoicePreferences(value) {
129
+ const record = value && typeof value === "object" && !Array.isArray(value)
130
+ ? value
131
+ : {};
132
+ return {
133
+ language: isOneOf(REALTIME_VOICE_LANGUAGES, record.language)
134
+ ? record.language
135
+ : DEFAULT_REALTIME_VOICE_PREFERENCES.language,
136
+ intelligence: isOneOf(REALTIME_VOICE_INTELLIGENCE_LEVELS, record.intelligence)
137
+ ? record.intelligence
138
+ : DEFAULT_REALTIME_VOICE_PREFERENCES.intelligence,
139
+ voice: isOneOf(REALTIME_VOICES, record.voice)
140
+ ? record.voice
141
+ : DEFAULT_REALTIME_VOICE_PREFERENCES.voice,
142
+ };
143
+ }
144
+ export function resolveRealtimeVoiceLanguage(language, browserLanguages = []) {
145
+ if (language !== "auto")
146
+ return language;
147
+ for (const locale of browserLanguages) {
148
+ const primary = locale.trim().split("-")[0]?.toLowerCase();
149
+ if (isOneOf(REALTIME_VOICE_LANGUAGES, primary) && primary !== "auto") {
150
+ return primary;
151
+ }
152
+ }
153
+ return "en";
154
+ }
155
+ export function realtimeVoiceReasoningEffort(intelligence) {
156
+ if (intelligence === "balanced")
157
+ return "low";
158
+ if (intelligence === "deep")
159
+ return "medium";
160
+ return "minimal";
161
+ }
162
+ export function createRealtimeVoicePreferenceUpdate(preferences, options = {}) {
163
+ return {
164
+ type: "session.update",
165
+ session: {
166
+ type: "realtime",
167
+ reasoning: {
168
+ effort: realtimeVoiceReasoningEffort(preferences.intelligence),
169
+ },
170
+ audio: {
171
+ input: {
172
+ transcription: {
173
+ model: "gpt-4o-mini-transcribe",
174
+ language: resolveRealtimeVoiceLanguage(preferences.language, options.browserLanguages),
175
+ },
176
+ },
177
+ ...(options.includeVoice
178
+ ? { output: { voice: preferences.voice } }
179
+ : {}),
180
+ },
181
+ },
182
+ };
183
+ }
184
+ /**
185
+ * Input transcription is produced by a separate ASR model and can finish after
186
+ * the assistant response. Reserve each message's position when OpenAI adds it
187
+ * to the conversation, then publish only the contiguous completed prefix.
188
+ */
189
+ export function createRealtimeVoiceTranscriptSequencer(publish) {
190
+ const order = [];
191
+ const items = new Map();
192
+ const settledItemIds = new Set();
193
+ const receivedTranscriptIds = new Set();
194
+ const reserve = (id, role) => {
195
+ if (settledItemIds.has(id))
196
+ return;
197
+ const existing = items.get(id);
198
+ if (existing) {
199
+ existing.role ??= role;
200
+ return;
201
+ }
202
+ order.push(id);
203
+ items.set(id, { status: "pending", role });
204
+ };
205
+ const drain = () => {
206
+ while (order.length > 0) {
207
+ const id = order[0];
208
+ const entry = items.get(id);
209
+ if (!entry || entry.status === "pending")
210
+ return;
211
+ order.shift();
212
+ items.delete(id);
213
+ settledItemIds.add(id);
214
+ if (entry.status === "completed" && entry.transcript) {
215
+ publish(entry.transcript);
216
+ }
217
+ }
218
+ };
219
+ const skip = (id) => {
220
+ if (typeof id !== "string" || !id)
221
+ return;
222
+ reserve(id);
223
+ const entry = items.get(id);
224
+ if (entry.status === "pending")
225
+ entry.status = "skipped";
226
+ };
227
+ return {
228
+ handle(event) {
229
+ if (event.type === "conversation.item.added" ||
230
+ event.type === "conversation.item.created") {
231
+ const item = event.item;
232
+ if (item && typeof item === "object") {
233
+ const record = item;
234
+ if (record.type === "message" &&
235
+ (record.role === "user" || record.role === "assistant") &&
236
+ typeof record.id === "string") {
237
+ reserve(record.id, record.role);
238
+ }
239
+ }
240
+ }
241
+ const transcript = extractCompletedRealtimeVoiceTranscript(event);
242
+ if (transcript) {
243
+ const transcriptId = transcript.providerId
244
+ ? `${transcript.role}:${transcript.providerId}`
245
+ : undefined;
246
+ if (transcriptId && receivedTranscriptIds.has(transcriptId))
247
+ return;
248
+ if (transcriptId)
249
+ receivedTranscriptIds.add(transcriptId);
250
+ const itemId = typeof event.item_id === "string" ? event.item_id : undefined;
251
+ if (!itemId) {
252
+ // Older providers may omit item_id. Match the first reserved slot for
253
+ // that role so one incomplete legacy event cannot strand the queue.
254
+ const reservedId = order.find((id) => {
255
+ const entry = items.get(id);
256
+ return (entry?.status === "pending" && entry.role === transcript.role);
257
+ });
258
+ if (reservedId) {
259
+ items.set(reservedId, {
260
+ status: "completed",
261
+ role: transcript.role,
262
+ transcript,
263
+ });
264
+ }
265
+ else {
266
+ publish(transcript);
267
+ }
268
+ }
269
+ else {
270
+ if (settledItemIds.has(itemId))
271
+ return;
272
+ reserve(itemId, transcript.role);
273
+ items.set(itemId, {
274
+ status: "completed",
275
+ role: transcript.role,
276
+ transcript,
277
+ });
278
+ }
279
+ }
280
+ else if (event.type === "conversation.item.input_audio_transcription.failed" ||
281
+ event.type === "conversation.item.deleted") {
282
+ skip(event.item_id);
283
+ }
284
+ else if (event.type === "response.done") {
285
+ const response = event.response;
286
+ const output = response && typeof response === "object"
287
+ ? response.output
288
+ : undefined;
289
+ if (Array.isArray(output)) {
290
+ for (const item of output) {
291
+ if (!item || typeof item !== "object")
292
+ continue;
293
+ const record = item;
294
+ if (record.type === "message")
295
+ skip(record.id);
296
+ }
297
+ }
298
+ }
299
+ drain();
300
+ },
301
+ reset() {
302
+ order.length = 0;
303
+ items.clear();
304
+ settledItemIds.clear();
305
+ receivedTranscriptIds.clear();
306
+ },
307
+ };
308
+ }
309
+ export function createRealtimeVoiceGreetingEvent() {
310
+ return {
311
+ type: "response.create",
312
+ response: {
313
+ output_modalities: ["audio"],
314
+ instructions: 'Say exactly: "How can I help you?" Do not add anything else.',
315
+ // Do not set max_output_tokens here. Realtime counts assistant audio at
316
+ // roughly one token per 50 ms, so a text-sized cap can cut speech off.
317
+ // The exact-response instruction already keeps this greeting bounded.
318
+ },
319
+ };
320
+ }
321
+ export function createRealtimeVoiceGreetingStarter(send) {
322
+ let started = false;
323
+ return {
324
+ start() {
325
+ if (started)
326
+ return false;
327
+ started = true;
328
+ send(createRealtimeVoiceGreetingEvent());
329
+ return true;
330
+ },
331
+ reset() {
332
+ started = false;
333
+ },
334
+ };
335
+ }
336
+ /**
337
+ * Voice mode owns the chat only temporarily. Restore the captured transcript
338
+ * when it is still the user's active thread (or the chat has no active thread)
339
+ * but never pull them back after they deliberately selected another thread.
340
+ */
341
+ export function shouldRestoreRealtimeVoiceTranscriptThread(transcriptThreadId, activeThreadId) {
342
+ return Boolean(transcriptThreadId &&
343
+ (!activeThreadId || activeThreadId === transcriptThreadId));
344
+ }
345
+ export function extractCompletedRealtimeVoiceTranscript(event) {
346
+ const userCompleted = event.type === "conversation.item.input_audio_transcription.completed";
347
+ const assistantCompleted = event.type === "response.output_audio_transcript.done";
348
+ if (!userCompleted && !assistantCompleted)
349
+ return null;
350
+ const text = typeof event.transcript === "string" ? event.transcript.trim() : "";
351
+ if (!text)
352
+ return null;
353
+ const providerId = [event.item_id, event.response_id, event.event_id].find((value) => typeof value === "string" && value.length > 0);
354
+ return {
355
+ role: userCompleted ? "user" : "assistant",
356
+ text,
357
+ ...(providerId ? { providerId } : {}),
358
+ };
359
+ }
360
+ function errorMessage(error) {
361
+ return error instanceof Error ? error.message : String(error);
362
+ }
363
+ export function isRealtimeVoiceAbortError(error) {
364
+ return Boolean(error &&
365
+ typeof error === "object" &&
366
+ error.name === "AbortError");
367
+ }
368
+ export function isRealtimeVoiceSetupRequiredError(error) {
369
+ return Boolean(error &&
370
+ typeof error === "object" &&
371
+ error.status === 409);
372
+ }
373
+ export function createRealtimeVoiceConnectionTimeout(onTimeout, timeoutMs = REALTIME_VOICE_CONNECTION_TIMEOUT_MS) {
374
+ const timeout = window.setTimeout(onTimeout, timeoutMs);
375
+ return () => window.clearTimeout(timeout);
376
+ }
377
+ export function createRealtimeVoiceConnectionGate(onTimeout, timeoutMs = REALTIME_VOICE_CONNECTION_TIMEOUT_MS) {
378
+ const cancel = createRealtimeVoiceConnectionTimeout(onTimeout, timeoutMs);
379
+ return {
380
+ // RTC connectivity alone is not enough: the session may never finish its
381
+ // Realtime handshake. Keep the deadline armed until session.created.
382
+ markTransportReady() { },
383
+ markSessionCreated: cancel,
384
+ cancel,
385
+ };
386
+ }
387
+ async function readErrorResponse(response) {
388
+ const raw = await response.text().catch(() => "");
389
+ if (!raw)
390
+ return response.statusText || `HTTP ${response.status}`;
391
+ try {
392
+ const parsed = JSON.parse(raw);
393
+ return String(parsed.error ?? parsed.message ?? raw);
394
+ }
395
+ catch {
396
+ return raw.slice(0, 500);
397
+ }
398
+ }
399
+ export async function createRealtimeVoiceSessionWithCapability(offerSdp, options = {}) {
400
+ const preferences = options.preferences;
401
+ const response = await fetch(REALTIME_VOICE_SESSION_PATH, {
402
+ method: "POST",
403
+ credentials: "same-origin",
404
+ headers: {
405
+ "Content-Type": "application/sdp",
406
+ ...(options.browserTabId
407
+ ? { "X-Agent-Native-Browser-Tab": options.browserTabId }
408
+ : {}),
409
+ ...(preferences
410
+ ? {
411
+ "X-Agent-Native-Realtime-Language": resolveRealtimeVoiceLanguage(preferences.language, options.browserLanguages),
412
+ "X-Agent-Native-Realtime-Intelligence": preferences.intelligence,
413
+ "X-Agent-Native-Realtime-Voice": preferences.voice,
414
+ }
415
+ : {}),
416
+ },
417
+ body: offerSdp,
418
+ signal: options.signal,
419
+ });
420
+ if (!response.ok) {
421
+ const message = await readErrorResponse(response);
422
+ const error = new Error(message);
423
+ error.status = response.status;
424
+ throw error;
425
+ }
426
+ const sdp = await response.text();
427
+ const capability = response.headers.get(REALTIME_VOICE_CAPABILITY_HEADER);
428
+ return {
429
+ sdp,
430
+ ...(capability ? { capability } : {}),
431
+ };
432
+ }
433
+ /** Backwards-compatible SDP-only session helper for existing client callers. */
434
+ export async function createRealtimeVoiceSession(offerSdp, options = {}) {
435
+ return (await createRealtimeVoiceSessionWithCapability(offerSdp, options))
436
+ .sdp;
437
+ }
438
+ export async function executeRealtimeVoiceTool(input) {
439
+ const response = await fetch(REALTIME_VOICE_TOOL_PATH, {
440
+ method: "POST",
441
+ credentials: "same-origin",
442
+ headers: {
443
+ "Content-Type": "application/json",
444
+ ...(input.browserTabId
445
+ ? { "X-Agent-Native-Browser-Tab": input.browserTabId }
446
+ : {}),
447
+ ...(input.capability
448
+ ? { [REALTIME_VOICE_CAPABILITY_HEADER]: input.capability }
449
+ : {}),
450
+ },
451
+ body: JSON.stringify({
452
+ name: input.name,
453
+ args: input.args,
454
+ callId: input.callId,
455
+ sessionId: input.sessionId,
456
+ browserTabId: input.browserTabId,
457
+ }),
458
+ signal: input.signal,
459
+ });
460
+ if (!response.ok) {
461
+ throw new Error(await readErrorResponse(response));
462
+ }
463
+ return (await response.json());
464
+ }
465
+ function jsonByteLength(value) {
466
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
467
+ }
468
+ function normalizeRealtimeVoiceFunctionTool(value) {
469
+ if (!value || typeof value !== "object" || Array.isArray(value))
470
+ return null;
471
+ const record = value;
472
+ if (record.type !== "function" ||
473
+ typeof record.name !== "string" ||
474
+ !REALTIME_VOICE_TOOL_NAME.test(record.name)) {
475
+ return null;
476
+ }
477
+ if (record.description !== undefined &&
478
+ typeof record.description !== "string") {
479
+ return null;
480
+ }
481
+ if (record.parameters !== undefined &&
482
+ (!record.parameters ||
483
+ typeof record.parameters !== "object" ||
484
+ Array.isArray(record.parameters))) {
485
+ return null;
486
+ }
487
+ try {
488
+ if (jsonByteLength(record) > REALTIME_VOICE_MAX_TOOL_SCHEMA_BYTES) {
489
+ return null;
490
+ }
491
+ return JSON.parse(JSON.stringify(record));
492
+ }
493
+ catch {
494
+ return null;
495
+ }
496
+ }
497
+ export function extractRealtimeVoiceSessionTools(event) {
498
+ if (event.type !== "session.created" && event.type !== "session.updated") {
499
+ return null;
500
+ }
501
+ const session = event.session;
502
+ if (!session || typeof session !== "object")
503
+ return null;
504
+ const tools = session.tools;
505
+ if (!Array.isArray(tools))
506
+ return null;
507
+ return tools
508
+ .map(normalizeRealtimeVoiceFunctionTool)
509
+ .filter((tool) => Boolean(tool));
510
+ }
511
+ function createRealtimeVoiceToolManifestUpdate(tools, eventId) {
512
+ return {
513
+ type: "session.update",
514
+ ...(eventId ? { event_id: eventId } : {}),
515
+ session: {
516
+ type: "realtime",
517
+ tools,
518
+ tool_choice: "auto",
519
+ },
520
+ };
521
+ }
522
+ /**
523
+ * Merge search-discovered schemas into the live manifest. Pinned navigation
524
+ * and discovery tools stay available, then the newest discoveries take the
525
+ * remaining slots ahead of lower-priority tools from the original manifest.
526
+ */
527
+ export function mergeRealtimeVoiceToolManifest(currentTools, expandedTools) {
528
+ const current = new Map();
529
+ for (const value of currentTools) {
530
+ const tool = normalizeRealtimeVoiceFunctionTool(value);
531
+ if (tool)
532
+ current.set(tool.name, tool);
533
+ }
534
+ const expanded = new Map();
535
+ for (const value of expandedTools) {
536
+ const tool = normalizeRealtimeVoiceFunctionTool(value);
537
+ if (tool)
538
+ expanded.set(tool.name, tool);
539
+ }
540
+ const candidates = [];
541
+ const seen = new Set();
542
+ const add = (tool) => {
543
+ if (!tool || seen.has(tool.name))
544
+ return;
545
+ seen.add(tool.name);
546
+ candidates.push(tool);
547
+ };
548
+ for (const name of REALTIME_VOICE_PRIORITY_TOOL_NAMES) {
549
+ add(expanded.get(name) ?? current.get(name));
550
+ }
551
+ for (const tool of expanded.values())
552
+ add(tool);
553
+ for (const tool of current.values())
554
+ add(tool);
555
+ const packed = [];
556
+ for (const tool of candidates) {
557
+ if (packed.length >= REALTIME_VOICE_MAX_TOOLS)
558
+ break;
559
+ const candidate = [...packed, tool];
560
+ if (jsonByteLength(createRealtimeVoiceToolManifestUpdate(candidate, "realtime_tool_manifest_999999999")) <= REALTIME_VOICE_MAX_SESSION_BYTES) {
561
+ packed.push(tool);
562
+ }
563
+ }
564
+ return packed;
565
+ }
566
+ function createRealtimeVoiceToolResultEvent(result) {
567
+ const modelResult = {
568
+ callId: result.callId,
569
+ status: result.status,
570
+ output: result.output,
571
+ ...(result.approvalKey ? { approvalKey: result.approvalKey } : {}),
572
+ };
573
+ return {
574
+ type: "conversation.item.create",
575
+ item: {
576
+ type: "function_call_output",
577
+ call_id: result.callId,
578
+ output: JSON.stringify(modelResult),
579
+ },
580
+ };
581
+ }
582
+ /**
583
+ * Realtime treats `session.update.tools` as a full replacement. Serialize
584
+ * discovery updates and wait for a confirming `session.updated` manifest
585
+ * before returning the search result and allowing the next model response.
586
+ */
587
+ export function createRealtimeVoiceToolManifestCoordinator(send, timeoutMs = REALTIME_VOICE_TOOL_UPDATE_TIMEOUT_MS) {
588
+ let currentTools = [];
589
+ const queue = [];
590
+ let updateSequence = 0;
591
+ let active;
592
+ const finish = (failureMessage) => {
593
+ if (!active)
594
+ return;
595
+ const { result, timer } = active;
596
+ clearTimeout(timer);
597
+ active = undefined;
598
+ send(createRealtimeVoiceToolResultEvent(failureMessage
599
+ ? {
600
+ callId: result.callId,
601
+ status: "failed",
602
+ output: `${failureMessage} The discovered tools were not added to this voice session. Original tool result: ${result.output}`,
603
+ }
604
+ : result));
605
+ send({ type: "response.create" });
606
+ drain();
607
+ };
608
+ const drain = () => {
609
+ if (active)
610
+ return;
611
+ const result = queue.shift();
612
+ if (!result)
613
+ return;
614
+ const expanded = Array.isArray(result.expandedTools)
615
+ ? result.expandedTools
616
+ .map(normalizeRealtimeVoiceFunctionTool)
617
+ .filter((tool) => Boolean(tool))
618
+ : [];
619
+ if (result.status !== "completed" || expanded.length === 0) {
620
+ send(createRealtimeVoiceToolResultEvent(result));
621
+ send({ type: "response.create" });
622
+ drain();
623
+ return;
624
+ }
625
+ const merged = mergeRealtimeVoiceToolManifest(currentTools, expanded);
626
+ const mergedNames = new Set(merged.map((tool) => tool.name));
627
+ const expectedNames = expanded
628
+ .map((tool) => tool.name)
629
+ .filter((name) => mergedNames.has(name));
630
+ if (expectedNames.length === 0) {
631
+ send(createRealtimeVoiceToolResultEvent(result));
632
+ send({ type: "response.create" });
633
+ drain();
634
+ return;
635
+ }
636
+ const eventId = `realtime_tool_manifest_${++updateSequence}`;
637
+ active = {
638
+ result,
639
+ expectedNames,
640
+ eventId,
641
+ timer: setTimeout(() => finish("Timed out while installing the discovered tools."), timeoutMs),
642
+ };
643
+ send(createRealtimeVoiceToolManifestUpdate(merged, eventId));
644
+ };
645
+ return {
646
+ enqueue(result) {
647
+ queue.push(result);
648
+ drain();
649
+ },
650
+ setSessionTools(tools) {
651
+ currentTools = mergeRealtimeVoiceToolManifest([], tools);
652
+ if (!active)
653
+ return;
654
+ const names = new Set(currentTools.map((tool) => tool.name));
655
+ if (active.expectedNames.every((name) => names.has(name)))
656
+ finish();
657
+ },
658
+ handleError(eventId, message) {
659
+ if (!active || !eventId || active.eventId !== eventId)
660
+ return false;
661
+ finish(message || "The provider rejected the discovered tool update.");
662
+ return true;
663
+ },
664
+ reset() {
665
+ if (active)
666
+ clearTimeout(active.timer);
667
+ active = undefined;
668
+ queue.length = 0;
669
+ currentTools = [];
670
+ },
671
+ getTools() {
672
+ return currentTools;
673
+ },
674
+ };
675
+ }
676
+ export function extractRealtimeVoiceFunctionCalls(event) {
677
+ if (event.type === "response.function_call_arguments.done") {
678
+ const name = typeof event.name === "string" ? event.name : "";
679
+ const callId = typeof event.call_id === "string" ? event.call_id : "";
680
+ if (!name || !callId)
681
+ return [];
682
+ return [
683
+ {
684
+ name,
685
+ callId,
686
+ argumentsText: typeof event.arguments === "string" ? event.arguments : "{}",
687
+ },
688
+ ];
689
+ }
690
+ if (event.type !== "response.done")
691
+ return [];
692
+ const response = event.response;
693
+ if (!response || typeof response !== "object")
694
+ return [];
695
+ const output = response.output;
696
+ if (!Array.isArray(output))
697
+ return [];
698
+ return output.flatMap((item) => {
699
+ if (!item || typeof item !== "object")
700
+ return [];
701
+ const record = item;
702
+ if (record.type !== "function_call")
703
+ return [];
704
+ const name = typeof record.name === "string" ? record.name : "";
705
+ const callId = typeof record.call_id === "string" ? record.call_id : "";
706
+ if (!name || !callId)
707
+ return [];
708
+ return [
709
+ {
710
+ name,
711
+ callId,
712
+ argumentsText: typeof record.arguments === "string" ? record.arguments : "{}",
713
+ },
714
+ ];
715
+ });
716
+ }
717
+ function parseFunctionArguments(text) {
718
+ const parsed = JSON.parse(text || "{}");
719
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
720
+ throw new Error("The voice model returned invalid tool arguments.");
721
+ }
722
+ return parsed;
723
+ }
724
+ function sendDataChannelEvent(channel, event) {
725
+ if (!channel || channel.readyState !== "open")
726
+ return;
727
+ channel.send(JSON.stringify(event));
728
+ }
729
+ function openOpenAiKeySettings() {
730
+ if (typeof window === "undefined")
731
+ return;
732
+ window.location.hash = "#secrets:OPENAI_API_KEY";
733
+ window.dispatchEvent(new Event("agent-panel:open"));
734
+ window.dispatchEvent(new CustomEvent("agent-panel:open-settings", {
735
+ detail: { section: "secrets" },
736
+ }));
737
+ }
738
+ export function listenForRealtimeVoicePageHide(cleanup) {
739
+ window.addEventListener("pagehide", cleanup);
740
+ return () => window.removeEventListener("pagehide", cleanup);
741
+ }
742
+ function voiceCopy(t) {
743
+ return {
744
+ entryButtonLabel: t("agentPanel.voiceMode.entryButtonLabel"),
745
+ promptTitle: t("agentPanel.voiceMode.promptTitle"),
746
+ promptDescription: t("agentPanel.voiceMode.promptDescription"),
747
+ setupTitle: t("agentPanel.voiceMode.setupTitle"),
748
+ setupDescription: t("agentPanel.voiceMode.setupDescription"),
749
+ connectBuilder: t("agentPanel.voiceMode.connectBuilder"),
750
+ useOpenAiKey: t("agentPanel.voiceMode.useOpenAiKey"),
751
+ startWithOpenAiKey: t("agentPanel.voiceMode.startWithOpenAiKey"),
752
+ startVoiceMode: t("agentPanel.voiceMode.start"),
753
+ keepDictating: t("agentPanel.voiceMode.keepDictating"),
754
+ showChat: t("agentPanel.voiceMode.showChat"),
755
+ hideChat: t("agentPanel.voiceMode.hideChat"),
756
+ endVoiceMode: t("agentPanel.voiceMode.end"),
757
+ voiceSettings: t("agentPanel.voiceMode.voiceSettings"),
758
+ settings: {
759
+ language: t("agentPanel.voiceMode.settings.language"),
760
+ autoLanguage: t("agentPanel.voiceMode.settings.autoLanguage"),
761
+ languages: {
762
+ en: t("agentPanel.voiceMode.settings.languages.en"),
763
+ es: t("agentPanel.voiceMode.settings.languages.es"),
764
+ fr: t("agentPanel.voiceMode.settings.languages.fr"),
765
+ de: t("agentPanel.voiceMode.settings.languages.de"),
766
+ it: t("agentPanel.voiceMode.settings.languages.it"),
767
+ pt: t("agentPanel.voiceMode.settings.languages.pt"),
768
+ ja: t("agentPanel.voiceMode.settings.languages.ja"),
769
+ ko: t("agentPanel.voiceMode.settings.languages.ko"),
770
+ zh: t("agentPanel.voiceMode.settings.languages.zh"),
771
+ },
772
+ intelligence: t("agentPanel.voiceMode.settings.intelligence"),
773
+ intelligenceLevels: {
774
+ instant: t("agentPanel.voiceMode.settings.intelligenceLevels.instant"),
775
+ balanced: t("agentPanel.voiceMode.settings.intelligenceLevels.balanced"),
776
+ deep: t("agentPanel.voiceMode.settings.intelligenceLevels.deep"),
777
+ },
778
+ voiceStyle: t("agentPanel.voiceMode.settings.voiceStyle"),
779
+ microphone: t("agentPanel.voiceMode.settings.microphone"),
780
+ defaultMicrophone: t("agentPanel.voiceMode.settings.defaultMicrophone"),
781
+ microphoneSwitchFailed: t("agentPanel.voiceMode.settings.microphoneSwitchFailed"),
782
+ voiceChangePending: t("agentPanel.voiceMode.settings.voiceChangePending"),
783
+ voiceDescriptions: {
784
+ marin: t("agentPanel.voiceMode.settings.voiceDescriptions.marin"),
785
+ cedar: t("agentPanel.voiceMode.settings.voiceDescriptions.cedar"),
786
+ coral: t("agentPanel.voiceMode.settings.voiceDescriptions.coral"),
787
+ sage: t("agentPanel.voiceMode.settings.voiceDescriptions.sage"),
788
+ verse: t("agentPanel.voiceMode.settings.voiceDescriptions.verse"),
789
+ alloy: t("agentPanel.voiceMode.settings.voiceDescriptions.alloy"),
790
+ ash: t("agentPanel.voiceMode.settings.voiceDescriptions.ash"),
791
+ ballad: t("agentPanel.voiceMode.settings.voiceDescriptions.ballad"),
792
+ echo: t("agentPanel.voiceMode.settings.voiceDescriptions.echo"),
793
+ shimmer: t("agentPanel.voiceMode.settings.voiceDescriptions.shimmer"),
794
+ },
795
+ },
796
+ status: {
797
+ connecting: t("agentPanel.voiceMode.status.connecting"),
798
+ listening: t("agentPanel.voiceMode.status.listening"),
799
+ speaking: t("agentPanel.voiceMode.status.speaking"),
800
+ working: t("agentPanel.voiceMode.status.working"),
801
+ error: t("agentPanel.voiceMode.status.error"),
802
+ ending: t("agentPanel.voiceMode.status.ending"),
803
+ },
804
+ errors: {
805
+ unsupported: t("agentPanel.voiceMode.errors.unsupported"),
806
+ responseFailed: t("agentPanel.voiceMode.errors.responseFailed"),
807
+ sessionFailed: t("agentPanel.voiceMode.errors.sessionFailed"),
808
+ channelDisconnected: t("agentPanel.voiceMode.errors.channelDisconnected"),
809
+ connectionTimedOut: t("agentPanel.voiceMode.errors.connectionTimedOut"),
810
+ connectionFailed: t("agentPanel.voiceMode.errors.connectionFailed"),
811
+ offerFailed: t("agentPanel.voiceMode.errors.offerFailed"),
812
+ },
813
+ };
814
+ }
815
+ export function useRealtimeVoiceModeCopy() {
816
+ const { translate: t } = useComposerRuntimeAdapters();
817
+ return useMemo(() => voiceCopy(t), [t]);
818
+ }
819
+ function useRealtimeVoiceModeController(browserTabId, copy) {
820
+ const adapters = useComposerRuntimeAdapters();
821
+ const [state, setState] = useState("idle");
822
+ const [error, setError] = useState(null);
823
+ const [chatVisible, setChatVisible] = useState(false);
824
+ const [preferences, setPreferences] = useState(DEFAULT_REALTIME_VOICE_PREFERENCES);
825
+ const [voiceChangePending, setVoiceChangePending] = useState(false);
826
+ const [microphones, setMicrophones] = useState([]);
827
+ const [microphoneDeviceId, setMicrophoneDeviceId] = useState(readRealtimeVoiceMicrophoneId);
828
+ const [microphoneSwitching, setMicrophoneSwitching] = useState(false);
829
+ const [microphoneError, setMicrophoneError] = useState(null);
830
+ const [audioLevels] = useState(createRealtimeVoiceAudioLevelStore);
831
+ const stateRef = useRef(state);
832
+ const preferencesRef = useRef(preferences);
833
+ const hasOutputAudioRef = useRef(false);
834
+ const peerRef = useRef(null);
835
+ const channelRef = useRef(null);
836
+ const streamRef = useRef(null);
837
+ const audioRef = useRef(null);
838
+ const audioContextRef = useRef(null);
839
+ const inputAnalyserRef = useRef(null);
840
+ const outputAnalyserRef = useRef(null);
841
+ const inputMeterBufferRef = useRef(null);
842
+ const outputMeterBufferRef = useRef(null);
843
+ const inputSourceRef = useRef(null);
844
+ const outputSourceRef = useRef(null);
845
+ const meterFrameRef = useRef(null);
846
+ const lastMeterSampleRef = useRef(0);
847
+ const abortRef = useRef(null);
848
+ const connectionGateRef = useRef(null);
849
+ const handledCallsRef = useRef(new Set());
850
+ const sessionIdRef = useRef(undefined);
851
+ const capabilityRef = useRef(undefined);
852
+ const transportGenerationRef = useRef(0);
853
+ const startedAtRef = useRef(undefined);
854
+ const lastUserTextRef = useRef("");
855
+ const lastAssistantTextRef = useRef("");
856
+ const transcriptThreadIdRef = useRef(undefined);
857
+ const transcriptSequenceRef = useRef(0);
858
+ const preferencesHydratedRef = useRef(false);
859
+ const preferencesEditedRef = useRef(false);
860
+ const preferenceWriteChainRef = useRef(Promise.resolve());
861
+ const toolManifestCoordinator = useMemo(() => createRealtimeVoiceToolManifestCoordinator((event) => {
862
+ sendDataChannelEvent(channelRef.current, event);
863
+ }), []);
864
+ const switchMicrophoneRef = useRef(async () => undefined);
865
+ useEffect(() => {
866
+ stateRef.current = state;
867
+ }, [state]);
868
+ useEffect(() => {
869
+ preferencesRef.current = preferences;
870
+ }, [preferences]);
871
+ const hydratePreferences = useCallback(async () => {
872
+ if (preferencesHydratedRef.current)
873
+ return;
874
+ try {
875
+ const stored = await adapters.voice.readAppState(REALTIME_VOICE_PREFERENCES_KEY);
876
+ if (!preferencesEditedRef.current && stored != null) {
877
+ const next = normalizeRealtimeVoicePreferences(stored);
878
+ preferencesRef.current = next;
879
+ setPreferences(next);
880
+ }
881
+ }
882
+ catch {
883
+ // Preferences are optional; keep safe defaults when storage is unavailable.
884
+ }
885
+ finally {
886
+ preferencesHydratedRef.current = true;
887
+ }
888
+ }, [adapters]);
889
+ const syncAppState = useCallback((nextState) => {
890
+ const value = nextState === "idle"
891
+ ? null
892
+ : {
893
+ active: true,
894
+ status: nextState,
895
+ model: "gpt-realtime-2.1",
896
+ startedAt: startedAtRef.current,
897
+ sessionId: sessionIdRef.current,
898
+ browserTabId,
899
+ preferences: preferencesRef.current,
900
+ lastUserText: lastUserTextRef.current || undefined,
901
+ lastAssistantText: lastAssistantTextRef.current || undefined,
902
+ };
903
+ void Promise.resolve(adapters.voice.setAppState(REALTIME_VOICE_STATE_KEY, value)).catch(() => undefined);
904
+ }, [adapters, browserTabId]);
905
+ const transition = useCallback((nextState) => {
906
+ stateRef.current = nextState;
907
+ setState(nextState);
908
+ syncAppState(nextState);
909
+ }, [syncAppState]);
910
+ const savePreferences = useCallback((next) => {
911
+ preferencesEditedRef.current = true;
912
+ preferencesHydratedRef.current = true;
913
+ preferencesRef.current = next;
914
+ setPreferences(next);
915
+ preferenceWriteChainRef.current = preferenceWriteChainRef.current
916
+ .catch(() => undefined)
917
+ .then(async () => {
918
+ await adapters.voice.setAppState(REALTIME_VOICE_PREFERENCES_KEY, next);
919
+ })
920
+ .catch(() => undefined);
921
+ }, [adapters]);
922
+ const updateLivePreferences = useCallback((next, includeVoice = false) => {
923
+ sendDataChannelEvent(channelRef.current, createRealtimeVoicePreferenceUpdate(next, {
924
+ browserLanguages: typeof navigator === "undefined" ? [] : navigator.languages,
925
+ includeVoice,
926
+ }));
927
+ }, []);
928
+ const setLanguage = useCallback((language) => {
929
+ const next = { ...preferencesRef.current, language };
930
+ savePreferences(next);
931
+ updateLivePreferences(next);
932
+ }, [savePreferences, updateLivePreferences]);
933
+ const setIntelligence = useCallback((intelligence) => {
934
+ const next = { ...preferencesRef.current, intelligence };
935
+ savePreferences(next);
936
+ updateLivePreferences(next);
937
+ }, [savePreferences, updateLivePreferences]);
938
+ const setVoice = useCallback((voice) => {
939
+ const next = { ...preferencesRef.current, voice };
940
+ savePreferences(next);
941
+ if (hasOutputAudioRef.current) {
942
+ setVoiceChangePending(true);
943
+ updateLivePreferences(next);
944
+ return;
945
+ }
946
+ setVoiceChangePending(false);
947
+ updateLivePreferences(next, true);
948
+ }, [savePreferences, updateLivePreferences]);
949
+ const refreshMicrophones = useCallback(async () => {
950
+ const mediaDevices = navigator.mediaDevices;
951
+ if (!mediaDevices?.enumerateDevices)
952
+ return null;
953
+ try {
954
+ const devices = await mediaDevices.enumerateDevices();
955
+ const inputs = devices
956
+ .filter((device) => device.kind === "audioinput" &&
957
+ device.deviceId &&
958
+ device.deviceId !== "default" &&
959
+ device.deviceId !== "communications")
960
+ .map((device, index) => ({
961
+ deviceId: device.deviceId,
962
+ label: device.label || `Microphone ${index + 1}`,
963
+ }));
964
+ setMicrophones(inputs);
965
+ return inputs;
966
+ }
967
+ catch {
968
+ // Enumeration is progressive enhancement; system default remains usable.
969
+ return null;
970
+ }
971
+ }, []);
972
+ const startMeterLoop = useCallback(() => {
973
+ if (meterFrameRef.current !== null)
974
+ return;
975
+ const sample = (timestamp) => {
976
+ meterFrameRef.current = requestAnimationFrame(sample);
977
+ if (timestamp - lastMeterSampleRef.current < 50)
978
+ return;
979
+ lastMeterSampleRef.current = timestamp;
980
+ const current = audioLevels.getSnapshot();
981
+ let input = current.input;
982
+ let output = current.output;
983
+ const inputAnalyser = inputAnalyserRef.current;
984
+ const inputBuffer = inputMeterBufferRef.current;
985
+ if (inputAnalyser && inputBuffer) {
986
+ inputAnalyser.getByteTimeDomainData(inputBuffer);
987
+ input = smoothRealtimeVoiceLevel(input, normalizeRealtimeVoiceRms(inputBuffer));
988
+ }
989
+ const outputAnalyser = outputAnalyserRef.current;
990
+ const outputBuffer = outputMeterBufferRef.current;
991
+ if (outputAnalyser && outputBuffer) {
992
+ outputAnalyser.getByteTimeDomainData(outputBuffer);
993
+ output = smoothRealtimeVoiceLevel(output, normalizeRealtimeVoiceRms(outputBuffer));
994
+ }
995
+ audioLevels.set({ input, output });
996
+ };
997
+ meterFrameRef.current = requestAnimationFrame(sample);
998
+ }, [audioLevels]);
999
+ const attachAudioMeter = useCallback((stream, channel) => {
1000
+ try {
1001
+ const AudioCtor = window.AudioContext ??
1002
+ window.webkitAudioContext;
1003
+ if (!AudioCtor)
1004
+ return;
1005
+ const context = audioContextRef.current ?? new AudioCtor();
1006
+ audioContextRef.current = context;
1007
+ void context.resume().catch(() => undefined);
1008
+ const analyser = context.createAnalyser();
1009
+ analyser.fftSize = 512;
1010
+ const source = context.createMediaStreamSource(stream);
1011
+ source.connect(analyser);
1012
+ const buffer = new Uint8Array(analyser.frequencyBinCount);
1013
+ if (channel === "input") {
1014
+ inputSourceRef.current?.disconnect();
1015
+ inputSourceRef.current = source;
1016
+ inputAnalyserRef.current = analyser;
1017
+ inputMeterBufferRef.current = buffer;
1018
+ }
1019
+ else {
1020
+ outputSourceRef.current?.disconnect();
1021
+ outputSourceRef.current = source;
1022
+ outputAnalyserRef.current = analyser;
1023
+ outputMeterBufferRef.current = buffer;
1024
+ }
1025
+ startMeterLoop();
1026
+ }
1027
+ catch {
1028
+ // Audio metering is visual-only; keep the realtime call healthy.
1029
+ }
1030
+ }, [startMeterLoop]);
1031
+ const setMicrophone = useCallback(async (deviceId, force = false) => {
1032
+ if ((!force && deviceId === microphoneDeviceId) || microphoneSwitching) {
1033
+ return;
1034
+ }
1035
+ const mediaDevices = navigator.mediaDevices;
1036
+ const peer = peerRef.current;
1037
+ if (!mediaDevices?.getUserMedia || !peer)
1038
+ return;
1039
+ setMicrophoneSwitching(true);
1040
+ setMicrophoneError(null);
1041
+ try {
1042
+ const replacementStream = await replaceRealtimeVoiceMicrophone({
1043
+ mediaDevices,
1044
+ peer,
1045
+ currentStream: streamRef.current,
1046
+ deviceId,
1047
+ });
1048
+ const replacementTrack = replacementStream.getAudioTracks()[0];
1049
+ streamRef.current = replacementStream;
1050
+ if (replacementTrack) {
1051
+ replacementTrack.onended = () => {
1052
+ void switchMicrophoneRef.current("default", true);
1053
+ };
1054
+ }
1055
+ attachAudioMeter(streamRef.current, "input");
1056
+ writeRealtimeVoiceMicrophoneId(deviceId);
1057
+ setMicrophoneDeviceId(deviceId);
1058
+ await refreshMicrophones();
1059
+ }
1060
+ catch {
1061
+ setMicrophoneError(copy?.settings.microphoneSwitchFailed ??
1062
+ "Could not switch microphones. Your current microphone is still active.");
1063
+ }
1064
+ finally {
1065
+ setMicrophoneSwitching(false);
1066
+ }
1067
+ }, [
1068
+ attachAudioMeter,
1069
+ copy,
1070
+ microphoneDeviceId,
1071
+ microphoneSwitching,
1072
+ refreshMicrophones,
1073
+ ]);
1074
+ switchMicrophoneRef.current = setMicrophone;
1075
+ const cleanupTransport = useCallback(() => {
1076
+ connectionGateRef.current?.cancel();
1077
+ connectionGateRef.current = null;
1078
+ transportGenerationRef.current += 1;
1079
+ capabilityRef.current = undefined;
1080
+ abortRef.current?.abort();
1081
+ abortRef.current = null;
1082
+ channelRef.current?.close();
1083
+ channelRef.current = null;
1084
+ peerRef.current?.close();
1085
+ peerRef.current = null;
1086
+ for (const track of streamRef.current?.getTracks() ?? []) {
1087
+ track.onended = null;
1088
+ track.stop();
1089
+ }
1090
+ streamRef.current = null;
1091
+ if (audioRef.current) {
1092
+ audioRef.current.pause();
1093
+ audioRef.current.srcObject = null;
1094
+ }
1095
+ audioRef.current = null;
1096
+ if (meterFrameRef.current !== null) {
1097
+ cancelAnimationFrame(meterFrameRef.current);
1098
+ meterFrameRef.current = null;
1099
+ }
1100
+ inputSourceRef.current?.disconnect();
1101
+ outputSourceRef.current?.disconnect();
1102
+ inputSourceRef.current = null;
1103
+ outputSourceRef.current = null;
1104
+ inputAnalyserRef.current = null;
1105
+ outputAnalyserRef.current = null;
1106
+ inputMeterBufferRef.current = null;
1107
+ outputMeterBufferRef.current = null;
1108
+ lastMeterSampleRef.current = 0;
1109
+ const audioContext = audioContextRef.current;
1110
+ audioContextRef.current = null;
1111
+ if (audioContext)
1112
+ void audioContext.close().catch(() => undefined);
1113
+ audioLevels.reset();
1114
+ handledCallsRef.current.clear();
1115
+ toolManifestCoordinator.reset();
1116
+ }, [audioLevels, toolManifestCoordinator]);
1117
+ const fail = useCallback((message, options) => {
1118
+ cleanupTransport();
1119
+ setError(message);
1120
+ transition("error");
1121
+ if (options?.openKeySettings)
1122
+ openOpenAiKeySettings();
1123
+ }, [cleanupTransport, transition]);
1124
+ const handleFunctionCall = useCallback(async (call) => {
1125
+ if (handledCallsRef.current.has(call.callId))
1126
+ return;
1127
+ handledCallsRef.current.add(call.callId);
1128
+ const transportGeneration = transportGenerationRef.current;
1129
+ const transportChannel = channelRef.current;
1130
+ transition("working");
1131
+ let result;
1132
+ try {
1133
+ const args = parseFunctionArguments(call.argumentsText);
1134
+ result = await executeRealtimeVoiceTool({
1135
+ name: call.name,
1136
+ args,
1137
+ callId: call.callId,
1138
+ sessionId: sessionIdRef.current,
1139
+ browserTabId,
1140
+ capability: capabilityRef.current,
1141
+ signal: abortRef.current?.signal,
1142
+ });
1143
+ }
1144
+ catch (toolError) {
1145
+ result = {
1146
+ callId: call.callId,
1147
+ status: "failed",
1148
+ output: errorMessage(toolError),
1149
+ };
1150
+ }
1151
+ if (transportGeneration !== transportGenerationRef.current ||
1152
+ channelRef.current !== transportChannel) {
1153
+ return;
1154
+ }
1155
+ toolManifestCoordinator.enqueue(result);
1156
+ }, [browserTabId, toolManifestCoordinator, transition]);
1157
+ const publishCompletedTranscript = useCallback((transcript) => {
1158
+ const threadId = transcriptThreadIdRef.current;
1159
+ if (!threadId)
1160
+ return;
1161
+ const sessionIdentity = sessionIdRef.current ?? startedAtRef.current ?? "pending";
1162
+ const providerIdentity = transcript.providerId ?? `sequence-${++transcriptSequenceRef.current}`;
1163
+ realtimeVoiceTranscriptRegistry.publish({
1164
+ id: `realtime-voice:${sessionIdentity}:${transcript.role}:${providerIdentity}`,
1165
+ threadId,
1166
+ role: transcript.role,
1167
+ text: transcript.text,
1168
+ createdAt: new Date().toISOString(),
1169
+ });
1170
+ }, []);
1171
+ const transcriptSequencer = useMemo(() => createRealtimeVoiceTranscriptSequencer(publishCompletedTranscript), [publishCompletedTranscript]);
1172
+ const greetingStarter = useMemo(() => createRealtimeVoiceGreetingStarter((event) => {
1173
+ sendDataChannelEvent(channelRef.current, event);
1174
+ }), []);
1175
+ const handleServerEvent = useCallback((event) => {
1176
+ transcriptSequencer.handle(event);
1177
+ const sessionTools = extractRealtimeVoiceSessionTools(event);
1178
+ if (sessionTools)
1179
+ toolManifestCoordinator.setSessionTools(sessionTools);
1180
+ if (event.type === "session.created") {
1181
+ connectionGateRef.current?.markSessionCreated();
1182
+ connectionGateRef.current = null;
1183
+ const session = event.session;
1184
+ if (session && typeof session === "object") {
1185
+ const id = session.id;
1186
+ if (typeof id === "string")
1187
+ sessionIdRef.current = id;
1188
+ }
1189
+ updateLivePreferences(preferencesRef.current, true);
1190
+ greetingStarter.start();
1191
+ transition("working");
1192
+ }
1193
+ else if (event.type === "input_audio_buffer.speech_started") {
1194
+ transition("listening");
1195
+ }
1196
+ else if (event.type === "input_audio_buffer.speech_stopped") {
1197
+ transition("working");
1198
+ }
1199
+ else if (event.type === "response.created") {
1200
+ // From this point onward the response is committed to audio output, so
1201
+ // changing voices risks violating Realtime's per-session voice lock.
1202
+ hasOutputAudioRef.current = true;
1203
+ lastAssistantTextRef.current = "";
1204
+ transition("working");
1205
+ }
1206
+ else if (event.type === "response.output_audio_transcript.delta") {
1207
+ hasOutputAudioRef.current = true;
1208
+ if (typeof event.delta === "string") {
1209
+ lastAssistantTextRef.current += event.delta;
1210
+ }
1211
+ transition("speaking");
1212
+ }
1213
+ else if (event.type === "response.output_audio_transcript.done") {
1214
+ if (typeof event.transcript === "string") {
1215
+ lastAssistantTextRef.current = event.transcript;
1216
+ }
1217
+ syncAppState("speaking");
1218
+ }
1219
+ else if (event.type === "conversation.item.input_audio_transcription.completed") {
1220
+ if (typeof event.transcript === "string") {
1221
+ lastUserTextRef.current = event.transcript;
1222
+ }
1223
+ syncAppState("working");
1224
+ }
1225
+ else if (event.type === "response.done") {
1226
+ const response = event.response;
1227
+ const status = response && typeof response === "object"
1228
+ ? response.status
1229
+ : undefined;
1230
+ if (status === "failed") {
1231
+ fail(copy?.errors.responseFailed ??
1232
+ "OpenAI could not complete the voice response.");
1233
+ return;
1234
+ }
1235
+ }
1236
+ else if (event.type === "error") {
1237
+ const detail = event.error;
1238
+ const message = detail && typeof detail === "object"
1239
+ ? String(detail.message ?? "")
1240
+ : typeof detail === "string"
1241
+ ? detail
1242
+ : "";
1243
+ const expansionEventId = detail && typeof detail === "object"
1244
+ ? (detail.event_id ?? event.event_id)
1245
+ : event.event_id;
1246
+ if (toolManifestCoordinator.handleError(typeof expansionEventId === "string" ? expansionEventId : undefined, message)) {
1247
+ return;
1248
+ }
1249
+ fail(message ||
1250
+ copy?.errors.sessionFailed ||
1251
+ "The realtime voice session encountered an error.");
1252
+ return;
1253
+ }
1254
+ const calls = extractRealtimeVoiceFunctionCalls(event);
1255
+ for (const call of calls)
1256
+ void handleFunctionCall(call);
1257
+ if (event.type === "response.done" && calls.length === 0) {
1258
+ transition("listening");
1259
+ }
1260
+ }, [
1261
+ copy,
1262
+ fail,
1263
+ greetingStarter,
1264
+ handleFunctionCall,
1265
+ syncAppState,
1266
+ transcriptSequencer,
1267
+ transition,
1268
+ toolManifestCoordinator,
1269
+ updateLivePreferences,
1270
+ ]);
1271
+ const start = useCallback(async () => {
1272
+ if (stateRef.current !== "idle")
1273
+ return;
1274
+ if (typeof RTCPeerConnection === "undefined" ||
1275
+ !navigator.mediaDevices?.getUserMedia) {
1276
+ fail(copy?.errors.unsupported ??
1277
+ "This browser does not support realtime voice conversations.");
1278
+ return;
1279
+ }
1280
+ setError(null);
1281
+ startedAtRef.current = new Date().toISOString();
1282
+ lastUserTextRef.current = "";
1283
+ lastAssistantTextRef.current = "";
1284
+ sessionIdRef.current = undefined;
1285
+ hasOutputAudioRef.current = false;
1286
+ greetingStarter.reset();
1287
+ setVoiceChangePending(false);
1288
+ transcriptThreadIdRef.current =
1289
+ realtimeVoiceTranscriptRegistry.activeThreadId();
1290
+ transcriptSequenceRef.current = 0;
1291
+ transcriptSequencer.reset();
1292
+ transition("connecting");
1293
+ setChatVisible(false);
1294
+ window.dispatchEvent(new Event("agent-panel:close"));
1295
+ await hydratePreferences();
1296
+ if (stateRef.current !== "connecting")
1297
+ return;
1298
+ const abortController = new AbortController();
1299
+ abortRef.current = abortController;
1300
+ const isCurrentAttempt = () => abortRef.current === abortController && !abortController.signal.aborted;
1301
+ const connectionGate = createRealtimeVoiceConnectionGate(() => {
1302
+ if (!isCurrentAttempt())
1303
+ return;
1304
+ fail(copy?.errors.connectionTimedOut ??
1305
+ "The realtime voice connection timed out.");
1306
+ });
1307
+ connectionGateRef.current = connectionGate;
1308
+ try {
1309
+ const stream = await navigator.mediaDevices.getUserMedia({
1310
+ audio: createRealtimeVoiceAudioConstraints(readRealtimeVoiceMicrophoneId()),
1311
+ });
1312
+ if (!isCurrentAttempt()) {
1313
+ for (const track of stream.getTracks())
1314
+ track.stop();
1315
+ return;
1316
+ }
1317
+ streamRef.current = stream;
1318
+ for (const track of stream.getAudioTracks()) {
1319
+ track.onended = () => {
1320
+ void switchMicrophoneRef.current("default", true);
1321
+ };
1322
+ }
1323
+ attachAudioMeter(stream, "input");
1324
+ void refreshMicrophones().then((inputs) => {
1325
+ const selected = readRealtimeVoiceMicrophoneId();
1326
+ if (inputs &&
1327
+ selected !== "default" &&
1328
+ !inputs.some((device) => device.deviceId === selected)) {
1329
+ // The persisted device was only an ideal preference, so getUserMedia
1330
+ // already selected a usable fallback for this new session.
1331
+ writeRealtimeVoiceMicrophoneId("default");
1332
+ setMicrophoneDeviceId("default");
1333
+ }
1334
+ });
1335
+ const peer = new RTCPeerConnection();
1336
+ peerRef.current = peer;
1337
+ for (const track of stream.getAudioTracks())
1338
+ peer.addTrack(track, stream);
1339
+ const audio = document.createElement("audio");
1340
+ audio.autoplay = true;
1341
+ audio.setAttribute("playsinline", "");
1342
+ audioRef.current = audio;
1343
+ peer.ontrack = (trackEvent) => {
1344
+ const remoteStream = trackEvent.streams[0] ?? null;
1345
+ audio.srcObject = remoteStream;
1346
+ if (remoteStream)
1347
+ attachAudioMeter(remoteStream, "output");
1348
+ void audio.play().catch(() => undefined);
1349
+ };
1350
+ const channel = peer.createDataChannel("oai-events");
1351
+ channelRef.current = channel;
1352
+ channel.onopen = connectionGate.markTransportReady;
1353
+ channel.onmessage = (messageEvent) => {
1354
+ if (!isCurrentAttempt())
1355
+ return;
1356
+ try {
1357
+ handleServerEvent(JSON.parse(String(messageEvent.data)));
1358
+ }
1359
+ catch {
1360
+ // Ignore malformed provider events without ending a healthy call.
1361
+ }
1362
+ };
1363
+ channel.onerror = () => {
1364
+ if (!isCurrentAttempt())
1365
+ return;
1366
+ fail(copy?.errors.channelDisconnected ??
1367
+ "The realtime voice control channel disconnected.");
1368
+ };
1369
+ peer.onconnectionstatechange = () => {
1370
+ if (!isCurrentAttempt())
1371
+ return;
1372
+ if (peer.connectionState === "connected") {
1373
+ connectionGate.markTransportReady();
1374
+ }
1375
+ if (peer.connectionState === "failed") {
1376
+ fail(copy?.errors.connectionFailed ??
1377
+ "The realtime voice connection failed.");
1378
+ }
1379
+ };
1380
+ const offer = await peer.createOffer();
1381
+ if (!isCurrentAttempt())
1382
+ return;
1383
+ await peer.setLocalDescription(offer);
1384
+ if (!isCurrentAttempt())
1385
+ return;
1386
+ if (!offer.sdp) {
1387
+ throw new Error(copy?.errors.offerFailed ??
1388
+ "The browser did not create an audio offer.");
1389
+ }
1390
+ const answer = await createRealtimeVoiceSessionWithCapability(offer.sdp, {
1391
+ browserTabId,
1392
+ signal: abortController.signal,
1393
+ preferences: preferencesRef.current,
1394
+ browserLanguages: navigator.languages,
1395
+ });
1396
+ if (!isCurrentAttempt())
1397
+ return;
1398
+ capabilityRef.current = answer.capability;
1399
+ await peer.setRemoteDescription({ type: "answer", sdp: answer.sdp });
1400
+ }
1401
+ catch (startError) {
1402
+ // Ending a connection aborts the SDP request by design. A superseded
1403
+ // attempt must never resurrect the dock or surface that cancellation as
1404
+ // an error after cleanup.
1405
+ if (isRealtimeVoiceAbortError(startError) ||
1406
+ abortController.signal.aborted ||
1407
+ abortRef.current !== abortController) {
1408
+ return;
1409
+ }
1410
+ if (isRealtimeVoiceSetupRequiredError(startError)) {
1411
+ // Credentials can disappear after the entry-point status check. Treat
1412
+ // the authoritative setup response as a gate, not as an active voice
1413
+ // session failure: clean up the mic/RTC attempt, reopen chat, and
1414
+ // refresh both the chat and voice provider setup surfaces.
1415
+ cleanupTransport();
1416
+ setError(null);
1417
+ startedAtRef.current = undefined;
1418
+ sessionIdRef.current = undefined;
1419
+ transcriptThreadIdRef.current = undefined;
1420
+ transition("idle");
1421
+ setChatVisible(true);
1422
+ window.dispatchEvent(new Event("agent-panel:open"));
1423
+ window.dispatchEvent(new Event("agent-engine:configured-changed"));
1424
+ return;
1425
+ }
1426
+ const status = startError?.status;
1427
+ fail(errorMessage(startError), { openKeySettings: status === 400 });
1428
+ }
1429
+ }, [
1430
+ attachAudioMeter,
1431
+ browserTabId,
1432
+ cleanupTransport,
1433
+ copy,
1434
+ fail,
1435
+ greetingStarter,
1436
+ handleServerEvent,
1437
+ hydratePreferences,
1438
+ refreshMicrophones,
1439
+ transcriptSequencer,
1440
+ transition,
1441
+ ]);
1442
+ const end = useCallback(() => {
1443
+ if (stateRef.current === "idle" || stateRef.current === "ending")
1444
+ return;
1445
+ const transcriptThreadId = transcriptThreadIdRef.current;
1446
+ const activeThreadId = realtimeVoiceTranscriptRegistry.activeThreadId();
1447
+ transition("ending");
1448
+ cleanupTransport();
1449
+ setError(null);
1450
+ sessionIdRef.current = undefined;
1451
+ startedAtRef.current = undefined;
1452
+ transcriptThreadIdRef.current = undefined;
1453
+ setChatVisible(true);
1454
+ if (shouldRestoreRealtimeVoiceTranscriptThread(transcriptThreadId, activeThreadId)) {
1455
+ adapters.agentChat.requestThreadOpen({
1456
+ threadId: transcriptThreadId,
1457
+ // The request is delivered asynchronously. Re-checking this at the
1458
+ // receiver prevents a navigation that happened during that gap from
1459
+ // being overwritten.
1460
+ onlyIfActiveThreadId: transcriptThreadId,
1461
+ });
1462
+ }
1463
+ else {
1464
+ window.dispatchEvent(new Event("agent-panel:open"));
1465
+ }
1466
+ transition("idle");
1467
+ }, [adapters, cleanupTransport, transition]);
1468
+ const toggleChat = useCallback(() => {
1469
+ setChatVisible((current) => !current);
1470
+ window.dispatchEvent(new Event("agent-panel:toggle"));
1471
+ }, []);
1472
+ useEffect(() => {
1473
+ const mediaDevices = navigator.mediaDevices;
1474
+ if (!mediaDevices?.addEventListener)
1475
+ return;
1476
+ const handleDeviceChange = () => {
1477
+ const selected = readRealtimeVoiceMicrophoneId();
1478
+ void refreshMicrophones().then((inputs) => {
1479
+ if (inputs &&
1480
+ selected !== "default" &&
1481
+ !inputs.some((device) => device.deviceId === selected)) {
1482
+ void setMicrophone("default");
1483
+ }
1484
+ });
1485
+ };
1486
+ mediaDevices.addEventListener("devicechange", handleDeviceChange);
1487
+ return () => mediaDevices.removeEventListener("devicechange", handleDeviceChange);
1488
+ }, [refreshMicrophones, setMicrophone]);
1489
+ useEffect(() => {
1490
+ return adapters.voice.subscribeSidebarState((detail) => {
1491
+ if (detail && typeof detail.open === "boolean") {
1492
+ setChatVisible(detail.open);
1493
+ }
1494
+ });
1495
+ }, [adapters]);
1496
+ useEffect(() => {
1497
+ const stopListening = listenForRealtimeVoicePageHide(cleanupTransport);
1498
+ return () => {
1499
+ stopListening();
1500
+ cleanupTransport();
1501
+ };
1502
+ }, [cleanupTransport]);
1503
+ return {
1504
+ state,
1505
+ active: state !== "idle",
1506
+ errorMessage: error,
1507
+ chatVisible,
1508
+ audioLevels,
1509
+ preferences,
1510
+ microphones,
1511
+ microphoneDeviceId,
1512
+ microphoneSwitching,
1513
+ microphoneError,
1514
+ voiceChangePending,
1515
+ setLanguage,
1516
+ setIntelligence,
1517
+ setVoice,
1518
+ setMicrophone,
1519
+ start,
1520
+ end,
1521
+ toggleChat,
1522
+ };
1523
+ }
1524
+ const RealtimeVoiceModeContext = createContext(null);
1525
+ export function RealtimeVoiceModeProvider({ children, browserTabId, }) {
1526
+ const adapters = useComposerRuntimeAdapters();
1527
+ const resolvedBrowserTabId = useMemo(() => browserTabId ?? adapters.voice.getBrowserTabId(), [adapters, browserTabId]);
1528
+ const copy = useRealtimeVoiceModeCopy();
1529
+ const voice = useRealtimeVoiceModeController(resolvedBrowserTabId, copy);
1530
+ const inlineSettings = useMemo(() => ({
1531
+ dialogLabel: copy.voiceSettings,
1532
+ ...(voice.voiceChangePending
1533
+ ? { appliesNextConversationNote: copy.settings.voiceChangePending }
1534
+ : {}),
1535
+ ...(voice.microphoneError
1536
+ ? { microphoneError: voice.microphoneError }
1537
+ : {}),
1538
+ microphone: {
1539
+ label: copy.settings.microphone,
1540
+ value: voice.microphoneDeviceId,
1541
+ disabled: voice.microphoneSwitching,
1542
+ options: [
1543
+ {
1544
+ value: "default",
1545
+ label: copy.settings.defaultMicrophone,
1546
+ },
1547
+ ...voice.microphones.map((microphone) => ({
1548
+ value: microphone.deviceId,
1549
+ label: microphone.label,
1550
+ })),
1551
+ ],
1552
+ onValueChange: (value) => {
1553
+ void voice.setMicrophone(value);
1554
+ },
1555
+ },
1556
+ language: {
1557
+ label: copy.settings.language,
1558
+ value: voice.preferences.language,
1559
+ options: REALTIME_VOICE_LANGUAGES.map((language) => ({
1560
+ value: language,
1561
+ label: language === "auto"
1562
+ ? copy.settings.autoLanguage
1563
+ : copy.settings.languages[language],
1564
+ })),
1565
+ onValueChange: (value) => {
1566
+ if (isOneOf(REALTIME_VOICE_LANGUAGES, value)) {
1567
+ voice.setLanguage(value);
1568
+ }
1569
+ },
1570
+ },
1571
+ intelligence: {
1572
+ label: copy.settings.intelligence,
1573
+ value: voice.preferences.intelligence,
1574
+ options: REALTIME_VOICE_INTELLIGENCE_LEVELS.map((intelligence) => ({
1575
+ value: intelligence,
1576
+ label: copy.settings.intelligenceLevels[intelligence],
1577
+ })),
1578
+ onValueChange: (value) => {
1579
+ if (isOneOf(REALTIME_VOICE_INTELLIGENCE_LEVELS, value)) {
1580
+ voice.setIntelligence(value);
1581
+ }
1582
+ },
1583
+ },
1584
+ voiceStyle: {
1585
+ label: copy.settings.voiceStyle,
1586
+ value: voice.preferences.voice,
1587
+ options: REALTIME_VOICES.map((voiceName) => ({
1588
+ value: voiceName,
1589
+ label: `${voiceName[0].toUpperCase()}${voiceName.slice(1)}`,
1590
+ description: copy.settings.voiceDescriptions[voiceName],
1591
+ })),
1592
+ onValueChange: (value) => {
1593
+ if (isOneOf(REALTIME_VOICES, value))
1594
+ voice.setVoice(value);
1595
+ },
1596
+ },
1597
+ }), [copy, voice]);
1598
+ return (_jsxs(RealtimeVoiceModeContext.Provider, { value: voice, children: [children, voice.active && typeof document !== "undefined"
1599
+ ? createPortal(_jsx(RealtimeVoiceModeDock, { state: voice.state === "idle" ? "ending" : voice.state, copy: copy, chatVisible: voice.chatVisible, audioLevels: voice.audioLevels, onToggleChat: voice.toggleChat, onEndVoiceMode: voice.end, settings: inlineSettings, errorMessage: voice.errorMessage }), document.body)
1600
+ : null] }));
1601
+ }
1602
+ /**
1603
+ * Ensure standalone/full-page composers get realtime voice without nesting a
1604
+ * second session owner inside the persistent AgentSidebar provider.
1605
+ */
1606
+ export function RealtimeVoiceModeBoundary({ children, browserTabId, }) {
1607
+ const existing = useRealtimeVoiceModeOptional();
1608
+ if (existing) {
1609
+ return (_jsx(RealtimeVoiceModeComposerSurface, { children: children }));
1610
+ }
1611
+ return (_jsx(RealtimeVoiceModeProvider, { browserTabId: browserTabId, children: _jsx(RealtimeVoiceModeComposerSurface, { children: children }) }));
1612
+ }
1613
+ /** Hide a composer while voice owns input; the dock can reveal it on demand. */
1614
+ function RealtimeVoiceModeComposerSurface({ children, }) {
1615
+ const voice = useRealtimeVoiceModeOptional();
1616
+ if (voice?.active && !voice.chatVisible)
1617
+ return null;
1618
+ return children;
1619
+ }
1620
+ export function useRealtimeVoiceMode() {
1621
+ const value = useContext(RealtimeVoiceModeContext);
1622
+ if (!value) {
1623
+ throw new Error("useRealtimeVoiceMode must be used inside RealtimeVoiceModeProvider.");
1624
+ }
1625
+ return value;
1626
+ }
1627
+ export function useRealtimeVoiceModeOptional() {
1628
+ return useContext(RealtimeVoiceModeContext);
1629
+ }
1630
+ export async function readRealtimeVoiceContext() {
1631
+ return readRealtimeVoiceContextWith({});
1632
+ }
1633
+ export async function readRealtimeVoiceContextWith(options) {
1634
+ if (options.readAppState) {
1635
+ const [navigation, url] = await Promise.all([
1636
+ Promise.resolve(options.readAppState("navigation")).catch(() => null),
1637
+ Promise.resolve(options.readAppState("__url__")).catch(() => null),
1638
+ ]);
1639
+ return { navigation, url };
1640
+ }
1641
+ const resolvePath = options.resolvePath ?? ((path) => path);
1642
+ const read = async (key) => {
1643
+ const response = await fetch(resolvePath(`/_agent-native/application-state/${encodeURIComponent(key)}`));
1644
+ if (!response.ok)
1645
+ return null;
1646
+ const body = (await response.json());
1647
+ return body && typeof body === "object" && "value" in body
1648
+ ? (body.value ?? null)
1649
+ : body;
1650
+ };
1651
+ const [navigation, url] = await Promise.all([
1652
+ read("navigation").catch(() => null),
1653
+ read("__url__").catch(() => null),
1654
+ ]);
1655
+ return { navigation, url };
1656
+ }
1657
+ //# sourceMappingURL=useRealtimeVoiceMode.js.map