diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index 416ac350..cfb9e782 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -54,7 +54,8 @@ export function Header() { const autoOfflineAttemptedRef = useRef(false) const { wrapUpSecondsRemaining } = useWrapUpCountdown() const { connectionState, reconnectPresence } = usePresence() - const { isRegistered, hasInitialized, currentCall } = useTelnyxContext() + const { isRegistered, hasInitialized, currentCall, reconnect, connectionStatus } = useTelnyxContext() + const isRegisteredRef = useRef(isRegistered) // Sync status when user.agent_status changes mid-session (e.g. a call is routed to this agent). // Without this, the dropdown stays editable even while the agent is on_call. @@ -76,10 +77,29 @@ export function Header() { return () => window.removeEventListener('agent-status-optimistic', handleOptimistic) }, []) + useEffect(() => { + isRegisteredRef.current = isRegistered + }, [isRegistered]) + + const waitForRegistration = useCallback(async (timeoutMs = 12000) => { + const startedAt = Date.now() + while (Date.now() - startedAt < timeoutMs) { + if (isRegisteredRef.current) return true + await new Promise((resolve) => setTimeout(resolve, 300)) + } + return false + }, []) + const handleStatusChange = useCallback(async (newStatus: AgentStatus): Promise => { if (newStatus === 'available' && !isRegistered) { - showPresenceToast('Cannot go available — phone not connected. Please refresh.', 'error') - return false + showPresenceToast('Phone reconnecting…', 'warning') + reconnect() + + const recovered = await waitForRegistration() + if (!recovered) { + showPresenceToast('Cannot go available — phone not connected. Click Agent Diagnostic → Reconnect.', 'error') + return false + } } const previousStatus = status @@ -129,7 +149,7 @@ export function Header() { ) return false } - }, [isRegistered, status]) + }, [isRegistered, status, reconnect, waitForRegistration]) useEffect(() => { if (isRegistered) { @@ -137,6 +157,15 @@ export function Header() { } }, [isRegistered]) + useEffect(() => { + if (document.visibilityState !== 'visible') return + if (connectionStatus !== 'disconnected' && connectionStatus !== 'error') return + + reconnect() + const retryTimer = setTimeout(() => reconnect(), 2500) + return () => clearTimeout(retryTimer) + }, [connectionStatus, reconnect]) + useEffect(() => { const activeCall = currentCall?.state && ['ringing_inbound', 'ringing_outbound', 'answering', 'active', 'held'].includes(currentCall.state) if (status === 'available' && hasInitialized && !isRegistered && !activeCall) { diff --git a/src/hooks/use-telnyx.ts b/src/hooks/use-telnyx.ts index 5c43712e..9fefbea1 100644 --- a/src/hooks/use-telnyx.ts +++ b/src/hooks/use-telnyx.ts @@ -667,7 +667,8 @@ export function useTelnyx(): UseTelnyxReturn { } const now = Date.now() - if (now - lastManualReconnectAtRef.current < MANUAL_RECONNECT_COOLDOWN_MS) { + const bypassCooldown = reason === 'manual-button' || reason === 'visibility-resume' || reason === 'go-available' + if (!bypassCooldown && now - lastManualReconnectAtRef.current < MANUAL_RECONNECT_COOLDOWN_MS) { console.log(`[useTelnyx] Manual reconnect skipped (${reason}) - in cooldown`) return } @@ -881,6 +882,13 @@ export function useTelnyx(): UseTelnyxReturn { function handleVisibilityChange() { if (document.visibilityState === 'visible') { console.log('[useTelnyx] Tab visible, checking registration...') + + if (connectionStatusRef.current === 'disconnected' || connectionStatusRef.current === 'error') { + void triggerManualReconnect('visibility-resume') + runLifecycleRecovery('visibilitychange', 500) + return + } + runLifecycleRecovery('visibilitychange', 1500) } }