/ hooks / useNotifyAfterTimeout.ts
useNotifyAfterTimeout.ts
 1  import { useEffect } from 'react'
 2  import {
 3    getLastInteractionTime,
 4    updateLastInteractionTime,
 5  } from '../bootstrap/state.js'
 6  import { useTerminalNotification } from '../ink/useTerminalNotification.js'
 7  import { sendNotification } from '../services/notifier.js'
 8  // The time threshold in milliseconds for considering an interaction "recent" (6 seconds)
 9  export const DEFAULT_INTERACTION_THRESHOLD_MS = 6000
10  
11  function getTimeSinceLastInteraction(): number {
12    return Date.now() - getLastInteractionTime()
13  }
14  
15  function hasRecentInteraction(threshold: number): boolean {
16    return getTimeSinceLastInteraction() < threshold
17  }
18  
19  function shouldNotify(threshold: number): boolean {
20    return process.env.NODE_ENV !== 'test' && !hasRecentInteraction(threshold)
21  }
22  
23  // NOTE: User interaction tracking is now done in App.tsx's processKeysInBatch
24  // function, which calls updateLastInteractionTime() when any input is received.
25  // This avoids having a separate stdin 'data' listener that would compete with
26  // the main 'readable' listener and cause dropped input characters.
27  
28  /**
29   * Hook that manages desktop notifications after a timeout period.
30   *
31   * Shows a notification in two cases:
32   * 1. Immediately if the app has been idle for longer than the threshold
33   * 2. After the specified timeout if the user doesn't interact within that time
34   *
35   * @param message - The notification message to display
36   * @param timeout - The timeout in milliseconds (defaults to 6000ms)
37   */
38  export function useNotifyAfterTimeout(
39    message: string,
40    notificationType: string,
41  ): void {
42    const terminal = useTerminalNotification()
43  
44    // Reset interaction time when hook is called to make sure that requests
45    // that took a long time to complete don't pop up a notification right away.
46    // Must be immediate because useEffect runs after Ink's render cycle has
47    // already flushed; without it the timestamp stays stale and a premature
48    // notification fires if the user is idle (no subsequent renders to flush).
49    useEffect(() => {
50      updateLastInteractionTime(true)
51    }, [])
52  
53    useEffect(() => {
54      let hasNotified = false
55      const timer = setInterval(() => {
56        if (shouldNotify(DEFAULT_INTERACTION_THRESHOLD_MS) && !hasNotified) {
57          hasNotified = true
58          clearInterval(timer)
59          void sendNotification({ message, notificationType }, terminal)
60        }
61      }, DEFAULT_INTERACTION_THRESHOLD_MS)
62  
63      return () => clearInterval(timer)
64    }, [message, notificationType, terminal])
65  }