({
executeQueuedInput,
hasActiveLocalJsxUI,
queryGuard,
}: UseQueueProcessorParams)
| 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 | } |
no test coverage detected