fix(react-native): Disconnecting voice conversations over WebSocket in RN - #622
fix(react-native): Disconnecting voice conversations over WebSocket in RN#622brojd wants to merge 36 commits into
Conversation
Enter pre-mode
…abs#525) * Migrate client package to TypeScript compiler for ESM builds Replaces microbundle with TypeScript compiler for building ESM and type definitions. This eliminates CommonJS support as a breaking change. Changes: - Add tsconfig.build.json and tsconfig.test.json with project references - Update package.json exports to use dist/index.js for ESM - Remove "main" and "module" fields from package.json - Keep microbundle only for UMD bundle (unpkg) - Update build scripts to use tsc for ESM and microbundle for UMD Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Fix react package Jest config to consume ESM client package - Configure ts-jest with useESM: true and module: esnext - Add moduleNameMapper to resolve @elevenlabs/client to ESM dist This allows the react package tests to properly consume the client package now that it only exports ESM (no CommonJS). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Update client package TypeScript target to ES2022 Bumps target and lib from ES2018 to ES2022 to take advantage of newer JavaScript features like top-level await, class fields, and private methods. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Use 'default' export condition instead of 'import' Since the package only provides ESM (no CommonJS), using 'default' as a catch-all is more appropriate than the specific 'import' condition. Order matters in export conditions, so 'default' is placed last as the fallback. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Pin module system to ES2022 and resolution to "bundler" --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(convai-widget): patch livekit-client to handle frozen RTCPeerConnection prototype Wix's security hardening freezes RTCPeerConnection.prototype.addEventListener as read-only, causing the widget to crash on load. livekit-client bundles webrtc-adapter inline, which monkey-patches addEventListener at module evaluation time. Add a pnpm patch that inserts a writability guard (try/catch no-op assignment) before the monkey-patch. If the prototype is frozen, the patching is skipped silently — this is safe since the shims are legacy cross-browser compat code that modern browsers don't need. * chore: add changeset for wix widget compat fix * Update patches/livekit-client@2.16.0.patch Co-authored-by: Kræn Hansen <kraen@elevenlabs.io> * fix: regenerate livekit-client patch to fix hunk header integrity check --------- Co-authored-by: Kræn Hansen <kraen@elevenlabs.io>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…enlabs#550) Microbundle's --generateTypes defaults to true when a `types` field exists in package.json, causing it to overwrite dist/index.d.ts after tsc emits it. This stripped the sourceMappingURL reference, orphaning the .d.ts.map file and breaking IDE "Go to Definition". Adding --no-generateTypes ensures tsc remains the sole source of truth for type declarations. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…bs#551) Applies the same migration pattern from PR elevenlabs#525 (@elevenlabs/client) to @elevenlabs/react: tsc now handles ESM output and type declarations, while microbundle is kept only for the UMD bundle. Drops CJS export (same breaking change as the client migration). - Replace monolithic tsconfig.json with project references root - Add tsconfig.build.json (composite, ES2022, react-jsx, declarations) - Add tsconfig.test.json (extends build, noEmit, jest types) - Update package.json: split build:esm/build:umd scripts, update exports to use `default` entry, add `files: ["dist"]`, remove legacy main/module/source fields - Update jest.config.cjs: jsx: 'react' → 'react-jsx' Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Migrate react package tests from jest to vitest Replaces jest + ts-jest + jest-environment-jsdom with vitest, matching the test runner used by the client package. - Add vitest.config.ts with jsdom environment (no browser mode) - Migrate src/index.test.ts: jest.mock/jest.fn → vi.mock/vi.fn, jest.Mock cast → as unknown as Conversation - Update package.json: test script jest → vitest, remove @types/jest / jest / jest-environment-jsdom / ts-jest, add vitest ^3.0.5 - Remove jest.config.cjs - Update tsconfig.test.json: types jest → vitest/globals Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Empty commit to kick off CI --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add `InputController` interface and implement it (elevenlabs#513) Add InputController interface and implement it * Make `Input` implement `EventTarget` (elevenlabs#518) * Make Input implement EventTarget * Export InputEventTarget type from input.ts Add a named type for the event-target surface of Input, providing a reusable interface for components that need to subscribe to input events without depending on the full Input class. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Rename addEventListener/removeEventListener to addListener/removeListener Based on PR elevenlabs#518 feedback, simplified the InputEventTarget API by: - Renaming addEventListener(type: "input", listener) to addListener(listener) - Renaming removeEventListener(type: "input", listener) to removeListener(listener) - Removing the type parameter since there's only one event type ("input") This makes the API simpler and more intuitive while maintaining the same functionality. Updated: - InputEventTarget type definition in input.ts - Input class methods in input.ts - VoiceConversation call sites (constructor and changeInputDevice) All tests passing (124 client tests, 87 convai-widget-core tests). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * Extract attachInputToConnection function (elevenlabs#519) * Extract attachInputToConnection function Move input-to-connection routing logic from VoiceConversation into a standalone attachInputToConnection function. This decouples the conversation class from encoding/sending details. Changes: - New attachInputToConnection.ts with routing logic - VoiceConversation now uses detachInput teardown function - Removes onInputWorkletMessage method from VoiceConversation - Teardown called before closing connection in handleEndSession Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Add back TODO comment about maxVolume --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * Hide Input's internal fields (worklet, context, inputStream) (elevenlabs#521) Makes worklet, context, and inputStream private in the Input class to enforce using the public API (InputController + InputEventTarget). - Changed worklet, context from public readonly to private readonly - Changed inputStream from public to private - Updated tests to assert behavior instead of internal state - Removed assertions on input.inputStream and output.audioElement - All tests pass (client: 124, widget-core: 87) Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * Rename Input to MediaDeviceInput and use interface types (elevenlabs#523) Rename `Input` to `MediaDeviceInput` and use interface types - Renamed Input class to MediaDeviceInput (internal implementation) - Changed input property type to InputController & InputEventTarget - Changed changeInputDevice() to return void for better encapsulation - Added error handling for optional analyser field - Removed Input export from package (use InputController interface) Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * Add integration test for MediaDeviceInput listener API (elevenlabs#535) Verifies that addListener receives audio data from the worklet, which requires port.start() to be called in the constructor. Uses real browser audio pipeline with fake media streams instead of mocks, catching the regression if port.start() is removed. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Implement InputController for WebRTC and platform setup strategy (elevenlabs#524) * Implement InputController for WebRTC and platform setup strategy Milestones 6 & 7: Completes the input abstraction by making WebRTCConnection implement InputController and extracting platform- specific session setup logic. **Breaking API Changes:** - InputConfig: `inputDeviceId` → `deviceId` (removes redundant "input" prefix) - New InputDeviceConfig type (deviceId, preferHeadphonesForIosDevices) in InputController - InputConfig now extends InputDeviceConfig and adds onError callback (in input.ts) - InputController.setInputDevice() uses `Partial<FormatConfig> & InputDeviceConfig` **WebRTCConnection as InputController:** - Implements InputController interface (not InputEventTarget - LiveKit handles audio routing) - Added setInputDevice(config), setMuted, isMuted getter, analyser (undefined for WebRTC) - Made close() async to match interface - Tracks mute state internally with _isMuted field - setInputDevice() throws error if sampleRate/format/preferHeadphonesForIosDevices are provided **Platform Setup Strategy:** - Created platform/VoiceSessionSetup.ts with pluggable strategy pattern - webSessionSetup branches on connection type: - WebSocket: creates MediaDeviceInput, attaches to connection - WebRTC: uses connection itself as InputController - Returns { input: InputController, output: Output, detachInput } **VoiceConversation Simplifications:** - Input property is now InputController (not MediaDeviceInput) - setMicMuted() delegates to input.setMuted() (no instanceof checks) - changeInputDevice() passes config to input.setInputDevice() - Uses setupStrategy for platform-agnostic initialization - Zero knowledge of MediaDeviceInput or WebRTC-specific details **Build Fix:** - Added runtime export to InputController.ts to work around microbundle tree-shaking - Pure type-only modules get excluded from builds, preventing .d.ts generation - Will be removed when migrating from microbundle to TypeScript compiler Prepares for React Native support where a different strategy will be used. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Rename muting methods to setInputMuted (elevenlabs#524) Breaking changes: - InputController.setMuted() renamed to setInputMuted() and now returns Promise<void> - MediaDeviceInput.setMuted() renamed to setInputMuted() - WebRTCConnection.setMicMuted() renamed to setInputMuted() Internal changes:- BaseConversation.setMicMuted() now throws an error for text-only conversations - VoiceConversation.setMicMuted() delegates to InputController.setInputMuted() with error handling Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Refactor WebRTCConnection to expose input as property Instead of WebRTCConnection implementing InputController directly, it now exposes a readonly `input` property that implements the interface. This resolves the namespace collision between: - WebRTCConnection.close() - closes the entire connection - InputController.close() - closes only input resources Changes: - WebRTCConnection now has `public readonly input: InputController` property - Input controller initialized in constructor with arrow functions capturing 'this' - Changed InputController.isMuted from property to function to support arrow function pattern - Updated MediaDeviceInput.isMuted to be a function - Updated VoiceSessionSetup to use connection.input instead of connection - Removed redundant InputController methods from WebRTCConnection (now on input property) This provides better separation of concerns - the input controller is now a facet of the connection rather than the connection itself. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Remove microbundle workaround from InputController The __inputControllerModule export was a workaround for microbundle's tree-shaking of type-only modules. Now that we've migrated to the TypeScript compiler, this workaround is no longer needed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Revert deviceId rename, keep as inputDeviceId for now Reverting the InputConfig.inputDeviceId → deviceId rename to reduce breaking changes in this PR. The config field remains as inputDeviceId. Future improvement: namespace input/output configs under "input" and "output" properties, which would then allow removing the "input" and "output" prefixes from deviceId fields. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Remove implementation-specific comments from generic code Removes comments mentioning concrete implementations (MediaDeviceInput, WebRTCConnection) from generic/interface-level code to keep the abstraction clean. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * Rename InputController methods to remove 'Input' prefix (elevenlabs#529) Since the methods are already namespaced under the 'input' property, remove the redundant 'Input' prefix from method names: - setInputDevice -> setDevice - setInputMuted -> setMuted Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * Extract OutputController interface and refactor audio routing (elevenlabs#533) * Extract OutputController interface and refactor audio routing Separate OutputController (control interface) from audio data flow, mirroring the InputController pattern. WebSocketConnection now implements OutputEventTarget for audio data routing, while MediaDeviceOutput handles playback state and interrupt logic internally. Key changes: - New OutputController interface with control-only methods - MediaDeviceOutput encapsulates interrupt state and playback - WebSocketConnection emits audio events via OutputEventTarget - Audio routing moved to VoiceSessionSetup via attachConnectionToOutput - VoiceConversation simplified: no more direct worklet access or as-any casts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Change analyser from readonly property to getAnalyser() method This eliminates the IIFE and "const self = this" pattern in WebRTCConnection's output property initializer. With getAnalyser() being a method, arrow functions in the object literal capture `this` naturally, so the output controller can use the same simple pattern as the input controller. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Encapsulate VoiceConversation internal fields (elevenlabs#537) * Encapsulate VoiceConversation internal fields Make input, output, playbackEventTarget, cleanUp, and wakeLock private to prevent external access and enforce proper encapsulation of VoiceConversation's internal state. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Remove direct wakeLock property checks from tests Test behavior (mock calls) instead of implementation details (private fields). This follows testing best practices and avoids coupling tests to internal implementation. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * Remove `outputDeviceId` from `FormatConfig` (elevenlabs#543) Remove outputDeviceId from FormatConfig FormatConfig should only describe the audio format (codec + sample rate). The outputDeviceId field was already provided by OutputDeviceConfig / OutputConfig in every intersection type that needed it, making its presence on FormatConfig redundant and confusing — it leaked into changeInputDevice's parameter type, making it look like an output device ID was relevant there. Removes outputDeviceId from FormatConfig and the React package's DeviceFormatConfig. Updates changeOutputDevice in the React hook to accept DeviceFormatConfig & OutputConfig so outputDeviceId is still accepted via the correct type. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Make `BaseConversation` abstract; replace `Conversation` class with type union (elevenlabs#538) * Make BaseConversation an abstract class Move voice-related method stubs (setVolume, setMicMuted, getInputByteFrequencyData, getOutputByteFrequencyData, getInputVolume, getOutputVolume) from concrete implementations in BaseConversation to abstract declarations. TextConversation now provides explicit implementations (no-ops/throws) instead of silently inheriting stubs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Replace abstract Conversation class with type union and object factory - `Conversation` is now `type Conversation = TextConversation | VoiceConversation` instead of an abstract class extending `BaseConversation` - `Conversation.startSession()` is now a method on a plain const object, routing via `isTextOnly()` rather than inheriting `getFullOptions()` - Fix: `createConnection` in both subclass `startSession` methods now receives `fullOptions` instead of raw `options`, ensuring `overrides.conversation.textOnly` is always populated before the connection is established Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Throw in TextConversation.setVolume and setMicMuted Silent no-ops could leave audio-related UI in an unexpected state. Throwing makes it explicit that these operations are not supported in text-only conversations. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * Update test to expect setVolume to throw on text conversation Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Add and update changesets for all breaking changes - Rename auto-generated changeset names to semantic names - Upgrade all changeset bump types from minor to major - Add before/after examples and migration steps to each changeset - Add missing changesets for: Output class removal, wakeLock private, Conversation class → namespace, and @elevenlabs/react outputDeviceId removal Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
) * Add ConversationProvider with lifecycle management and mergeOptions utility (elevenlabs#560) * Add ConversationProvider skeleton with raw context and lifecycle management Introduces the foundational ConversationProvider component and ConversationContext for the new granular context-based React API. Includes stable ref-backed callbacks, session lifecycle management (start/end/cancel), and useRawConversation as a public escape hatch for advanced use cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add generic mergeOptions utility with deep merge and function composition Introduce a reusable mergeOptions<T> function in the client package that deep-merges plain objects, composes functions (all fire in order), and uses last-wins for other values. Use it in ConversationProvider to merge default props, stable callbacks, and per-session options so that callbacks provided both via props and startSession() both fire. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove unused awaits of empty promises * Use Promise.withResolvers() in ConversationProvider tests Replaces the manual resolver pattern (let resolve!; new Promise(r => resolve = r)) with the cleaner built-in Promise.withResolvers() API. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Add eslint-plugin-react-hooks to react package (elevenlabs#568) * Add eslint-plugin-react-hooks to react package Replaces the empty ESLint config with the official recommended flat config for react-hooks. Catches exhaustive-deps and rules-of-hooks violations. Ignores dist/. Also updates react-dom to 19.2.4 to match the resolved react version. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix lint violations from typescript-eslint and react-hooks rules Replace `any` with proper types in useStableCallbacks and device change casts, and suppress intentional ref-during-render patterns. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Expose conversationRef from ConversationContext (elevenlabs#572) Sub-providers (Controls, Input, Feedback) each duplicated a local ref mirroring ctx.conversation to build stable callbacks. Expose the existing conversationRef through the context so they can use it directly, eliminating the repeated ref + eslint-disable pattern. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Add ConversationControlsProvider with stable action references (elevenlabs#562) * Add ConversationControlsProvider with stable action references - ConversationControlsProvider reads ConversationContext, mirrors the conversation instance in a ref, and builds stable useCallback delegates for all action methods (sendUserMessage, setVolume, changeInputDevice, etc.) - Methods throw "No active conversation" when called without an active session rather than silently no-oping, making misuse easier to catch - changeInputDevice/changeOutputDevice use instanceof VoiceConversation to give a clear error for text-only conversations - ConversationProvider updated to render <ConversationControlsProvider> as a child — no state or logic added to the Provider itself - All control callbacks are stable: the context value reference never changes, preventing re-renders in consumers when conversation state changes Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * Fix lint error: expand useMemo deps instead of disabling eslint rule The react-hooks/exhaustive-deps rule is not configured in the project's eslint config, so the eslint-disable comment caused a lint error in CI. All callbacks are stable useCallback refs so listing them explicitly is safe and removes the need for the suppression comment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix react-hooks lint violations in ConversationControls Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use conversationRef from context in ConversationControls Replace the local ref + eslint-disable pattern with the shared conversationRef now available from ConversationContext. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * Add ConversationStatus context + callback registry (elevenlabs#565) * Add ListenerSet and ListenerMap utilities for callback composition ListenerSet<Args> is a generic typed multicast delegate that holds a Set of listeners and provides add (returns a removal function), invoke, and size. It is parameterized by the argument tuple type. ListenerMap<T> maps named callback keys to ListenerSet instances. - register(callbacks) adds listeners for one or more keys and returns a single removal function that unsubscribes all of them. - compose() returns a Partial<T> where each key fans out to all registered listeners. Composed functions reflect live listener state, so additions/removals after compose still take effect. - Uses a never[] constraint to accept any function signature without contravariance issues, and an assertFunction guard to reject undefined values at runtime. Both utilities are fully tested in isolation with vitest. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add ConversationStatus context, registerCallbacks, and ListenerMap wiring ConversationContext now exposes registerCallbacks(Partial<Callbacks>), allowing sub-providers to declare callback handlers that are composed into the Conversation.startSession() call via ListenerMap.compose(). ConversationStatusProvider registers onStatusChange and onError: - Maps client Status values to "disconnected" | "connecting" | "connected" | "error" (dropping transient "disconnecting") - Tracks an optional error message from onError, cleared on any subsequent non-error status transition - Exports useConversationStatus() hook Also updates test files to use destructured mock access pattern: const [[opts]] = vi.mocked(Conversation.startSession).mock.calls Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix exhaustive-deps lint warning in ConversationStatus Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Co-locate ConversationStatusValue type with its provider Move ConversationStatusValue from types.ts into ConversationStatus.tsx for consistency with ConversationControlsValue and ConversationInputValue which are already co-located with their providers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Pre-initialize ListenerMap with CALLBACK_KEYS from @elevenlabs/client Export a CALLBACK_KEYS runtime array from @elevenlabs/types (co-located with the Callbacks type) through @elevenlabs/client. Use it in ConversationProvider to pre-initialize all ListenerMap sets, ensuring compose() returns functions for every callback key — even for listeners registered after compose was called. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Narrow ListenerMap constructor keys type to keyof T & string Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Address Copilot review feedback on PR elevenlabs#565 - Lazy-initialize ListenerMap via useState to avoid re-construction on every render - Skip undefined values in ListenerMap.register to match Partial<T> type contract - Export ConversationProvider, useConversationStatus, and ConversationStatusValue from public entrypoint Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Revert public exports of ConversationProvider and useConversationStatus Not ready to be part of the public API yet. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add compile-time exhaustiveness check for CALLBACK_KEYS Ensures CALLBACK_KEYS stays in sync with the Callbacks type via a type-level Exclude check and a runtime duplicates guard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix useConversationStatus doc comment to reflect actual render behavior Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Throw on unknown callback keys in ListenerMap.register Since all keys are pre-initialized in the constructor, a missing key indicates a typo or misuse. Throwing early surfaces these mistakes instead of silently dropping callbacks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Change ListenerMap.compose return type from Partial<T> to T All keys are pre-initialized in the constructor, so compose() always produces every key. Returning T instead of Partial<T> reflects this and eliminates the doc/type contradiction. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Address latest Copilot review feedback - Add listenerMap to useCallback dependency arrays (stable, silences lint) - Use `as const satisfies` for CALLBACK_KEYS to preserve literal tuple type - Fix compile-time exhaustiveness check to use AssertNever pattern Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Depend on stable registerCallbacks instead of ctx in ConversationStatus Avoids unnecessary unsubscribe/re-subscribe cycles when the context value changes due to session lifecycle events. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Extract useRegisterCallbacks hook for sub-provider callback registration Provides a ref-based hook that registers stable callback wrappers once, delegating to the latest callback values without re-subscribing. Simplifies ConversationStatusProvider and establishes the pattern for future sub-providers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Memoize ConversationStatusContext value to avoid unnecessary consumer re-renders Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Re-subscribe useRegisterCallbacks when the set of active keys changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove unused eslint-disable for react-hooks/refs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use useLayoutEffect in useRegisterCallbacks to catch synchronous events startSession fires onStatusChange synchronously, so callbacks must be registered before paint to avoid missing the initial status event. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use stable scalar dep instead of spreading activeKeys in useRegisterCallbacks Replace ...activeKeys spread in the effect dep array with activeKeys.join("|") — a fixed-length dep list that's easier to reason about. The effect still closes over activeKeys directly, so no split/reconstruction is needed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix misleading comment in ListenerMap test about post-removal behavior Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Add ConversationInput context + useConversationInput hook (elevenlabs#566) * Add ConversationInput context with useConversationInput hook setMuted throws when called without an active session, matching the established pattern in ConversationControls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix react-hooks/refs lint violation in ConversationInput Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use conversationRef from context in ConversationInput Replace the local ref + eslint-disable pattern with the shared conversationRef from ConversationContext. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add useRawConversationRef hook and apply React.PropsWithChildren Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Reset isMuted state on session disconnect Register an onDisconnect callback to reset isMuted to false when the conversation ends, preventing stale mute state across sessions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add tests for useRawConversationRef hook Cover the throw-outside-provider and returns-ref-from-context cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Clean up conversation test wrappers (elevenlabs#567) Remove unnecessary signedUrl prop (mocked, never used) and replace { children: React.ReactNode } with React.PropsWithChildren. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Add ConversationMode context with useConversationMode hook (elevenlabs#569) * Add ConversationMode context with useConversationMode hook Registers onModeChange via registerCallbacks and exposes mode, isSpeaking, and isListening. Wired into the ConversationProvider sub-provider stack. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Reset mode to listening on session disconnect Register an onDisconnect callback to reset mode state, preventing stale speaking/listening state across sessions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Add ConversationFeedback context with useConversationFeedback hook (elevenlabs#571) * Add ConversationFeedback context with useConversationFeedback hook Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Reset canSendFeedback on session disconnect Register an onDisconnect callback to reset canSendFeedback to false, preventing stale feedback state across sessions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Sync ConversationProvider state on external disconnect (elevenlabs#574) Register an internal onDisconnect listener via the listener map so the provider clears its conversation ref and state when the session ends externally (agent hangs up, raw endSession call, etc.). Composes with user-provided onDisconnect callbacks. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Export conversation context API, fix circular dependency, add import/no-cycle (elevenlabs#575) * Export conversation context API from @elevenlabs/react Add public exports for ConversationProvider, all context hooks (useConversationControls, useConversationStatus, useConversationInput, useConversationMode, useConversationFeedback, useRawConversation), and their associated value types. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Replace DeviceFormatConfig/DeviceInputConfig with client types These were duplicates of FormatConfig and InputDeviceConfig already exported by @elevenlabs/client. Also re-exports InputDeviceConfig and OutputDeviceConfig for symmetry, inlines ConversationProviderProps into ConversationProvider.tsx, and deletes the now-empty types.ts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Move location utilities to @elevenlabs/client parseLocation, getOriginForLocation, getLivekitUrlForLocation and the Location type are not React-specific — they map a region string to client-level origin/livekitUrl values. Moving them to the client package breaks the circular dependency between index.ts and ConversationProvider.tsx. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add eslint-plugin-import-x with import-x/no-cycle to react and client packages Enforces no circular dependencies at lint time. Also migrates the client eslint config from CJS to ESM (matching the package's "type": "module"). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Replace eslint-plugin-import-x with eslint-plugin-import for defineConfig compatibility eslint-plugin-import-x has type incompatibilities with eslint's defineConfig (un-ts/eslint-plugin-import-x#421). Switch to the original eslint-plugin-import with eslint-import-resolver-typescript, and configure import/extensions + import/parsers for TypeScript file resolution. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add typescript-eslint to client eslint config Configure @eslint/js recommended and typescript-eslint recommended so that eslint can parse .ts files and import/no-cycle actually detects cycles. Suppress pre-existing lint violations to keep the change focused. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove redundant import/parsers setting from eslint configs typescript-eslint already sets @typescript-eslint/parser via languageOptions.parser, making the import/parsers setting unnecessary. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update packages/client/src/utils/location.ts Co-authored-by: Paul Asjes <paul.asjes@elevenlabs.io> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Paul Asjes <paul.asjes@elevenlabs.io> * Migrate testbench to SDK conversation providers (elevenlabs#577) * Migrate testbench to SDK conversation providers Replace the local conversation-provider.tsx prototype with imports from @elevenlabs/react. Updates hook and method names to match the SDK API: start→startSession, end→endSession, useConversationMicrophone→ useConversationInput, isMicMuted→isMuted, setMicMuted→setMuted. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Wrap ConversationProvider in ClientOnly to avoid SSR warnings ConversationProvider uses useLayoutEffect internally, which triggers warnings when rendered on the server in TanStack React Start. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Rename logged method from "start" to "startSession" and fix deps array Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Clean up ConversationProvider sub-providers (elevenlabs#584) * Replace nested provider pyramid with reduceRight composition Use a module-level SUB_PROVIDERS array and reduceRight to compose sub-providers, making it trivial to add new providers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove redundant type annotations on useRegisterCallbacks arguments The Callbacks type already provides full parameter typing, so inline annotations on callback arguments are unnecessary. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use method shorthand in useRegisterCallbacks calls Replace arrow function syntax with method shorthand for less verbosity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Empty commit to trigger CI --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Preserve callback-presence semantics in composed callbacks (elevenlabs#586) * Preserve callback-presence semantics in useStableCallbacks and ListenerMap.compose() The client uses callback presence as feature guards (e.g. onUnhandledClientToolCall triggers early return in handleClientToolCall). Previously, both useStableCallbacks and ListenerMap.compose() provided no-op wrappers for every callback key, defeating these guards. Now both only include keys that actually have handlers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Address review comments on callback-presence PR - Update ListenerMap doc comment to clarify compose() omits empty keys - Fix eslint-disable comment referencing non-existent variable - Add assertion that unprovided callbacks are undefined in startSession options Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Reimplement useConversation from granular hooks (elevenlabs#585) * Reimplement useConversation from granular hooks (elevenlabs#585) Replace the standalone useConversation with a thin wrapper that composes useConversationControls, useConversationStatus, useConversationInput, useConversationMode and useConversationFeedback. This removes ~250 lines of duplicated session-management logic. BREAKING CHANGES: - useConversation now requires a <ConversationProvider> wrapper - startSession returns void (was Promise<string>) — use getId() instead - endSession returns void (was Promise<void>) - micMuted prop renamed to isMuted in the return value (controlled micMuted prop is still accepted as input) - ControlledState type removed from exports Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Forward hook options to startSession and add callback composition tests Extract useStableCallbacks for reuse, register hook callbacks via useRegisterCallbacks, and forward non-callback session config (e.g. agentId) as defaults to startSession. Add tests covering callback composition across provider, hook, and startSession layers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add tests for useStableCallbacks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix controlled micMuted/volume sync on connect Depend on the reactive conversation value from useRawConversation() instead of the stable conversationRef, so micMuted and volume effects re-fire when a session connects. Add test for micMuted set before startSession. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Export UseConversationOptions type from public API Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Add changesets for react conversation API (elevenlabs#587) * Add changesets for react conversation API changes Add two changesets covering the externally-facing changes in the react refactor: - major: useConversation now requires ConversationProvider - minor: new granular conversation hooks for render optimization Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Replace AudioVisualizer with ModeIndicator in changeset example Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Paul Asjes <paul.asjes@elevenlabs.io> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Replace microbundle with rolldown/tsc - Client + React: replace microbundle UMD build with rolldown IIFE (functionally equivalent for CDN/script-tag consumers) - React-native: drop microbundle entirely, use tsc for compilation - Rename build:umd → build:iife, output file lib.umd.js → lib.iife.js - Update unpkg export paths accordingly Resolves elevenlabs#599. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove jest and tests from @elevenlabs/react-native The react-native package is being replaced by re-exports from @elevenlabs/react. Remove jest config, test files, and test-related devDependencies. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Address review feedback for react-native package config - Add "composite": true to tsconfig.build.json (consistent with other packages) - Add "type": "module" to package.json (ESM output needs ESM package type) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add changeset for microbundle → rolldown migration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add setClient internal API to @elevenlabs/client Replace per-session overrides.client mechanism with a module-level setClient() function exported from @elevenlabs/client/internal. Wrapper packages call setClient at import time to register their identity, removing the need to pass source info through session config overrides. - Add ./internal subpath export with setClient/client - Remove overrides.client from public BaseSessionConfig type - Always emit source_info in conversation initiation data - React package calls setClient in index.ts - Widget-core calls setClient in index.ts - Update client tests for always-present source_info Note: react UMD build (microbundle) fails because its bundled TypeScript 4.9.5 cannot resolve subpath exports. Tracked separately. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Rename setClient to setSourceInfo and simplify internal export Rename Client → SourceInfo, client → sourceInfo, setClient → setSourceInfo to match the source_info override field name. Simplify the ./internal export to a plain string path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Freeze sourceInfo to make it read-only Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove redundant overrides spread in widget-core conversation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Move internal utilities from client public API to internal entrypoint Move CALLBACK_KEYS, mergeOptions, parseLocation, getOriginForLocation, and getLivekitUrlForLocation from @elevenlabs/client to @elevenlabs/client/internal. Update @elevenlabs/react imports accordingly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Move Location type to internal entrypoint and update changeset Move the Location type from @elevenlabs/client public API to @elevenlabs/client/internal alongside the location utility functions. Update changeset to reflect these are internal utilities. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Clarify changeset: location utilities were removed, not moved These were originally defined in @elevenlabs/react's index.ts, not re-exported from @elevenlabs/client. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove unreleased location utilities from changeset Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Keep Location type in client public API Location is part of the public type surface (used by HookOptions in @elevenlabs/react), so it should stay in @elevenlabs/client rather than @elevenlabs/client/internal. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add useConversationClientTool hook to @elevenlabs/react Enables React components to dynamically register client tools via a hook. Uses a Proxy-backed mutable target so tools added/removed after session start are immediately visible to BaseConversation at call time. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use function declarations for ClientTools generic type and add tests Change the useConversationClientTool generic from a single TParams type to a ClientTools map of function declarations, enabling type-safe tool name constraints and handler param/return inference. Add unit tests for registration, unmount cleanup, latest-ref delegation, and name changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add changeset for useConversationClientTool hook Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Replace Proxy with registry pattern for client tools - Remove no-op Proxy (createClientToolsProxy) that just delegated to Reflect - Use registry (Map) + live ref pattern: fresh clientTools object per session, hook-registered tools survive across sessions via registry - Add duplicate detection: hook-vs-hook at registration, hook-vs-option at startSession - Extract buildClientTools() to keep ConversationProvider minimal - Export ClientTool type from index.ts - Fix JSDoc for useConversationClientTool TTools type param Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix changeset text and use Object.hasOwn for duplicate detection - Update changeset to describe registry pattern instead of Proxy - Use Object.hasOwn instead of `in` operator to avoid matching inherited keys like toString/constructor Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
… support (elevenlabs#580) * Add React Native platform entrypoint to @elevenlabs/client Adds a `react-native` export condition that calls `registerGlobals()` from `@livekit/react-native` before re-exporting the main index. This polyfills WebRTC globals needed by `livekit-client` in React Native environments. Also adds `@livekit/react-native` as an optional peer dependency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Export ConversationStatus type from @elevenlabs/react Adds a `ConversationStatus` type alias for the status union ("disconnected" | "connecting" | "connected" | "error") used by `ConversationStatusValue`. This makes the status type directly importable without having to extract it from the value type. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Replace @elevenlabs/react-native implementation with @elevenlabs/react re-export Replaces the custom LiveKit-based implementation with a simple re-export of @elevenlabs/react. The package now serves as a convenience wrapper with explicit peer dependencies on @livekit/react-native and @livekit/react-native-webrtc, and a dedicated README with React Native setup instructions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Migrate expo example to @elevenlabs/react conversation providers Replace @elevenlabs/react-native usage with granular hooks from @elevenlabs/react (ConversationProvider, useConversationControls, useConversationStatus, useConversationInput, useConversationMode, useConversationFeedback). Add web platform support via react-native-web. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add React Native voice session setup and fix expo example for Android - Refactor VoiceSessionSetupStrategy to own connection creation, removing the separate preConnectionSetup hook - Add React Native session setup: configures AudioSession before connecting, stops it on cleanup - Enable microphone on SignalConnected in WebRTCConnection (matching useLiveKitRoom behaviour) so the server can establish the subscriber PC - Stop preliminary getUserMedia stream before connecting to avoid blocking LiveKit media negotiation on React Native - Restore @livekit/react-native-expo-plugin and @config-plugins/react-native-webrtc in expo example (required for LiveKitReactNative.setup on Android) - Add metro.config.js with resolveRequest to deduplicate singleton packages across pnpm workspace boundaries - Pass connectionType: "webrtc" explicitly in expo example Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Re-export only conversation API from @elevenlabs/react-native Replace blanket `export * from "@elevenlabs/react"` with explicit conversation-related exports, excluding scribe. Update expo example to import from @elevenlabs/react-native instead of @elevenlabs/react. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add setClient call to @elevenlabs/react-native Call setClient after importing from @elevenlabs/react to override the source from "react_sdk" to "react_native_sdk". Add @elevenlabs/client as a direct dependency for the /internal import. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Move reactNativeSessionSetup into @elevenlabs/react-native Move the React Native voice session setup strategy (AudioSession management) from @elevenlabs/client into @elevenlabs/react-native. The client's react-native entrypoint now only polyfills WebRTC globals. Add export conditions to @elevenlabs/react-native: - "browser" → index.js (re-exports only) - "react-native" → index.native.js (with session setup) - "default" → index.js (fallback) Extract sourceInfo into its own file to break the circular dependency between internal.ts and VoiceSessionSetup.ts, then re-export setSetupStrategy and webSessionSetup from @elevenlabs/client/internal. Remove setSetupStrategy and related types from @elevenlabs/client public API since they are internal implementation details. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove React Native entrypoint from @elevenlabs/client The registerGlobals call and session setup now live in @elevenlabs/react-native, so the client package no longer needs a react-native export condition or @livekit/react-native peer dep. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix TextInput focus loss on web in expo example Remove TouchableWithoutFeedback wrapper that was calling Keyboard.dismiss() on every tap, preventing the TextInput from receiving focus on web. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Rename index.native.ts to index.react-native.ts to fix circular import Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add WebRTC/WebSocket connection type toggle in expo example Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add changesets for react-native rewrite and react type export Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix test failures: getUserMedia timing and WebRTC mock - Move preliminary getUserMedia stream stop back to after setupStrategy completes — MediaDeviceInput.create needs mic access still granted - Add SignalConnected event and once() to WebRTCConnection test mock to match the new mic-enable-on-signal-connected behavior Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Upgrade react-native-web to ~0.21.2 in expo example Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Simplify package READMEs to defer to online documentation Replace verbose API reference documentation in all three package READMEs with concise quick-start examples and links to the official docs. The react-native README is rewritten to reflect the new architecture (thin wrapper re-exporting from @elevenlabs/react with platform setup). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Restore "TypeScript" naming in client README title Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Mention JavaScript alongside TypeScript in client README Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Restore logo image in client and react READMEs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add logo and badges to react-native README Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update Twitter/X handle from elevenlabsio to ElevenLabs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Pass agentId to ConversationProvider, callbacks to startSession Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use realistic agent ID placeholder in README examples Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add "replace with your agent's ID" comment to README examples Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Replace agents-platform with eleven-agents in all doc URLs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…, react, react-native - Change pre.json tag from "next" to "rc" so versions are x.y.z-rc.N - Add publishConfig.tag "next" to client, react, react-native packages - Ignore non-scoped packages during pre-release period Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Version Packages (rc) * Trigger CI --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Kræn Hansen <kraen@elevenlabs.io>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…venlabs#613) * support persisting mute state across conversation sessions * Apply suggestion from @kraenhansen * get rid of unneeded onConnect * Add tests for controlled mute edge cases - Verify disconnect does not reset controlled isMuted - Verify onMutedChange fires in uncontrolled mode - Verify setMuted throws pre-session in controlled mode Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add changeset for controlled mute state feature Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix controlled setMuted eagerly mutating SDK state In controlled mode, setMuted no longer calls conversation.setMicMuted() directly. Instead, it only notifies the parent via onMutedChange and lets the useEffect sync the SDK when the controlled prop changes. This prevents state desync when the parent rejects the change and eliminates double setMicMuted calls in the normal flow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Kræn Hansen <mail@kraenhansen.dk> Co-authored-by: Kræn Hansen <kraen@elevenlabs.io> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Infer default connection type from conversation mode Voice conversations now default to "webrtc" and text-only conversations default to "websocket" when connectionType is not explicitly specified. Closes elevenlabs#579 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix test to exercise textOnly inference code path Use agentId instead of signedUrl so the test actually reaches the textOnly inference logic in determineConnectionType(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Throw when signedUrl is used with connectionType webrtc signedUrl only supports websocket connections, so reject the invalid combination early with a clear error message. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use vi.stubGlobal for fetch mock to ensure test isolation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…et in React Native
| audio: true, | ||
| }); | ||
| } catch (_e) { | ||
| // Non-fatal: platform strategy will handle mic access |
There was a problem hiding this comment.
Not a huge fan of silencing errors in general 🤔 At the very least we should warn here. It seems we could easily be moved up above the first try { to prevent a try in a try.
| playbackEventTarget?.addListener(this.handlePlaybackEvent); | ||
|
|
||
| if (wakeLock) { | ||
| if (wakeLock && typeof document !== "undefined") { |
There was a problem hiding this comment.
I'd like to avoid runtime checks like these in these common files, instead this should be a part of the client/src/platform/ abstraction and injected by the platform-specific SDK.
There was a problem hiding this comment.
Actually now that I'm looking at those checks, they are unneeded bcs wakeLock is null in RN, I'll revert them. Thanks for pointing out!
| await super.handleEndSession(); | ||
|
|
||
| if (this.visibilityChangeHandler) { | ||
| if (this.visibilityChangeHandler && typeof document !== "undefined") { |
| const output = await ReactNativeOutputForWebSocket.create( | ||
| connection.outputFormat | ||
| ); | ||
| const input = await ReactNativeInputForWebSocket.create( | ||
| connection.inputFormat, | ||
| output.audioContext | ||
| ); |
There was a problem hiding this comment.
I was expecting that we'd be able to reuse the LiveKit AudioSession utility / MediaRecorder shim to start and stop sound capturing even for WebSocket - was that something you experimented with initially or did you go straight for a "react-native-audio-api" based solution?
I see your PR description says the LiveKit MediaRecorder shim was rejected, but have you actually tried experimented with an approach based off that? I'd like for us to avoid taking on another native dependency, if at all possible.
There was a problem hiding this comment.
I tried but it appeared that they are designed to work with WebRTC peer connection which is undefined in case of WebSocket and audio processing didn't work and I failed there :( Then I tried expo-av but the sound was breaking up. The last 2 options were writing native code for iOS and Android or use this react-native-audio-api lib. I decided to go with the last to not introduce Swift/Kotlin code there.
Agree! I was hesitant bcs android/ios dirs were already committed. Let's remove them and add to .gitignore 👍 |
|
Once #624 merge I'll update |
| }); | ||
| } catch (e) { | ||
| console.warn("Failed to acquire preliminary media stream:", e); | ||
| } |
There was a problem hiding this comment.
Silenced getUserMedia changes web error flow
Medium Severity
Wrapping the preliminary getUserMedia in a separate try/catch that only logs a warning changes the error flow on web. Previously, a denied mic permission would fail immediately before establishing a server connection. Now, the error is silently swallowed, a server connection is established (consuming server resources), and the session only fails later when MediaDeviceInput.create() tries getUserMedia again — producing a less clear error path and wasting a connection slot.
| ); | ||
| const detachInput = attachInputToConnection(input, connection); | ||
| const detachOutput = attachConnectionToOutput(connection, output); | ||
|
|
There was a problem hiding this comment.
Resource leak on partial failure during WebSocket setup
Medium Severity
In reactNativeSessionSetup, the WebSocket path creates connection, input, and output sequentially with no try/catch around the group. If ReactNativeOutputForWebSocket.create throws after input was successfully created, neither input.close() nor connection.close() is called, and the audio session remains active (AudioManager.setAudioSessionActivity is never set to false). Similarly, if input creation fails, the connection is leaked. The web version avoids this partially via Promise.all, but here sequential creation requires explicit cleanup of earlier resources on later failures.


Demo: https://youtu.be/tax9cbgDYMM
Fixes #605
Problem
WebSocket voice conversations disconnect immediately on React Native while working fine on web. The root cause: the WebSocket voice path uses
MediaDeviceInputandMediaDeviceOutputfrom@elevenlabs/client, which rely on web-only APIs (AudioContext,AudioWorklet,HTMLAudioElement) that don't exist in React Native.The React Native setup strategy (
reactNativeSessionSetup) delegated towebSessionSetupfor all connection types. This worked for WebRTC (where LiveKit's native polyfills handle audio), but crashed immediately for WebSocket voice because the web audio pipeline tried to instantiate browser APIs.Solution
Introduce React Native-specific input and output controllers for the WebSocket path using
react-native-audio-api:Input (
ReactNativeInputForWebSocket): UsesAudioRecorderconnected to anAudioContextvia aRecorderAdapterNodeto capture microphone audio as float PCM, converts to 16-bit PCM, and dispatches to the WebSocket connection.Output (
ReactNativeOutputForWebSocket): UsesAudioBufferQueueSourceNodefor gapless streaming playback. PCM chunks from the server are converted toAudioBufferobjects and enqueued for continuous, gap-free native playback.The setup strategy now branches on
connectionType:"websocket": Uses the new RN-specific controllers withreact-native-audio-apimanaging the audio session.AudioSessionand delegates towebSessionSetup.Why
react-native-audio-api?Few options have been evaluated:
react-native-audio-apiAudioBufferQueueSourceNodefor gapless output streaming andAudioRecorderwithonAudioReadycallback for real-time mic input. Maintained by Software Mansion, MIT license.expo-avAVAudioEngine) and Android (AudioTrack) native code.react-native-audio-apiprovides the same capability from JS.MediaRecordershim@livekit/react-nativeMediaRecordershim only works with tracks from active WebRTC peer connections, not standalonegetUserMediastreams.Changes
@elevenlabs/client(patch):VoiceConversation.ts: Wrap preliminarygetUserMedia()in try/catch (non-fatal on RN). Guarddocument.addEventListener/document.removeEventListenerwithtypeof documentchecks.ConnectionFactory.ts: Add function overloads socreateConnection({ connectionType: "websocket" })returnsPromise<WebSocketConnection>without casts.internal.ts: ExportattachInputToConnection,attachConnectionToOutput,createConnection, and related types for the RN package.@elevenlabs/react-native(patch):ReactNativeInputForWebSocket— mic capture viareact-native-audio-apiReactNativeOutputForWebSocket— gapless audio playback viareact-native-audio-apiindex.react-native.ts— branch setup strategy onconnectionTypereact-native-audio-apias optional peer dependencyExample app:
react-native-audio-apidependencymetro.config.jsfor monorepo resolutionexpo prebuild(addingreact-native-audio-apinative module)Note
Medium Risk
Introduces a new React Native audio capture/playback pipeline for WebSocket voice using
react-native-audio-apiand changes session setup branching, which can affect microphone permissions, audio session behavior, and streaming stability on mobile.Overview
Fixes React Native WebSocket voice sessions disconnecting by adding RN-specific input/output controllers (mic capture via
AudioRecorderand gapless playback viaAudioBufferQueueSourceNode) and routing the RN setup strategy to use them whenconnectionType: "websocket".@elevenlabs/clientis updated to be more RN-tolerant (non-fatal preliminarygetUserMediapermission warmup, typedcreateConnectionoverloads) and to export additional internal helpers (createConnection,attachInputToConnection,attachConnectionToOutput, and related types) consumed by the RN package.The Expo RN example is updated to include
react-native-audio-apiand Metro singleton resolution, and removes committed nativeios/andandroid/project artifacts (with.gitignoreadjusted accordingly).Written by Cursor Bugbot for commit ee24197. This will update automatically on new commits. Configure here.