@agent-native/toolkit 0.221.0-same-host-mcp-20261027-20261027081850 → 0.221.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 (259) hide show
  1. package/agent-native.eject.json +16 -1
  2. package/dist/app/agentkit/react/components.d.ts +5 -3
  3. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  4. package/dist/app/agentkit/react/components.js +103 -42
  5. package/dist/app/agentkit/react/components.js.map +1 -1
  6. package/dist/app/agentkit/react/styles.css +93 -65
  7. package/dist/app/agentkit/react/voice-transcripts.d.ts +22 -1
  8. package/dist/app/agentkit/react/voice-transcripts.d.ts.map +1 -1
  9. package/dist/app/agentkit/react/voice-transcripts.js +209 -8
  10. package/dist/app/agentkit/react/voice-transcripts.js.map +1 -1
  11. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  12. package/dist/app/chat/AgentKitAssistantChat.js +55 -40
  13. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  14. package/dist/app/chat/agentkit-chat/guided-questions.d.ts +1 -0
  15. package/dist/app/chat/agentkit-chat/guided-questions.d.ts.map +1 -1
  16. package/dist/app/chat/agentkit-chat/guided-questions.js +18 -6
  17. package/dist/app/chat/agentkit-chat/guided-questions.js.map +1 -1
  18. package/dist/app/chat/agentkit-chat/index.d.ts +1 -1
  19. package/dist/app/chat/agentkit-chat/index.d.ts.map +1 -1
  20. package/dist/app/chat/agentkit-chat/index.js +1 -1
  21. package/dist/app/chat/agentkit-chat/index.js.map +1 -1
  22. package/dist/app/chat/chat/run-recovery.d.ts.map +1 -1
  23. package/dist/app/chat/chat/run-recovery.js +5 -1
  24. package/dist/app/chat/chat/run-recovery.js.map +1 -1
  25. package/dist/app/chat/composer/runtime-adapters.d.ts.map +1 -1
  26. package/dist/app/chat/composer/runtime-adapters.js +2 -0
  27. package/dist/app/chat/composer/runtime-adapters.js.map +1 -1
  28. package/dist/app/chat/reply-connection-suggestion.d.ts +29 -0
  29. package/dist/app/chat/reply-connection-suggestion.d.ts.map +1 -0
  30. package/dist/app/chat/reply-connection-suggestion.js +42 -0
  31. package/dist/app/chat/reply-connection-suggestion.js.map +1 -0
  32. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  33. package/dist/app/i18n/catalogs/ar-SA.js +7 -0
  34. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  35. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  36. package/dist/app/i18n/catalogs/de-DE.js +7 -0
  37. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  38. package/dist/app/i18n/catalogs/en-US.d.ts +7 -0
  39. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  40. package/dist/app/i18n/catalogs/en-US.js +7 -0
  41. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  42. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  43. package/dist/app/i18n/catalogs/es-ES.js +7 -0
  44. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  45. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  46. package/dist/app/i18n/catalogs/fr-FR.js +7 -0
  47. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  48. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  49. package/dist/app/i18n/catalogs/hi-IN.js +7 -0
  50. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  51. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  52. package/dist/app/i18n/catalogs/ja-JP.js +7 -0
  53. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  54. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  55. package/dist/app/i18n/catalogs/ko-KR.js +7 -0
  56. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  57. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  58. package/dist/app/i18n/catalogs/pt-BR.js +7 -0
  59. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  60. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  61. package/dist/app/i18n/catalogs/zh-CN.js +7 -0
  62. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  63. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  64. package/dist/app/i18n/catalogs/zh-TW.js +7 -0
  65. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  66. package/dist/app/mcp-approval/McpApprovalReview.d.ts +25 -0
  67. package/dist/app/mcp-approval/McpApprovalReview.d.ts.map +1 -0
  68. package/dist/app/mcp-approval/McpApprovalReview.js +342 -0
  69. package/dist/app/mcp-approval/McpApprovalReview.js.map +1 -0
  70. package/dist/app/onboarding/FirstRunOnboarding.d.ts.map +1 -1
  71. package/dist/app/onboarding/FirstRunOnboarding.js +299 -71
  72. package/dist/app/onboarding/FirstRunOnboarding.js.map +1 -1
  73. package/dist/app/onboarding/GettingStartedCard.js +1 -1
  74. package/dist/app/onboarding/GettingStartedCard.js.map +1 -1
  75. package/dist/app/onboarding/first-run-builder-credits-flag.d.ts +2 -0
  76. package/dist/app/onboarding/first-run-builder-credits-flag.d.ts.map +1 -0
  77. package/dist/app/onboarding/first-run-builder-credits-flag.js +8 -0
  78. package/dist/app/onboarding/first-run-builder-credits-flag.js.map +1 -0
  79. package/dist/app/providers/AppProviders.d.ts.map +1 -1
  80. package/dist/app/providers/AppProviders.js +14 -1
  81. package/dist/app/providers/AppProviders.js.map +1 -1
  82. package/dist/app/resources/McpConnectionSuggestion.d.ts +2 -4
  83. package/dist/app/resources/McpConnectionSuggestion.d.ts.map +1 -1
  84. package/dist/app/resources/McpConnectionSuggestion.js +59 -36
  85. package/dist/app/resources/McpConnectionSuggestion.js.map +1 -1
  86. package/dist/app/settings/account/PreferencesSettings.d.ts.map +1 -1
  87. package/dist/app/settings/account/PreferencesSettings.js +14 -1
  88. package/dist/app/settings/account/PreferencesSettings.js.map +1 -1
  89. package/dist/app/settings/account/ThemeFamilySettings.js +2 -2
  90. package/dist/app/settings/account/ThemeFamilySettings.js.map +1 -1
  91. package/dist/app/settings/account/search-entries.d.ts +1 -0
  92. package/dist/app/settings/account/search-entries.d.ts.map +1 -1
  93. package/dist/app/settings/account/search-entries.js +5 -0
  94. package/dist/app/settings/account/search-entries.js.map +1 -1
  95. package/dist/app/settings/shell/SettingsShell.d.ts.map +1 -1
  96. package/dist/app/settings/shell/SettingsShell.js +26 -2
  97. package/dist/app/settings/shell/SettingsShell.js.map +1 -1
  98. package/dist/app/shared/AppShellSkeleton.d.ts.map +1 -1
  99. package/dist/app/shared/AppShellSkeleton.js +4 -10
  100. package/dist/app/shared/AppShellSkeleton.js.map +1 -1
  101. package/dist/app/shared/EnvironmentBadge.d.ts +2 -1
  102. package/dist/app/shared/EnvironmentBadge.d.ts.map +1 -1
  103. package/dist/app/shared/EnvironmentBadge.js +2 -3
  104. package/dist/app/shared/EnvironmentBadge.js.map +1 -1
  105. package/dist/app/shared/is-builder-io-employee.d.ts +8 -0
  106. package/dist/app/shared/is-builder-io-employee.d.ts.map +1 -0
  107. package/dist/app/shared/is-builder-io-employee.js +10 -0
  108. package/dist/app/shared/is-builder-io-employee.js.map +1 -0
  109. package/dist/app/styles/agent-conversation.css +30 -17
  110. package/dist/app/styles/theme-families.css +22 -5
  111. package/dist/app/styles/tokens/agent-kit.css +3 -3
  112. package/dist/composer/PromptComposer.d.ts +15 -3
  113. package/dist/composer/PromptComposer.d.ts.map +1 -1
  114. package/dist/composer/PromptComposer.js +74 -16
  115. package/dist/composer/PromptComposer.js.map +1 -1
  116. package/dist/composer/RealtimeVoiceMode.d.ts +7 -0
  117. package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -1
  118. package/dist/composer/RealtimeVoiceMode.js +9 -2
  119. package/dist/composer/RealtimeVoiceMode.js.map +1 -1
  120. package/dist/composer/TiptapComposer.d.ts +7 -0
  121. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  122. package/dist/composer/TiptapComposer.js +12 -4
  123. package/dist/composer/TiptapComposer.js.map +1 -1
  124. package/dist/composer/VoiceButton.d.ts +2 -0
  125. package/dist/composer/VoiceButton.d.ts.map +1 -1
  126. package/dist/composer/VoiceButton.js +5 -4
  127. package/dist/composer/VoiceButton.js.map +1 -1
  128. package/dist/composer/index.d.ts +2 -2
  129. package/dist/composer/index.d.ts.map +1 -1
  130. package/dist/composer/index.js +1 -1
  131. package/dist/composer/index.js.map +1 -1
  132. package/dist/composer/prompt-attachments.d.ts +20 -0
  133. package/dist/composer/prompt-attachments.d.ts.map +1 -1
  134. package/dist/composer/prompt-attachments.js +188 -11
  135. package/dist/composer/prompt-attachments.js.map +1 -1
  136. package/dist/composer/realtime-voice-transcript.d.ts +27 -0
  137. package/dist/composer/realtime-voice-transcript.d.ts.map +1 -1
  138. package/dist/composer/realtime-voice-transcript.js +34 -1
  139. package/dist/composer/realtime-voice-transcript.js.map +1 -1
  140. package/dist/composer/realtime-voice-turn-metrics.d.ts +43 -0
  141. package/dist/composer/realtime-voice-turn-metrics.d.ts.map +1 -0
  142. package/dist/composer/realtime-voice-turn-metrics.js +132 -0
  143. package/dist/composer/realtime-voice-turn-metrics.js.map +1 -0
  144. package/dist/composer/runtime-adapters.d.ts +4 -1
  145. package/dist/composer/runtime-adapters.d.ts.map +1 -1
  146. package/dist/composer/runtime-adapters.js +2 -1
  147. package/dist/composer/runtime-adapters.js.map +1 -1
  148. package/dist/composer/useRealtimeVoiceMode.d.ts +61 -9
  149. package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
  150. package/dist/composer/useRealtimeVoiceMode.js +536 -99
  151. package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
  152. package/dist/composer/voice-chat-icon.d.ts +7 -0
  153. package/dist/composer/voice-chat-icon.d.ts.map +1 -0
  154. package/dist/composer/voice-chat-icon.js +14 -0
  155. package/dist/composer/voice-chat-icon.js.map +1 -0
  156. package/dist/editor/DragHandle.d.ts +12 -1
  157. package/dist/editor/DragHandle.d.ts.map +1 -1
  158. package/dist/editor/DragHandle.js +165 -41
  159. package/dist/editor/DragHandle.js.map +1 -1
  160. package/dist/editor/RegistryBlockNode.d.ts +20 -0
  161. package/dist/editor/RegistryBlockNode.d.ts.map +1 -1
  162. package/dist/editor/RegistryBlockNode.js +58 -13
  163. package/dist/editor/RegistryBlockNode.js.map +1 -1
  164. package/dist/editor/index.d.ts +2 -2
  165. package/dist/editor/index.d.ts.map +1 -1
  166. package/dist/editor/index.js +1 -1
  167. package/dist/editor/index.js.map +1 -1
  168. package/dist/styles.css +2 -0
  169. package/dist/ui/dropdown-menu.d.ts.map +1 -1
  170. package/dist/ui/dropdown-menu.js +5 -1
  171. package/dist/ui/dropdown-menu.js.map +1 -1
  172. package/package.json +14 -4
  173. package/src/app/agentkit/react/components.regenerate-readiness.spec.tsx +3 -1
  174. package/src/app/agentkit/react/components.spec.tsx +24 -4
  175. package/src/app/agentkit/react/components.tsx +174 -44
  176. package/src/app/agentkit/react/composer-context.spec.tsx +3 -0
  177. package/src/app/agentkit/react/disclosures.spec.tsx +124 -0
  178. package/src/app/agentkit/react/lifecycle.spec.tsx +8 -2
  179. package/src/app/agentkit/react/message-actions.spec.tsx +161 -5
  180. package/src/app/agentkit/react/saved-reply-reload.spec.tsx +206 -0
  181. package/src/app/agentkit/react/styles.css +93 -65
  182. package/src/app/agentkit/react/voice-transcripts.spec.tsx +239 -4
  183. package/src/app/agentkit/react/voice-transcripts.ts +245 -8
  184. package/src/app/chat/AgentKitAssistantChat.spec.tsx +59 -11
  185. package/src/app/chat/AgentKitAssistantChat.tsx +74 -62
  186. package/src/app/chat/agentkit-chat/guided-answers-context.spec.ts +9 -0
  187. package/src/app/chat/agentkit-chat/guided-questions.tsx +17 -7
  188. package/src/app/chat/agentkit-chat/index.ts +1 -0
  189. package/src/app/chat/chat/run-recovery.spec.tsx +45 -0
  190. package/src/app/chat/chat/run-recovery.tsx +6 -1
  191. package/src/app/chat/composer/runtime-adapters.tsx +2 -0
  192. package/src/app/chat/reply-connection-suggestion.spec.ts +121 -0
  193. package/src/app/chat/reply-connection-suggestion.ts +76 -0
  194. package/src/app/i18n/catalogs/ar-SA.ts +10 -0
  195. package/src/app/i18n/catalogs/de-DE.ts +10 -0
  196. package/src/app/i18n/catalogs/en-US.ts +10 -0
  197. package/src/app/i18n/catalogs/es-ES.ts +10 -0
  198. package/src/app/i18n/catalogs/fr-FR.ts +10 -0
  199. package/src/app/i18n/catalogs/hi-IN.ts +10 -0
  200. package/src/app/i18n/catalogs/ja-JP.ts +10 -0
  201. package/src/app/i18n/catalogs/ko-KR.ts +10 -0
  202. package/src/app/i18n/catalogs/pt-BR.ts +10 -0
  203. package/src/app/i18n/catalogs/zh-CN.ts +10 -0
  204. package/src/app/i18n/catalogs/zh-TW.ts +10 -0
  205. package/src/app/mcp-approval/McpApprovalReview.spec.tsx +687 -0
  206. package/src/app/mcp-approval/McpApprovalReview.tsx +913 -0
  207. package/src/app/onboarding/FirstRunOnboarding.spec.tsx +736 -26
  208. package/src/app/onboarding/FirstRunOnboarding.tsx +473 -145
  209. package/src/app/onboarding/GettingStartedCard.tsx +1 -1
  210. package/src/app/onboarding/first-run-builder-credits-flag.spec.ts +13 -0
  211. package/src/app/onboarding/first-run-builder-credits-flag.ts +9 -0
  212. package/src/app/providers/AppProviders.spec.tsx +109 -0
  213. package/src/app/providers/AppProviders.tsx +53 -1
  214. package/src/app/resources/McpConnectionSuggestion.spec.ts +77 -12
  215. package/src/app/resources/McpConnectionSuggestion.tsx +69 -51
  216. package/src/app/settings/VoiceChatSettings.spec.tsx +6 -1
  217. package/src/app/settings/account/PreferencesSettings.spec.tsx +73 -0
  218. package/src/app/settings/account/PreferencesSettings.tsx +24 -7
  219. package/src/app/settings/account/ThemeFamilySettings.tsx +2 -2
  220. package/src/app/settings/account/search-entries.spec.ts +14 -0
  221. package/src/app/settings/account/search-entries.ts +9 -0
  222. package/src/app/settings/shell/SettingsShell.tsx +29 -2
  223. package/src/app/shared/AppShellSkeleton.spec.tsx +2 -2
  224. package/src/app/shared/AppShellSkeleton.tsx +3 -16
  225. package/src/app/shared/EnvironmentBadge.tsx +3 -4
  226. package/src/app/shared/is-builder-io-employee.ts +17 -0
  227. package/src/app/styles/agent-conversation.css +30 -17
  228. package/src/app/styles/theme-families.css +22 -5
  229. package/src/app/styles/theme-families.spec.ts +73 -0
  230. package/src/app/styles/tokens/agent-kit.css +3 -3
  231. package/src/composer/AgentSuggestionBar.overflow.spec.tsx +1 -1
  232. package/src/composer/AttachmentChip.spec.tsx +87 -0
  233. package/src/composer/PromptComposer.context.spec.tsx +8 -4
  234. package/src/composer/PromptComposer.setup-card.spec.tsx +170 -0
  235. package/src/composer/PromptComposer.tsx +125 -14
  236. package/src/composer/RealtimeVoiceMode.spec.tsx +48 -0
  237. package/src/composer/RealtimeVoiceMode.tsx +45 -2
  238. package/src/composer/TiptapComposer.tsx +25 -0
  239. package/src/composer/TiptapComposer.voice-toolbar.spec.tsx +4 -1
  240. package/src/composer/VoiceButton.tsx +8 -3
  241. package/src/composer/index.ts +4 -0
  242. package/src/composer/prompt-attachments.spec.ts +160 -8
  243. package/src/composer/prompt-attachments.ts +221 -12
  244. package/src/composer/realtime-voice-transcript.spec.ts +36 -0
  245. package/src/composer/realtime-voice-transcript.ts +56 -1
  246. package/src/composer/realtime-voice-turn-metrics.spec.ts +175 -0
  247. package/src/composer/realtime-voice-turn-metrics.ts +192 -0
  248. package/src/composer/runtime-adapters.tsx +5 -1
  249. package/src/composer/useRealtimeVoiceMode.session.spec.tsx +487 -2
  250. package/src/composer/useRealtimeVoiceMode.spec.ts +267 -65
  251. package/src/composer/useRealtimeVoiceMode.tsx +716 -139
  252. package/src/composer/voice-chat-icon.tsx +19 -0
  253. package/src/editor/DragHandle.spec.ts +217 -0
  254. package/src/editor/DragHandle.ts +214 -55
  255. package/src/editor/RegistryBlockNode.spec.ts +172 -2
  256. package/src/editor/RegistryBlockNode.tsx +91 -13
  257. package/src/editor/index.ts +4 -0
  258. package/src/styles.css +2 -0
  259. package/src/ui/dropdown-menu.tsx +4 -0
@@ -7,6 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
7
  import {
8
8
  realtimeVoiceTranscriptRegistry,
9
9
  type RealtimeVoiceTranscriptMessage,
10
+ type RealtimeVoiceWorkUpdate,
10
11
  } from "./realtime-voice-transcript.js";
11
12
  import { ComposerRuntimeAdaptersProvider } from "./runtime-adapters.js";
12
13
  import {
@@ -14,16 +15,20 @@ import {
14
15
  RealtimeVoiceModeProvider,
15
16
  useRealtimeVoiceMode,
16
17
  type RealtimeVoiceModeApi,
18
+ type RealtimeVoiceStartOptions,
17
19
  } from "./useRealtimeVoiceMode.js";
18
20
 
19
21
  interface FakeChannel {
20
22
  onmessage?: (event: { data: string }) => void;
21
23
  close: () => void;
24
+ readyState?: string;
25
+ send?: (data: string) => void;
22
26
  }
23
27
 
24
28
  let channel: FakeChannel;
25
29
  let track: { enabled: boolean; stop: () => void; onended: unknown };
26
30
  let audio: HTMLAudioElement | undefined;
31
+ let setAppState: ReturnType<typeof vi.fn>;
27
32
 
28
33
  class FakePeerConnection {
29
34
  connectionState = "new";
@@ -46,6 +51,7 @@ beforeEach(() => {
46
51
  channel = { close: vi.fn() };
47
52
  track = { enabled: true, stop: vi.fn(), onended: null };
48
53
  audio = undefined;
54
+ setAppState = vi.fn();
49
55
  vi.stubGlobal("RTCPeerConnection", FakePeerConnection);
50
56
  Object.defineProperty(navigator, "mediaDevices", {
51
57
  configurable: true,
@@ -75,6 +81,11 @@ afterEach(() => {
75
81
 
76
82
  async function startSession(
77
83
  readAppState: (key: string) => unknown = () => undefined,
84
+ startOptions?: RealtimeVoiceStartOptions,
85
+ trackEvent: (
86
+ name: string,
87
+ properties: Record<string, unknown>,
88
+ ) => void = () => undefined,
78
89
  ) {
79
90
  let api: RealtimeVoiceModeApi | undefined;
80
91
  function Probe() {
@@ -89,8 +100,9 @@ async function startSession(
89
100
  adapters={{
90
101
  voice: {
91
102
  readAppState: readAppState as never,
92
- setAppState: () => undefined,
103
+ setAppState: setAppState as never,
93
104
  getBrowserTabId: () => "tab-1",
105
+ trackEvent,
94
106
  },
95
107
  }}
96
108
  >
@@ -101,7 +113,7 @@ async function startSession(
101
113
  );
102
114
  });
103
115
  await act(async () => {
104
- void api!.start();
116
+ void api!.start(startOptions);
105
117
  await new Promise((resolve) => setTimeout(resolve, 0));
106
118
  });
107
119
  return {
@@ -116,6 +128,381 @@ async function startSession(
116
128
  }
117
129
 
118
130
  describe("realtime voice session controls", () => {
131
+ it("groups one reply's tool calls into one piece of work", async () => {
132
+ const work: RealtimeVoiceWorkUpdate[] = [];
133
+ const unregister = realtimeVoiceTranscriptRegistry.register({
134
+ threadId: "thread-work",
135
+ active: true,
136
+ append: () => true,
137
+ appendWork: (update) => {
138
+ work.push(update);
139
+ return true;
140
+ },
141
+ });
142
+ const results: Array<(value: Response) => void> = [];
143
+ vi.stubGlobal(
144
+ "fetch",
145
+ vi.fn(() => new Promise<Response>((resolve) => results.push(resolve))),
146
+ );
147
+ const session = await startSession();
148
+ const resolveCall = async (callId: string) => {
149
+ await act(async () => {
150
+ results.shift()?.(
151
+ Response.json({ callId, status: "completed", output: "{}" }),
152
+ );
153
+ await new Promise((done) => setTimeout(done, 0));
154
+ });
155
+ };
156
+
157
+ await session.send({ type: "response.created" });
158
+ for (const [callId, name] of [
159
+ ["call-search", "web-search"],
160
+ ["call-read", "web-request"],
161
+ ] as const) {
162
+ await session.send({
163
+ type: "response.function_call_arguments.done",
164
+ call_id: callId,
165
+ name,
166
+ arguments: "{}",
167
+ });
168
+ }
169
+ // The response that asked for the tools is not the end of the reply.
170
+ await session.send({
171
+ type: "response.done",
172
+ response: { status: "completed", output: [] },
173
+ });
174
+ expect(session.api().currentToolName).toBe("web-request");
175
+ await resolveCall("call-search");
176
+ expect(session.api().currentToolName).toBe("web-request");
177
+ await resolveCall("call-read");
178
+ expect(session.api().currentToolName).toBeNull();
179
+ expect(work.every((update) => !update.completedAt)).toBe(true);
180
+
181
+ await session.send({ type: "response.created" });
182
+ await session.send({
183
+ type: "response.done",
184
+ response: { status: "completed", output: [] },
185
+ });
186
+
187
+ const last = work.at(-1);
188
+ expect(new Set(work.map((update) => update.id))).toEqual(
189
+ new Set(["call-search"]),
190
+ );
191
+ expect(last?.calls.map((call) => [call.id, call.status])).toEqual([
192
+ ["call-search", "completed"],
193
+ ["call-read", "completed"],
194
+ ]);
195
+ expect(last?.completedAt).toBe(last?.calls.at(-1)?.completedAt);
196
+ unregister();
197
+ session.unmount();
198
+ });
199
+
200
+ it("groups a GPT-Live delegation's tool calls into one piece of work", async () => {
201
+ const work: RealtimeVoiceWorkUpdate[] = [];
202
+ const unregister = realtimeVoiceTranscriptRegistry.register({
203
+ threadId: "thread-live-work",
204
+ active: true,
205
+ append: () => true,
206
+ appendWork: (update) => {
207
+ work.push(update);
208
+ return true;
209
+ },
210
+ });
211
+ const results: Array<(value: Response) => void> = [];
212
+ vi.stubGlobal(
213
+ "fetch",
214
+ vi.fn(() => new Promise<Response>((resolve) => results.push(resolve))),
215
+ );
216
+ const session = await startSession();
217
+ const nested = (delegationId: string, event: Record<string, unknown>) =>
218
+ session.send({
219
+ type: "response.event",
220
+ delegation_id: delegationId,
221
+ event,
222
+ });
223
+ // Live response snapshots never carry their output.
224
+ const doneSnapshot = {
225
+ type: "response.done",
226
+ response: { status: "completed" },
227
+ };
228
+ const callResponse = async (delegationId: string, callId: string) => {
229
+ await nested(delegationId, { type: "response.created" });
230
+ await nested(delegationId, {
231
+ type: "response.output_item.done",
232
+ item: {
233
+ type: "function_call",
234
+ call_id: callId,
235
+ name: "web-search",
236
+ arguments: "{}",
237
+ },
238
+ });
239
+ await nested(delegationId, doneSnapshot);
240
+ await act(async () => {
241
+ results.shift()?.(
242
+ Response.json({ callId, status: "completed", output: "{}" }),
243
+ );
244
+ await new Promise((done) => setTimeout(done, 0));
245
+ });
246
+ };
247
+
248
+ await callResponse("delegation-1", "call-1");
249
+ await callResponse("delegation-1", "call-2");
250
+ await callResponse("delegation-1", "call-3");
251
+ expect(work.every((update) => !update.completedAt)).toBe(true);
252
+ await nested("delegation-1", { type: "response.created" });
253
+ await nested("delegation-1", doneSnapshot);
254
+ expect(work.at(-1)).toMatchObject({ id: "call-1" });
255
+ expect(work.at(-1)?.calls.map((call) => call.id)).toEqual([
256
+ "call-1",
257
+ "call-2",
258
+ "call-3",
259
+ ]);
260
+ expect(work.at(-1)?.completedAt).toBeTruthy();
261
+
262
+ // The next request is a new delegation and new work.
263
+ await callResponse("delegation-2", "call-4");
264
+ expect(work.at(-1)).toMatchObject({ id: "call-4" });
265
+ unregister();
266
+ session.unmount();
267
+ });
268
+
269
+ it.each([
270
+ ["approved", true],
271
+ ["declined", false],
272
+ ])(
273
+ "holds a call that needs approval until the user has %s it",
274
+ async (decision, approved) => {
275
+ const callId = `call-delete-${decision}`;
276
+ const work: RealtimeVoiceWorkUpdate[] = [];
277
+ const unregister = realtimeVoiceTranscriptRegistry.register({
278
+ threadId: `thread-approval-${String(approved)}`,
279
+ active: true,
280
+ append: () => true,
281
+ appendWork: (update) => {
282
+ work.push(update);
283
+ return true;
284
+ },
285
+ });
286
+ const sent: Array<Record<string, unknown>> = [];
287
+ channel = {
288
+ close: vi.fn(),
289
+ readyState: "open",
290
+ send: (data: string) => sent.push(JSON.parse(data)),
291
+ } as FakeChannel;
292
+ const requests: Array<Record<string, unknown>> = [];
293
+ vi.stubGlobal(
294
+ "fetch",
295
+ vi.fn(async (_url: string, init?: RequestInit) => {
296
+ const body = JSON.parse(String(init?.body)) as Record<
297
+ string,
298
+ unknown
299
+ >;
300
+ requests.push(body);
301
+ return Response.json(
302
+ body.approvalId || body.declinedApprovalId
303
+ ? { callId: body.callId, status: "completed", output: "deleted" }
304
+ : // No key: arguments too long for one still get an approval.
305
+ {
306
+ callId: body.callId,
307
+ status: "approval_required",
308
+ output: "Awaiting approval",
309
+ approvalId: "approval-1",
310
+ },
311
+ );
312
+ }),
313
+ );
314
+ const session = await startSession();
315
+ await session.send({ type: "response.created" });
316
+ await session.send({
317
+ type: "response.function_call_arguments.done",
318
+ call_id: callId,
319
+ name: "delete-slide",
320
+ arguments: JSON.stringify({ slideId: "s1" }),
321
+ });
322
+ await act(async () => {
323
+ await new Promise((done) => setTimeout(done, 0));
324
+ });
325
+
326
+ expect(session.api().approvals).toEqual([
327
+ { callId, toolName: "delete-slide" },
328
+ ]);
329
+ expect(work.at(-1)?.calls.map((call) => call.status)).toEqual([
330
+ "running",
331
+ ]);
332
+ sent.length = 0;
333
+
334
+ await act(async () => {
335
+ await session.api().resolveApproval(callId, approved);
336
+ });
337
+
338
+ expect(session.api().approvals).toEqual([]);
339
+ expect(requests.slice(1)).toEqual([
340
+ approved
341
+ ? expect.objectContaining({
342
+ name: "delete-slide",
343
+ args: { slideId: "s1" },
344
+ approvalId: "approval-1",
345
+ })
346
+ : expect.objectContaining({ declinedApprovalId: "approval-1" }),
347
+ ]);
348
+ expect(work.at(-1)?.calls.map((call) => call.status)).toEqual([
349
+ approved ? "completed" : "cancelled",
350
+ ]);
351
+ // A session that has not connected yet speaks GPT-Live.
352
+ expect(sent.map((event) => event.type)).toEqual([
353
+ "response.item.create",
354
+ "response.create",
355
+ ]);
356
+ expect(JSON.stringify(sent[0])).toContain(
357
+ approved ? "The user approved delete-slide" : "The user declined",
358
+ );
359
+ unregister();
360
+ await session.unmount();
361
+ },
362
+ );
363
+
364
+ it("asks again when an approved call's approval no longer applies", async () => {
365
+ const work: RealtimeVoiceWorkUpdate[] = [];
366
+ const unregister = realtimeVoiceTranscriptRegistry.register({
367
+ threadId: "thread-approval-renewed",
368
+ active: true,
369
+ append: () => true,
370
+ appendWork: (update) => {
371
+ work.push(update);
372
+ return true;
373
+ },
374
+ });
375
+ const sent: Array<Record<string, unknown>> = [];
376
+ channel = {
377
+ close: vi.fn(),
378
+ readyState: "open",
379
+ send: (data: string) => sent.push(JSON.parse(data)),
380
+ } as FakeChannel;
381
+ const requests: Array<Record<string, unknown>> = [];
382
+ vi.stubGlobal(
383
+ "fetch",
384
+ vi.fn(async (_url: string, init?: RequestInit) => {
385
+ const body = JSON.parse(String(init?.body)) as Record<string, unknown>;
386
+ requests.push(body);
387
+ // The first approval expired, so the approved retry asks again.
388
+ return Response.json(
389
+ body.approvalId === "approval-2"
390
+ ? { callId: body.callId, status: "completed", output: "deleted" }
391
+ : {
392
+ callId: body.callId,
393
+ status: "approval_required",
394
+ output: "Awaiting approval",
395
+ approvalKey: "delete-slide:key",
396
+ approvalId: body.approvalId ? "approval-2" : "approval-1",
397
+ },
398
+ );
399
+ }),
400
+ );
401
+ const session = await startSession();
402
+ await session.send({ type: "response.created" });
403
+ await session.send({
404
+ type: "response.function_call_arguments.done",
405
+ call_id: "call-renewed",
406
+ name: "delete-slide",
407
+ arguments: JSON.stringify({ slideId: "s1" }),
408
+ });
409
+ await act(async () => {
410
+ await new Promise((done) => setTimeout(done, 0));
411
+ });
412
+ sent.length = 0;
413
+
414
+ await act(async () => {
415
+ await session.api().resolveApproval("call-renewed", true);
416
+ });
417
+ expect(session.api().approvals).toEqual([
418
+ { callId: "call-renewed", toolName: "delete-slide" },
419
+ ]);
420
+ expect(work.at(-1)?.calls.map((call) => call.status)).toEqual(["running"]);
421
+ expect(JSON.stringify(sent[0])).toContain("asked for approval again");
422
+
423
+ await act(async () => {
424
+ await session.api().resolveApproval("call-renewed", true);
425
+ });
426
+ expect(requests.at(-1)).toMatchObject({ approvalId: "approval-2" });
427
+ expect(work.at(-1)?.calls.map((call) => call.status)).toEqual([
428
+ "completed",
429
+ ]);
430
+ unregister();
431
+ await session.unmount();
432
+ });
433
+
434
+ it("records a tool call that teardown aborted as cancelled", async () => {
435
+ const work: RealtimeVoiceWorkUpdate[] = [];
436
+ const unregister = realtimeVoiceTranscriptRegistry.register({
437
+ threadId: "thread-teardown",
438
+ active: true,
439
+ append: () => true,
440
+ appendWork: (update) => {
441
+ work.push(update);
442
+ return true;
443
+ },
444
+ });
445
+ vi.stubGlobal(
446
+ "fetch",
447
+ vi.fn(
448
+ (_url: string, init?: RequestInit) =>
449
+ new Promise<Response>((_resolve, reject) => {
450
+ init?.signal?.addEventListener("abort", () =>
451
+ reject(new DOMException("Aborted", "AbortError")),
452
+ );
453
+ }),
454
+ ),
455
+ );
456
+ const session = await startSession();
457
+ await session.send({ type: "response.created" });
458
+ await session.send({
459
+ type: "response.function_call_arguments.done",
460
+ call_id: "call-edit",
461
+ name: "web-search",
462
+ arguments: "{}",
463
+ });
464
+
465
+ await act(async () => {
466
+ session.api().end();
467
+ await new Promise((done) => setTimeout(done, 0));
468
+ });
469
+
470
+ expect(work.at(-1)?.calls.map((call) => [call.id, call.status])).toEqual([
471
+ ["call-edit", "cancelled"],
472
+ ]);
473
+ unregister();
474
+ session.unmount();
475
+ });
476
+
477
+ it("reports each turn's timing without the words spoken", async () => {
478
+ const trackEvent = vi.fn();
479
+ const session = await startSession(undefined, undefined, trackEvent);
480
+
481
+ await session.send({ type: "input_audio_buffer.speech_started" });
482
+ await session.send({ type: "input_audio_buffer.speech_stopped" });
483
+ await session.send({ type: "output_audio_buffer.started" });
484
+ await session.send({
485
+ type: "response.output_audio_transcript.delta",
486
+ item_id: "assistant-1",
487
+ delta: "Opening the secret plans",
488
+ });
489
+ expect(trackEvent).not.toHaveBeenCalled();
490
+ await session.send({ type: "input_audio_buffer.speech_started" });
491
+
492
+ expect(trackEvent).toHaveBeenCalledTimes(1);
493
+ const [name, properties] = trackEvent.mock.calls[0]!;
494
+ expect(name).toBe("realtime_voice_turn");
495
+ expect(properties).toMatchObject({
496
+ turn_index: 0,
497
+ tool_calls: 0,
498
+ latency_clock: "browser",
499
+ spoke_again_after_edit: null,
500
+ });
501
+ expect(properties.speech_to_audio_ms).toEqual(expect.any(Number));
502
+ expect(JSON.stringify(properties)).not.toContain("secret");
503
+ session.unmount();
504
+ });
505
+
119
506
  it("mutes the microphone track and the assistant's playback", async () => {
120
507
  const session = await startSession();
121
508
  expect(session.api().state).toBe("connecting");
@@ -199,6 +586,103 @@ describe("realtime voice session controls", () => {
199
586
  session.unmount();
200
587
  });
201
588
 
589
+ it("streams into the thread it was started for until that thread is shown", async () => {
590
+ const onScreen: RealtimeVoiceTranscriptMessage[] = [];
591
+ const unregisterOnScreen = realtimeVoiceTranscriptRegistry.register({
592
+ threadId: "thread-on-screen",
593
+ active: true,
594
+ append: (message) => {
595
+ onScreen.push(message);
596
+ return true;
597
+ },
598
+ });
599
+ const session = await startSession(undefined, {
600
+ threadId: "thread-handoff",
601
+ });
602
+
603
+ await session.send({
604
+ type: "conversation.item.input_audio_transcription.completed",
605
+ item_id: "user-handoff",
606
+ transcript: "Plan my week.",
607
+ });
608
+ expect(onScreen).toEqual([]);
609
+
610
+ const handedOff: RealtimeVoiceTranscriptMessage[] = [];
611
+ const unregisterHandoff = realtimeVoiceTranscriptRegistry.register({
612
+ threadId: "thread-handoff",
613
+ active: true,
614
+ append: (message) => {
615
+ handedOff.push(message);
616
+ return true;
617
+ },
618
+ });
619
+
620
+ expect(
621
+ handedOff.map(({ threadId, role, text, status }) => ({
622
+ threadId,
623
+ role,
624
+ text,
625
+ status,
626
+ })),
627
+ ).toEqual([
628
+ {
629
+ threadId: "thread-handoff",
630
+ role: "user",
631
+ text: "Plan my week.",
632
+ status: "complete",
633
+ },
634
+ ]);
635
+ unregisterHandoff();
636
+ unregisterOnScreen();
637
+ session.unmount();
638
+ });
639
+
640
+ it("saves the app's default level as not chosen so Settings can show it", async () => {
641
+ vi.stubGlobal(
642
+ "RTCPeerConnection",
643
+ class extends FakePeerConnection {
644
+ override createOffer() {
645
+ return Promise.resolve({ type: "offer", sdp: "offer-sdp" });
646
+ }
647
+ setLocalDescription() {
648
+ return Promise.resolve();
649
+ }
650
+ setRemoteDescription() {
651
+ return Promise.resolve();
652
+ }
653
+ },
654
+ );
655
+ vi.stubGlobal(
656
+ "fetch",
657
+ vi.fn(
658
+ async () =>
659
+ new Response("answer-sdp", {
660
+ headers: {
661
+ "X-Agent-Native-Realtime-Protocol": "live",
662
+ "X-Agent-Native-Realtime-Intelligence": "balanced",
663
+ },
664
+ }),
665
+ ),
666
+ );
667
+ const session = await startSession();
668
+ await act(async () => {
669
+ await new Promise((resolve) => setTimeout(resolve, 0));
670
+ });
671
+
672
+ expect(session.api().preferences).toMatchObject({
673
+ intelligence: "balanced",
674
+ intelligenceChosen: false,
675
+ });
676
+ expect(setAppState).toHaveBeenCalledWith(
677
+ REALTIME_VOICE_PREFERENCES_KEY,
678
+ expect.objectContaining({
679
+ intelligence: "balanced",
680
+ intelligenceChosen: false,
681
+ }),
682
+ );
683
+ session.unmount();
684
+ });
685
+
202
686
  it("starts from the voice chat choices saved in Settings", async () => {
203
687
  window.localStorage.setItem(
204
688
  "agent-native:realtime-voice-microphone",
@@ -216,6 +700,7 @@ describe("realtime voice session controls", () => {
216
700
  language: "ja",
217
701
  intelligence: "deep",
218
702
  voice: "cedar",
703
+ intelligenceChosen: true,
219
704
  });
220
705
  expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith({
221
706
  audio: expect.objectContaining({ deviceId: { ideal: "studio" } }),