Preserve voice session across tab navigation

Add watchdog mechanism to ensure STT continues when switching tabs:
- Monitor STT state every 500ms and restart if session active but STT stopped
- Handle AppState changes to restart STT when app returns to foreground
- Prevents session interruption due to native audio session changes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sergei 2026-01-27 16:47:27 -08:00
parent 5efd696ef2
commit bdb4ceb8d2

View File

@ -1,6 +1,6 @@
import { Tabs } from 'expo-router';
import React, { useCallback, useEffect, useRef } from 'react';
import { Platform, View } from 'react-native';
import { Platform, View, AppState, AppStateStatus } from 'react-native';
import { Feather } from '@expo/vector-icons';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
@ -143,6 +143,55 @@ export default function TabLayout() {
}
}, [status, sttIsListening, startListening]);
// ============================================================================
// TAB NAVIGATION PERSISTENCE
// Ensure voice session continues when user switches between tabs.
// The session state is in VoiceContext (root level), but STT may stop due to:
// 1. Native audio session changes
// 2. Tab unmount/remount (though tabs layout doesn't unmount)
// 3. AppState changes (background/foreground)
// ============================================================================
// Monitor and recover STT state during tab navigation
// If session is active but STT stopped unexpectedly, restart it
useEffect(() => {
// Check every 500ms if STT needs to be restarted
const intervalId = setInterval(() => {
// Only act if session should be active (isListening from VoiceContext)
// but STT is not actually listening, and we're not in speaking/processing mode
if (
sessionActiveRef.current &&
!sttIsListening &&
status !== 'speaking' &&
status !== 'processing'
) {
console.log('[TabLayout] STT watchdog: restarting STT (session active but STT stopped)');
startListening();
}
}, 500);
return () => clearInterval(intervalId);
}, [sttIsListening, status, startListening]);
// Handle app state changes (background/foreground)
// When app comes back to foreground, restart STT if session was active
useEffect(() => {
const handleAppStateChange = (nextAppState: AppStateStatus) => {
if (nextAppState === 'active' && sessionActiveRef.current) {
// App came to foreground, give it a moment then check STT
setTimeout(() => {
if (sessionActiveRef.current && !sttIsListening && status !== 'speaking' && status !== 'processing') {
console.log('[TabLayout] App foregrounded - restarting STT');
startListening();
}
}, 300);
}
};
const subscription = AppState.addEventListener('change', handleAppStateChange);
return () => subscription.remove();
}, [sttIsListening, status, startListening]);
// Handle voice FAB press - toggle listening mode
const handleVoiceFABPress = useCallback(() => {
if (isListening) {