@assistant-ui/react 0.15.31 → 0.15.33

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 (228) hide show
  1. package/dist/_virtual/_rolldown/runtime.js +1 -2
  2. package/dist/adapters/GeminiLiveVoiceAdapter.d.ts +26 -0
  3. package/dist/adapters/GeminiLiveVoiceAdapter.d.ts.map +1 -0
  4. package/dist/adapters/GeminiLiveVoiceAdapter.js +346 -0
  5. package/dist/adapters/GeminiLiveVoiceAdapter.js.map +1 -0
  6. package/dist/adapters/OpenAIRealtimeVoiceAdapter.js +1 -2
  7. package/dist/adapters/gemini-live/audio.d.ts +27 -0
  8. package/dist/adapters/gemini-live/audio.d.ts.map +1 -0
  9. package/dist/adapters/gemini-live/audio.js +291 -0
  10. package/dist/adapters/gemini-live/audio.js.map +1 -0
  11. package/dist/assistant-transport.js +1 -2
  12. package/dist/cloud-renderer/CloudRendererHost.js +1 -2
  13. package/dist/context/ReadonlyStore.js +1 -2
  14. package/dist/context/providers/MessageProvider.js +1 -2
  15. package/dist/context/providers/ThreadViewportProvider.js +1 -2
  16. package/dist/context/react/ThreadViewportContext.js +1 -2
  17. package/dist/context/react/utils/createContextHook.js +1 -2
  18. package/dist/context/react/utils/createContextStoreHook.js +1 -2
  19. package/dist/context/stores/ThreadViewport.js +2 -3
  20. package/dist/devtools/DevToolsHooks.js +1 -2
  21. package/dist/hooks/useMessageQuote.js +1 -2
  22. package/dist/hooks/useMessageTiming.js +1 -2
  23. package/dist/hooks/useToolCallElapsed.js +1 -2
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +2 -1
  27. package/dist/internal.js +2 -3
  28. package/dist/legacy-runtime/AssistantRuntimeProvider.js +1 -2
  29. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  30. package/dist/mcp-apps/McpAppRenderer.js +4 -4
  31. package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
  32. package/dist/mcp-apps/McpAppsRemoteHost.js +1 -2
  33. package/dist/mcp-apps/app-frame.js +2 -3
  34. package/dist/mcp-apps/bridge.js +1 -2
  35. package/dist/mcp-apps/csp.js +1 -2
  36. package/dist/mcp-apps/types.js +1 -2
  37. package/dist/mcp-apps/utils.js +1 -2
  38. package/dist/model-context/frame/useAssistantFrameHost.js +1 -2
  39. package/dist/model-context/makeAssistantVisible.js +1 -2
  40. package/dist/primitives/actionBar/ActionBarCopy.js +1 -2
  41. package/dist/primitives/actionBar/ActionBarEdit.js +1 -2
  42. package/dist/primitives/actionBar/ActionBarExportMarkdown.js +1 -2
  43. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +1 -2
  44. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +1 -2
  45. package/dist/primitives/actionBar/ActionBarInteractionContext.js +1 -2
  46. package/dist/primitives/actionBar/ActionBarReload.js +1 -2
  47. package/dist/primitives/actionBar/ActionBarRoot.js +1 -2
  48. package/dist/primitives/actionBar/ActionBarSpeak.js +1 -2
  49. package/dist/primitives/actionBar/ActionBarStopSpeaking.js +1 -2
  50. package/dist/primitives/actionBar/useActionBarFloatStatus.js +1 -2
  51. package/dist/primitives/actionBar.js +1 -2
  52. package/dist/primitives/actionBarMore/ActionBarMoreContent.js +1 -2
  53. package/dist/primitives/actionBarMore/ActionBarMoreItem.js +1 -2
  54. package/dist/primitives/actionBarMore/ActionBarMoreRoot.js +1 -2
  55. package/dist/primitives/actionBarMore/ActionBarMoreSeparator.js +1 -2
  56. package/dist/primitives/actionBarMore/ActionBarMoreTrigger.js +1 -2
  57. package/dist/primitives/actionBarMore/scope.js +1 -2
  58. package/dist/primitives/actionBarMore.js +1 -2
  59. package/dist/primitives/assistantModal/AssistantModalAnchor.js +1 -2
  60. package/dist/primitives/assistantModal/AssistantModalContent.js +1 -2
  61. package/dist/primitives/assistantModal/AssistantModalRoot.js +1 -2
  62. package/dist/primitives/assistantModal/AssistantModalTrigger.js +1 -2
  63. package/dist/primitives/assistantModal/popoverRenderPrimitives.js +1 -2
  64. package/dist/primitives/assistantModal/scope.js +1 -2
  65. package/dist/primitives/assistantModal.js +1 -2
  66. package/dist/primitives/attachment/AttachmentName.js +1 -2
  67. package/dist/primitives/attachment/AttachmentRemove.js +1 -2
  68. package/dist/primitives/attachment/AttachmentRoot.js +1 -2
  69. package/dist/primitives/attachment/AttachmentThumb.js +1 -2
  70. package/dist/primitives/attachment.js +1 -2
  71. package/dist/primitives/branchPicker/BranchPickerCount.js +1 -2
  72. package/dist/primitives/branchPicker/BranchPickerNext.js +1 -2
  73. package/dist/primitives/branchPicker/BranchPickerNumber.js +1 -2
  74. package/dist/primitives/branchPicker/BranchPickerPrevious.js +1 -2
  75. package/dist/primitives/branchPicker/BranchPickerRoot.js +1 -2
  76. package/dist/primitives/branchPicker.js +1 -2
  77. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.d.ts +5 -4
  78. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.d.ts.map +1 -1
  79. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js +57 -20
  80. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js.map +1 -1
  81. package/dist/primitives/chainOfThought/ChainOfThoughtRoot.js +1 -2
  82. package/dist/primitives/chainOfThought.js +1 -2
  83. package/dist/primitives/composer/ComposerAddAttachment.js +1 -2
  84. package/dist/primitives/composer/ComposerAttachmentDropzone.js +1 -2
  85. package/dist/primitives/composer/ComposerCancel.js +1 -2
  86. package/dist/primitives/composer/ComposerCompactContext.js +1 -2
  87. package/dist/primitives/composer/ComposerDictate.js +1 -2
  88. package/dist/primitives/composer/ComposerDictationTranscript.js +1 -2
  89. package/dist/primitives/composer/ComposerInput.js +1 -2
  90. package/dist/primitives/composer/ComposerInputPluginContext.js +1 -2
  91. package/dist/primitives/composer/ComposerQuote.js +1 -2
  92. package/dist/primitives/composer/ComposerResume.js +1 -2
  93. package/dist/primitives/composer/ComposerRoot.js +1 -2
  94. package/dist/primitives/composer/ComposerSend.js +1 -2
  95. package/dist/primitives/composer/ComposerStopDictation.js +1 -2
  96. package/dist/primitives/composer/composerInputSelector.js +1 -2
  97. package/dist/primitives/composer/trigger/TriggerPopover.js +1 -2
  98. package/dist/primitives/composer/trigger/TriggerPopoverAction.js +1 -2
  99. package/dist/primitives/composer/trigger/TriggerPopoverBack.js +1 -2
  100. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js +1 -2
  101. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +1 -2
  102. package/dist/primitives/composer/trigger/TriggerPopoverItems.js +1 -2
  103. package/dist/primitives/composer/trigger/TriggerPopoverResource.js +1 -2
  104. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js +1 -2
  105. package/dist/primitives/composer/trigger/detectTrigger.js +1 -2
  106. package/dist/primitives/composer/trigger/index.js +1 -2
  107. package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js +1 -2
  108. package/dist/primitives/composer/trigger/triggerDetectionResource.js +1 -2
  109. package/dist/primitives/composer/trigger/triggerKeyboardResource.js +1 -2
  110. package/dist/primitives/composer/trigger/triggerNavigationResource.js +1 -2
  111. package/dist/primitives/composer/trigger/triggerSelectionResource.js +1 -2
  112. package/dist/primitives/composer/useComposerCancelWithFocus.js +1 -2
  113. package/dist/primitives/composer/useComposerInputState.js +1 -2
  114. package/dist/primitives/composer.js +1 -2
  115. package/dist/primitives/dropdownMenuRenderPrimitives.js +1 -2
  116. package/dist/primitives/error/ErrorMessage.js +1 -2
  117. package/dist/primitives/error/ErrorRoot.js +1 -2
  118. package/dist/primitives/error.js +1 -2
  119. package/dist/primitives/message/MessageError.js +1 -2
  120. package/dist/primitives/message/MessageIf.js +1 -2
  121. package/dist/primitives/message/MessageParts.js +1 -2
  122. package/dist/primitives/message/MessagePartsGrouped.js +2 -3
  123. package/dist/primitives/message/MessageRoot.js +1 -2
  124. package/dist/primitives/message.js +1 -2
  125. package/dist/primitives/messagePart/MessagePartImage.js +1 -2
  126. package/dist/primitives/messagePart/MessagePartText.js +1 -2
  127. package/dist/primitives/messagePart/useMessagePartData.js +1 -2
  128. package/dist/primitives/messagePart/useMessagePartFile.js +1 -2
  129. package/dist/primitives/messagePart/useMessagePartImage.js +1 -2
  130. package/dist/primitives/messagePart/useMessagePartReasoning.js +1 -2
  131. package/dist/primitives/messagePart/useMessagePartSource.js +1 -2
  132. package/dist/primitives/messagePart/useMessagePartText.js +1 -2
  133. package/dist/primitives/messagePart.js +1 -2
  134. package/dist/primitives/queueItem/QueueItemRemove.js +1 -2
  135. package/dist/primitives/queueItem/QueueItemSteer.js +1 -2
  136. package/dist/primitives/queueItem/QueueItemText.js +1 -2
  137. package/dist/primitives/queueItem.js +1 -2
  138. package/dist/primitives/reasoning/useScrollLock.d.ts.map +1 -1
  139. package/dist/primitives/reasoning/useScrollLock.js +41 -25
  140. package/dist/primitives/reasoning/useScrollLock.js.map +1 -1
  141. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +1 -2
  142. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +1 -2
  143. package/dist/primitives/selectionToolbar.js +1 -2
  144. package/dist/primitives/suggestion/SuggestionDescription.js +1 -2
  145. package/dist/primitives/suggestion/SuggestionTitle.js +1 -2
  146. package/dist/primitives/suggestion/SuggestionTrigger.js +1 -2
  147. package/dist/primitives/suggestion.js +1 -2
  148. package/dist/primitives/thread/ThreadEmpty.js +1 -2
  149. package/dist/primitives/thread/ThreadIf.js +1 -2
  150. package/dist/primitives/thread/ThreadLoadEarlier.js +1 -2
  151. package/dist/primitives/thread/ThreadRoot.js +1 -2
  152. package/dist/primitives/thread/ThreadRootElementContext.js +1 -2
  153. package/dist/primitives/thread/ThreadScrollToBottom.js +1 -2
  154. package/dist/primitives/thread/ThreadSuggestion.js +1 -2
  155. package/dist/primitives/thread/ThreadViewport.js +1 -2
  156. package/dist/primitives/thread/ThreadViewportFooter.js +1 -2
  157. package/dist/primitives/thread/topAnchor/computeTopAnchorSlack.js +1 -2
  158. package/dist/primitives/thread/topAnchor/createReserveObservers.js +1 -2
  159. package/dist/primitives/thread/topAnchor/mountTopAnchorReserve.js +1 -2
  160. package/dist/primitives/thread/topAnchor/topAnchorTurn.js +1 -2
  161. package/dist/primitives/thread/topAnchor/topAnchorUtils.js +1 -2
  162. package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js +1 -2
  163. package/dist/primitives/thread/useThreadViewportAutoScroll.js +1 -2
  164. package/dist/primitives/thread.js +1 -2
  165. package/dist/primitives/threadList/ThreadListLoadMore.js +1 -2
  166. package/dist/primitives/threadList/ThreadListNew.js +1 -2
  167. package/dist/primitives/threadList/ThreadListRoot.js +1 -2
  168. package/dist/primitives/threadList.js +1 -2
  169. package/dist/primitives/threadListFocusGroup.js +1 -2
  170. package/dist/primitives/threadListItem/ThreadListItemArchive.js +1 -2
  171. package/dist/primitives/threadListItem/ThreadListItemDelete.js +1 -2
  172. package/dist/primitives/threadListItem/ThreadListItemRoot.d.ts.map +1 -1
  173. package/dist/primitives/threadListItem/ThreadListItemRoot.js +2 -3
  174. package/dist/primitives/threadListItem/ThreadListItemRoot.js.map +1 -1
  175. package/dist/primitives/threadListItem/ThreadListItemTrigger.js +1 -2
  176. package/dist/primitives/threadListItem/ThreadListItemUnarchive.js +1 -2
  177. package/dist/primitives/threadListItem.js +1 -2
  178. package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js +1 -2
  179. package/dist/primitives/threadListItemMore/ThreadListItemMoreItem.js +1 -2
  180. package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.js +1 -2
  181. package/dist/primitives/threadListItemMore/ThreadListItemMoreSeparator.js +1 -2
  182. package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js +1 -2
  183. package/dist/primitives/threadListItemMore/scope.js +1 -2
  184. package/dist/primitives/threadListItemMore.js +1 -2
  185. package/dist/sandbox-host/SandboxHost.js +2 -3
  186. package/dist/unstable/useComposerInput.js +1 -2
  187. package/dist/unstable/useComposerInputHistory.js +2 -3
  188. package/dist/unstable/useLiveCompletionAdapter.js +1 -2
  189. package/dist/unstable/useMentionAdapter.js +1 -2
  190. package/dist/unstable/useMessageStallDetection.js +1 -2
  191. package/dist/unstable/useModelContextSnapshot.js +1 -2
  192. package/dist/unstable/useSlashCommandAdapter.js +1 -2
  193. package/dist/unstable/webmcp/WebMcpRegistrationResource.js +1 -2
  194. package/dist/unstable/webmcp/convertTools.js +1 -2
  195. package/dist/unstable/webmcp/useWebMcpProvider.js +1 -2
  196. package/dist/unstable/webmcp/webmcp-host.js +1 -2
  197. package/dist/utils/Primitive.js +1 -2
  198. package/dist/utils/createActionButton.js +1 -2
  199. package/dist/utils/getSelectionMessageId.js +1 -2
  200. package/dist/utils/hooks/useManagedRef.js +1 -2
  201. package/dist/utils/hooks/useMediaQuery.js +1 -2
  202. package/dist/utils/hooks/useOnScrollToBottom.js +1 -2
  203. package/dist/utils/hooks/useSizeHandle.js +1 -2
  204. package/dist/utils/isCompositionKey.js +1 -2
  205. package/dist/utils/smooth/SmoothContext.js +1 -2
  206. package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
  207. package/dist/utils/smooth/useSmooth.js +11 -4
  208. package/dist/utils/smooth/useSmooth.js.map +1 -1
  209. package/dist/utils/useIsomorphicLayoutEffect.js +0 -1
  210. package/package.json +8 -8
  211. package/src/adapters/GeminiLiveVoiceAdapter.test.ts +992 -0
  212. package/src/adapters/GeminiLiveVoiceAdapter.ts +454 -0
  213. package/src/adapters/gemini-live/audio.test.ts +880 -0
  214. package/src/adapters/gemini-live/audio.ts +333 -0
  215. package/src/index.test.ts +34 -0
  216. package/src/index.ts +4 -0
  217. package/src/mcp-apps/McpAppRenderer.composer.test.tsx +111 -0
  218. package/src/mcp-apps/McpAppRenderer.test.tsx +24 -23
  219. package/src/mcp-apps/McpAppRenderer.tsx +9 -1
  220. package/src/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.test.tsx +91 -0
  221. package/src/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.tsx +54 -0
  222. package/src/primitives/reasoning/useScrollLock.test.tsx +71 -2
  223. package/src/primitives/reasoning/useScrollLock.ts +78 -51
  224. package/src/primitives/threadListItem/ThreadListItemRoot.tsx +4 -1
  225. package/src/primitives/threadListItem/ThreadListKeyboardNav.test.tsx +25 -0
  226. package/src/utils/smooth/useSmooth.test.tsx +113 -4
  227. package/src/utils/smooth/useSmooth.ts +16 -2
  228. package/src/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.ts +0 -49
@@ -0,0 +1,333 @@
1
+ import {
2
+ bytesToBase64,
3
+ raceWithAbortSignal,
4
+ } from "@assistant-ui/core/internal";
5
+
6
+ export const encodePcm16 = (samples: Float32Array): string => {
7
+ const bytes = new Uint8Array(samples.length * 2);
8
+ const view = new DataView(bytes.buffer);
9
+ for (let i = 0; i < samples.length; i++) {
10
+ const s = Math.max(-1, Math.min(1, samples[i]!));
11
+ view.setInt16(i * 2, s < 0 ? s * 0x8000 : s * 0x7fff, true);
12
+ }
13
+ return bytesToBase64(bytes);
14
+ };
15
+
16
+ export const decodePcm16 = (base64: string): Float32Array<ArrayBuffer> => {
17
+ const binary = atob(base64);
18
+ const samples = new Float32Array(new ArrayBuffer((binary.length >> 1) * 4));
19
+ const view = new DataView(new ArrayBuffer(samples.length * 2));
20
+ for (let i = 0; i < samples.length * 2; i++)
21
+ view.setUint8(i, binary.charCodeAt(i));
22
+ for (let i = 0; i < samples.length; i++)
23
+ samples[i] = view.getInt16(i * 2, true) / 0x8000;
24
+ return samples;
25
+ };
26
+
27
+ export const readLevel = (analyser: AnalyserNode): number => {
28
+ const data = new Uint8Array(analyser.fftSize);
29
+ analyser.getByteTimeDomainData(data);
30
+ let sum = 0;
31
+ for (const v of data) {
32
+ const centered = (v - 128) / 128;
33
+ sum += centered * centered;
34
+ }
35
+ return Math.min(1, Math.sqrt(sum / data.length) * 2);
36
+ };
37
+
38
+ export type MicrophoneCapture = {
39
+ setMuted: (muted: boolean) => void;
40
+ level: () => number;
41
+ close: () => Promise<void>;
42
+ };
43
+
44
+ const createLowPass = (ratio: number) => {
45
+ const omega = (Math.PI * 0.9) / ratio;
46
+ const cosine = Math.cos(omega);
47
+ const sine = Math.sin(omega);
48
+ const sections = [0.5176380902, 0.7071067812, 1.9318516526].map((q) => {
49
+ const alpha = sine / (2 * q);
50
+ const a0 = 1 + alpha;
51
+ const b = (1 - cosine) / (2 * a0);
52
+ return { b, a1: (-2 * cosine) / a0, a2: (1 - alpha) / a0, z1: 0, z2: 0 };
53
+ });
54
+ return (samples: Float32Array) => {
55
+ for (let i = 0; i < samples.length; i++) {
56
+ let x = samples[i]!;
57
+ for (const s of sections) {
58
+ const y = s.b * x + s.z1;
59
+ s.z1 = 2 * s.b * x - s.a1 * y + s.z2;
60
+ s.z2 = s.b * x - s.a2 * y;
61
+ x = y;
62
+ }
63
+ samples[i] = x;
64
+ }
65
+ };
66
+ };
67
+
68
+ export const WORKLET_SOURCE = `
69
+ class PcmCapture extends AudioWorkletProcessor {
70
+ constructor(options) {
71
+ super();
72
+ this.ratio = sampleRate / options.processorOptions.targetRate;
73
+ this.chunk = Math.round(options.processorOptions.targetRate / 50);
74
+ this.position = 0;
75
+ this.last = 0;
76
+ this.pending = [];
77
+ this.sections = [];
78
+ if (this.ratio > 1) {
79
+ const omega = (Math.PI * 0.9) / this.ratio;
80
+ const cosine = Math.cos(omega);
81
+ const sine = Math.sin(omega);
82
+ for (const q of [0.5176380902, 0.7071067812, 1.9318516526]) {
83
+ const alpha = sine / (2 * q);
84
+ const a0 = 1 + alpha;
85
+ const b = (1 - cosine) / (2 * a0);
86
+ this.sections.push({ b, a1: (-2 * cosine) / a0, a2: (1 - alpha) / a0, z1: 0, z2: 0 });
87
+ }
88
+ }
89
+ }
90
+ process(inputs) {
91
+ const channels = inputs[0];
92
+ if (!channels || channels.length === 0 || channels[0].length === 0) return true;
93
+ const input = channels[0].slice();
94
+ for (let c = 1; c < channels.length; c++)
95
+ for (let i = 0; i < input.length; i++) input[i] += channels[c][i];
96
+ if (channels.length > 1)
97
+ for (let i = 0; i < input.length; i++) input[i] /= channels.length;
98
+ for (let i = 0; i < input.length; i++) {
99
+ let x = input[i];
100
+ for (const s of this.sections) {
101
+ const y = s.b * x + s.z1;
102
+ s.z1 = 2 * s.b * x - s.a1 * y + s.z2;
103
+ s.z2 = s.b * x - s.a2 * y;
104
+ x = y;
105
+ }
106
+ input[i] = x;
107
+ }
108
+ while (this.position < input.length - 1) {
109
+ const index = Math.floor(this.position);
110
+ const prev = index < 0 ? this.last : input[index];
111
+ const next = input[index + 1];
112
+ this.pending.push(prev + (next - prev) * (this.position - index));
113
+ this.position += this.ratio;
114
+ }
115
+ this.position -= input.length;
116
+ this.last = input[input.length - 1];
117
+ if (this.pending.length >= this.chunk) {
118
+ this.port.postMessage(Float32Array.from(this.pending));
119
+ this.pending = [];
120
+ }
121
+ return true;
122
+ }
123
+ }
124
+ registerProcessor("pcm-capture", PcmCapture);
125
+ `;
126
+
127
+ export const createResampler = (ratio: number) => {
128
+ let position = 0;
129
+ let last = 0;
130
+ const lowPass = ratio > 1 ? createLowPass(ratio) : undefined;
131
+ return (input: Float32Array): Float32Array => {
132
+ if (input.length === 0) return new Float32Array(0);
133
+ const samples = lowPass ? input.slice() : input;
134
+ lowPass?.(samples);
135
+ const out: number[] = [];
136
+ while (position < samples.length - 1) {
137
+ const index = Math.floor(position);
138
+ const prev = index < 0 ? last : samples[index]!;
139
+ const next = samples[index + 1]!;
140
+ out.push(prev + (next - prev) * (position - index));
141
+ position += ratio;
142
+ }
143
+ position -= samples.length;
144
+ last = samples[samples.length - 1]!;
145
+ return Float32Array.from(out);
146
+ };
147
+ };
148
+
149
+ export const openMicrophone = async (options: {
150
+ sampleRate: number;
151
+ onChunk: (base64: string) => void;
152
+ signal?: AbortSignal;
153
+ }): Promise<MicrophoneCapture> => {
154
+ let stream: MediaStream | undefined;
155
+ const stopTracks = () => {
156
+ stream?.getTracks().forEach((track) => track.stop());
157
+ stream = undefined;
158
+ };
159
+ const throwIfAborted = () => {
160
+ if (options.signal?.aborted) throw options.signal.reason;
161
+ };
162
+ let context: AudioContext | undefined;
163
+ try {
164
+ const openedStream = await raceWithAbortSignal(options.signal, async () => {
165
+ const acquired = await navigator.mediaDevices.getUserMedia({
166
+ audio: true,
167
+ });
168
+ stream = acquired;
169
+ if (options.signal?.aborted) {
170
+ stopTracks();
171
+ throw options.signal.reason;
172
+ }
173
+ return acquired;
174
+ });
175
+ throwIfAborted();
176
+ context = new AudioContext();
177
+ if (context.state === "suspended")
178
+ await raceWithAbortSignal(options.signal, () => context!.resume());
179
+ throwIfAborted();
180
+ const source = context.createMediaStreamSource(openedStream);
181
+ const analyser = context.createAnalyser();
182
+ analyser.fftSize = 256;
183
+ source.connect(analyser);
184
+
185
+ let muted = false;
186
+ let closed = false;
187
+ let closing: Promise<void> | undefined;
188
+ const push = (samples: Float32Array) => {
189
+ if (!muted && !closed && samples.length)
190
+ options.onChunk(encodePcm16(samples));
191
+ };
192
+
193
+ let node: AudioNode | undefined;
194
+ let detach = () => {};
195
+ if (context.audioWorklet) {
196
+ const url = URL.createObjectURL(
197
+ new Blob([WORKLET_SOURCE], { type: "text/javascript" }),
198
+ );
199
+ try {
200
+ await raceWithAbortSignal(options.signal, () =>
201
+ context!.audioWorklet.addModule(url),
202
+ );
203
+ throwIfAborted();
204
+ const worklet = new AudioWorkletNode(context, "pcm-capture", {
205
+ processorOptions: { targetRate: options.sampleRate },
206
+ });
207
+ worklet.port.onmessage = (event) => push(event.data as Float32Array);
208
+ detach = () => {
209
+ worklet.port.onmessage = null;
210
+ };
211
+ node = worklet;
212
+ } catch {
213
+ // A CSP can block blob worklets even when AudioWorklet is available.
214
+ throwIfAborted();
215
+ } finally {
216
+ URL.revokeObjectURL(url);
217
+ }
218
+ }
219
+ if (!node) {
220
+ const resample = createResampler(context.sampleRate / options.sampleRate);
221
+ const processor = context.createScriptProcessor(1024, 1, 1);
222
+ processor.onaudioprocess = (event) =>
223
+ push(resample(event.inputBuffer.getChannelData(0)));
224
+ detach = () => {
225
+ processor.onaudioprocess = null;
226
+ };
227
+ node = processor;
228
+ }
229
+ analyser.connect(node);
230
+ node.connect(context.destination);
231
+ const opened = context;
232
+
233
+ return {
234
+ setMuted: (value) => {
235
+ muted = value;
236
+ openedStream.getAudioTracks().forEach((track) => {
237
+ track.enabled = !value;
238
+ });
239
+ },
240
+ level: () => readLevel(analyser),
241
+ close: () => {
242
+ if (closing) return closing;
243
+ closed = true;
244
+ detach();
245
+ node.disconnect();
246
+ source.disconnect();
247
+ stopTracks();
248
+ return (closing = opened.close());
249
+ },
250
+ };
251
+ } catch (error) {
252
+ stopTracks();
253
+ await context?.close().catch(() => {});
254
+ throw error;
255
+ }
256
+ };
257
+
258
+ export type PcmPlayer = {
259
+ enqueue: (base64: string) => void;
260
+ clear: () => void;
261
+ isPlaying: () => boolean;
262
+ level: () => number;
263
+ close: () => Promise<void>;
264
+ };
265
+
266
+ const IDLE_GRACE_MS = 100;
267
+
268
+ export const createPcmPlayer = (options: {
269
+ sampleRate: number;
270
+ onIdle?: () => void;
271
+ }): PcmPlayer => {
272
+ const context = new AudioContext({ sampleRate: options.sampleRate });
273
+ const analyser = context.createAnalyser();
274
+ analyser.fftSize = 256;
275
+ analyser.connect(context.destination);
276
+ const sources = new Set<AudioBufferSourceNode>();
277
+ let cursor = 0;
278
+ let closing: Promise<void> | undefined;
279
+ let idleTimer: ReturnType<typeof setTimeout> | undefined;
280
+ const scheduleIdle = () => {
281
+ clearTimeout(idleTimer);
282
+ idleTimer = setTimeout(() => {
283
+ if (sources.size === 0) options.onIdle?.();
284
+ }, IDLE_GRACE_MS);
285
+ };
286
+
287
+ return {
288
+ enqueue: (base64) => {
289
+ const samples = decodePcm16(base64);
290
+ if (samples.length === 0) return;
291
+ const buffer = context.createBuffer(
292
+ 1,
293
+ samples.length,
294
+ options.sampleRate,
295
+ );
296
+ buffer.copyToChannel(samples, 0);
297
+ const source = context.createBufferSource();
298
+ source.buffer = buffer;
299
+ source.connect(analyser);
300
+ const start = Math.max(context.currentTime, cursor);
301
+ source.start(start);
302
+ cursor = start + buffer.duration;
303
+ sources.add(source);
304
+ source.onended = () => {
305
+ sources.delete(source);
306
+ if (sources.size === 0) scheduleIdle();
307
+ };
308
+ clearTimeout(idleTimer);
309
+ if (context.state === "suspended") void context.resume().catch(() => {});
310
+ },
311
+ clear: () => {
312
+ clearTimeout(idleTimer);
313
+ for (const source of sources) {
314
+ source.onended = null;
315
+ source.stop();
316
+ }
317
+ sources.clear();
318
+ cursor = 0;
319
+ },
320
+ isPlaying: () => sources.size > 0,
321
+ level: () => readLevel(analyser),
322
+ close: () => {
323
+ if (closing) return closing;
324
+ clearTimeout(idleTimer);
325
+ for (const source of sources) {
326
+ source.onended = null;
327
+ source.stop();
328
+ }
329
+ sources.clear();
330
+ return (closing = context.close());
331
+ },
332
+ };
333
+ };
@@ -0,0 +1,34 @@
1
+ /// <reference types="node" />
2
+ import { spawnSync } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+ import { expect, it } from "vitest";
5
+
6
+ const PACKAGE_DIR = fileURLToPath(new URL("..", import.meta.url));
7
+
8
+ // The built entry imports React through @assistant-ui/tap/react-shim. Vite
9
+ // dev pre-bundles that shim with esbuild, whose CommonJS interop hands it
10
+ // React's default export only; the loader below recreates that for the shim.
11
+ const script = `
12
+ import { registerHooks } from "node:module";
13
+ registerHooks({
14
+ resolve(specifier, context, nextResolve) {
15
+ const resolved = nextResolve(specifier, context);
16
+ if (specifier !== "react" || !context.parentURL?.endsWith("/react-shim/index.js")) return resolved;
17
+ const source = "import React from " + JSON.stringify(resolved.url) + "; export default React;";
18
+ return { url: "data:text/javascript," + encodeURIComponent(source), shortCircuit: true };
19
+ },
20
+ });
21
+ const entry = await import("@assistant-ui/react");
22
+ console.log(typeof entry.ComposerPrimitive.Root, typeof entry.CloudRendererHost);
23
+ `;
24
+
25
+ it("loads the public entry when the react-shim only sees React's default export", () => {
26
+ const result = spawnSync(
27
+ process.execPath,
28
+ [...process.execArgv, "--input-type=module", "--eval", script],
29
+ { cwd: PACKAGE_DIR, encoding: "utf8" },
30
+ );
31
+
32
+ expect(result.status, result.stderr).toBe(0);
33
+ expect(result.stdout.trim()).toBe("object function");
34
+ });
package/src/index.ts CHANGED
@@ -69,6 +69,10 @@ export {
69
69
  OpenAIRealtimeVoiceAdapter,
70
70
  type OpenAIRealtimeVoiceAdapterOptions,
71
71
  } from "./adapters/OpenAIRealtimeVoiceAdapter";
72
+ export {
73
+ GeminiLiveVoiceAdapter,
74
+ type GeminiLiveVoiceAdapterOptions,
75
+ } from "./adapters/GeminiLiveVoiceAdapter";
72
76
  export type {
73
77
  VoiceSessionControls,
74
78
  VoiceSessionHelpers,
@@ -0,0 +1,111 @@
1
+ // @vitest-environment jsdom
2
+ import { act, render, waitFor } from "@testing-library/react";
3
+ import { resource, useResource } from "@assistant-ui/tap";
4
+ import {
5
+ AssistantRuntimeProvider,
6
+ MessageByIndexProvider,
7
+ useExternalStoreRuntime,
8
+ type ToolCallMessagePartProps,
9
+ } from "@assistant-ui/core/react";
10
+ import type { ThreadMessageLike } from "@assistant-ui/core";
11
+ import { expect, it, vi } from "vitest";
12
+ import type { AssistantRuntime } from "../index";
13
+ import type { McpAppBridgeHandlers, McpAppsHost } from "./types";
14
+
15
+ const { framePropsMock } = vi.hoisted(() => ({ framePropsMock: vi.fn() }));
16
+
17
+ vi.mock("./app-frame", () => ({
18
+ McpAppFrame: (props: unknown) => {
19
+ framePropsMock(props);
20
+ return null;
21
+ },
22
+ }));
23
+
24
+ import { McpAppRenderer } from "./McpAppRenderer";
25
+
26
+ const Host = resource(({ host }: { host: McpAppsHost }) => host);
27
+
28
+ const host: McpAppsHost = {
29
+ loadResource: vi.fn(async ({ uri }) => ({
30
+ uri,
31
+ mimeType: "text/html;profile=mcp-app" as const,
32
+ html: "",
33
+ })),
34
+ callTool: vi.fn(),
35
+ readResource: vi.fn(),
36
+ listResources: vi.fn(),
37
+ };
38
+
39
+ const part: ToolCallMessagePartProps = {
40
+ type: "tool-call",
41
+ toolCallId: "call-1",
42
+ toolName: "search",
43
+ args: {},
44
+ argsText: "{}",
45
+ status: { type: "complete" },
46
+ mcp: { app: { resourceUri: "ui://example/search" } },
47
+ addResult: vi.fn(),
48
+ resume: vi.fn(),
49
+ respondToApproval: vi.fn(),
50
+ };
51
+
52
+ const messages: ThreadMessageLike[] = [
53
+ { role: "assistant", content: "Here is the app" },
54
+ ];
55
+
56
+ const AppPart = () => {
57
+ const renderer = useResource(McpAppRenderer({ host: Host({ host }) }));
58
+ const Renderer = renderer.render;
59
+ return <Renderer {...part} />;
60
+ };
61
+
62
+ let runtime!: AssistantRuntime;
63
+
64
+ const App = () => {
65
+ runtime = useExternalStoreRuntime({
66
+ messages,
67
+ convertMessage: (message: ThreadMessageLike) => message,
68
+ onNew: async () => {},
69
+ });
70
+ return (
71
+ <AssistantRuntimeProvider runtime={runtime}>
72
+ <MessageByIndexProvider index={0}>
73
+ <AppPart />
74
+ </MessageByIndexProvider>
75
+ </AssistantRuntimeProvider>
76
+ );
77
+ };
78
+
79
+ const renderApp = async () => {
80
+ framePropsMock.mockClear();
81
+ render(<App />);
82
+ await waitFor(() => expect(framePropsMock).toHaveBeenCalled());
83
+ const handlers = framePropsMock.mock.lastCall?.[0]
84
+ .handlers as McpAppBridgeHandlers;
85
+ return handlers;
86
+ };
87
+
88
+ it("fills the thread composer from an MCP App message inside a message", async () => {
89
+ const handlers = await renderApp();
90
+
91
+ let result: unknown;
92
+ act(() => {
93
+ result = handlers.sendMessage?.({ text: "Open Grade 5 Math" });
94
+ });
95
+
96
+ expect(result).toEqual({ ok: true });
97
+ expect(runtime.thread.composer.getState().text).toBe("Open Grade 5 Math");
98
+ });
99
+
100
+ it("keeps the user's draft when an MCP App message arrives", async () => {
101
+ const handlers = await renderApp();
102
+ act(() => runtime.thread.composer.setText("Show my classes"));
103
+
104
+ act(() => {
105
+ handlers.sendMessage?.({ text: "Open Grade 5 Math" });
106
+ });
107
+
108
+ expect(runtime.thread.composer.getState().text).toBe(
109
+ "Show my classes Open Grade 5 Math",
110
+ );
111
+ });
@@ -16,15 +16,22 @@ import type {
16
16
  McpAppsRemoteHostOptions,
17
17
  } from "./types";
18
18
 
19
- const { appendMock, framePropsMock } = vi.hoisted(() => ({
19
+ const { appendMock, setTextMock, framePropsMock } = vi.hoisted(() => ({
20
20
  appendMock: vi.fn(),
21
+ setTextMock: vi.fn(),
21
22
  framePropsMock: vi.fn(),
22
23
  }));
23
24
 
24
25
  vi.mock("@assistant-ui/store", async (importOriginal) => ({
25
26
  ...(await importOriginal()),
26
27
  useAui: () => ({
27
- thread: { append: appendMock },
28
+ thread: {
29
+ append: appendMock,
30
+ composer: () => ({
31
+ setText: setTextMock,
32
+ getState: () => ({ text: "" }),
33
+ }),
34
+ },
28
35
  }),
29
36
  }));
30
37
 
@@ -190,6 +197,7 @@ function RemoteHarness({
190
197
  describe("McpAppRenderer", () => {
191
198
  beforeEach(() => {
192
199
  appendMock.mockReset();
200
+ setTextMock.mockReset();
193
201
  framePropsMock.mockReset();
194
202
  });
195
203
 
@@ -676,7 +684,7 @@ describe("McpAppRenderer", () => {
676
684
  expect(listResources).toHaveBeenCalledWith(params);
677
685
  });
678
686
 
679
- it("appends MCP Apps message content in the default sendMessage handler", async () => {
687
+ it("puts MCP Apps text content in the composer without appending", async () => {
680
688
  render(<Harness host={loadingHost()} />);
681
689
  await waitFor(() => expect(framePropsMock).toHaveBeenCalled());
682
690
  const handlers = framePropsMock.mock.lastCall?.[0]
@@ -692,19 +700,13 @@ describe("McpAppRenderer", () => {
692
700
  ],
693
701
  }),
694
702
  ).toEqual({ ok: true });
695
- expect(appendMock).toHaveBeenCalledWith({
696
- content: [
697
- { type: "text", text: "Open Grade 5 Math" },
698
- { type: "text", text: "Period 4" },
699
- ],
700
- });
703
+ expect(setTextMock).toHaveBeenCalledWith("Open Grade 5 Math\nPeriod 4");
704
+ expect(appendMock).not.toHaveBeenCalled();
701
705
 
702
706
  expect(handlers.sendMessage?.({ text: "typed instead" })).toEqual({
703
707
  ok: true,
704
708
  });
705
- expect(appendMock).toHaveBeenLastCalledWith({
706
- content: [{ type: "text", text: "typed instead" }],
707
- });
709
+ expect(setTextMock).toHaveBeenLastCalledWith("typed instead");
708
710
 
709
711
  expect(
710
712
  handlers.sendMessage?.({
@@ -722,7 +724,8 @@ describe("McpAppRenderer", () => {
722
724
  ok: false,
723
725
  reason: "unrecognised params shape",
724
726
  });
725
- expect(appendMock).toHaveBeenCalledTimes(2);
727
+ expect(setTextMock).toHaveBeenCalledTimes(2);
728
+ expect(appendMock).not.toHaveBeenCalled();
726
729
  });
727
730
 
728
731
  it("extracts sparse message content without traversing holes", async () => {
@@ -755,9 +758,8 @@ describe("McpAppRenderer", () => {
755
758
  }
756
759
 
757
760
  expect(visitedHole).not.toHaveBeenCalled();
758
- expect(appendMock).toHaveBeenCalledWith({
759
- content: [{ type: "text", text: "sparse prompt" }],
760
- });
761
+ expect(setTextMock).toHaveBeenCalledExactlyOnceWith("sparse prompt");
762
+ expect(appendMock).not.toHaveBeenCalled();
761
763
  });
762
764
 
763
765
  it("ignores non-index message content properties after structured clone", async () => {
@@ -781,13 +783,10 @@ describe("McpAppRenderer", () => {
781
783
  ).toEqual({ ok: true });
782
784
  }
783
785
 
784
- expect(appendMock).toHaveBeenCalledTimes(2);
785
- expect(appendMock).toHaveBeenNthCalledWith(1, {
786
- content: [{ type: "text", text: "message text" }],
787
- });
788
- expect(appendMock).toHaveBeenNthCalledWith(2, {
789
- content: [{ type: "text", text: "message text" }],
790
- });
786
+ expect(setTextMock).toHaveBeenCalledTimes(2);
787
+ expect(setTextMock).toHaveBeenNthCalledWith(1, "message text");
788
+ expect(setTextMock).toHaveBeenNthCalledWith(2, "message text");
789
+ expect(appendMock).not.toHaveBeenCalled();
791
790
  });
792
791
 
793
792
  it("uses caller UI handlers and keeps data-plane handlers on the host", async () => {
@@ -838,6 +837,8 @@ describe("McpAppRenderer", () => {
838
837
  expect(updateModelContext).toHaveBeenCalledWith({ text: "context" });
839
838
  expect(openLink).toHaveBeenCalledWith({ url: "https://example.com" });
840
839
  expect(sendMessage).toHaveBeenCalledWith({ text: "hello" });
840
+ expect(setTextMock).not.toHaveBeenCalled();
841
+ expect(appendMock).not.toHaveBeenCalled();
841
842
  expect(onInitialized).toHaveBeenCalledOnce();
842
843
  expect(host.callTool).toHaveBeenCalledWith({ name: "search" });
843
844
  expect(host.readResource).toHaveBeenCalledWith({ uri: "ui://resource" });
@@ -277,7 +277,15 @@ function InlineRenderer({
277
277
  if ("reason" in result) {
278
278
  return { isError: true, ok: false, reason: result.reason };
279
279
  }
280
- aui.thread.append({ content: result.content });
280
+ const composer = aui.thread.composer();
281
+ composer.setText(
282
+ [
283
+ composer.getState().text,
284
+ result.content.map((part) => part.text).join("\n"),
285
+ ]
286
+ .filter((part) => part.trim())
287
+ .join(" "),
288
+ );
281
289
  return { ok: true };
282
290
  }),
283
291
  callTool: (params) =>