/ hooks / useQueueProcessor.ts
useQueueProcessor.ts
 1  import { useEffect, useSyncExternalStore } from 'react'
 2  import type { QueuedCommand } from '../types/textInputTypes.js'
 3  import {
 4    getCommandQueueSnapshot,
 5    subscribeToCommandQueue,
 6  } from '../utils/messageQueueManager.js'
 7  import type { QueryGuard } from '../utils/QueryGuard.js'
 8  import { processQueueIfReady } from '../utils/queueProcessor.js'
 9  
10  type UseQueueProcessorParams = {
11    executeQueuedInput: (commands: QueuedCommand[]) => Promise<void>
12    hasActiveLocalJsxUI: boolean
13    queryGuard: QueryGuard
14  }
15  
16  /**
17   * Hook that processes queued commands when conditions are met.
18   *
19   * Uses a single unified command queue (module-level store). Priority determines
20   * processing order: 'now' > 'next' (user input) > 'later' (task notifications).
21   * The dequeue() function handles priority ordering automatically.
22   *
23   * Processing triggers when:
24   * - No query active (queryGuard — reactive via useSyncExternalStore)
25   * - Queue has items
26   * - No active local JSX UI blocking input
27   */
28  export function useQueueProcessor({
29    executeQueuedInput,
30    hasActiveLocalJsxUI,
31    queryGuard,
32  }: UseQueueProcessorParams): void {
33    // Subscribe to the query guard. Re-renders when a query starts or ends
34    // (or when reserve/cancelReservation transitions dispatching state).
35    const isQueryActive = useSyncExternalStore(
36      queryGuard.subscribe,
37      queryGuard.getSnapshot,
38    )
39  
40    // Subscribe to the unified command queue via useSyncExternalStore.
41    // This guarantees re-render when the store changes, bypassing
42    // React context propagation delays that cause missed notifications in Ink.
43    const queueSnapshot = useSyncExternalStore(
44      subscribeToCommandQueue,
45      getCommandQueueSnapshot,
46    )
47  
48    useEffect(() => {
49      if (isQueryActive) return
50      if (hasActiveLocalJsxUI) return
51      if (queueSnapshot.length === 0) return
52  
53      // Reservation is now owned by handlePromptSubmit (inside executeUserInput's
54      // try block). The sync chain executeQueuedInput → handlePromptSubmit →
55      // executeUserInput → queryGuard.reserve() runs before the first real await,
56      // so by the time React re-runs this effect (due to the dequeue-triggered
57      // snapshot change), isQueryActive is already true (dispatching) and the
58      // guard above returns early. handlePromptSubmit's finally releases the
59      // reservation via cancelReservation() (no-op if onQuery already ran end()).
60      processQueueIfReady({ executeInput: executeQueuedInput })
61    }, [
62      queueSnapshot,
63      isQueryActive,
64      executeQueuedInput,
65      hasActiveLocalJsxUI,
66      queryGuard,
67    ])
68  }