()
| 631 | |
| 632 | // ── Start a new recording session (voice_stream connect + audio) ── |
| 633 | async function startRecordingSession(): Promise<void> { |
| 634 | if (!voiceModule) { |
| 635 | onErrorRef.current?.( |
| 636 | 'Voice module not loaded yet. Try again in a moment.', |
| 637 | ) |
| 638 | return |
| 639 | } |
| 640 | |
| 641 | // Transition to 'recording' synchronously, BEFORE any await. Callers |
| 642 | // read state synchronously right after `void startRecordingSession()`: |
| 643 | // - useVoiceIntegration.tsx space-hold guard reads voiceState from the |
| 644 | // store immediately — if it sees 'idle' it clears isSpaceHoldActiveRef |
| 645 | // and space auto-repeat leaks into the text input (100% repro) |
| 646 | // - handleKeyEvent's `currentState === 'idle'` re-entry check below |
| 647 | // If an await runs first, both see stale 'idle'. See PR #20873 review. |
| 648 | updateState('recording') |
| 649 | recordingStartRef.current = Date.now() |
| 650 | accumulatedRef.current = '' |
| 651 | seenRepeatRef.current = false |
| 652 | hasAudioSignalRef.current = false |
| 653 | retryUsedRef.current = false |
| 654 | silentDropRetriedRef.current = false |
| 655 | fullAudioRef.current = [] |
| 656 | focusFlushedCharsRef.current = 0 |
| 657 | everConnectedRef.current = false |
| 658 | const myGen = ++sessionGenRef.current |
| 659 | |
| 660 | // ── Pre-check: can we actually record audio? ────────────── |
| 661 | const availability = await voiceModule.checkRecordingAvailability() |
| 662 | if (!availability.available) { |
| 663 | logForDebugging( |
| 664 | `[voice] Recording not available: ${availability.reason ?? 'unknown'}`, |
| 665 | ) |
| 666 | onErrorRef.current?.( |
| 667 | availability.reason ?? 'Audio recording is not available.', |
| 668 | ) |
| 669 | cleanup() |
| 670 | updateState('idle') |
| 671 | return |
| 672 | } |
| 673 | |
| 674 | logForDebugging( |
| 675 | '[voice] Starting recording session, connecting voice stream', |
| 676 | ) |
| 677 | // Clear any previous error |
| 678 | setVoiceState(prev => { |
| 679 | if (!prev.voiceError) return prev |
| 680 | return { ...prev, voiceError: null } |
| 681 | }) |
| 682 | |
| 683 | // Buffer audio chunks while the WebSocket connects. Once the connection |
| 684 | // is ready (onReady fires), buffered chunks are flushed and subsequent |
| 685 | // chunks are sent directly. |
| 686 | const audioBuffer: Buffer[] = [] |
| 687 | |
| 688 | // Start recording IMMEDIATELY — audio is buffered until the WebSocket |
| 689 | // opens, eliminating the 1-2s latency from waiting for OAuth + WS connect. |
| 690 | logForDebugging( |
no test coverage detected