({
onTranscript,
onError,
enabled,
focusMode,
}: UseVoiceOptions)
| 197 | } |
| 198 | |
| 199 | export function useVoice({ |
| 200 | onTranscript, |
| 201 | onError, |
| 202 | enabled, |
| 203 | focusMode, |
| 204 | }: UseVoiceOptions): UseVoiceReturn { |
| 205 | const [state, setState] = useState<VoiceState>('idle') |
| 206 | const stateRef = useRef<VoiceState>('idle') |
| 207 | const connectionRef = useRef<VoiceStreamConnection | null>(null) |
| 208 | const accumulatedRef = useRef('') |
| 209 | const onTranscriptRef = useRef(onTranscript) |
| 210 | const onErrorRef = useRef(onError) |
| 211 | const cleanupTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null) |
| 212 | const releaseTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null) |
| 213 | // True once we've seen a second keypress (auto-repeat) while recording. |
| 214 | // The OS key repeat delay (~500ms on macOS) means the first keypress is |
| 215 | // solo — arming the release timer before auto-repeat starts would cause |
| 216 | // a false release. |
| 217 | const seenRepeatRef = useRef(false) |
| 218 | const repeatFallbackTimerRef = useRef<ReturnType<typeof setTimeout> | null>( |
| 219 | null, |
| 220 | ) |
| 221 | // True when the current recording session was started by terminal focus |
| 222 | // (not by a keypress). Focus-driven sessions end on blur, not key release. |
| 223 | const focusTriggeredRef = useRef(false) |
| 224 | // Timer that tears down the session after prolonged silence in focus mode. |
| 225 | const focusSilenceTimerRef = useRef<ReturnType<typeof setTimeout> | null>( |
| 226 | null, |
| 227 | ) |
| 228 | // Set when a focus-mode session is torn down due to silence. Prevents |
| 229 | // the focus effect from immediately restarting. Cleared on blur so the |
| 230 | // next focus cycle re-arms recording. |
| 231 | const silenceTimedOutRef = useRef(false) |
| 232 | const recordingStartRef = useRef(0) |
| 233 | // Incremented on each startRecordingSession(). Callbacks capture their |
| 234 | // generation and bail if a newer session has started — prevents a zombie |
| 235 | // slow-connecting WS from an abandoned session from overwriting |
| 236 | // connectionRef mid-way through the next session. |
| 237 | const sessionGenRef = useRef(0) |
| 238 | // True if the early-error retry fired during this session. |
| 239 | // Tracked for the tengu_voice_recording_completed analytics event. |
| 240 | const retryUsedRef = useRef(false) |
| 241 | // Full audio captured this session, kept for silent-drop replay. ~1% of |
| 242 | // sessions get a sticky-broken CE pod that accepts audio but returns zero |
| 243 | // transcripts (anthropics/anthropic#287008 session-sticky variant); when |
| 244 | // finalize() resolves via no_data_timeout with hadAudioSignal=true, we |
| 245 | // replay the buffer on a fresh WS once. Bounded: 32KB/s × ~60s max ≈ 2MB. |
| 246 | const fullAudioRef = useRef<Buffer[]>([]) |
| 247 | const silentDropRetriedRef = useRef(false) |
| 248 | // Bumped when the early-error retry is scheduled. Captured per |
| 249 | // attemptConnect — onError swallows stale-gen events (conn 1's |
| 250 | // trailing close-error) but surfaces current-gen ones (conn 2's |
| 251 | // genuine failure). Same shape as sessionGenRef, one level down. |
| 252 | const attemptGenRef = useRef(0) |
| 253 | // Running total of chars flushed in focus mode (each final transcript is |
| 254 | // injected immediately and accumulatedRef reset). Added to transcriptChars |
| 255 | // in the completed event so focus-mode sessions don't false-positive as |
| 256 | // silent-drops (transcriptChars=0 despite successful transcription). |
nothing calls this directly
no test coverage detected