(keyterms: string[])
| 777 | const isStale = () => sessionGenRef.current !== myGen |
| 778 | |
| 779 | const attemptConnect = (keyterms: string[]): void => { |
| 780 | const myAttemptGen = attemptGenRef.current |
| 781 | void connectVoiceStream( |
| 782 | { |
| 783 | onTranscript: (text: string, isFinal: boolean) => { |
| 784 | if (isStale()) return |
| 785 | sawTranscript = true |
| 786 | logForDebugging( |
| 787 | `[voice] onTranscript: isFinal=${String(isFinal)} text="${text}"`, |
| 788 | ) |
| 789 | if (isFinal && text.trim()) { |
| 790 | if (focusTriggeredRef.current) { |
| 791 | // Focus mode: flush each final transcript immediately and |
| 792 | // keep recording. This gives continuous transcription while |
| 793 | // the terminal is focused. |
| 794 | logForDebugging( |
| 795 | `[voice] Focus mode: flushing final transcript immediately: "${text.trim()}"`, |
| 796 | ) |
| 797 | onTranscriptRef.current(text.trim()) |
| 798 | focusFlushedCharsRef.current += text.trim().length |
| 799 | setVoiceState(prev => { |
| 800 | if (prev.voiceInterimTranscript === '') return prev |
| 801 | return { ...prev, voiceInterimTranscript: '' } |
| 802 | }) |
| 803 | accumulatedRef.current = '' |
| 804 | // User is actively speaking — reset the silence timer. |
| 805 | armFocusSilenceTimer() |
| 806 | } else { |
| 807 | // Hold-to-talk: accumulate final transcripts separated by spaces |
| 808 | if (accumulatedRef.current) { |
| 809 | accumulatedRef.current += ' ' |
| 810 | } |
| 811 | accumulatedRef.current += text.trim() |
| 812 | logForDebugging( |
| 813 | `[voice] Accumulated final transcript: "${accumulatedRef.current}"`, |
| 814 | ) |
| 815 | // Clear interim since final supersedes it |
| 816 | setVoiceState(prev => { |
| 817 | const preview = accumulatedRef.current |
| 818 | if (prev.voiceInterimTranscript === preview) return prev |
| 819 | return { ...prev, voiceInterimTranscript: preview } |
| 820 | }) |
| 821 | } |
| 822 | } else if (!isFinal) { |
| 823 | // Active interim speech resets the focus silence timer. |
| 824 | // Nova 3 disables auto-finalize so isFinal is never true |
| 825 | // mid-stream — without this, the 5s timer fires during |
| 826 | // active speech and tears down the session. |
| 827 | if (focusTriggeredRef.current) { |
| 828 | armFocusSilenceTimer() |
| 829 | } |
| 830 | // Show accumulated finals + current interim as live preview |
| 831 | const interim = text.trim() |
| 832 | const preview = accumulatedRef.current |
| 833 | ? accumulatedRef.current + (interim ? ' ' + interim : '') |
| 834 | : interim |
| 835 | setVoiceState(prev => { |
| 836 | if (prev.voiceInterimTranscript === preview) return prev |
nothing calls this directly
no test coverage detected