import { useEffect, useSyncExternalStore } from 'react' import type { QueuedCommand } from '../types/textInputTypes.js' import { getCommandQueueSnapshot, subscribeToCommandQueue, } from '../utils/messageQueueManager.js' import type { QueryGuard } from '../utils/QueryGuard.js' import { processQueueIfReady } from '../utils/queueProcessor.js' type UseQueueProcessorParams = { executeQueuedInput: (commands: QueuedCommand[]) => Promise hasActiveLocalJsxUI: boolean queryGuard: QueryGuard } /** * Hook that processes queued commands when conditions are met. * * Uses a single unified command queue (module-level store). Priority determines * processing order: 'now' > 'next' (user input) > 'later' (task notifications). * The dequeue() function handles priority ordering automatically. * * Processing triggers when: * - No query active (queryGuard — reactive via useSyncExternalStore) * - Queue has items * - No active local JSX UI blocking input */ export function useQueueProcessor({ executeQueuedInput, hasActiveLocalJsxUI, queryGuard, }: UseQueueProcessorParams): void { // Subscribe to the query guard. Re-renders when a query starts or ends // (or when reserve/cancelReservation transitions dispatching state). const isQueryActive = useSyncExternalStore( queryGuard.subscribe, queryGuard.getSnapshot, ) // Subscribe to the unified command queue via useSyncExternalStore. // This guarantees re-render when the store changes, bypassing // React context propagation delays that cause missed notifications in Ink. const queueSnapshot = useSyncExternalStore( subscribeToCommandQueue, getCommandQueueSnapshot, ) useEffect(() => { if (isQueryActive) return if (hasActiveLocalJsxUI) return if (queueSnapshot.length === 0) return // Reservation is now owned by handlePromptSubmit (inside executeUserInput's // try block). The sync chain executeQueuedInput → handlePromptSubmit → // executeUserInput → queryGuard.reserve() runs before the first real await, // so by the time React re-runs this effect (due to the dequeue-triggered // snapshot change), isQueryActive is already true (dispatching) and the // guard above returns early. handlePromptSubmit's finally releases the // reservation via cancelReservation() (no-op if onQuery already ran end()). processQueueIfReady({ executeInput: executeQueuedInput }) }, [ queueSnapshot, isQueryActive, executeQueuedInput, hasActiveLocalJsxUI, queryGuard, ]) }